263 Commits

Author SHA1 Message Date
mjh
05ea5f9aaa merge: feature/20260805_mjh into feature/20260805_emis_create 2026-08-05 09:41:09 +09:00
mjh
50315ad245 feat: add ONCSC1340 MCI client and IO DTOs 2026-08-05 09:33:58 +09:00
jade
65222b5b1a feat: index.html 메인 화면에 Tester, Console 카드 추가
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m49s
2026-08-05 00:06:02 +09:00
jade
b9f98e89ce fix: index.html 헤더 링크 추가 및 tester.html CSV 한글 깨짐 방지(BOM 추가)
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m49s
2026-08-05 00:01:59 +09:00
jade
96e308aefb fix: ZtUsacRepository (MyBatis Mapper) 의존성 주입 에러 수정 (MybatisConfig에 @MapperScan 추가)
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m52s
2026-08-04 23:56:23 +09:00
jade
96a26bc93d fix: ToolManifestService 등의 Bean 로드 에러 방지를 위해 scanBasePackages 범위를 io.shinhanlife.dap.lib 전체로 확장
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m50s
2026-08-04 23:53:58 +09:00
jade
a2586fd6d3 fix: ToolPodMcpToolSynchronizer가 dap-gateway에서도 스캔되어 발생하는 Bean 에러 수정 (usecase 패키지로 이동)
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m48s
2026-08-04 23:51:07 +09:00
jade
cdfb6ef901 fix: ToolRegistryHeartbeatSender Bean을 인식하지 못하는 런타임 에러 수정 (scanBasePackages 추가)
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m53s
2026-08-04 23:46:30 +09:00
jade
3729e2d6ee fix: McpRouterController, ChatController 불필요한 FQCN 선언 제거 및 import 통합
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m52s
2026-08-04 23:43:44 +09:00
jade
946ec6d70f chore: DapToolOthApplication, logback 등 코드 내 남아있던 이전 명칭(찌꺼기) 일괄 치환 및 정리
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m7s
2026-08-04 23:36:50 +09:00
jade
136ad2fa71 refactor: dap-tool-* 모듈을 dap-was-* 이름으로 전면 개편 및 컨테이너명/파이프라인 연동
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m57s
2026-08-04 23:33:06 +09:00
jade
3e69436bfe chore: 잘못 생성되어 있던 중복 ToolMetadata 클래스(mcg.dto 패키지) 삭제
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m54s
2026-08-04 23:10:02 +09:00
jade
f827760513 refactor: 공용 클래스(MCP, Manifest, UseCase 등) lib 패키지로 이관 및 통합
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m17s
2026-08-04 23:06:01 +09:00
jade
1a70495686 feat(ui): add tool-test-console and tester dashboards
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m51s
- Add Auto-Tester Dashboard (tester.html) in gateway for batch testing tools

- Add standalone Tool Test Console (tool-test-console.html) in core

- Fix Tailwind CSS Preflight conflicts in console UI

- Update console payload schema resolution to support both gateway and pod modes

- Strip JSON-RPC metadata from tool payload output in console

- Add unified navigation headers across all static HTML pages
2026-08-04 18:24:52 +09:00
jade
39c1f4ee12 UI: chat.html 상단 GNB 디자인을 scaffold.html과 동일하게 동기화
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m52s
2026-08-04 16:45:57 +09:00
jade
e858859021 feat: standardize tool names by pod domain action
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m55s
2026-08-04 16:39:03 +09:00
jade
a58af1606e feat: publish tool manifest endpoint
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m50s
2026-08-04 15:58:59 +09:00
jade
08ccc043da feat: organize tool schemas by category
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m58s
2026-08-04 14:42:08 +09:00
jade
4393071212 chore: log validated tool response
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m54s
2026-08-04 13:38:43 +09:00
jade
1bac8d9d87 feat: enrich claim search output schema
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m53s
2026-08-04 13:34:02 +09:00
jade
31e6fd605d feat: support MCP output schemas
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m1s
2026-08-04 11:24:59 +09:00
jade
15262d5a0f test: keep input schemas in tool modules
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m56s
2026-08-04 11:03:35 +09:00
jade
2957272cc3 feat: support explicit MCP input schemas
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m18s
2026-08-04 10:50:25 +09:00
jade
52f585fa45 Refactor: Remove SensitiveDataMasker and fix PaginationRequestValidator
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m55s
2026-08-03 17:41:02 +09:00
jade
bc86f36825 feat(scaffold): 템플릿 코드에 @McpValidation 예시 추가
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m42s
2026-08-03 16:37:11 +09:00
jade
55dcffa2c9 feat(schema): 고급 JSON Schema 제약조건(format, default, examples 등) 지원 및 McpFunction 수정 반영
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-08-03 16:35:46 +09:00
jade
be0314afe8 feat(schema): @McpAnyOf 어노테이션 및 JSON Schema 파싱 로직 추가
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m56s
2026-08-03 16:31:43 +09:00
jade
39eab0429e Revert "feat(security): ToolSecurityContext 및 KMS 복호화 공통 모듈 추가"
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m1s
This reverts commit cf958622bd.
2026-08-03 15:35:20 +09:00
jade
cf958622bd feat(security): ToolSecurityContext 및 KMS 복호화 공통 모듈 추가
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m18s
2026-08-03 13:45:16 +09:00
jade
d858f2ab90 Merge branch 'main' of https://git.devjun.net/kimhyungsik/ax_hub_mcp_tool
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m53s
2026-07-31 14:59:17 +09:00
jade
2bc896a130 application 업데이트 2026-07-31 14:59:00 +09:00
b22cd779cf Merge pull request 'fix: paser로 인해 인공지능의 띄어쓰기가 이상해지는 문제 해결' (#5) from feature/markdown into main
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m20s
Reviewed-on: #5
2026-07-31 14:48:01 +09:00
juheelee
b3908f771f fix: paser로 인해 인공지능의 띄어쓰기가 이상해지는 문제 해결 2026-07-31 14:45:31 +09:00
jade
58234b1c59 refactor: align Glow transaction field metadata
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m20s
2026-07-31 14:44:21 +09:00
jade
541d6d1938 config: centralize Glow MCI settings
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m15s
2026-07-31 14:41:35 +09:00
628925dce9 Merge pull request 'feature/markdown' (#4) from feature/markdown into main
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m58s
Reviewed-on: #4
2026-07-30 18:01:02 +09:00
juheelee
9d16b8ef3a feat: markdown 추가 2026-07-30 17:59:27 +09:00
juheelee
39f68ccf46 feat: markdown 추가 2026-07-30 17:59:06 +09:00
jade
1bf09705d1 build: validate duplicate MCP tool names before packaging
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m1s
2026-07-30 17:41:06 +09:00
jade
60055dd9ed feat: propagate MCP request headers to tools
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m28s
2026-07-30 17:10:15 +09:00
jade
b5ff930556 docs: refresh project README
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m1s
2026-07-30 10:16:25 +09:00
Gitea CI
2c4c447a8f feat: OpenRouter 실존 무료 모델 교체 및 Gemini API 라우팅 전환
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m9s
- chat.html: 드롭다운을 검증된 무료 모델로 교체
  * Ling 3.0 Flash (inclusionai/ling-3.0-flash:free) 기본값으로 설정
  * GPT-OSS 20B, Gemma 4 31B, NVIDIA Nemotron Nano, Cohere North Mini 추가
  * 삭제된 모델(qwen-2-7b, llama-3-8b, openchat-7b 등) 제거
- ChatController.java: Gemini 직접 API(비정상 키) 제거
  * gemini-flash-latest 선택 시 OpenRouter Google Gemma 4 31B 로 라우팅
  * 불필요한 70여 라인 직접 HTTP 호출 코드 제거
- E2E 검증 완료: Ling 3.0 Flash 정상 응답 확인
2026-07-30 07:28:56 +09:00
hjgram
68e1bf95fd merge: remote updates into main with tool-core schema updates
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m17s
2026-07-30 06:26:41 +09:00
hjgram
204dd04d5f feat: OCI 배포 듀얼 LLM 스위칭 및 신규 툴 DTO 의존성 주입 병합 2026-07-30 06:19:06 +09:00
jade
5fe8590695 feat: use MCP SDK for tool pod calls
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m18s
2026-07-29 23:03:11 +09:00
0a2464d426 Merge pull request 'feat: 메타 테이블 조회' (#3) from feature/meta-table-check into main
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m21s
Reviewed-on: #3
2026-07-29 17:30:32 +09:00
juheelee
a667431de1 feat: 메타 테이블 조회 2026-07-29 17:27:27 +09:00
mjh
aca0e009bc Merge feature/20260729-mjh: add SOL request detail tool
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m52s
2026-07-29 17:17:37 +09:00
df537fa77c Merge pull request 'DEV-SRTEST' (#2) from DEV-SRTEST into main
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m51s
Reviewed-on: #2
2026-07-29 17:04:40 +09:00
jade
8b4d4da0b9 Merge branch 'main' of https://git.devjun.net/kimhyungsik/ax_hub_mcp_tool into DEV-SRTEST 2026-07-29 17:02:21 +09:00
jade
c551b15378 테스트 푸쉬 2026-07-29 17:01:05 +09:00
juheelee
b448e68f45 Merge branch 'main' of https://git.devjun.net/kimhyungsik/ax_hub_mcp_tool
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m49s
2026-07-29 16:50:56 +09:00
juheelee
b68fbb9726 feat: 메타 통합 코드 조회 2026-07-29 16:48:28 +09:00
c6a2f82455 push TEST
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m16s
2026-07-29 16:08:45 +09:00
juheelee
2f5dad05df feat: 메타 통합 코드 조회 2026-07-29 15:34:01 +09:00
ea1aea81bb feat: add SOL request detail tool 2026-07-29 14:26:38 +09:00
jade
b0bf4a9c68 fix: use rsync to clean stale files during host volume sync
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m1s
2026-07-29 13:25:10 +09:00
jade
752aae4dde fix: run gradle natively in runner to avoid isolated mount issue
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 1m50s
2026-07-29 13:22:03 +09:00
jade
5cb37cfda6 fix: mount correct app path for gradle build
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 4s
2026-07-29 13:16:31 +09:00
jade
2a4b3e68e9 fix: allow gradle build libs to be copied into docker container
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 2m3s
2026-07-29 13:03:11 +09:00
jade
ca1082f1c2 fix: rollback register to false as per user request
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 31s
2026-07-29 12:58:22 +09:00
jade
7b1d3131e6 fix: set register true for tools to fix catalog display
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 33s
2026-07-29 12:55:06 +09:00
jade
139ddbc6ca fix: optimize dockerfile to use external gradle build
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 37s
2026-07-29 12:54:02 +09:00
jade
870ebb723f fix: hide markdown emphasis markers in chat responses
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m22s
2026-07-29 11:35:22 +09:00
jade
a08a7d25df refactor: move team member tool to smp category
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-29 11:30:52 +09:00
jade
60acc5df81 refactor: rename solgit tool to sol
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m23s
2026-07-29 11:23:49 +09:00
jade
f5be5b2514 refactor: remove unused team member legacy types
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m32s
2026-07-29 11:14:44 +09:00
jade
aae4898d18 feat: invoke Solgit MCI before returning dummy response
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m26s
2026-07-29 11:02:10 +09:00
jade
06b724459e feat: add solgitReqList and get_mcp_tool_team_members tools
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m31s
2026-07-29 10:26:32 +09:00
jade
4133b63595 불필요 한 파일 삭제
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m19s
2026-07-28 14:39:50 +09:00
jade
1e18d2bc47 feat: support nested MCP input schema validation
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-28 14:39:31 +09:00
jade
0c89a09ccf docs: Update README.md with Web UI Scaffolder enhancements
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 8m5s
2026-07-27 15:41:32 +09:00
jade
3ce9201cd5 fix: Add throws Exception to MciClient template 2026-07-27 15:37:27 +09:00
jade
be81c095c9 fix: Correct text block alignment and formatting arguments 2026-07-27 15:36:40 +09:00
jade
d4554ce34f fix: Remove excessive indentation in string formatting for UseCaseImpl text block 2026-07-27 15:30:58 +09:00
jade
b6d4ab1bec fix: Remove duplicated package path in MCI DTOs and Converter 2026-07-27 15:26:38 +09:00
jade
2c8497ebee fix: Correct ToolScaffolder template issues 2026-07-27 15:10:15 +09:00
jade
8a93d5d6dc feat: Support 4-letter MCI system code in Scaffolder and refactor Onnba classes 2026-07-27 15:03:13 +09:00
jade
c0de1f4edb fix: Include local fallback tools in generateToolsMarkdown API
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m15s
2026-07-27 14:11:25 +09:00
jade
1ee9f9e802 style: Add chat.html link to all top navigation bars 2026-07-27 14:07:33 +09:00
jade
25f0d2bab5 feat: move Onnba MCI request conversion to use case
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m1s
2026-07-27 14:01:04 +09:00
jade
9b9159a605 feat: Add UI/UX improvements to catalog.html including search, config json copy, and guide modal
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 34s
2026-07-27 14:00:27 +09:00
jade
c8dd43221e ci: Fix Node.js installation by dropping sudo/curl and supporting alpine
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m41s
2026-07-27 13:09:01 +09:00
jade
7b8c38489e ci: Add Node.js installation step to Gitea Actions deploy workflow
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 1s
2026-07-27 13:07:29 +09:00
jade
63187429b4 feat: validate MCP tool input constraints
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 1s
2026-07-27 13:01:40 +09:00
jade
d4953d60ec test: fix gateway test package references
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 18s
2026-07-27 12:51:28 +09:00
jade
72891dc2db 불필요한 소스 삭제
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-27 12:38:31 +09:00
jade
82c27bd468 test: prevent duplicate MCP tool names
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-27 12:37:35 +09:00
jade
ff5ca4fc7a Fix: Add missing formatted arguments for MCI and EAI ToolScaffolder templates 2026-07-27 11:43:37 +09:00
jade
ba39a23b09 Fix: Add missing @Service annotations to MCP Tool use case implementations
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-27 10:45:09 +09:00
jade
579979b5fe Refactor: Move MCP tool annotations to interface level 2026-07-27 10:21:24 +09:00
jade
db46f30e72 refactor: DTO 레코드를 별도 파일(dto 패키지)로 분리
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-27 10:06:43 +09:00
jade
2c64d86a56 refactor: UseCase 인터페이스 내 DTO 분리 및 강결합 제거
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-27 10:04:56 +09:00
jade
ded5ef9e8f 신한 라이프 경로에 맞게 수정
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-27 08:57:03 +09:00
jade
a6ca2b82a7 scaffold 수정
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-26 16:00:45 +09:00
jade
94cdaaa8c9 불필요 없는 소스 삭제
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-26 15:46:48 +09:00
jade
bea517c4c1 fix: resolve bean injection error and gateway routing configs
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-26 15:34:43 +09:00
jade
ad950071b2 refactor: fix annotation package - remove duplicate annotation.annotation directory
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-25 02:00:47 +09:00
jade
e6407b07a2 refactor: apply Glow standard to dap-tool-core (usecase, javadoc, etc)
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-25 01:56:41 +09:00
jade
e58c004d9d refactor: rename biz/other to biz/oth
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-25 01:43:47 +09:00
jade
92fe2399be refactor: rename dap-tool-other to dap-tool-oth
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-25 01:36:12 +09:00
jade
7ba18f7809 refactor: rename domain categories (common->cmm, sample->smp) and restrict category key to 3 chars
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-25 01:29:48 +09:00
jade
4dcc5a2978 refactor: restructure MCP tools without Controller layer, using UseCase pattern per Glow standards
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-25 01:13:25 +09:00
jade
994f5b4348 fix: remove docker compose down from deploy pipeline to prevent killing the CI runner
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-25 00:13:57 +09:00
jade
0a50b20960 feat: add chat.html link to index.html dashboard
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-25 00:13:00 +09:00
jade
97ba0c3267 fix: add --remove-orphans to docker compose down to prevent port conflicts with legacy Blue/Green containers
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-25 00:09:36 +09:00
jade
388fa33c67 fix: revert Nginx and Blue/Green deployment to restore stable Apache connection
Some checks are pending
Deploy to OCIWP / deploy (push) Waiting to run
2026-07-25 00:05:54 +09:00
jade
4015207b86 fix: map Nginx 8281 to host 8080 to restore Apache proxy connection
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 7m53s
2026-07-24 23:52:55 +09:00
jade
4dcc85eff0 fix: override gateway fallback URLs via env vars to support blue/green
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-24 23:42:30 +09:00
jade
72f04aef62 fix: remove curl command that fails in act_runner
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 11m35s
2026-07-24 23:26:48 +09:00
jade
b5565a4da0 fix: repair blue-green deployment architecture
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 54s
2026-07-24 23:24:55 +09:00
jade
c38d59887c fix: remove 8080 from deploy script
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 10m59s
2026-07-24 23:04:38 +09:00
jade
f3639043fc chore: fetch gateway logs in deploy action 2026-07-24 17:52:15 +09:00
jade
e278602d32 fix: map gateway to 8281 to match Apache 2026-07-24 17:51:38 +09:00
jade
f6f86e5afb chore: test curl to gateway inside deploy action 2026-07-24 17:51:03 +09:00
jade
a89843518b fix: restore gateway 8081 port mapping 2026-07-24 17:48:34 +09:00
jade
d7b80a644c fix: bypass nginx and map gateway directly to 8281 2026-07-24 17:47:41 +09:00
jade
05a242e5de chore: test nginx static HTML response 2026-07-24 17:46:46 +09:00
jade
a409652bdf fix: Nginx port 8281 mapping restored 2026-07-24 17:46:09 +09:00
jade
fa268edf7d fix: Nginx SSL support for Apache proxy 2026-07-24 17:44:23 +09:00
jade
c69f11e1c9 fix: add http2 support to Nginx to match Apache h2c proxying
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 13m41s
2026-07-24 17:42:25 +09:00
jade
56483d804a chore: test nginx proxy to google
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-24 17:41:33 +09:00
jade
1c353ce3a7 chore: test nginx plain response
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-24 17:39:36 +09:00
jade
35f361a976 fix: restore Nginx port 8281 mapping - Apache VirtualHost proxies to 8281
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 13m41s
2026-07-24 16:08:59 +09:00
jade
9066c621a0 fix: resolve Nginx 503 error on rollback and apply package refactoring
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 11m44s
2026-07-24 15:00:01 +09:00
jade
4ed534b1d6 fix: restore Nginx 8080 port mapping for Apache and refactor MciCfpaClient
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 13m42s
2026-07-24 14:27:00 +09:00
jade
0892597c31 fix: start containers sequentially and fix nginx startup config
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 3m20s
2026-07-24 13:29:43 +09:00
jade
6416d3f844 fix: revert Nginx port mapping to 8281 to match external Apache proxy
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-24 13:21:10 +09:00
jade
dbe479a64b fix: build sequentially to avoid OOM on dev server
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 13m8s
2026-07-24 13:04:42 +09:00
jade
53f5047c8e fix: map Nginx 8281 to host 8080 to restore Apache proxy connection
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 12m3s
2026-07-24 11:44:01 +09:00
jade
f56e503a29 fix: make Blue-Green deployment safe by checking Nginx reload status and removing redundant host Gradle build to prevent OOM server crashes
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 10m6s
2026-07-24 10:55:32 +09:00
jade
aae95591a0 fix: add Nginx SSE proxy configs to prevent buffering of AI streams, and fix Gateway internal routing to use Nginx
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-24 10:45:27 +09:00
jade
227edf9744 fix: remove stale src directories in host volume before copy to resolve compilation conflict
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 12m14s
2026-07-24 10:28:24 +09:00
jade
12ac05c805 fix: add clean to gradle build in Gitea deploy pipeline to clear persistent host volume cache
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 38s
2026-07-24 10:26:09 +09:00
jade
74ea89bf1c refactor: apply naming convention for MCI interface IO objects (CLCNNB00001_I, CUST_INQ_001_I) and separate layers
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 24s
2026-07-24 10:24:00 +09:00
jade
d562e35ad8 fix: resolve stale class conflict in Docker build by adding clean and .dockerignore
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 23s
2026-07-24 10:20:00 +09:00
jade
fc8c3da666 refactor: apply MSA naming standard for DTOs and Scaffold template (Req->Request, Res->Response)
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 30s
2026-07-24 10:07:50 +09:00
jade
438c190447 refactor: rename packages dapms to mcg and dapmt to mcc
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 42s
2026-07-24 10:00:45 +09:00
jade
51f37cfcbc chore: add restart always policy to nginx
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 14m33s
2026-07-24 09:45:23 +09:00
jade
44c2fcebba fix: invalid docker filter syntax in deploy.yml
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 13m9s
2026-07-24 09:15:02 +09:00
jade
2758f6da8e fix: Nginx port conflict with legacy containers
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 14m45s
2026-07-24 08:55:30 +09:00
jade
8b11c1aae8 feat: 적용 무중단 배포 시스템 (Blue-Green Deployment)
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 51s
2026-07-24 08:51:01 +09:00
jade
60a7226199 chore: remove unnecessary Node.js installation from deploy.yml
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 13m32s
2026-07-24 00:29:02 +09:00
jade
b3bdaa2b28 fix: revert ports and add zombie container cleanup to CI
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-24 00:22:20 +09:00
jade
260f59b143 fix: change dozzle port to 18288 to resolve conflict
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 13m26s
2026-07-24 00:02:54 +09:00
jade
ad2ab985d8 feat: allow Chat AI to use unregistered (register=false) tools
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 13m58s
2026-07-23 23:46:04 +09:00
jade
18959b5128 fix: install nodejs for checkout action
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-23 23:32:44 +09:00
jade
7f85b10938 chore: remove accidentally committed SearchHr generated files
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 1s
2026-07-23 23:30:53 +09:00
jade
b5dbcc54f0 fix: add checkout step and sync code to host volume in deployment pipeline
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 8s
2026-07-23 23:28:29 +09:00
jade
abf7a435a4 feat/fix: update ToolScaffolder interface, update scaffold.html, fix mci-mock port
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 14m20s
2026-07-23 23:11:30 +09:00
jade
f0a295632f ci: add docker system prune before deployment
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 16m15s
2026-07-23 22:52:48 +09:00
jade
cec56e015b fix: change Redis host port mapping to avoid conflicts
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 11m48s
2026-07-23 22:33:09 +09:00
kimhyungsik
1e54370fb8 fix: restore missing closing brace in DynamicMcpController
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 15m36s
2026-07-23 13:08:16 +09:00
kimhyungsik
408a3f6b28 fix: add DELETE mapping and .well-known endpoints for Cursor compatibility
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 1m23s
2026-07-23 13:05:20 +09:00
jade
541150d3f4 Fix: gateway local profile fallback URL을 컨테이너 이름으로 변경
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-23 13:01:53 +09:00
jade
165a6340ea Refactor: ToolService 인터페이스 분리 및 GlowEaiComponent 로드 에러 수정
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-23 12:59:34 +09:00
kimhyungsik
5f8228bfce fix: add EnableScheduling to enable dynamic tool synchronization
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 17m7s
2026-07-23 11:16:54 +09:00
kimhyungsik
6893bb6b35 fix: comment out emitter.onCompletion/onTimeout session removal to keep sessions persistent
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 14m21s
2026-07-23 10:33:56 +09:00
jade
3c183e901b chore: Remove hardcoded axhub.tool.url from application-dev.yml
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 13m57s
2026-07-23 10:17:38 +09:00
kimhyungsik
2948465b8a fix: comment out emitter.complete() to allow keep-alive sessions for standard SSE MCP client
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-23 10:16:15 +09:00
jade
2b4adb45d2 fix: Correct tool-payment internal port mapping to 8085
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-23 10:10:45 +09:00
jade
323d227eee feat: Add AxhubEaiComponent and MCI/EAI mock frameworks
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 3m26s
2026-07-23 09:19:21 +09:00
kimhyungsik
74b50482bf chore: optimize dev fallback routes to use internal docker service names
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 13m24s
2026-07-22 17:29:00 +09:00
kimhyungsik
dfa5a73b20 chore: force docker compose project name to ax_hub_mcp_tool
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 7m48s
2026-07-22 16:44:29 +09:00
kimhyungsik
86302d0947 chore: optimize deploy pipeline to ultra-fast relay
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-22 16:31:13 +09:00
kimhyungsik
ad37714c5b chore_fix_docker_compose_typo
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 13m31s
2026-07-22 16:01:03 +09:00
jade
b57d96f83f refactor: rename project codes mcg to dapms and mcc to dapmt
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 37s
2026-07-22 15:48:45 +09:00
kimhyungsik
fabbd368a2 chore_resolve_path_and_arg_issues
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 57s
2026-07-22 15:37:33 +09:00
kimhyungsik
f3cdfaccc3 chore_apply_gradle_jdk21_build_and_compose_direct_mount
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 34s
2026-07-22 15:33:57 +09:00
kimhyungsik
e42ca92811 chore_resolve_git_conflict
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 15s
2026-07-22 15:31:13 +09:00
jade
c18efe251e refactor: migrate all properties files to yaml
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
2026-07-22 14:57:44 +09:00
jade
8d4e8fd976 chore: replace author name with employee ID 0986406
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 1s
2026-07-22 14:36:15 +09:00
kimhyungsik
edec314a83 test: final verify auto deploy
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
2026-07-22 14:08:45 +09:00
kimhyungsik
da09997452 chore: enable docker run relay deploy
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
2026-07-22 14:05:46 +09:00
kimhyungsik
5c15ae3505 test: verify auto deploy triggering
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
2026-07-22 14:03:22 +09:00
kimhyungsik
3eebf07b0e chore: restore standard runs-on label
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
2026-07-22 14:00:36 +09:00
kimhyungsik
a161dcf319 chore: quote runs-on host label
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-22 13:59:46 +09:00
kimhyungsik
ae3352ccb4 chore: fix runs-on syntax and add gradle caching
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
2026-07-22 13:57:00 +09:00
kimhyungsik
2deffd3f10 chore: change runs-on to host mode
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-22 13:52:22 +09:00
kimhyungsik
f334b8ecc5 feat: add deploy workflow
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
2026-07-22 13:47:01 +09:00
jade
1db19344bc fix: restore broken Korean comments in application-dev.properties and add UTF-8 encoding rule 2026-07-22 13:36:22 +09:00
jade
8e6682d089 chore: migrate environment endpoints from render to OCI 2026-07-22 13:35:01 +09:00
jade
6968c7dfb7 refactor: use Spring DI for MapStruct and update AGENTS.md rule 2026-07-22 12:37:16 +09:00
jade
416b068fcb 소스 삭제 2026-07-22 11:36:29 +09:00
jade
933ba2ecfa 소스 수정 2026-07-22 11:35:56 +09:00
jade
ce96137948 fix: resolve Format specifier '%s' error in ToolScaffolder and generate SearchHr tool 2026-07-22 11:31:39 +09:00
jade
e983b47af6 feat: add MapStruct mapping samples to ToolScaffolder template 2026-07-22 11:22:36 +09:00
jade
1796370a35 docs: add Shinhan Life logging standard rules to AGENTS.md 2026-07-22 10:40:16 +09:00
jade
680dbe098b feat: apply Shinhan Life logging standards to all modules 2026-07-22 10:22:16 +09:00
jade
a877103d93 fix: add missing java.util.Map import and remove fully qualified names in ToolScaffolder template 2026-07-22 09:54:57 +09:00
jade
6e1ef40380 fix: use Map instead of JSON string for fallback returns in ToolScaffolder and SearchHrService to resolve serialization and IDE warnings 2026-07-22 09:51:27 +09:00
jade
a59249e1db fix: always generate legacy DTOs and converter regardless of routing type 2026-07-22 09:24:07 +09:00
jade
128fc3d022 feat: update ToolScaffolder to generate legacy DTOs in 'legacy' and converters in 'converter' package 2026-07-22 09:22:50 +09:00
jade
899053aef5 소스 경로 변경 2026-07-22 09:21:05 +09:00
jade
0a5bc52e6a 소스 경로 변경 2026-07-22 09:14:14 +09:00
jade
885963c81c 소스 경로 변경 2026-07-21 22:33:35 +09:00
jade
b8a8da3b3c 소스 경로 변경 2026-07-21 22:29:06 +09:00
jade
1a30c3a0cd 필요 없는 소스 삭제 2026-07-21 22:19:09 +09:00
jade
50b6b8b9b0 remove: Delete MciSampleTool and related DTOs 2026-07-21 22:18:10 +09:00
jade
140be4c7e9 fix: Scaffold dropdown missing modules on Render 2026-07-21 22:11:21 +09:00
jade
75b93d47c2 fix: Restore Korean character encoding in application-dev.properties 2026-07-21 21:57:34 +09:00
jade
f880f391d5 fix: Update fallback URLs from dap-tool to axhub-tool 2026-07-21 21:55:40 +09:00
jade
8a53bff74f Revert "refactor: Remove fallback HTTP logic from McpRouterController"
This reverts commit a46943ca23.
2026-07-21 21:53:36 +09:00
jade
a46943ca23 refactor: Remove fallback HTTP logic from McpRouterController 2026-07-21 21:49:32 +09:00
jade
11b8b08890 redis 연동 테스트 2026-07-21 21:30:28 +09:00
jade
2138ab7efa feat: Add MCI Scaffolding support and fix bean collision 2026-07-21 21:22:38 +09:00
jade
7dca243380 fix: add io.shinhanlife.dap.common.integration to component scan paths across all application modules 2026-07-21 17:09:46 +09:00
jade
c90654bd1b refactor: move AxhubMciComponent and GlowMockConfig to core module for common usage 2026-07-21 17:07:02 +09:00
jade
334af3e40e fix: register GlowMciComponent as Spring Bean via config to resolve IntelliJ autowiring error 2026-07-21 17:02:57 +09:00
jade
ea27ed621a fix: resolve IntelliJ autowiring and inspection warnings for AxhubMciComponent 2026-07-21 16:59:52 +09:00
jade
fbe7091975 refactor: rename LicoMciComponent to AxhubMciComponent 2026-07-21 16:57:10 +09:00
jade
7ccca1812f refactor: apply GlowMciComponent architecture as requested by Shinhan Life standard 2026-07-21 16:53:31 +09:00
jade
123fdb30a9 fix: correct Onnba3011ReqDto fields from new API spec and fix OnnbaMciToolService compile error 2026-07-21 16:42:43 +09:00
jade
0ed927f845 feat: update Onnba3011ReqDto fields from API spec 2026-07-21 16:38:54 +09:00
jade
60ac3bc4be refactor: extract Onnba3011ReqDto into separate file 2026-07-21 16:32:40 +09:00
jade
0afa489165 feat: add OnnbaMciToolService converted from legacy MCI code 2026-07-21 15:56:37 +09:00
jade
8cd07a255c fix: add sample category to pre-initialized servers to avoid 400 bad request 2026-07-21 15:13:31 +09:00
jade
cc98d173ad fix: set register=true for sample tools to enable auto-registration on Render 2026-07-21 15:04:25 +09:00
jade
c450f26d53 fix: correct gateway URL in application-dev.properties for all tools 2026-07-21 14:55:06 +09:00
jade
7660db3c38 fix: disable auto namespace prefix in PodScaffolder 2026-07-21 14:24:49 +09:00
jade
beed6e7001 docs: add sample category to scaffolder and readme 2026-07-21 14:22:22 +09:00
jade
8612dd6ea7 feat: rename sample tools 2026-07-21 14:19:08 +09:00
jade
db6ed5b09a feat: add get_exchange_rate and get_daily_quote tools and set categoryKey to sample 2026-07-21 14:17:36 +09:00
jade
db949be3f3 feat: generate and log new request-id for outbound HTTP calls in WeatherTool 2026-07-21 14:12:58 +09:00
jade
9edcdab450 관련 소스 수정 2026-07-21 14:05:39 +09:00
jade
82b1e6e267 feat: rename tool get_weather to weather 2026-07-21 14:04:48 +09:00
jade
e35b378d61 fix: wrap execute tool response to match expected client format 2026-07-21 14:00:51 +09:00
jade
f8d6ed8da3 feat: remove namespace prefix from all remaining tool modules 2026-07-21 13:52:37 +09:00
jade
437d061bef feat: remove namespace prefix from dap-tool-other 2026-07-21 13:50:54 +09:00
jade
9cce77a142 fix: add missing register=false to register_vacation tool 2026-07-21 13:45:03 +09:00
jade
115aafa220 fix: swap trace-id and request-id header mapping 2026-07-21 13:43:13 +09:00
jade
da7ed2e28b fix: remove duplicate register property in TemplateUtilityService 2026-07-21 13:32:40 +09:00
jade
48ac7b9f9f chore: disable redis heartbeat registration for all tools (register=false) 2026-07-21 13:26:02 +09:00
jade
580edd98a6 feat: add trace-id and request-id header logging to tool controller 2026-07-21 13:18:38 +09:00
jade
b9966bbbda feat: rename weather tool from get_current_weather to get_weather 2026-07-21 13:10:52 +09:00
jade
438e30f987 refactor: change internal gateway-to-tool routing from JSON-RPC to REST API 2026-07-21 12:50:28 +09:00
jade
fe9e00b5fb feat: add missing globalId to SlCommonHeaderDto 2026-07-21 10:48:13 +09:00
jade
b0f12b578d feat: add SL and OL standard header DTOs from spec 2026-07-21 10:44:17 +09:00
jade
39513a1470 feat: apply full spec to ShinhanCommonHeaderDto (42 fields) 2026-07-21 10:36:34 +09:00
jade
821b249d50 fix: resolve MCI sample tool execution errors and improve parameter definitions in UI 2026-07-21 10:29:30 +09:00
jade
e02a4e24e8 fix: Add dap-tool-core to dap-gateway Dockerfile 2026-07-21 09:06:34 +09:00
jade
6b8ca5581f fix: Remove COPY dap-common from Dockerfiles 2026-07-21 09:04:07 +09:00
jade
d6869f8191 refactor: Merge dap-common into dap-tool-core 2026-07-21 09:02:48 +09:00
jade
515ab409ec fix: Update BusinessToolController to use generateSchema 2026-07-20 17:15:49 +09:00
jade
b55f863de9 fix: Complete JsonSchemaGenerator with required properties for MCP compliance 2026-07-20 17:07:39 +09:00
jade
80ff392b5a feat: Add SampleMciToolService for MCI integration sample 2026-07-20 16:44:12 +09:00
jade
6dd1fe6cda refactor: apply Glow Framework package naming conventions (mcg, mcc)
- io.shinhanlife.dap.biz.mcp.gateway -> io.shinhanlife.dap.mcg
- io.shinhanlife.dap.biz.mcp.tool -> io.shinhanlife.dap.mcc
- io.shinhanlife.dap.biz.mcp.adapter -> io.shinhanlife.dap.common.adapter
- io.shinhanlife.dap.biz.mcp.sample -> io.shinhanlife.dap.mcc.sample
2026-07-20 11:39:36 +09:00
jade
2162587a46 fix: 딥 빌더 테스트 도구 실행 시 system 권한 도메인 허용 설정 추가 2026-07-20 10:28:11 +09:00
jade
736593c516 fix: Deep Agent Builder 무한 대기 현상 해결을 위해 JSON-RPC Notification 처리 후 SSE 강제 종료 2026-07-20 10:15:07 +09:00
jade
bd85ca9f88 fix: Deep Agent Builder JSON 파싱 에러 방지를 위해 커스텀 SSE 엔드포인트 이벤트 제거 2026-07-20 09:53:44 +09:00
jade
d6801b7ba0 fix: Deep Agent Builder 호환성을 위한 mcp-http Warm Pool 세션 버그 및 CORS 헤더 수정 2026-07-20 09:36:15 +09:00
jade
2e6b28e1c1 log 경로 수정 2026-07-20 09:01:44 +09:00
jade
0ebac4e375 McpFunction에 MCP 표준 힌트 4개 추가 2026-07-17 18:08:12 +09:00
jade
8359371122 필요 없는 부분 삭제 2026-07-17 17:38:13 +09:00
jade
7eef3a65c5 refactor: Fix all inline FQ names with proper imports and add Javadoc to 67 files 2026-07-17 15:20:26 +09:00
jade
467289bf9f skill 추가 2026-07-17 01:06:51 +09:00
jade
c05bfb0ab3 skill 추가 2026-07-17 01:05:27 +09:00
jade
ac15d38468 skill 추가 2026-07-17 01:01:24 +09:00
jade
87f367b2c1 git 변경 테스트 2026-07-17 00:50:06 +09:00
jade
5a5e67629d chore: Add explicit import for CustomWebMvcSseServerTransportProvider 2026-07-16 21:37:57 +09:00
jade
e1591761d2 refactor: Replace inline fully qualified package names with proper imports 2026-07-16 21:36:51 +09:00
jade
c982696631 git 변경 테스트 2026-07-16 21:25:40 +09:00
jade
2c0c0a3790 fix: Rename AXHUB-Gateway to DAP-Gateway in DynamicMcpServerManager 2026-07-16 21:11:11 +09:00
453 changed files with 12668 additions and 5149 deletions

View File

@@ -5,9 +5,10 @@
## 개발 가이드라인
* 패키지명은 `controller` 대신 `presentation`을 사용합니다.
* MapStruct 사용 시, 테스트 환경 에러를 방지하기 위해 생성자(`new ...Impl()`) 대신 `Mappers.getMapper(인터페이스명.class)` 방식으로 인스턴스를 가져옵니다.
* MapStruct 사용 시, Spring DI를 활용하여 의존성 주입(`private final Converter converter;`)을 받는 방식을 권장합니다. (단위 테스트 시에는 `@MockBean` 또는 직접 구현체를 주입하여 테스트)
## 명심해야 할 규칙 추가란
* `application.yml` 등 설정 파일 수정 시 한글이 깨지지 않도록 항상 UTF-8 인코딩을 유지하고, 깨진 문자열(`?\uFFFD` 등)이 발생하지 않도록 각별히 주의한다.
* 이모지는 무조건 넣지 않는다
* import 할것 무조건 한다
* Git commit과 push는 사용자의 명시적인 허락(지시) 없이는 절대 수행하지 않는다.
@@ -16,13 +17,13 @@
* @package io.shinhanlife.axhub.biz.mcp.tool.sms
* @className AxHubToolSmsApplication
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@@ -30,3 +31,9 @@
* 현재 개발 대상은 **대내MCI / EAI (JSON)** 연동으로 한정한다. (대외MCI FixedLength 연동은 범위에서 제외)
* 통신 노선, 데이터 변환 규격, 시스템별 연계 방식 등은 MCI/EIMS 상에서 관리되므로 코드 레벨에서 식별하거나 분기 처리하지 않는다. (단순 통합 JSON 요청만 수행)
* 처리계 UI ↔ 처리계 AP 구간: `HTTPS` / `SSV`를 사용하며, FW에서 x-api를 통해 SSV↔DTO 변환을 수행한다.
* 신한라이프 표준 로그 기준 (Logback 설정):
* **로그 생성 경로:** `/swlog/어플리케이션명(모듈명)/코드명/` (예: `/swlog/dap-gateway/A01/`)
* **로그 네이밍 규칙:** `${HOSTNAME}_코드명_yyyyMMdd.log` (예: `${HOSTNAME}_A01_20260722.log`)
* **기본 로그 구분 코드:** 시스템 운영기록 가동기록의 경우 `A01`을 기본으로 사용한다.
* **호스트명 동적 할당:** Logback 설정 시 `<property name="HOSTNAME" value="${HOSTNAME}" />` 를 선언하여 사용한다.

View File

@@ -1,33 +0,0 @@
# AX HUB 프로젝트 개발 규칙 (AGENTS.md)
이 파일은 AI Agent(Antigravity)가 이 프로젝트에서 작업할 때 항상 명심하고 지켜야 할 규칙을 정의하는 파일입니다.
작업 시 유의해야 할 사항이 생기면 언제든지 이 아래에 자유롭게 내용을 추가해 주세요!
## 💡 개발 가이드라인
* 패키지명은 `controller` 대신 `presentation`을 사용합니다.
* MapStruct 사용 시, 테스트 환경 에러를 방지하기 위해 생성자(`new ...Impl()`) 대신 `Mappers.getMapper(인터페이스명.class)` 방식으로 인스턴스를 가져옵니다.
## 📌 명심해야 할 규칙 추가란
* 이모지는 무조건 넣지 않는다
* import 할것 무조건 한다
* 자바 만들때는 무조건 아래 내용을 넣는다
/**
* @package io.shinhanlife.axhub.biz.mcp.tool.sms
* @className AxHubToolSmsApplication
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/

View File

@@ -0,0 +1,16 @@
---
name: chaos-tool-tester
description: 현재 연결된 MCP 도구들에 대해 엣지 케이스(Null, 타입 오류, SQL 인젝션 문자열 등)를 고의로 주입하여 안정성을 테스트합니다.
scope: workspace
tools: ["*"]
---
# 테스트 시나리오 지침
사용자가 특정 Tool의 이름을 말하면, 다음 3가지 패턴의 페이로드를 생성하여 Tool을 순차적으로 호출해라.
1. **타입 브레이커:** Integer가 들어가야 할 곳에 긴 String 넣기
2. **누락 테스트:** 필수(Required) 파라미터를 누락시키고 호출하기
3. **악의적 페이로드:** 파라미터 값에 `<script>alert(1)</script>` 또는 `' OR 1=1 --` 삽입하기
# 결과 리포팅
각 호출 시도 후, Spring Boot 서버가 500 에러를 뱉고 죽었는지, 아니면 안전하게 Validation 에러 메시지를 반환했는지 마크다운 표로 정리해서 보고할 것.

View File

@@ -0,0 +1,12 @@
---
name: gateway-routing-tracer
description: 게이트웨이 로그를 분석하여 특정 Tool Call이나 Request ID의 라우팅 경로와 소요 시간을 추적합니다.
scope: workspace
---
# 실행 스크립트
이 스킬은 같은 폴더에 위치한 `trace.sh` 스크립트를 실행하여 로그를 파싱합니다.
# 에이전트 지침
1. 사용자가 조회하고자 하는 `Request ID` 또는 `Tool 이름`을 감지하면, 백그라운드 터미널에서 `trace.sh <인자>`를 실행해라.
2. 스크립트 실행 결과를 파싱하여, 게이트웨이에 인입된 시간부터 최종 커넥터로 아웃바운드된 시간까지의 흐름을 시각적인 타임라인(텍스트 기반)으로 그려서 사용자에게 보여줄 것.

View File

@@ -0,0 +1,18 @@
#!/bin/bash
# Spring Boot 로그 파일 경로 (기본 설정: ./logs/spring-boot-logger.log)
LOG_FILE="./logs/spring-boot-logger.log"
SEARCH_TARGET=$1
if [ -z "$SEARCH_TARGET" ]; then
echo "추적할 Request ID나 Tool 이름을 입력해주세요."
exit 1
fi
if [ ! -f "$LOG_FILE" ]; then
echo "오류: 로그 파일($LOG_FILE)을 찾을 수 없습니다. 경로를 확인해주세요."
exit 1
fi
echo "[$SEARCH_TARGET] 라우팅 추적 시작..."
# Spring Boot 로그에서 대상 검색하여 주요 필드 출력
grep -i "$SEARCH_TARGET" "$LOG_FILE" | awk '{print $1, $2, $3, $5, $NF}'

View File

@@ -0,0 +1,16 @@
---
name: mcp-schema-autogen
description: Java 21 Record 클래스나 Spring Controller 코드를 분석하여 MCP 표준 JSON Schema(Tool 명세)를 자동 생성합니다.
scope: workspace
---
# 시스템 지침 (System Prompt)
너는 MCP(Model Context Protocol) Tool 스키마 전문 엔지니어다.
사용자가 Java 코드를 제공하면 다음 규칙에 따라 JSON Schema를 작성해라.
1. **타입 매핑:** Java 21 `Record`의 필드 타입을 완벽하게 JSON Schema 타입(string, integer, boolean, object, array 등)으로 매핑할 것.
2. **필수값 추출:** `@NotNull`, `@NotBlank` 또는 Java 21 Record의 Non-null 필드는 반드시 `required` 배열에 포함시킬 것.
3. **설명 추가:** JavaDoc 주석이 있거나 필드 위에 주석이 있다면 스키마의 `description` 필드로 적극 반영할 것.
# 출력 형식
결과물은 반드시 순수한 JSON 형태로만 출력하고, 불필요한 부연 설명은 생략한다.

View File

@@ -0,0 +1,249 @@
# MCP Server Best Practices
## Quick Reference
### Server Naming
- **Python**: `{service}_mcp` (e.g., `slack_mcp`)
- **Node/TypeScript**: `{service}-mcp-server` (e.g., `slack-mcp-server`)
### Tool Naming
- Use snake_case with service prefix
- Format: `{service}_{action}_{resource}`
- Example: `slack_send_message`, `github_create_issue`
### Response Formats
- Support both JSON and Markdown formats
- JSON for programmatic processing
- Markdown for human readability
### Pagination
- Always respect `limit` parameter
- Return `has_more`, `next_offset`, `total_count`
- Default to 20-50 items
### Transport
- **Streamable HTTP**: For remote servers, multi-client scenarios
- **stdio**: For local integrations, command-line tools
- Avoid SSE (deprecated in favor of streamable HTTP)
---
## Server Naming Conventions
Follow these standardized naming patterns:
**Python**: Use format `{service}_mcp` (lowercase with underscores)
- Examples: `slack_mcp`, `github_mcp`, `jira_mcp`
**Node/TypeScript**: Use format `{service}-mcp-server` (lowercase with hyphens)
- Examples: `slack-mcp-server`, `github-mcp-server`, `jira-mcp-server`
The name should be general, descriptive of the service being integrated, easy to infer from the task description, and without version numbers.
---
## Tool Naming and Design
### Tool Naming
1. **Use snake_case**: `search_users`, `create_project`, `get_channel_info`
2. **Include service prefix**: Anticipate that your MCP server may be used alongside other MCP servers
- Use `slack_send_message` instead of just `send_message`
- Use `github_create_issue` instead of just `create_issue`
3. **Be action-oriented**: Start with verbs (get, list, search, create, etc.)
4. **Be specific**: Avoid generic names that could conflict with other servers
### Tool Design
- Tool descriptions must narrowly and unambiguously describe functionality
- Descriptions must precisely match actual functionality
- Provide tool annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint)
- Keep tool operations focused and atomic
---
## Response Formats
All tools that return data should support multiple formats:
### JSON Format (`response_format="json"`)
- Machine-readable structured data
- Include all available fields and metadata
- Consistent field names and types
- Use for programmatic processing
### Markdown Format (`response_format="markdown"`, typically default)
- Human-readable formatted text
- Use headers, lists, and formatting for clarity
- Convert timestamps to human-readable format
- Show display names with IDs in parentheses
- Omit verbose metadata
---
## Pagination
For tools that list resources:
- **Always respect the `limit` parameter**
- **Implement pagination**: Use `offset` or cursor-based pagination
- **Return pagination metadata**: Include `has_more`, `next_offset`/`next_cursor`, `total_count`
- **Never load all results into memory**: Especially important for large datasets
- **Default to reasonable limits**: 20-50 items is typical
Example pagination response:
```json
{
"total": 150,
"count": 20,
"offset": 0,
"items": [...],
"has_more": true,
"next_offset": 20
}
```
---
## Transport Options
### Streamable HTTP
**Best for**: Remote servers, web services, multi-client scenarios
**Characteristics**:
- Bidirectional communication over HTTP
- Supports multiple simultaneous clients
- Can be deployed as a web service
- Enables server-to-client notifications
**Use when**:
- Serving multiple clients simultaneously
- Deploying as a cloud service
- Integration with web applications
### stdio
**Best for**: Local integrations, command-line tools
**Characteristics**:
- Standard input/output stream communication
- Simple setup, no network configuration needed
- Runs as a subprocess of the client
**Use when**:
- Building tools for local development environments
- Integrating with desktop applications
- Single-user, single-session scenarios
**Note**: stdio servers should NOT log to stdout (use stderr for logging)
### Transport Selection
| Criterion | stdio | Streamable HTTP |
|-----------|-------|-----------------|
| **Deployment** | Local | Remote |
| **Clients** | Single | Multiple |
| **Complexity** | Low | Medium |
| **Real-time** | No | Yes |
---
## Security Best Practices
### Authentication and Authorization
**OAuth 2.1**:
- Use secure OAuth 2.1 with certificates from recognized authorities
- Validate access tokens before processing requests
- Only accept tokens specifically intended for your server
**API Keys**:
- Store API keys in environment variables, never in code
- Validate keys on server startup
- Provide clear error messages when authentication fails
### Input Validation
- Sanitize file paths to prevent directory traversal
- Validate URLs and external identifiers
- Check parameter sizes and ranges
- Prevent command injection in system calls
- Use schema validation (Pydantic/Zod) for all inputs
### Error Handling
- Don't expose internal errors to clients
- Log security-relevant errors server-side
- Provide helpful but not revealing error messages
- Clean up resources after errors
### DNS Rebinding Protection
For streamable HTTP servers running locally:
- Enable DNS rebinding protection
- Validate the `Origin` header on all incoming connections
- Bind to `127.0.0.1` rather than `0.0.0.0`
---
## Tool Annotations
Provide annotations to help clients understand tool behavior:
| Annotation | Type | Default | Description |
|-----------|------|---------|-------------|
| `readOnlyHint` | boolean | false | Tool does not modify its environment |
| `destructiveHint` | boolean | true | Tool may perform destructive updates |
| `idempotentHint` | boolean | false | Repeated calls with same args have no additional effect |
| `openWorldHint` | boolean | true | Tool interacts with external entities |
**Important**: Annotations are hints, not security guarantees. Clients should not make security-critical decisions based solely on annotations.
---
## Error Handling
- Use standard JSON-RPC error codes
- Report tool errors within result objects (not protocol-level errors)
- Provide helpful, specific error messages with suggested next steps
- Don't expose internal implementation details
- Clean up resources properly on errors
Example error handling:
```typescript
try {
const result = performOperation();
return { content: [{ type: "text", text: result }] };
} catch (error) {
return {
isError: true,
content: [{
type: "text",
text: `Error: ${error.message}. Try using filter='active_only' to reduce results.`
}]
};
}
```
---
## Testing Requirements
Comprehensive testing should cover:
- **Functional testing**: Verify correct execution with valid/invalid inputs
- **Integration testing**: Test interaction with external systems
- **Security testing**: Validate auth, input sanitization, rate limiting
- **Performance testing**: Check behavior under load, timeouts
- **Error handling**: Ensure proper error reporting and cleanup
---
## Documentation Requirements
- Provide clear documentation of all tools and capabilities
- Include working examples (at least 3 per major feature)
- Document security considerations
- Specify required permissions and access levels
- Document rate limits and performance characteristics

View File

@@ -0,0 +1,602 @@
# MCP Server Evaluation Guide
## Overview
This document provides guidance on creating comprehensive evaluations for MCP servers. Evaluations test whether LLMs can effectively use your MCP server to answer realistic, complex questions using only the tools provided.
---
## Quick Reference
### Evaluation Requirements
- Create 10 human-readable questions
- Questions must be READ-ONLY, INDEPENDENT, NON-DESTRUCTIVE
- Each question requires multiple tool calls (potentially dozens)
- Answers must be single, verifiable values
- Answers must be STABLE (won't change over time)
### Output Format
```xml
<evaluation>
<qa_pair>
<question>Your question here</question>
<answer>Single verifiable answer</answer>
</qa_pair>
</evaluation>
```
---
## Purpose of Evaluations
The measure of quality of an MCP server is NOT how well or comprehensively the server implements tools, but how well these implementations (input/output schemas, docstrings/descriptions, functionality) enable LLMs with no other context and access ONLY to the MCP servers to answer realistic and difficult questions.
## Evaluation Overview
Create 10 human-readable questions requiring ONLY READ-ONLY, INDEPENDENT, NON-DESTRUCTIVE, and IDEMPOTENT operations to answer. Each question should be:
- Realistic
- Clear and concise
- Unambiguous
- Complex, requiring potentially dozens of tool calls or steps
- Answerable with a single, verifiable value that you identify in advance
## Question Guidelines
### Core Requirements
1. **Questions MUST be independent**
- Each question should NOT depend on the answer to any other question
- Should not assume prior write operations from processing another question
2. **Questions MUST require ONLY NON-DESTRUCTIVE AND IDEMPOTENT tool use**
- Should not instruct or require modifying state to arrive at the correct answer
3. **Questions must be REALISTIC, CLEAR, CONCISE, and COMPLEX**
- Must require another LLM to use multiple (potentially dozens of) tools or steps to answer
### Complexity and Depth
4. **Questions must require deep exploration**
- Consider multi-hop questions requiring multiple sub-questions and sequential tool calls
- Each step should benefit from information found in previous questions
5. **Questions may require extensive paging**
- May need paging through multiple pages of results
- May require querying old data (1-2 years out-of-date) to find niche information
- The questions must be DIFFICULT
6. **Questions must require deep understanding**
- Rather than surface-level knowledge
- May pose complex ideas as True/False questions requiring evidence
- May use multiple-choice format where LLM must search different hypotheses
7. **Questions must not be solvable with straightforward keyword search**
- Do not include specific keywords from the target content
- Use synonyms, related concepts, or paraphrases
- Require multiple searches, analyzing multiple related items, extracting context, then deriving the answer
### Tool Testing
8. **Questions should stress-test tool return values**
- May elicit tools returning large JSON objects or lists, overwhelming the LLM
- Should require understanding multiple modalities of data:
- IDs and names
- Timestamps and datetimes (months, days, years, seconds)
- File IDs, names, extensions, and mimetypes
- URLs, GIDs, etc.
- Should probe the tool's ability to return all useful forms of data
9. **Questions should MOSTLY reflect real human use cases**
- The kinds of information retrieval tasks that HUMANS assisted by an LLM would care about
10. **Questions may require dozens of tool calls**
- This challenges LLMs with limited context
- Encourages MCP server tools to reduce information returned
11. **Include ambiguous questions**
- May be ambiguous OR require difficult decisions on which tools to call
- Force the LLM to potentially make mistakes or misinterpret
- Ensure that despite AMBIGUITY, there is STILL A SINGLE VERIFIABLE ANSWER
### Stability
12. **Questions must be designed so the answer DOES NOT CHANGE**
- Do not ask questions that rely on "current state" which is dynamic
- For example, do not count:
- Number of reactions to a post
- Number of replies to a thread
- Number of members in a channel
13. **DO NOT let the MCP server RESTRICT the kinds of questions you create**
- Create challenging and complex questions
- Some may not be solvable with the available MCP server tools
- Questions may require specific output formats (datetime vs. epoch time, JSON vs. MARKDOWN)
- Questions may require dozens of tool calls to complete
## Answer Guidelines
### Verification
1. **Answers must be VERIFIABLE via direct string comparison**
- If the answer can be re-written in many formats, clearly specify the output format in the QUESTION
- Examples: "Use YYYY/MM/DD.", "Respond True or False.", "Answer A, B, C, or D and nothing else."
- Answer should be a single VERIFIABLE value such as:
- User ID, user name, display name, first name, last name
- Channel ID, channel name
- Message ID, string
- URL, title
- Numerical quantity
- Timestamp, datetime
- Boolean (for True/False questions)
- Email address, phone number
- File ID, file name, file extension
- Multiple choice answer
- Answers must not require special formatting or complex, structured output
- Answer will be verified using DIRECT STRING COMPARISON
### Readability
2. **Answers should generally prefer HUMAN-READABLE formats**
- Examples: names, first name, last name, datetime, file name, message string, URL, yes/no, true/false, a/b/c/d
- Rather than opaque IDs (though IDs are acceptable)
- The VAST MAJORITY of answers should be human-readable
### Stability
3. **Answers must be STABLE/STATIONARY**
- Look at old content (e.g., conversations that have ended, projects that have launched, questions answered)
- Create QUESTIONS based on "closed" concepts that will always return the same answer
- Questions may ask to consider a fixed time window to insulate from non-stationary answers
- Rely on context UNLIKELY to change
- Example: if finding a paper name, be SPECIFIC enough so answer is not confused with papers published later
4. **Answers must be CLEAR and UNAMBIGUOUS**
- Questions must be designed so there is a single, clear answer
- Answer can be derived from using the MCP server tools
### Diversity
5. **Answers must be DIVERSE**
- Answer should be a single VERIFIABLE value in diverse modalities and formats
- User concept: user ID, user name, display name, first name, last name, email address, phone number
- Channel concept: channel ID, channel name, channel topic
- Message concept: message ID, message string, timestamp, month, day, year
6. **Answers must NOT be complex structures**
- Not a list of values
- Not a complex object
- Not a list of IDs or strings
- Not natural language text
- UNLESS the answer can be straightforwardly verified using DIRECT STRING COMPARISON
- And can be realistically reproduced
- It should be unlikely that an LLM would return the same list in any other order or format
## Evaluation Process
### Step 1: Documentation Inspection
Read the documentation of the target API to understand:
- Available endpoints and functionality
- If ambiguity exists, fetch additional information from the web
- Parallelize this step AS MUCH AS POSSIBLE
- Ensure each subagent is ONLY examining documentation from the file system or on the web
### Step 2: Tool Inspection
List the tools available in the MCP server:
- Inspect the MCP server directly
- Understand input/output schemas, docstrings, and descriptions
- WITHOUT calling the tools themselves at this stage
### Step 3: Developing Understanding
Repeat steps 1 & 2 until you have a good understanding:
- Iterate multiple times
- Think about the kinds of tasks you want to create
- Refine your understanding
- At NO stage should you READ the code of the MCP server implementation itself
- Use your intuition and understanding to create reasonable, realistic, but VERY challenging tasks
### Step 4: Read-Only Content Inspection
After understanding the API and tools, USE the MCP server tools:
- Inspect content using READ-ONLY and NON-DESTRUCTIVE operations ONLY
- Goal: identify specific content (e.g., users, channels, messages, projects, tasks) for creating realistic questions
- Should NOT call any tools that modify state
- Will NOT read the code of the MCP server implementation itself
- Parallelize this step with individual sub-agents pursuing independent explorations
- Ensure each subagent is only performing READ-ONLY, NON-DESTRUCTIVE, and IDEMPOTENT operations
- BE CAREFUL: SOME TOOLS may return LOTS OF DATA which would cause you to run out of CONTEXT
- Make INCREMENTAL, SMALL, AND TARGETED tool calls for exploration
- In all tool call requests, use the `limit` parameter to limit results (<10)
- Use pagination
### Step 5: Task Generation
After inspecting the content, create 10 human-readable questions:
- An LLM should be able to answer these with the MCP server
- Follow all question and answer guidelines above
## Output Format
Each QA pair consists of a question and an answer. The output should be an XML file with this structure:
```xml
<evaluation>
<qa_pair>
<question>Find the project created in Q2 2024 with the highest number of completed tasks. What is the project name?</question>
<answer>Website Redesign</answer>
</qa_pair>
<qa_pair>
<question>Search for issues labeled as "bug" that were closed in March 2024. Which user closed the most issues? Provide their username.</question>
<answer>sarah_dev</answer>
</qa_pair>
<qa_pair>
<question>Look for pull requests that modified files in the /api directory and were merged between January 1 and January 31, 2024. How many different contributors worked on these PRs?</question>
<answer>7</answer>
</qa_pair>
<qa_pair>
<question>Find the repository with the most stars that was created before 2023. What is the repository name?</question>
<answer>data-pipeline</answer>
</qa_pair>
</evaluation>
```
## Evaluation Examples
### Good Questions
**Example 1: Multi-hop question requiring deep exploration (GitHub MCP)**
```xml
<qa_pair>
<question>Find the repository that was archived in Q3 2023 and had previously been the most forked project in the organization. What was the primary programming language used in that repository?</question>
<answer>Python</answer>
</qa_pair>
```
This question is good because:
- Requires multiple searches to find archived repositories
- Needs to identify which had the most forks before archival
- Requires examining repository details for the language
- Answer is a simple, verifiable value
- Based on historical (closed) data that won't change
**Example 2: Requires understanding context without keyword matching (Project Management MCP)**
```xml
<qa_pair>
<question>Locate the initiative focused on improving customer onboarding that was completed in late 2023. The project lead created a retrospective document after completion. What was the lead's role title at that time?</question>
<answer>Product Manager</answer>
</qa_pair>
```
This question is good because:
- Doesn't use specific project name ("initiative focused on improving customer onboarding")
- Requires finding completed projects from specific timeframe
- Needs to identify the project lead and their role
- Requires understanding context from retrospective documents
- Answer is human-readable and stable
- Based on completed work (won't change)
**Example 3: Complex aggregation requiring multiple steps (Issue Tracker MCP)**
```xml
<qa_pair>
<question>Among all bugs reported in January 2024 that were marked as critical priority, which assignee resolved the highest percentage of their assigned bugs within 48 hours? Provide the assignee's username.</question>
<answer>alex_eng</answer>
</qa_pair>
```
This question is good because:
- Requires filtering bugs by date, priority, and status
- Needs to group by assignee and calculate resolution rates
- Requires understanding timestamps to determine 48-hour windows
- Tests pagination (potentially many bugs to process)
- Answer is a single username
- Based on historical data from specific time period
**Example 4: Requires synthesis across multiple data types (CRM MCP)**
```xml
<qa_pair>
<question>Find the account that upgraded from the Starter to Enterprise plan in Q4 2023 and had the highest annual contract value. What industry does this account operate in?</question>
<answer>Healthcare</answer>
</qa_pair>
```
This question is good because:
- Requires understanding subscription tier changes
- Needs to identify upgrade events in specific timeframe
- Requires comparing contract values
- Must access account industry information
- Answer is simple and verifiable
- Based on completed historical transactions
### Poor Questions
**Example 1: Answer changes over time**
```xml
<qa_pair>
<question>How many open issues are currently assigned to the engineering team?</question>
<answer>47</answer>
</qa_pair>
```
This question is poor because:
- The answer will change as issues are created, closed, or reassigned
- Not based on stable/stationary data
- Relies on "current state" which is dynamic
**Example 2: Too easy with keyword search**
```xml
<qa_pair>
<question>Find the pull request with title "Add authentication feature" and tell me who created it.</question>
<answer>developer123</answer>
</qa_pair>
```
This question is poor because:
- Can be solved with a straightforward keyword search for exact title
- Doesn't require deep exploration or understanding
- No synthesis or analysis needed
**Example 3: Ambiguous answer format**
```xml
<qa_pair>
<question>List all the repositories that have Python as their primary language.</question>
<answer>repo1, repo2, repo3, data-pipeline, ml-tools</answer>
</qa_pair>
```
This question is poor because:
- Answer is a list that could be returned in any order
- Difficult to verify with direct string comparison
- LLM might format differently (JSON array, comma-separated, newline-separated)
- Better to ask for a specific aggregate (count) or superlative (most stars)
## Verification Process
After creating evaluations:
1. **Examine the XML file** to understand the schema
2. **Load each task instruction** and in parallel using the MCP server and tools, identify the correct answer by attempting to solve the task YOURSELF
3. **Flag any operations** that require WRITE or DESTRUCTIVE operations
4. **Accumulate all CORRECT answers** and replace any incorrect answers in the document
5. **Remove any `<qa_pair>`** that require WRITE or DESTRUCTIVE operations
Remember to parallelize solving tasks to avoid running out of context, then accumulate all answers and make changes to the file at the end.
## Tips for Creating Quality Evaluations
1. **Think Hard and Plan Ahead** before generating tasks
2. **Parallelize Where Opportunity Arises** to speed up the process and manage context
3. **Focus on Realistic Use Cases** that humans would actually want to accomplish
4. **Create Challenging Questions** that test the limits of the MCP server's capabilities
5. **Ensure Stability** by using historical data and closed concepts
6. **Verify Answers** by solving the questions yourself using the MCP server tools
7. **Iterate and Refine** based on what you learn during the process
---
# Running Evaluations
After creating your evaluation file, you can use the provided evaluation harness to test your MCP server.
## Setup
1. **Install Dependencies**
```bash
pip install -r scripts/requirements.txt
```
Or install manually:
```bash
pip install anthropic mcp
```
2. **Set API Key**
```bash
export ANTHROPIC_API_KEY=your_api_key_here
```
## Evaluation File Format
Evaluation files use XML format with `<qa_pair>` elements:
```xml
<evaluation>
<qa_pair>
<question>Find the project created in Q2 2024 with the highest number of completed tasks. What is the project name?</question>
<answer>Website Redesign</answer>
</qa_pair>
<qa_pair>
<question>Search for issues labeled as "bug" that were closed in March 2024. Which user closed the most issues? Provide their username.</question>
<answer>sarah_dev</answer>
</qa_pair>
</evaluation>
```
## Running Evaluations
The evaluation script (`scripts/evaluation.py`) supports three transport types:
**Important:**
- **stdio transport**: The evaluation script automatically launches and manages the MCP server process for you. Do not run the server manually.
- **sse/http transports**: You must start the MCP server separately before running the evaluation. The script connects to the already-running server at the specified URL.
### 1. Local STDIO Server
For locally-run MCP servers (script launches the server automatically):
```bash
python scripts/evaluation.py \
-t stdio \
-c python \
-a my_mcp_server.py \
evaluation.xml
```
With environment variables:
```bash
python scripts/evaluation.py \
-t stdio \
-c python \
-a my_mcp_server.py \
-e API_KEY=abc123 \
-e DEBUG=true \
evaluation.xml
```
### 2. Server-Sent Events (SSE)
For SSE-based MCP servers (you must start the server first):
```bash
python scripts/evaluation.py \
-t sse \
-u https://example.com/mcp \
-H "Authorization: Bearer token123" \
-H "X-Custom-Header: value" \
evaluation.xml
```
### 3. HTTP (Streamable HTTP)
For HTTP-based MCP servers (you must start the server first):
```bash
python scripts/evaluation.py \
-t http \
-u https://example.com/mcp \
-H "Authorization: Bearer token123" \
evaluation.xml
```
## Command-Line Options
```
usage: evaluation.py [-h] [-t {stdio,sse,http}] [-m MODEL] [-c COMMAND]
[-a ARGS [ARGS ...]] [-e ENV [ENV ...]] [-u URL]
[-H HEADERS [HEADERS ...]] [-o OUTPUT]
eval_file
positional arguments:
eval_file Path to evaluation XML file
optional arguments:
-h, --help Show help message
-t, --transport Transport type: stdio, sse, or http (default: stdio)
-m, --model Claude model to use (default: claude-3-7-sonnet-20250219)
-o, --output Output file for report (default: print to stdout)
stdio options:
-c, --command Command to run MCP server (e.g., python, node)
-a, --args Arguments for the command (e.g., server.py)
-e, --env Environment variables in KEY=VALUE format
sse/http options:
-u, --url MCP server URL
-H, --header HTTP headers in 'Key: Value' format
```
## Output
The evaluation script generates a detailed report including:
- **Summary Statistics**:
- Accuracy (correct/total)
- Average task duration
- Average tool calls per task
- Total tool calls
- **Per-Task Results**:
- Prompt and expected response
- Actual response from the agent
- Whether the answer was correct (✅/❌)
- Duration and tool call details
- Agent's summary of its approach
- Agent's feedback on the tools
### Save Report to File
```bash
python scripts/evaluation.py \
-t stdio \
-c python \
-a my_server.py \
-o evaluation_report.md \
evaluation.xml
```
## Complete Example Workflow
Here's a complete example of creating and running an evaluation:
1. **Create your evaluation file** (`my_evaluation.xml`):
```xml
<evaluation>
<qa_pair>
<question>Find the user who created the most issues in January 2024. What is their username?</question>
<answer>alice_developer</answer>
</qa_pair>
<qa_pair>
<question>Among all pull requests merged in Q1 2024, which repository had the highest number? Provide the repository name.</question>
<answer>backend-api</answer>
</qa_pair>
<qa_pair>
<question>Find the project that was completed in December 2023 and had the longest duration from start to finish. How many days did it take?</question>
<answer>127</answer>
</qa_pair>
</evaluation>
```
2. **Install dependencies**:
```bash
pip install -r scripts/requirements.txt
export ANTHROPIC_API_KEY=your_api_key
```
3. **Run evaluation**:
```bash
python scripts/evaluation.py \
-t stdio \
-c python \
-a github_mcp_server.py \
-e GITHUB_TOKEN=ghp_xxx \
-o github_eval_report.md \
my_evaluation.xml
```
4. **Review the report** in `github_eval_report.md` to:
- See which questions passed/failed
- Read the agent's feedback on your tools
- Identify areas for improvement
- Iterate on your MCP server design
## Troubleshooting
### Connection Errors
If you get connection errors:
- **STDIO**: Verify the command and arguments are correct
- **SSE/HTTP**: Check the URL is accessible and headers are correct
- Ensure any required API keys are set in environment variables or headers
### Low Accuracy
If many evaluations fail:
- Review the agent's feedback for each task
- Check if tool descriptions are clear and comprehensive
- Verify input parameters are well-documented
- Consider whether tools return too much or too little data
- Ensure error messages are actionable
### Timeout Issues
If tasks are timing out:
- Use a more capable model (e.g., `claude-3-7-sonnet-20250219`)
- Check if tools are returning too much data
- Verify pagination is working correctly
- Consider simplifying complex questions

10
.dockerignore Normal file
View File

@@ -0,0 +1,10 @@
.git
.gradle
.idea
/build/
target/
*/target/
bin/
*/bin/
out/
*/out/

View File

@@ -0,0 +1,67 @@
name: Deploy to OCIWP
on:
push:
branches:
- main
- master
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Prepare Environment (Install Node.js & Git & Java 21)
run: |
if command -v apt-get &> /dev/null; then
apt-get update
apt-get install -y nodejs git openjdk-21-jdk rsync
elif command -v apk &> /dev/null; then
apk add --no-cache nodejs git openjdk21 rsync
fi
- name: Checkout Code
uses: actions/checkout@v3
- name: Sync Code to Host Volume
run: |
echo "Copying latest code to /app (Host Volume) and removing stale files..."
if command -v rsync &> /dev/null; then
rsync -a --delete --exclude='.git' . /app/
else
rm -rf /app/dap-* /app/src /app/build.gradle /app/settings.gradle
cp -a . /app/
fi
- name: Deploy Task on Host
run: |
echo "Starting Standard CI/CD Deploy pipeline..."
# 1. runner에 설치된 java로 직접 빌드! (격리된 환경 문제 해결)
cd /app
chmod +x gradlew
./gradlew bootJar -x test
# 2. 빌드 결과 plain jar 아카이브 정리
find /app -name "*-plain.jar" -delete
# 3. 좀비 컨테이너 청소 (현재 우리가 쓸 포트를 점유 중인 옛날 컨테이너들만 정확히 색출해서 사살)
echo "Finding and killing zombie containers holding our ports..."
for port in 6379 8080 8089 8281 8282 8283 8284 8285 8288; do
cid=$(docker ps -q --filter "publish=$port")
if [ ! -z "$cid" ]; then
echo "Force killing container $cid using port $port"
docker rm -f $cid
fi
done
# 4. 마운트된 /app 디렉토리로 이동하여 호스트의 도커 컴포즈 제어!
cd /app
docker system prune -f
ACTIVE_PROFILE=dev docker compose up -d --build --remove-orphans gateway redis mci-mock dozzle was-sms was-oth
# 5. 배포 후 대롱대롱 매달려 있는 가비지 이미지 자동 소거 청소!
docker image prune -f
echo "CI/CD Deploy Success!"

62
.gitignore vendored
View File

@@ -56,11 +56,69 @@ build/
# ===== VS Code =====
.vscode/
# ===== 鍮뚮뱶 寃곌낵臾?=====
target/
!**/src/main/**/target/
!**/src/test/**/target/
*.class
*.jar
!gradle/wrapper/gradle-wrapper.jar
*.war
*.ear
*.nar
# ===== 濡쒓렇 =====
*.log
logs/
spring-shell.log
# ===== Maven =====
.mvn/wrapper/maven-wrapper.jar
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
# ===== IntelliJ IDEA =====
.idea/
*.iws
*.iml
*.ipr
out/
# ===== Eclipse / STS =====
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
# ===== NetBeans =====
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
.gradle/
!**/src/main/**/build/
!**/src/test/**/build/
# ===== VS Code =====
.vscode/
# ===== OS =====
.DS_Store
Thumbs.db
ehthumbs.db
# ===== ?섍꼍?ㅼ젙 (誘쇨컧?뺣낫 遺꾨━ ?? =====
# application-local.properties
# application-secret.properties
# application-local.yml
# application-secret.yml

View File

@@ -1,30 +1,31 @@
# 1. 鍮뚮뱶 ?섍꼍 (JDK 21)
# 1. ??<3F><><EFBFBD>???<3F>꼍 (JDK 21)
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
# Gradle ?띁?€ ?뚯뒪 蹂듭
# Gradle ??<3F>띁?? ???<3F><> 蹂듭<EFBFBD>?
COPY gradlew .
COPY gradle gradle
COPY build.gradle settings.gradle ./
COPY src src
# 沅뚰븳 遺€??諛?鍮뚮뱶 (?뚯뒪???쇅)
# 沅뚰<EFBFBD>??<3F>€??<3F>???<3F><><EFBFBD>?(???<3F><>????<3F>쇅)
RUN chmod +x gradlew
RUN ./gradlew clean build -x test
# 2. ?뻾 ?꼍 (JRE 21)
# 2. ??<3F>뻾 ??<3F>꼍 (JRE 21)
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
# ?€?꾩〈 ?ㅼ젙 (?뎅 ?컙)
# ???꾩<EFBFBD>???<3F>젙 (??<3F>뎅 ??<3F>컙)
RUN apk add --no-cache tzdata
ENV TZ=Asia/Seoul
# 鍮뚮뱶??JAR ?뚯씪 蹂듭
# ??<3F><><EFBFBD>??JAR ???<3F><> 蹂듭<EFBFBD>?
COPY --from=builder /app/build/libs/*.jar app.jar
# 湲곕낯 ?ы듃 ?몄텧
# 湲곕???????몄텧
EXPOSE 8081
# 而⑦뀒?대꼫 ?ㅽ뻾 ??JAR ?
# ?<3F>⑦???<3F><>???<3F>뻾 ??JAR ??<3F>
ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@@ -1,3 +1,17 @@
/**
* @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;

692
README.md
View File

@@ -1,164 +1,606 @@
# DAP Backend
# AX HUB MCP Tool
Spring Boot 기반 DAP 관리자 백엔드 API 서버 및 MCP(Model Context Protocol) Gateway / Tool 분산 서버 프로젝트입니다.
신한라이프 업무 시스템과 AI Agent를 연결하는 MCP(Model Context Protocol) Gateway Tool 서버 프로젝트입니다.
---
Agent는 Gateway에서 Tool 목록과 입력 명세를 받고, Gateway는 권한과 정책을 확인한 뒤 Tool 서버로 요청을 전달합니다. 업무 Tool은 `DTO → UseCase → Converter → MCI/EAI Client` 구조로 레거시 시스템을 호출합니다.
## 아키텍처 개요 (Architecture Overview)
## 전체 흐름
DAP Backend는 2개의 주요 애플리케이션으로 분리 운영됩니다:
```text
AI Agent / MCP Client
MCP Gateway (dap-gateway)
├─ Tool 목록·스키마 제공
├─ Tool 권한·승인·가드레일 확인
├─ Redis Registry 및 실행 추적
└─ 대상 Tool 서버로 라우팅
Tool Server (dap-tool-sms / dap-tool-oth)
└─ BusinessToolController
UseCase → Converter → MCI/EAI Client → 레거시 시스템
```
1. **MCP Gateway (`DapGatewayApplication`)**: 외부 LLM(Claude, GPT 등) 서버의 MCP 통신을 받아, 내부 Tool 서버들로 분배(라우팅)하는 허브 서버이자 관리자 웹(Scaffolder)을 제공하는 통합 서버 (포트: 8081)
2. **MCP Tool (`DapTool*Application`)**: 실제 레거시 시스템(MCI, EAI 등)과 통신하여 비즈니스 로직(결제, 휴가신청 등)을 수행하는 어댑터 서버 (포트: 8082~8085 등 분산 구성 가능)
## 현재 구조와 목표 구조
---
현재는 Gateway와 두 개의 Tool 애플리케이션으로 구성됩니다.
## 환경 (Environment)
```text
현재: Gateway + SMS Tool Pod + OTH Tool Pod
목표: Gateway + 고객 Pod + 영업 Pod + 지급/납입 Pod + 알림 Pod + 인사 Pod + 공통 Pod
```
| 항목 | 버전 |
|------|------|
`dap-tool-oth`에는 여러 업무 카테고리가 함께 있습니다. AA 협의 후에는 부서·업무 소유권 단위로 Tool 서버, 이미지, Pod, 배포 파이프라인을 분리합니다. 이 목표 구조는 향후 전환 방향이며 현재 구현 완료 상태가 아닙니다.
## Gradle 멀티모듈
| 모듈 | 역할 | 실행 포트 |
|---|---|---:|
| `dap-gateway` | MCP 진입점, Tool Registry, 라우팅, 권한·가드레일, Chat API | 8081 |
| `dap-tool-core` | 공통 어노테이션, Controller, JSON Schema, MCI/EAI 지원, 보안·로깅 공통 기능 | 라이브러리 |
| `dap-tool-sms` | SMS/알림 Tool 서버 | 8082 |
| `dap-tool-oth` | 공통·업무·샘플·MCI Tool 서버 | 8084 |
기술 기준은 Java 21, Spring Boot 4, Gradle, Spring AI MCP Server, Redis, MapStruct, MyBatis, Resilience4j입니다.
## 환경 (Environment)
| 항목 | 버전 / 기준 |
|---|---|
| Java | 21 |
| Spring Boot | 4.0.5 |
| Build Tool | Gradle |
| 주요 기술 스택 | MyBatis, Lombok, MapStruct, P6Spy |
| 데이터베이스 | H2 (in-memory, 로컬 개발용) |
| 세션/캐시 저장소 | Redis |
| **장애 격리 / 제어** | **Resilience4j (RateLimiter, CircuitBreaker, Retry)** |
| **메시지 큐** | **Kafka (트래픽 폭주 시 대기열 전환용)** |
| Gradle Wrapper | 8.14.3 |
| Spring AI BOM | 2.0.0 |
| Spring AI MCP Server | `spring-ai-starter-mcp-server-webmvc` |
| Redis Client | Lettuce 6.6.0.RELEASE |
| Resilience | Resilience4j 2.2.0 |
| MyBatis Spring Boot Starter | 3.0.3 |
| MapStruct | 1.5.5.Final |
| Lombok | 1.18.32 |
| JSON Schema Validator | networknt 1.4.0 |
| OpenAPI UI | springdoc 2.5.0 |
| 컨테이너 실행 | Docker Compose |
프로젝트는 JDK 21을 기준으로 컴파일됩니다. IntelliJ에서는 Project SDK, Gradle JVM, Run Configuration JRE를 모두 JDK 21로 맞춰야 합니다.
## 5분 빠른 시작
Docker와 JDK 21이 준비된 로컬 개발 환경 기준입니다.
```powershell
# 1. Redis와 MCI Mock만 먼저 실행
$env:ACTIVE_PROFILE = 'local'
docker compose up -d redis mci-mock
# 2. Gateway 실행 (새 PowerShell)
$env:SPRING_PROFILES_ACTIVE = 'local'
$env:OPENROUTER_API_KEY = '<개발용 비밀 저장소의 키>'
.\gradlew.bat :dap-gateway:bootRun
# 3. OTH Tool 실행 (또 다른 PowerShell)
$env:SPRING_PROFILES_ACTIVE = 'local'
$env:AXHUB_GATEWAY_URL = 'http://localhost:8081'
$env:AXHUB_TOOL_URL = 'http://localhost:8084'
.\gradlew.bat :dap-tool-oth:bootRun
```
Tool 서버가 기동된 뒤 아래 URL로 등록된 Tool 목록을 확인합니다.
```text
http://localhost:8081/mcp/api/v1/tools/list
```
SMS Tool도 함께 확인하려면 별도 PowerShell에서 아래 명령을 실행합니다.
```powershell
$env:SPRING_PROFILES_ACTIVE = 'local'
$env:AXHUB_GATEWAY_URL = 'http://localhost:8081'
$env:AXHUB_TOOL_URL = 'http://localhost:8082'
.\gradlew.bat :dap-tool-sms:bootRun
```
전체 컨테이너 환경이 필요하면 개별 실행 대신 다음 한 줄을 사용합니다.
```powershell
$env:ACTIVE_PROFILE = 'local'
docker compose up -d --build
```
## Tool 등록과 실행
### 등록과 목록 제공
1. Tool 서버 기동 시 `ToolRegistryHeartbeatSender``@McpTool`, `@McpFunction`을 스캔합니다.
2. Tool 이름, 설명, 입력 JSON Schema, `categoryKey`, 연동 방식, 실행 URL을 메타데이터로 생성합니다.
3. Gateway Redis Registry에 등록·Heartbeat 정보를 전송합니다.
4. Agent와 관리 화면은 Gateway에서 Tool 목록과 명세를 조회합니다.
### 실행
1. Agent가 Gateway에 Tool 이름과 입력값을 보냅니다.
2. Gateway가 Tool 존재 여부, 허용 Tool, 쓰기 승인, 가드레일을 확인합니다.
3. Gateway가 Tool 서버의 `/mcp/{toolName}`으로 요청을 전달합니다.
4. `BusinessToolController`가 Tool 메서드를 찾아 DTO로 변환하고 JSON Schema를 검증합니다.
5. UseCase가 업무 흐름을 수행합니다.
6. Converter가 업무 DTO를 인터페이스 ID 기반 MCI 요청 DTO로 변환합니다.
7. MCI/EAI Client가 레거시를 호출하고 결과를 Tool 응답으로 반환합니다.
## 주요 URL
로컬에서 Gateway를 직접 실행할 때의 기준입니다. Docker Compose를 사용하면 Gateway 호스트 포트는 `8281`입니다.
| 용도 | 메서드 | URL |
|---|---|---|
| Tool 목록 | `GET` | `http://localhost:8081/mcp/api/v1/tools/list` |
| Tool 실행 | `POST` | `http://localhost:8081/mcp/api/v1/tools/call` |
| Tool Markdown 문서 | `GET` | `http://localhost:8081/mcp/api/v1/tools/docs/markdown` |
| 카테고리별 SSE MCP 연결 | `GET` | `http://localhost:8081/mcp/sse/{categoryKey}` |
| 카테고리별 MCP 호출 채널 | `POST` | `http://localhost:8081/mcp/custom/{categoryKey}` |
| Chat 스트리밍 API | `POST` | `http://localhost:8081/api/chat` |
| Scaffold API | `POST` | `http://localhost:8081/api/v1/scaffold/pod` 또는 `/tool` |
`/mcp/sse/{categoryKey}`는 SSE 연결을 여는 전송 경로이고, `/mcp/custom/{categoryKey}`는 같은 카테고리의 MCP 요청을 처리하는 호출 경로입니다. 두 URL은 역할이 다릅니다.
## Tool 개발 표준
| 구성 요소 | 책임 |
|---|---|
| `XxxRequest`, `XxxResponse` | Agent/Tool 관점의 입력·응답 DTO |
| `XxxUseCase` | Tool 계약과 MCP 메타데이터 선언 |
| `XxxUseCaseImpl` | 업무 흐름 조합과 Client 호출 |
| `XxxConverter` | 업무 DTO와 레거시 인터페이스 DTO 사이 변환 |
| `MciXxxClient` | Glow/MCI 또는 EAI 통신 호출 |
| `INTERFACE_ID_I`, `INTERFACE_ID_O` | 인터페이스 ID 기준 MCI 요청·응답 DTO |
### Tool 선언
Tool 그룹에는 `@McpTool`, Agent가 호출하는 함수에는 `@McpFunction`을 사용합니다.
```java
@McpTool(routingType = "MCI", categoryKey = "claim")
public interface ClaimInquiryUseCase {
@McpFunction(
name = "claim_inquiry",
displayName = "보험금 청구 조회",
description = "청구 번호로 보험금 청구 상태를 조회합니다.",
mappingId = "CLM00000001"
)
ClaimInquiryResponse inquire(ClaimInquiryRequest request);
}
```
`categoryKey`는 Tool의 업무 그룹입니다. Gateway의 목록 필터링, Agent 권한, 동적 MCP 서버 구분에 사용하므로 합의된 업무 키를 사용합니다.
- `MCI`: 사내 MCI 인터페이스 호출
- `EAI`: EAI 연동
- `DIRECT`: 외부 HTTP 또는 내부 직접 연동
### 변환 원칙
UseCase 구현체는 Tool 요청을 레거시 요청과 섞어 쓰지 않습니다. Converter에서 변환한 뒤 Client에 전달합니다.
```java
@Override
public ClaimInquiryResponse inquire(ClaimInquiryRequest request) {
CLM00000001_I mciRequest = converter.toMciRequest(request);
CLM00000001_O mciResponse = mciClmClient.callClm00000001(mciRequest);
return converter.toResponse(mciResponse);
}
```
MCI 입출력 객체는 업무 이름이 아니라 인터페이스 ID를 기준으로 둡니다.
```text
CLCNNB00001_I : CLCNNB00001 요청 DTO
CLCNNB00001_O : CLCNNB00001 응답 DTO
```
예를 들어 `Onnba3011Request``CLCNNB00001_I`는 같은 업무 데이터를 담을 수 있지만 같은 객체가 아닙니다. 둘 사이의 변환 책임은 `Onnba3011Converter`에 둡니다.
### 새 Tool 추가 체크리스트
새 업무 Tool을 추가할 때는 아래 순서로 확인합니다.
- [ ] 소속 모듈과 `categoryKey`를 업무 소유 조직 기준으로 결정한다.
- [ ] Agent 입력·응답 DTO인 `XxxRequest`, `XxxResponse`를 만든다.
- [ ] `XxxUseCase``@McpTool`을 선언하고, 호출 메서드에 `@McpFunction`의 이름·설명·연동 ID를 선언한다.
- [ ] `XxxUseCaseImpl`에서 업무 흐름만 조합한다.
- [ ] `XxxConverter`에 업무 DTO ↔ 인터페이스 ID DTO 변환을 둔다.
- [ ] `MciXxxClient``INTERFACE_ID_I`, `INTERFACE_ID_O`를 인터페이스 ID 기준으로 만든다.
- [ ] DTO에 Bean Validation을 선언하고, 중첩 DTO가 있으면 입력 Schema와 검증 대상에 포함되는지 확인한다.
- [ ] 조회·변경 작업 특성에 따라 `readOnlyHint`, `requiresApproval`, `idempotentHint`를 설정한다.
- [ ] 단위 테스트를 작성하고 `:dap-tool-core:test` 또는 대상 모듈 테스트를 실행한다.
- [ ] Tool 서버 기동 후 `/mcp/api/v1/tools/list`에서 이름, 설명, category, JSON Schema가 맞는지 확인한다.
- [ ] 요청·응답 로그에 개인정보나 인증값이 남지 않는지 확인한다.
### 등록·노출 제어
| 속성 | 의미 |
|---|---|
| `register` | Redis Registry와 Gateway 카탈로그에 등록할지 여부 |
| `visible` | Agent/클라이언트 목록에 표시할지 여부 |
| `requiresApproval` | 쓰기·고위험 작업의 승인 요구 여부 |
| `readOnlyHint`, `destructiveHint`, `idempotentHint` | Agent 호출 특성 힌트 |
`register = false`인 Tool은 자동 카탈로그 등록 대상이 아닙니다. 별도 실행 목적이 있는 경우에만 사용하고, 필요한 fallback 경로를 운영 설정으로 확인합니다.
## 빌드·테스트·로컬 실행
### 사전 조건
- JDK 21
- Gradle Wrapper 사용 권장
- 로컬 Redis 또는 Docker Compose 환경
- 필요 시 MCI Mock 또는 사내 MCI/EAI 접근 환경
### 전체 빌드와 대표 검증
```powershell
.\gradlew.bat clean build
.\gradlew.bat :dap-tool-core:test
.\gradlew.bat :dap-tool-core:compileJava
```
### 애플리케이션 실행
각 애플리케이션은 별도 PowerShell에서 실행합니다.
```powershell
# Gateway
.\gradlew.bat :dap-gateway:bootRun
# SMS Tool
.\gradlew.bat :dap-tool-sms:bootRun
# 기타 업무 Tool
.\gradlew.bat :dap-tool-oth:bootRun
```
기본 프로필은 `local`입니다. 개발 서버 설정이 필요하면 실행 환경에 프로필을 지정합니다.
```powershell
$env:SPRING_PROFILES_ACTIVE = 'dev'
.\gradlew.bat :dap-tool-oth:bootRun
```
## Docker Compose 실행
Docker Compose는 Redis, Gateway, MCI Mock, SMS Tool, OTH Tool을 함께 기동합니다.
```powershell
$env:ACTIVE_PROFILE = 'local'
docker compose up -d --build
docker compose ps
```
| 서비스 | 컨테이너 포트 | 호스트 포트 |
|---|---:|---:|
| Redis | 6379 | 6379 |
| Gateway | 8081 | 8281 |
| MCI Mock | 8080 | 8089 |
| SMS Tool | 8082 | 8282 |
| OTH Tool | 8084 | 8284 |
컨테이너 내부와 PC 브라우저의 접속 주소는 다릅니다.
```text
컨테이너 내부: http://gateway:8081, http://tool-oth:8084
PC 브라우저: http://localhost:8281, http://localhost:8284
```
## 환경 설정
### 프로필
기본 프로필은 `local`입니다. Tool 서버와 Gateway 모두 `local`, `dev` 프로필 파일을 사용합니다.
| 프로필 | 목적 | 주요 설정 파일 |
|---|---|---|
| `local` | PC 개발·MCI Mock·H2 메모리 DB 기반 실행 | `application-local.yml` |
| `dev` | 개발 서버 배포 실행 | `application-dev.yml` |
프로필은 환경 변수로 지정합니다.
```powershell
# 로컬 개발
$env:SPRING_PROFILES_ACTIVE = 'local'
# 개발 서버 설정으로 실행
$env:SPRING_PROFILES_ACTIVE = 'dev'
```
### 환경별 연결 주소 비교
| 항목 | `local` 프로세스 실행 | Docker Compose | `dev` 프로필 |
|---|---|---|---|
| Gateway 접근 주소 | `http://localhost:8081` | 컨테이너 내부 `http://gateway:8081` | 배포 환경 Gateway URL |
| SMS Tool 주소 | `http://localhost:8082` | `http://tool-sms:8082` | `PORT` 기본 8082 |
| OTH Tool 주소 | `http://localhost:8084` | `http://tool-oth:8084` | `PORT` 기본 8084 |
| Redis 주소 | 로컬 Redis 또는 `localhost:6379` | `redis:6379` | 운영/개발 Redis 설정 |
| MCI/EAI 대상 | MCI Mock 또는 로컬 설정 | `mci-mock:8080` | 개발망 연동 설정 |
| 브라우저 Gateway 접속 | `http://localhost:8081` | `http://localhost:8281` | 운영·개발 도메인 |
`local`에서 프로세스를 직접 실행하면 Tool의 `AXHUB_GATEWAY_URL``localhost`를 사용합니다. Docker에서는 각 컨테이너가 서로 다른 네트워크 공간에 있으므로 반드시 Compose 서비스 이름을 사용합니다.
### Gateway 환경 변수
| 변수 | 적용 대상 | 설명 | 로컬 기본값/예시 |
|---|---|---|---|
| `SPRING_PROFILES_ACTIVE` | Gateway, 모든 Tool | 활성 Spring 프로필 | `local` |
| `OPENROUTER_API_KEY` | Gateway | Chat/LLM 호출 API 키 | 운영·개발 환경의 비밀 저장소에서 주입 |
| `SPRING_DATA_REDIS_HOST` | Gateway, 모든 Tool | Redis 호스트 | Docker: `redis` |
| `SPRING_DATA_REDIS_PORT` | Gateway, 모든 Tool | Redis 포트 | `6379` |
| `MCP_GATEWAY_FALLBACK_DEFAULT_URL` | Gateway | Registry에 없는 Tool의 기본 fallback URL | Docker: `http://tool-oth:8084` |
| `MCP_GATEWAY_FALLBACK_ROUTES_SMS` | Gateway | SMS 계열 fallback URL | Docker: `http://tool-sms:8082` |
| `ACTIVE_PROFILE` | Docker Compose | Compose가 `SPRING_PROFILES_ACTIVE`에 전달할 프로필 | `local` |
`OPENROUTER_API_KEY` 같은 인증값은 `application.yml`, README, Git 커밋에 직접 넣지 않습니다. 개발·운영 환경의 Secret, CI/CD 변수 또는 안전한 환경 변수로 주입합니다.
### Tool 서버 환경 변수
| 변수 | 적용 대상 | 설명 | 로컬 기본값/예시 |
|---|---|---|---|
| `PORT` | `dap-tool-sms`, `dap-tool-oth` | 개발 프로필에서 Tool 서버 포트 변경 | SMS `8082`, OTH `8084` |
| `AXHUB_GATEWAY_URL` | 모든 Tool | Tool 등록·Heartbeat 대상 Gateway 주소 | 로컬 `http://localhost:8081`, Docker `http://gateway:8081` |
| `AXHUB_TOOL_URL` | 모든 Tool | Gateway가 해당 Tool Pod를 호출할 주소 | 로컬 `http://localhost:{server.port}` |
| `GLOW_COMMUNICATION_MCI_HOMT` | Docker Tool 컨테이너 | MCI 대상 호스트 | 로컬 Compose는 `mci-mock` |
| `GLOW_COMMUNICATION_MCI_PORT` | Docker Tool 컨테이너 | MCI 대상 포트 | 로컬 Compose는 `8080` |
| `GLOW_COMMUNICATION_EAI_HOMT` | Docker Tool 컨테이너 | EAI 대상 호스트 | 로컬 Compose는 `mci-mock` |
| `GLOW_COMMUNICATION_EAI_PORT` | Docker Tool 컨테이너 | EAI 대상 포트 | 로컬 Compose는 `8080` |
`AXHUB_TOOL_URL`은 반드시 Gateway가 실제로 접근 가능한 주소여야 합니다. PC에서 각각 실행할 때는 `localhost`를 사용하고, Docker 컨테이너 안에서는 `tool-sms`, `tool-oth` 같은 Compose 서비스 이름을 사용합니다.
### 로컬 실행용 권장 설정
아래는 키 값 없이 로컬 프로세스를 실행하는 예시입니다. Redis를 Docker로 먼저 기동하거나 전체 Docker Compose를 사용합니다.
```powershell
# 선택 1: Redis만 기동
$env:ACTIVE_PROFILE = 'local'
docker compose up -d redis mci-mock
# 선택 2: 각 프로세스를 로컬에서 기동
$env:SPRING_PROFILES_ACTIVE = 'local'
$env:OPENROUTER_API_KEY = '<개인 또는 개발용 비밀 저장소의 키>'
.\gradlew.bat :dap-gateway:bootRun
```
다른 PowerShell에서 Tool을 실행합니다.
```powershell
$env:SPRING_PROFILES_ACTIVE = 'local'
$env:AXHUB_GATEWAY_URL = 'http://localhost:8081'
$env:AXHUB_TOOL_URL = 'http://localhost:8084'
.\gradlew.bat :dap-tool-oth:bootRun
```
### 권한 도메인 설정
`mcp.security.tenant-domains`는 테넌트 또는 호출 주체가 접근할 수 있는 `categoryKey`를 정의합니다. 운영 환경에서는 `ALL`을 무분별하게 사용하지 않고, Agent·조직별 허용 도메인을 최소 권한으로 설정합니다.
```yaml
mcp:
security:
tenant-domains:
claims-agent: claim, customer
notification-agent: notification
```
## 공통 기능과 운영 기준
### Redis
Redis는 Tool Registry의 등록 상태와 Heartbeat, Gateway 실행 추적 정보에 사용됩니다. 현재 Redis는 PII 원문 보관소가 아닙니다.
### 권한과 승인
Gateway는 Agent가 전달한 허용 Tool 목록, 서버 정책, 신뢰된 Claim 여부, 쓰기 승인 여부를 확인합니다. Tool마다 권한 로직을 중복 구현하지 말고 Gateway 공통 정책과 `@McpFunction` 메타데이터를 사용합니다.
### Trace ID와 Request ID
```text
trace-id : 사용자 요청 전체에서 유지되는 상관관계 ID
request-id : Gateway → Tool, Tool → MCI 등 HTTP 호출마다 새로 생성되는 ID
```
Gateway와 Tool에는 관련 헤더 및 MDC 기반 로그 처리가 있습니다. 신규 HTTP Client도 공통 전파 정책을 따르며, Tool별로 임의의 헤더 이름을 추가하지 않습니다.
### 로그와 개인정보
Gateway에는 민감 키와 일부 형식을 마스킹하는 공통 기능이 있습니다. 마스킹은 원문을 Agent에서 분리하는 PII 토큰화와는 다릅니다.
- 요청·응답 전문을 로그에 남길 때는 반드시 마스킹합니다.
- 운영 로그에 주민번호, 계좌번호, 전화번호, 이메일, 인증값을 남기지 않습니다.
- Tool 서버의 신규 로그도 같은 마스킹 정책을 적용합니다.
- 민감정보 복원 필요 여부는 Tool 개발자가 임의로 결정하지 않고 보안·AA 정책을 따릅니다.
## AA 협의 기반 향후 전환 과제
아래 항목은 현재 구현 완료 기능이 아니라 회의에서 합의한 목표 구조입니다.
### 부서별 Tool Pod와 저장소 경계
현재 `dap-tool-oth`에 함께 있는 업무 Tool을 부서·업무 소유권 단위로 분리합니다. 각 Pod는 독립 이미지, 독립 배포, 독립 장애 범위를 갖도록 구성합니다. 실제 분리는 AA가 확정한 Tool 소유 부서와 운영 책임 매핑을 기준으로 수행합니다.
### Agent별 Tool 노출 수 제한
Agent에게 모든 Tool을 한 번에 제공하지 않고, 업무 도메인과 권한에 따라 약 10~20개 수준의 Tool 그룹을 제공합니다. `categoryKey`는 이를 위한 기초 메타데이터이며, 향후 Agent-Tool Group 정책으로 확장합니다.
### PII 토큰화
```text
원문 개인정보
→ PII Gateway가 Redis에 짧은 TTL로 보관
→ Agent에는 PII 토큰 또는 안전한 식별자만 전달
→ 인가된 Tool이 MCI 호출 직전에 필요한 항목만 복원
```
이 전환 전까지는 현행 마스킹 기능을 PII 분리 구현으로 오해하지 않아야 합니다.
### MCP SDK와 Glow Framework
Gateway에는 Spring AI MCP Server 의존성이 포함되어 있습니다. MCP SDK/Glow 표준 적용 시에는 업무 Tool을 재작성하지 않고, 기존 `@McpTool`·`@McpFunction`과 UseCase를 표준 MCP Tool 명세·호출 콜백으로 연결하는 Adapter 계층을 공통 Core에 추가합니다.
```text
MCP SDK 표준 tools/list, tools/call
공통 Adapter
기존 UseCase → Converter → MCI Client
```
따라서 업무 DTO, Converter, MCI Client의 책임은 유지됩니다.
## 참고 소스
| 주제 | 대표 위치 |
|---|---|
| Gateway Tool API | `dap-gateway/.../presentation/McpRouterController.java` |
| 동적 MCP SSE/호출 경로 | `dap-gateway/.../sync/DynamicMcpController.java` |
| Tool 실행 Controller | `dap-tool-core/.../presentation/BusinessToolController.java` |
| Tool 자동 등록 | `dap-tool-core/.../usecase/ToolRegistryHeartbeatSender.java` |
| Tool 어노테이션 | `dap-tool-core/.../annotation/McpTool.java`, `McpFunction.java` |
| Tool 예시 | `dap-tool-oth/.../biz/oth`, `biz/sol`, `biz/smp` |
| SMS Tool 예시 | `dap-tool-sms/.../biz/sms` |
| Docker 환경 | `docker-compose.yml` |
---
## ▶ 실행 방법 (How to Run)
문서에 없는 업무·보안·배포 기준은 임의로 추가하지 말고 AA 및 플랫폼 운영 기준과 먼저 합의합니다.
## Input/Output Schema 작성 가이드
### 1. Gateway & Tool 서버 실행 (MCP 연동용)
- **Gateway 서버 기동:**
- `./gradlew :dap-gateway:bootRun`
- **Tool 서버 기동:**
- `./gradlew :dap-tool-other:bootRun` (또는 dap-tool-payment 등)
- Tool 서버가 기동되면 자동으로 Gateway(8081)에 자신을 등록(Auto-Registration)합니다.
- **(선택) 특정 Tool 그룹만 실행하기:**
- 업무 특성에 따라 세분화된 그룹에 속한 Tool만 띄우고 싶다면, 실행 인수에 `--mcp.tool.target=그룹명`을 추가합니다.
- **지원되는 그룹명:**
- `NOTIFICATION`: 이메일, SMS 발송
- `CLAIM`: 청구 처리, 심사 상태 조회
- `POLICY`: 증권 발행, 발행 가능 여부 조회
- `HR`: 휴가 등록, 연차 갯수 조회
- `CONTRACT`: 계약 상태, 계약 상세 조회
- `CUSTOMER`: 고객 등급, 고객 상세 정보 조회
- IntelliJ IDEA: `Run/Debug Configurations`에서 `DapTool*Application``Program arguments``--mcp.tool.target=NOTIFICATION` 입력
Tool Schema는 Agent가 Tool을 정확히 호출하고, 반환값의 의미를 일관되게 해석하도록 하는 계약입니다. 인증 정보·사번·주민번호 등 민감정보(PII)는 Input/Output Schema와 Tool 응답에 포함하지 않습니다.
### Input Schema
---
Input Schema는 Agent가 Tool에 전달하는 파라미터의 이름, 타입, 필수 여부, 허용값, 형식 등을 정의합니다.
## 🤖 AI Agent 연동 아키텍처 (MCP & Agent Builder)
적용 우선순위는 다음과 같습니다.
본 시스템은 **투트랙(Two-Track) AI 연동 아키텍처**를 제공하여 로컬 개발 환경과 프로덕션 환경 모두를 완벽하게 지원합니다.
1. `inputSchemaResource` — 복잡한 규칙을 담은 JSON Schema 리소스
2. `inputSchema` — 어노테이션에 직접 선언한 JSON Schema
3. 요청 DTO 필드의 `@McpValidation` — 자동 JSON Schema 생성
### 1. 로컬 코딩 AI (Antigravity, Cursor, Claude Desktop 등) 연동
표준 MCP 통신(Stdio)을 요구하는 로컬 AI 에이전트를 위해 자바 기반의 브릿지 스크립트(`McpBridge.java`)를 내장하고 있습니다. 브릿지가 Stdio 요청을 HTTP로 변환하여 로컬 환경의 Gateway(포트: 8281)로 전달합니다.
단순한 요청 DTO는 `@McpValidation`만으로 관리합니다.
- **설정 방법**: IDE의 `mcp_config.json` 설정 파일에 아래와 같이 등록합니다.
```json
"mcpServers": {
"dap-gateway": {
"command": "java",
"args": ["C:/절대경로/dap-backend-main/McpBridge.java"]
}
}
```
- **특정 카테고리 툴 필터링**: `McpBridge.java` 내부의 URI 파라미터(`?categoryKey=common`)를 수정하여 원하는 도메인의 툴만 선택적으로 AI에게 학습시킬 수 있습니다.
```java
public class ClaimSearchRequest {
### 2. 프로덕션 클라우드 AI (Google Cloud Agent Builder 등) 연동
실제 라이브 서비스에서 동작하는 클라우드 Agent Builder는 REST API 기반의 OpenAPI Spec을 요구합니다.
`dap-gateway`는 이미 **Agent Builder 규격의 REST API(`/mcp/api/v1/tools/call`)를 네이티브로 제공**하므로, 별도의 브릿지나 어댑터 없이 Endpoint URL과 Swagger(OpenAPI) 문서만 클라우드 콘솔에 등록하면 즉시 라이브 챗봇/에이전트로 서비스할 수 있습니다.
@McpValidation(required = true, pattern = "^CLM[0-9]{13}$")
private String claimNo;
---
@McpValidation(minimum = 1, maximum = 100)
private Integer size;
}
```
## 비공개 Tool 관리 및 Fallback 연동 (Visibility & Routing)
### Output Schema
저희 시스템은 MSA 보안 및 아키텍처 원칙에 따라 Tool의 **레지스트리 등록 여부(라우팅)**와 **API 노출 여부(가시성)**를 완벽히 분리하여 관리합니다.
Output Schema는 Tool이 반환하는 결과의 타입과 의미를 정의합니다. `BusinessToolController`는 Tool 실행 후 반환값을 Output Schema 기준으로 검증합니다.
1. **`visible = false`**:
레지스트리에 정상적으로 등록되어 게이트웨이가 동적으로 라우팅하지만, 클라이언트에게 제공되는 `/tools/list` API 목록에서는 숨겨집니다.
2. **`register = false`**:
내부 레지스트리(Redis)에 툴 정보를 등록하지 않습니다 (외부 레지스트리를 독자적으로 사용할 경우 등).
이 경우 게이트웨이는 `application.properties`의 `mcp.gateway.fallback.routes` 설정을 참조하여 **Fallback 정적 라우팅**을 수행하므로 연동이 100% 보장됩니다.
적용 우선순위는 다음과 같습니다.
1. `outputSchemaResource` — 조건부 필드·중첩 배열 등 복잡한 규칙을 담은 JSON Schema 리소스
2. `outputSchema` — 어노테이션에 직접 선언한 JSON Schema
3. 반환 DTO의 `@McpOutputSchema`와 필드 `@McpValidation` — 자동 JSON Schema 생성
4. 위 설정이 모두 없으면 Output Schema 검증을 수행하지 않음
따라서 단순한 응답은 별도 `outputSchemaResource` 없이 반환 DTO에 `@McpOutputSchema`를 선언하면 됩니다. `null`이 정상 값일 수 있는 필드는 `nullable = true`를 반드시 지정합니다.
```java
@McpOutputSchema
public class ClaimSearchResponse {
@McpValidation(required = true, allowedValues = {"SUCCESS", "FAILURE"})
private String resultCode;
@McpValidation(nullable = true, minimum = 0)
private Long approvedAmount;
}
```
### 복잡한 Schema는 Tool 모듈별 리소스로 관리
조건부 응답, 중첩 DTO, 배열 정렬 기준처럼 어노테이션만으로 표현하기 어려운 규칙은 Tool Core가 아니라 각 Tool 모듈의 리소스에 JSON Schema로 둡니다.
```text
src/main/resources/
└─ tool-schemas/
└─ {categoryKey}/
├─ claim-search-resource-input-schema.json
└─ claim-search-resource-output-schema.json
```
예를 들어 `categoryKey``cmm`이면 아래와 같이 선언합니다.
```java
@McpFunction(
name = "secret_tool",
visible = false, // 목록 숨김 여부 (기본값: true)
register = false // 내부 Redis 등록 여부 (기본값: true)
name = "oth.cmm.claim.search",
inputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-input-schema.json",
outputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-output-schema.json"
)
public ClaimSearchResponse search(ClaimSearchRequest request) {
// ...
}
```
---
`inputSchemaResource``outputSchemaResource`는 복잡한 경우에만 선언합니다. 단순한 Tool까지 JSON 파일을 별도 생성할 필요는 없습니다.
## 🛡️ 시스템 안정성 및 네트워크 제어 (Resilience & Network)
### Output 설계 규칙
MSA 및 외부 시스템(MCI) 연동 환경의 안정성을 위해 완벽한 3-Tier 방어 체계를 구축했습니다.
- 코드와 표시용 라벨을 함께 반환합니다. 예: `status` + `statusLabel`
- `null`이 정상인 값은 의미를 설명에 명시하고 DTO에는 `nullable = true`를 설정합니다.
- 조건부 필드는 어떤 조건에서 값이 존재하는지 JSON Schema에 명시합니다.
- 배열은 정렬 기준을 설명에 명시합니다. 예: `접수일 내림차순`
- 목록 응답에는 추가 조회 여부를 나타내는 `hasMore`를 포함합니다.
- 민감정보는 마스킹보다 **응답에서 제외**하는 것을 우선합니다.
1. **Gateway 라우팅 방어 (Timeout & Fallback):**
- MCP 라우터(`McpRouterController`) 단에 1초 타임아웃을 강제 적용하여 특정 Tool Pod의 응답 지연이 전체 시스템 장애로 이어지는 것을 방지하고 신속하게 정적 Fallback 라우팅으로 전환합니다.
2. **MCI 네트워크 안정화 (HTTP/1.1 Downgrade):**
- 기존 HTTP/2 사용 시 레거시 시스템 연동 중 간헐적으로 발생하던 `RST_STREAM` 오류를 원천 차단하기 위해, MCI 전용 `HttpEimsSender`에는 고도로 최적화된 **HTTP/1.1 전용 커넥션 풀(Factory)**이 고정 적용되어 네트워크 단절을 방지합니다.
3. **Resilience4j 기반 트래픽 제어:**
- **Gateway 계층 (동적 방어):** Tool 등록 시 제출된 SLA 메타데이터를 기반으로 동적 CircuitBreaker 및 RateLimiter를 가동하며, 한계치 초과 시 Kafka 큐로 비동기 전환합니다.
- **Tool 계층 (정적 방어):** 레거시 커넥터 내부에 `@CircuitBreaker`, `@RateLimiter` 어노테이션 기반의 장애 전파 차단 로직이 2차적으로 가동됩니다.
### 실행 로그 및 확인
---
## 모듈(Pod) 및 Tool 코드 자동 생성 (Scaffolders)
새로운 도메인의 기능을 추가할 때 발생하는 반복적인 설정(보일러플레이트, 설정 파일 복사 등)을 1초 만에 자동화하기 위해 **DAP Developer Portal (Web UI)** 및 **CLI 스캐폴더 2종**을 제공합니다.
### 1. DAP Developer Portal (Web UI) - 가장 추천하는 방식!
이제 더 이상 터미널에서 명령어를 칠 필요가 없습니다. Gateway 모듈에 내장된 웹 화면에서 빈칸만 채우면 코드가 마법처럼 찍혀 나옵니다.
1. **접속 방법**: Gateway 서버 기동 후 브라우저에서 `http://localhost:8081/admin/scaffold.html` 접속
2. **Pod (모듈) 생성 탭**: 모듈명(예: hr)과 포트만 입력하면 독립적인 Spring Boot 모듈이 디렉토리부터 빌드 스크립트까지 완벽히 생성됩니다.
3. **Tool (기능) 생성 탭**: 생성된 모듈에 새로운 툴(서비스/DTO) 코드를 자동으로 주입합니다.
### 2. CLI 스캐폴더 (기존 터미널 방식)
웹 화면을 사용할 수 없는 환경이거나 터미널이 익숙한 경우, 아래 명령어를 통해 CLI 마법사를 사용할 수 있습니다.
### 1⃣ 새로운 Pod(모듈) 전체를 생성할 때: `PodScaffolder`
새로운 도메인(예: 결제, HR)을 위한 완전히 독립적인 Spring Boot 모듈을 생성합니다. 폴더 구조, 빌드 스크립트, 각종 프로퍼티 및 도커 설정까지 완벽하게 세팅됩니다.
```bash
# 사용법: javac로 컴파일 후 실행
javac -encoding UTF-8 dap-common/src/main/java/io/shinhanlife/dap/common/util/PodScaffolder.java
java -cp dap-common/src/main/java io.shinhanlife.dap.common.util.PodScaffolder [모듈명] [포트번호]
# 실행 예시 (dap-tool-hr 모듈을 8086 포트로 생성)
java -cp dap-common/src/main/java io.shinhanlife.dap.common.util.PodScaffolder hr 8086
```
### 2⃣ 생성된 모듈에 새로운 툴(Function)을 추가할 때: `ToolScaffolder`
어노테이션(`@McpTool`, `@McpFunction`)이 완벽히 달린 Service와 입출력 DTO 코드를 지정된 모듈 패키지 룰에 맞춰 자동 생성합니다.
```bash
# 사용법: javac로 컴파일 후 실행
javac -encoding UTF-8 dap-common/src/main/java/io/shinhanlife/dap/common/util/ToolScaffolder.java
java -cp dap-common/src/main/java io.shinhanlife.dap.common.util.ToolScaffolder [Tool이름] [인터페이스ID] "[기능설명]" "[그룹명]" "[통신방식]" "[모듈명]"
# 실행 예시 (payment 모듈에 결제 승인 기능 추가)
java -cp dap-common/src/main/java io.shinhanlife.dap.common.util.ToolScaffolder PaymentApproval PAY_001 "결제 승인 처리 기능" "COMMON" "HTTP" "dap-tool-payment"
```
---
## 패키지 구조 (Package Structure)
Tool 실행이 끝나면 아래 로그는 Schema 정의가 아니라 **검증을 통과한 실제 최종 응답값**을 출력합니다.
```text
dap-backend-main (Root)
├── dap-gateway # MCP 라우팅 허브 서버 (외부 LLM과 통신 및 Tool 분배)
├── dap-common # 공통 모듈 (Security, Session, Config 등)
├── dap-tool-core # Tool 공통 기능 (AbstractMcpToolService, Annotation, Scaffolder)
├── dap-tool-email # [Tool] 이메일 발송 특화 어댑터 모듈
├── dap-tool-sms # [Tool] SMS 발송 특화 어댑터 모듈
├── dap-tool-payment # [Tool] 결제 비즈니스 어댑터 모듈 (Scaffolded)
└── dap-tool-other # [Tool] 기타 비즈니스(청구, 계약, 고객, HR 등) 어댑터 모듈
[Tool -> MCP Gateway] Output Schema Result: { ... }
```
*(참고: 기존 단일 모듈 프로젝트에서 마이크로서비스 확장을 위해 모듈별로 분리되었으며, 각 Tool 서버는 독립적으로 확장 및 배포할 수 있습니다.)*
따라서 로그에도 실제 응답이 남으므로, 응답 DTO와 Output Schema에 민감정보가 포함되지 않도록 설계해야 합니다.
스키마 리소스와 DTO 기반 자동 Schema는 아래 테스트로 함께 검증할 수 있습니다.
```powershell
.\gradlew.bat :dap-tool-oth:test --tests "io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequestSchemaTest"
```
### Tool Naming Convention
All Tool names use the four-level lowercase format `pod.domain.service.action`. Do not use underscores or CamelCase; use a hyphen (`-`) only when a single level has multiple words.
- `pod`: deployment Tool Pod/module (`dap-tool-oth``oth`, `dap-tool-sms``sms`)
- `domain`: business-domain package (`cmm`, `smp`, `sol`, etc.)
- `service`: business service or resource
- `action`: the requested operation (`search`, `list`, `detail`, `issue`, `inquiry`, etc.)
```text
oth.cmm.bond.issue
oth.cmm.claim.search
oth.sol.request.list
oth.smp.weather.inquiry
```
When Scaffold receives `dap-tool-oth`, `cmm`, and `ClaimSearch`, it generates `oth.cmm.claim.search`. The `validateMcpToolNames` Gradle task rejects both a duplicate name and any name outside this format before packaging, including its source file and line number.
### Tool Test Console
각 Tool Pod는 공통 테스트 화면을 제공합니다.
```text
http://localhost:8084/tool-test-console.html
```
화면은 현재 Pod의 `/tool-manifest`에서 Tool 목록과 `inputSchema`를 읽습니다. Tool을 선택한 뒤 `Schema 샘플 채우기`로 요청 JSON을 만들고 실행할 수 있습니다. 업무에 맞게 보정한 요청은 `현재 요청 저장`으로 브라우저의 `localStorage`에 보관합니다.
`Run saved cases`는 저장된 테스트 케이스를 순차 실행해 성공/실패, HTTP 상태, 소요 시간을 보여줍니다. 따라서 Tool이 수백 개여도 각 Tool마다 테스트 화면을 만들 필요 없이, 유효한 업무 테스트 데이터만 한 번 저장하면 이후에는 몇 번의 클릭으로 회귀 테스트할 수 있습니다.
- Tool 호출은 현재 Pod의 `/mcp/{toolName}`로 수행합니다.
- 매 실행마다 `trace-id`, `request-id`를 새로 생성하여 응답과 함께 표시합니다.
- 외부 MCI/EAI Tool은 샘플값 대신 개발계에서 허용된 테스트 데이터를 저장해서 사용해야 합니다.

View File

@@ -49,3 +49,20 @@ subprojects {
useJUnitPlatform()
}
}
def toolCoreProject = project(':dap-was-lib')
tasks.register('validateMcpToolNames', JavaExec) {
group = 'verification'
description = 'Checks duplicate @McpFunction names across all Tool modules before packaging.'
dependsOn toolCoreProject.tasks.named('classes')
classpath = toolCoreProject.sourceSets.main.runtimeClasspath
mainClass.set('io.shinhanlife.dap.lib.validation.McpToolNameValidationRunner')
args rootProject.projectDir.absolutePath
}
subprojects {
tasks.matching { it.name == 'bootJar' }.configureEach {
dependsOn rootProject.tasks.named('validateMcpToolNames')
}
}

View File

@@ -1,87 +0,0 @@
package io.shinhanlife.dap.biz.mcp.gateway.dto;
import lombok.Getter;
import lombok.Setter;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
/**
* Tool(Agent)의 명세 및 라우팅 정보를 담고 있는 메타데이터 클래스
* Redis 레지스트리에 저장되며, Planner와 Router 간의 통신 객체(Plan)로 사용됩니다.
*/
/**
* @package io.shinhanlife.dap.biz.mcp.gateway.dto
* @className ToolMetadata
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class ToolMetadata {
// 1. Tool 기본 정보
private String uid; // UUID 형식의 고유 식별자
private String semver; // 버전 (예: 1.0.0)
private String displayName; // 사람이 읽는 라벨 (1-128자)
private String name; // MCP 서브툴 명칭 (64자 이하, 예: CustomerSearchTool)
private String description; // 툴의 목적 및 설명 (LLM 프롬프트에 활용 가능)
// 2. 파라미터 스키마 (JSON Schema 형태의 Map)
private Map<String, Object> parametersSchema;
// 2-0. 프론트엔드 UI용 함수별 프롬프트 매핑 (추가됨)
private Map<String, String> actionPrompts;
// 2-1. 도메인 부서 그룹명 (category_key, 슬러그 형식)
private String categoryKey;
// 2-2. 툴 처리 엔드포인트 URI 경로 (예: /api/tool/customer-info)
private String endpoint;
// 2-3. Pod 실행 URL (독립적인 Microservice 라우팅용, 예: http://localhost:8082)
private String podUrl;
// 2-4. 가시성 여부
@Builder.Default
private Boolean visible = true;
// 2-5. Redis 등록 여부 (UI 표출용)
@Builder.Default
private Boolean isRegistered = true;
// 2-6. HITL 승인 필요 여부
@Builder.Default
private Boolean requiresApproval = false;
// 3. 연동 아키텍처 구분 (DIRECT / MCI_EAI)
private String integrationType; // 연동 타입: "DIRECT" 또는 "MCI_EAI"
// 4. 레거시(MCI/EAI) 연동 시 필수 정보 (integrationType이 "MCI_EAI"일 때 사용)
private String mciServiceId; // MCI/EAI 호출을 위한 서비스 ID (예: CRM_001, LICO_992)
// 5. 인프라 상태 정보 (DIRECT 연동 시 사용)
private Long lastHeartbeat; // Redis TTL 갱신용 마지막 하트비트 타임스탬프
// 6. 동적 서킷 브레이커 & 속도 제어 설정 (Registry 기반)
private Integer failureRateThreshold; // 서킷 브레이커 동작 기준 실패율 (%)
private Integer slidingWindowSize; // 서킷 브레이커 에러율 계산 표본 요청 수
private Integer rateLimitForPeriod; // 속도 제어: 1초당 허용 최대 요청 수
}

View File

@@ -1,40 +0,0 @@
package io.shinhanlife.dap.common.integration.mci.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @package io.shinhanlife.dap.common.integration.mci.dto
* @className ShinhanCommonHeaderDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ShinhanCommonHeaderDto {
private String envrTypeCd; // 환경유형코드 (D, T, R)
private String glbId; // 전사공통키 (37 Byte)
private String pgrsSriaNo; // 진행일련번호
private String trgmVrsnInfoValu; // 전문버전정보값
private String tgrmEncrYn; // 전문암호화여부
private String gpcpCd; // 글로벌법인코드
private String appliDutjCd; // 어플리케이션업무코드
private String rcvSvcId; // 수신서비스ID
private String reqRspnScCd; // 요청응답구분코드 (S:요청, R:응답)
private String inqrTraTypeCd; // 조회거래유형코드
private String reqTgrmTnsmDtptDt; // 요청전문전송일시
private String itrIfId; // 인터페이스ID
}

View File

@@ -1,372 +0,0 @@
package io.shinhanlife.dap.common.util;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Scanner;
/**
* MCP Tool 코드를 자동 생성(Scaffolding)하는 유틸리티 클래스
*
* [실행 방법]
* 방법 1. IDE(IntelliJ 등)에서 직접 실행 (대화형 모드 추천 ⭐)
* - 이 클래스(ToolScaffolder.java)를 열고 main 메서드를 직접 실행(Run)합니다.
* - 콘솔 창에 뜨는 질문에 차례대로 값을 입력하기만 하면 파일이 생성됩니다.
*
* 방법 2. 커맨드라인(터미널)에서 실행 (명령어 기반)
* - 컴파일: javac -encoding UTF-8 dap-tool-core/src/main/java/io/shinhanlife/dap/biz/mcp/tool/util/ToolScaffolder.java
* - 실행: java -cp dap-tool-core/src/main/java io.shinhanlife.dap.biz.mcp.tool.util.ToolScaffolder [이름] [ID] "[설명]" "[그룹]" "[통신방식]" "[모듈명]"
*/
/**
* @package io.shinhanlife.dap.biz.mcp.tool.util
* @className ToolScaffolder
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
public class ToolScaffolder {
private static final String BASE_PACKAGE = "io.shinhanlife.dap.biz.mcp.tool";
private static final String BASE_PACKAGE_PATH = "src/main/java/io/shinhanlife/dap/biz/mcp/tool";
public static void main(String[] args) throws IOException {
Scanner scanner = new Scanner(System.in);
System.out.println("=========================================");
System.out.println(" MCP Tool Scaffolder (Java CLI) ");
System.out.println("=========================================\n");
String baseName = getOrAsk(args, 0, scanner, "1. 생성할 Tool의 기본 이름 (예: ExchangeRate) [영문 PascalCase]: ");
String interfaceId = getOrAsk(args, 1, scanner, "2. 레거시 API 인터페이스 ID (예: EXCH_001): ");
String description = getOrAsk(args, 2, scanner, "3. Tool 기능 설명 (예: 환율 조회): ");
String group = getOrAsk(args, 3, scanner, "4. Tool 소속 그룹 (예: NOTIFICATION, CLAIM, POLICY, HR, CONTRACT, CUSTOMER 등): ");
if (group.isEmpty()) group = "COMMON";
String routingType = getOrAsk(args, 4, scanner, "5. 통신 프로토콜 (예: HTTP, TCP, MCI, EAI): ");
if (routingType.trim().isEmpty()) {
routingType = "HTTP";
}
String moduleName = getOrAsk(args, 5, scanner, "6. 코드를 생성할 모듈 (기본: dap-tool-other): ");
if (moduleName.trim().isEmpty()) {
moduleName = "dap-tool-other";
}
String defaultAuthor = System.getProperty("user.name");
String defaultDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
String author = getOrAsk(args, 6, scanner, "7. 작성자 (엔터 입력 시 '" + defaultAuthor + "'): ");
if (author.trim().isEmpty()) author = defaultAuthor;
String createDate = getOrAsk(args, 7, scanner, "8. 작성일 (엔터 입력 시 '" + defaultDate + "'): ");
if (createDate.trim().isEmpty()) createDate = defaultDate;
String result = scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, true);
System.out.println(result);
}
private static String getOrAsk(String[] args, int index, Scanner scanner, String prompt) {
if (args.length > index) {
return args[index];
}
System.out.print(prompt);
return scanner.nextLine().trim();
}
public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register) throws IOException {
baseName = toPascalCase(baseName);
String envSourceDir = System.getenv("AXHUB_SOURCE_DIR");
Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get(".");
Path serviceDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "service"));
Path dtoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "dto"));
String shortName = moduleName.replace("dap-tool-", "").replace("-", "");
Path legacyDtoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, shortName, "dto"));
Path converterDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, shortName, "converter"));
Files.createDirectories(serviceDir);
Files.createDirectories(dtoDir);
Files.createDirectories(legacyDtoDir);
Files.createDirectories(converterDir);
StringBuilder log = new StringBuilder();
// Generate Req DTO
String reqContent = """
package %s.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
/**
* @package %s.dto
* @className %sReq
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class %sReq {
// TODO: Add request fields here
}
""".formatted(BASE_PACKAGE, BASE_PACKAGE, baseName, author, createDate, createDate, author, baseName);
Files.writeString(dtoDir.resolve(baseName + "Req.java"), reqContent);
// Generate Res DTO
String resContent = """
package %s.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
/**
* @package %s.dto
* @className %sRes
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class %sRes {
private String status;
private String message;
// TODO: Add response fields here
}
""".formatted(BASE_PACKAGE, BASE_PACKAGE, baseName, author, createDate, createDate, author, baseName);
Files.writeString(dtoDir.resolve(baseName + "Res.java"), resContent);
String toolName = baseName.isEmpty() ? baseName : Character.toLowerCase(baseName.charAt(0)) + baseName.substring(1);
// Generate Service
String serviceContent = """
package %s.service;
import %s.annotation.McpFunction;
import %s.annotation.McpTool;
import %s.dto.%sReq;
import %s.dto.%sRes;
import %s.%s.converter.%sLegacyConverter;
import org.springframework.stereotype.Service;
import org.mapstruct.factory.Mappers;
/**
* @package %s.service
* @className %sService
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Service
@McpTool(
routingType = "%s",
categoryKey = "%s"
)
public class %sService extends AbstractMcpToolService {
private final %sLegacyConverter converter = Mappers.getMapper(%sLegacyConverter.class);
@McpFunction(
displayName = "%s 툴",
name = "%s",
description = "%s",
prompt = "%s",
mappingId = "%s",
register = %s,
requiresApproval = false
)
public Object execute(%sReq req) {
// %sLegacyReq legacyReq = converter.toLegacyReq(req);
return executeLegacy("%s", "%s", req); // Or pass legacyReq
}
}
""".formatted(
BASE_PACKAGE,
BASE_PACKAGE,
BASE_PACKAGE,
BASE_PACKAGE, baseName,
BASE_PACKAGE, baseName,
BASE_PACKAGE, shortName, baseName,
BASE_PACKAGE,
baseName,
author,
createDate,
createDate, author,
routingType, group.toLowerCase(),
baseName,
baseName, baseName,
baseName, toolName, description, description + " 해줘.", interfaceId, register,
baseName,
baseName,
routingType, interfaceId
);
Files.writeString(serviceDir.resolve(baseName + "Service.java"), serviceContent);
// Generate Legacy Req DTO
String legacyReqContent = """
package %s.%s.dto;
import lombok.Data;
/**
* @package %s.%s.dto
* @className %sLegacyReq
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Data
public class %sLegacyReq {
// TODO: Add legacy request fields here
}
""".formatted(BASE_PACKAGE, shortName, BASE_PACKAGE, shortName, baseName, author, createDate, createDate, author, baseName);
Files.writeString(legacyDtoDir.resolve(baseName + "LegacyReq.java"), legacyReqContent);
// Generate Legacy Res DTO
String legacyResContent = """
package %s.%s.dto;
import lombok.Data;
/**
* @package %s.%s.dto
* @className %sLegacyRes
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Data
public class %sLegacyRes {
// TODO: Add legacy response fields here
}
""".formatted(BASE_PACKAGE, shortName, BASE_PACKAGE, shortName, baseName, author, createDate, createDate, author, baseName);
Files.writeString(legacyDtoDir.resolve(baseName + "LegacyRes.java"), legacyResContent);
// Generate Legacy Converter
String converterContent = """
package %s.%s.converter;
import %s.dto.%sReq;
import %s.dto.%sRes;
import %s.%s.dto.%sLegacyReq;
import %s.%s.dto.%sLegacyRes;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
/**
* @package %s.%s.converter
* @className %sLegacyConverter
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Mapper(componentModel = "spring")
public interface %sLegacyConverter {
// @Mapping(source = "sourceField", target = "targetField")
%sLegacyReq toLegacyReq(%sReq req);
%sRes toRes(%sLegacyRes legacyRes);
}
""".formatted(
BASE_PACKAGE, shortName,
BASE_PACKAGE, baseName,
BASE_PACKAGE, baseName,
BASE_PACKAGE, shortName, baseName,
BASE_PACKAGE, shortName, baseName,
BASE_PACKAGE, shortName, baseName, author, createDate, createDate, author,
baseName, baseName, baseName, baseName, baseName, baseName
);
Files.writeString(converterDir.resolve(baseName + "LegacyConverter.java"), converterContent);
log.append("\n=========================================\n");
log.append(" Scaffolding Complete!\n");
log.append("=========================================\n");
log.append("[Service] ").append(serviceDir.resolve(baseName + "Service.java")).append("\n");
log.append("[Req DTO] ").append(dtoDir.resolve(baseName + "Req.java")).append("\n");
log.append("[Res DTO] ").append(dtoDir.resolve(baseName + "Res.java")).append("\n");
log.append("[Legacy Req DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyReq.java")).append("\n");
log.append("[Legacy Res DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyRes.java")).append("\n");
log.append("[Legacy Converter] ").append(converterDir.resolve(baseName + "LegacyConverter.java")).append("\n");
log.append("\n Tip: ").append(interfaceId).append(" 목업 데이터를 mock-responses.json에 추가하세요.\n");
return log.toString();
}
private static String toPascalCase(String str) {
if (str == null || str.isEmpty()) {
return str;
}
StringBuilder result = new StringBuilder();
boolean capitalizeNext = true;
for (char c : str.toCharArray()) {
if (c == '_' || c == '-' || c == ' ') {
capitalizeNext = true;
} else if (capitalizeNext) {
result.append(Character.toUpperCase(c));
capitalizeNext = false;
} else {
result.append(c);
}
}
if (result.length() > 0) {
result.setCharAt(0, Character.toUpperCase(result.charAt(0)));
}
return result.toString();
}
}

View File

@@ -1,13 +0,0 @@
package io.shinhanlife.glow;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GlowAppServiceId {
String value();
String description() default "";
}

View File

@@ -1,5 +0,0 @@
package io.shinhanlife.glow;
public @interface GlowControllerId {
String value();
}

View File

@@ -1,9 +0,0 @@
package io.shinhanlife.glow;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GlowIndexPaging {
}

View File

@@ -1,7 +0,0 @@
package io.shinhanlife.glow;
public @interface GlowServiceGroupId {
String value();
String description();
}

View File

@@ -1,25 +0,0 @@
package io.shinhanlife.glow;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GlowTrgmField {
/**
* 필드 순서
*/
int order();
/**
* 필드 길이
*/
int length();
/**
* 필드 설명
*/
String description() default "";
}

View File

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

View File

@@ -3,7 +3,7 @@ plugins {
}
dependencies {
implementation project(':dap-common')
implementation project(':dap-was-lib')
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
@@ -11,6 +11,7 @@ dependencies {
// Spring AI MCP Server
implementation 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc'
implementation 'org.springframework.ai:spring-ai-starter-model-openai'
implementation 'org.springframework.ai:spring-ai-openai:2.0.0'
// MyBatis & DB
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'

View File

@@ -1,15 +0,0 @@
package io.shinhanlife.dap.biz.mcp.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication(scanBasePackages = {"io.shinhanlife.dap.biz.mcp.gateway", "io.shinhanlife.dap.common.mcp", "io.shinhanlife.dap.common.config"})
@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dap.biz.mcp.gateway", "io.shinhanlife.dap.common.mcp", "io.shinhanlife.dap.common.config"})
@EnableCaching
public class DapGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(DapGatewayApplication.class, args);
}
}

View File

@@ -1,77 +0,0 @@
package io.shinhanlife.dap.biz.mcp.gateway.guardrail;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
/**
* Audit Log, Redis Trace, Agent 응답 preview에 남으면 안 되는 민감정보를 마스킹합니다.
*/
@Component
public class SensitiveDataMasker {
private static final Set<String> SENSITIVE_KEYS = Set.of(
"password", "passwd", "pwd", "token", "accessToken", "refreshToken", "secret",
"ssn", "rrn", "residentNumber", "cardNumber", "accountNumber", "accountNo",
"phone", "mobile", "email", "idempotencyKey");
private static final Pattern EMAIL = Pattern.compile("([a-zA-Z0-9._%+-]{2})[a-zA-Z0-9._%+-]*(@[a-zA-Z0-9.-]+)");
private static final Pattern CARD_OR_ACCOUNT = Pattern.compile("\\b(\\d{4})\\d{4,12}(\\d{2,4})\\b");
private final ObjectMapper json;
public SensitiveDataMasker(ObjectMapper json) {
this.json = json;
}
/**
* JsonNode 전체를 재귀적으로 순회하며 민감 key와 민감 패턴을 마스킹합니다.
*/
public JsonNode mask(JsonNode input) {
if (input == null || input.isMissingNode() || input.isNull()) {
return json.createObjectNode();
}
if (input.isArray()) {
ArrayNode masked = json.createArrayNode();
for (JsonNode item : input) {
masked.add(mask(item));
}
return masked;
}
if (input.isObject()) {
ObjectNode masked = json.createObjectNode();
Iterator<Map.Entry<String, JsonNode>> fields = input.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> entry = fields.next();
String key = entry.getKey();
JsonNode value = entry.getValue();
if (isSensitiveKey(key)) {
masked.put(key, "***");
} else {
masked.set(key, mask(value));
}
}
return masked;
}
if (input.isTextual()) {
return json.valueToTree(maskText(input.asText()));
}
return input;
}
private boolean isSensitiveKey(String key) {
return key != null && SENSITIVE_KEYS.stream().anyMatch(sensitive -> sensitive.equalsIgnoreCase(key));
}
private String maskText(String value) {
if (value == null || value.isBlank()) {
return value;
}
String masked = EMAIL.matcher(value).replaceAll("$1***$2");
return CARD_OR_ACCOUNT.matcher(masked).replaceAll("$1********$2");
}
}

View File

@@ -1,12 +0,0 @@
package io.shinhanlife.dap.biz.mcp.gateway.resilience;
/**
* Circuit Breaker의 현재 상태입니다.
*
* CLOSED는 정상 호출 가능, OPEN은 호출 차단, HALF_OPEN은 복구 확인 상태를 의미합니다.
*/
public enum CircuitBreakerState {
CLOSED,
OPEN,
HALF_OPEN
}

View File

@@ -1,51 +0,0 @@
package io.shinhanlife.dap.biz.mcp.gateway.transport;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.FailureType;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.ToolExecutionException;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.UUID;
@Slf4j
@Component
public class HttpToolInvoker implements ToolInvoker {
private final RestClient restClient;
private final ObjectMapper objectMapper;
public HttpToolInvoker(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
this.restClient = RestClient.create();
}
@Override
public JsonNode invoke(Map<String, Object> payload, String targetUrl) {
try {
Object httpResult = restClient.post()
.uri(targetUrl)
.contentType(MediaType.APPLICATION_JSON)
// TODO: Use actual tenant's key
.header("X-Trace-Id", UUID.randomUUID().toString())
.body(payload)
.retrieve()
.body(Object.class);
return extractData(objectMapper.valueToTree(httpResult));
} catch (Exception e) {
throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod HTTP 호출 실패: " + e.getMessage(), e);
}
}
private JsonNode extractData(JsonNode root) {
if (!root.path("success").asBoolean(true)) {
throw new ToolExecutionException(FailureType.BUSINESS_ERROR, "Tool 서버 업무 오류: " + root.path("error").asText());
}
return root.has("data") ? root.get("data") : root;
}
}

View File

@@ -1,11 +0,0 @@
package io.shinhanlife.dap.biz.mcp.gateway.transport;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.Map;
/**
* Tool 서버 호출 transport의 최소 공통 인터페이스입니다.
*/
public interface ToolInvoker {
JsonNode invoke(Map<String, Object> payload, String targetUrl);
}

View File

@@ -1,37 +0,0 @@
package io.shinhanlife.dap.biz.so.atm.converter;
import io.shinhanlife.dap.biz.so.atm.dto.AthrOutDto;
import io.shinhanlife.dap.biz.so.atm.dto.AthrSaveInDto;
import io.shinhanlife.dap.biz.so.atm.dto.AthrSearchInDto;
import io.shinhanlife.dap.biz.so.atm.dto.RoleListInDto;
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSaveRequest;
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSearchRequest;
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSearchResponse;
import io.shinhanlife.dap.biz.so.atm.presentation.io.RoleListRequest;
import org.mapstruct.Mapper;
/**
* @package io.shinhanlife.dap.biz.so.atm.converter
* @className AccessMgmtConverter
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Mapper(componentModel = "spring")
public abstract class AccessMgmtConverter {
public abstract RoleListInDto toInDto(RoleListRequest request);
public abstract AthrSearchInDto toInDto(AthrSearchRequest request);
public abstract AthrSaveInDto toInDto(AthrSaveRequest request);
public abstract AthrSearchResponse toResponse(AthrOutDto outDto);
}

View File

@@ -1,48 +0,0 @@
package io.shinhanlife.dap.biz.so.atm.domain.repository;
import io.shinhanlife.dap.biz.so.atm.dto.AthrItemOutDto;
import io.shinhanlife.dap.biz.so.atm.dto.AthrSearchInDto;
import io.shinhanlife.dap.biz.so.atm.dto.RoleKnwlAthrInDto;
import io.shinhanlife.dap.biz.so.atm.dto.RoleListInDto;
import io.shinhanlife.dap.biz.so.atm.dto.RoleListOutDto;
import io.shinhanlife.dap.biz.so.atm.dto.RoleToolAthrInDto;
import io.shinhanlife.glow.GlowMybatisMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* @package io.shinhanlife.dap.biz.so.atm.domain.repository
* @className AccessMgmtMapper
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@GlowMybatisMapper
public interface AccessMgmtMapper {
List<RoleListOutDto> selectRoles(RoleListInDto inDto);
List<AthrItemOutDto> selectAllTools();
List<AthrItemOutDto> selectAllKnwls();
List<String> selectGrantedToolIds(AthrSearchInDto inDto);
List<String> selectGrantedKnwlIds(AthrSearchInDto inDto);
void deleteToolAthr(@Param("systId") String systId, @Param("roleNo") String roleNo);
void insertToolAthr(RoleToolAthrInDto inDto);
void deleteKnwlAthr(@Param("systId") String systId, @Param("roleNo") String roleNo);
void insertKnwlAthr(RoleKnwlAthrInDto inDto);
}

View File

@@ -1,32 +0,0 @@
package io.shinhanlife.dap.biz.so.atm.domain.service;
import io.shinhanlife.dap.biz.so.atm.dto.AthrOutDto;
import io.shinhanlife.dap.biz.so.atm.dto.AthrSaveInDto;
import io.shinhanlife.dap.biz.so.atm.dto.AthrSearchInDto;
import io.shinhanlife.dap.biz.so.atm.dto.RoleListInDto;
import io.shinhanlife.dap.biz.so.atm.dto.RoleListOutDto;
import java.util.List;
/**
* @package io.shinhanlife.dap.biz.so.atm.domain.service
* @className AccessMgmtService
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
public interface AccessMgmtService {
List<RoleListOutDto> getRoles(RoleListInDto inDto);
AthrOutDto getAthr(AthrSearchInDto inDto);
void saveAthr(AthrSaveInDto inDto);
}

View File

@@ -1,142 +0,0 @@
package io.shinhanlife.dap.biz.so.atm.domain.service.impl;
import io.shinhanlife.dap.biz.so.atm.domain.repository.AccessMgmtMapper;
import io.shinhanlife.dap.biz.so.atm.domain.service.AccessMgmtService;
import io.shinhanlife.dap.biz.so.atm.dto.AthrItemOutDto;
import io.shinhanlife.dap.biz.so.atm.dto.AthrOutDto;
import io.shinhanlife.dap.biz.so.atm.dto.AthrSaveInDto;
import io.shinhanlife.dap.biz.so.atm.dto.AthrSearchInDto;
import io.shinhanlife.dap.biz.so.atm.dto.RoleKnwlAthrInDto;
import io.shinhanlife.dap.biz.so.atm.dto.RoleListInDto;
import io.shinhanlife.dap.biz.so.atm.dto.RoleListOutDto;
import io.shinhanlife.dap.biz.so.atm.dto.RoleToolAthrInDto;
import io.shinhanlife.dap.common.util.SessionUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* @package io.shinhanlife.dap.biz.so.atm.domain.service.impl
* @className AccessMgmtServiceImpl
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Service
@RequiredArgsConstructor
public class AccessMgmtServiceImpl implements AccessMgmtService {
private final AccessMgmtMapper accessMgmtMapper;
private static final String SYSTEM_CD = "AXH";
private static final String SYSTEM_PRG = "SOATM0100";
@Override
public List<RoleListOutDto> getRoles(RoleListInDto inDto) {
return accessMgmtMapper.selectRoles(inDto);
}
@Override
public AthrOutDto getAthr(AthrSearchInDto inDto) {
Set<String> grantedToolIds = accessMgmtMapper.selectGrantedToolIds(inDto)
.stream().collect(Collectors.toSet());
Set<String> grantedKnwlIds = accessMgmtMapper.selectGrantedKnwlIds(inDto)
.stream().collect(Collectors.toSet());
List<AthrItemOutDto> tools = accessMgmtMapper.selectAllTools()
.stream()
.peek(item -> item.setGranted(grantedToolIds.contains(item.getResourceId())))
.collect(Collectors.toList());
List<AthrItemOutDto> knwls = accessMgmtMapper.selectAllKnwls()
.stream()
.peek(item -> item.setGranted(grantedKnwlIds.contains(item.getResourceId())))
.collect(Collectors.toList());
return new AthrOutDto(tools, knwls);
}
@Override
@Transactional
public void saveAthr(AthrSaveInDto inDto) {
Date now = new Date();
String systId = inDto.getSystId();
String roleNo = inDto.getRoleNo();
accessMgmtMapper.deleteToolAthr(systId, roleNo);
if (inDto.getGrantedToolIds() != null) {
for (String toolId : inDto.getGrantedToolIds()) {
accessMgmtMapper.insertToolAthr(buildToolAthrInDto(systId, roleNo, toolId, now));
}
}
accessMgmtMapper.deleteKnwlAthr(systId, roleNo);
if (inDto.getGrantedKnwlIds() != null) {
for (String knwlId : inDto.getGrantedKnwlIds()) {
accessMgmtMapper.insertKnwlAthr(buildKnwlAthrInDto(systId, roleNo, knwlId, now));
}
}
}
private RoleToolAthrInDto buildToolAthrInDto(String systId, String roleNo, String toolId, Date now) {
RoleToolAthrInDto dto = new RoleToolAthrInDto();
dto.setRoleToolAthrId("RTA-" + shortUuid());
dto.setSystId(systId);
dto.setRoleNo(roleNo);
dto.setToolId(toolId);
dto.setPuseYn("Y");
fillAudit(dto, now);
return dto;
}
private RoleKnwlAthrInDto buildKnwlAthrInDto(String systId, String roleNo, String knwlId, Date now) {
RoleKnwlAthrInDto dto = new RoleKnwlAthrInDto();
dto.setRoleKnwlAthrId("RKA-" + shortUuid());
dto.setSystId(systId);
dto.setRoleNo(roleNo);
dto.setKnwlId(knwlId);
dto.setPuseYn("Y");
fillAudit(dto, now);
return dto;
}
private void fillAudit(RoleToolAthrInDto dto, Date now) {
String prafNo = SessionUtil.getPrafNo();
String ognzNo = SessionUtil.getOgnzNo();
dto.setSystRgiDt(now); dto.setSystRgiPrafNo(prafNo);
dto.setSystRgiOgnzNo(ognzNo); dto.setSystRgiSystCd(SYSTEM_CD);
dto.setSystRgiPrgrId(SYSTEM_PRG);
dto.setSystChgDt(now); dto.setSystChgPrafNo(prafNo);
dto.setSystChgOgnzNo(ognzNo); dto.setSystChgSystCd(SYSTEM_CD);
dto.setSystChgPrgrId(SYSTEM_PRG);
}
private void fillAudit(RoleKnwlAthrInDto dto, Date now) {
String prafNo = SessionUtil.getPrafNo();
String ognzNo = SessionUtil.getOgnzNo();
dto.setSystRgiDt(now); dto.setSystRgiPrafNo(prafNo);
dto.setSystRgiOgnzNo(ognzNo); dto.setSystRgiSystCd(SYSTEM_CD);
dto.setSystRgiPrgrId(SYSTEM_PRG);
dto.setSystChgDt(now); dto.setSystChgPrafNo(prafNo);
dto.setSystChgOgnzNo(ognzNo); dto.setSystChgSystCd(SYSTEM_CD);
dto.setSystChgPrgrId(SYSTEM_PRG);
}
private String shortUuid() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 12).toUpperCase();
}
}

View File

@@ -1,35 +0,0 @@
package io.shinhanlife.dap.biz.so.atm.dto;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
* @package io.shinhanlife.dap.biz.so.atm.dto
* @className AthrItemOutDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AthrItemOutDto {
private String resourceId;
private String resourceNm;
private String resourceDs;
private String typeCode;
private boolean granted;
}

View File

@@ -1,34 +0,0 @@
package io.shinhanlife.dap.biz.so.atm.dto;
import lombok.Setter;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.List;
/**
* @package io.shinhanlife.dap.biz.so.atm.dto
* @className AthrOutDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@AllArgsConstructor
@Setter
@Builder
@NoArgsConstructor
public class AthrOutDto {
private List<AthrItemOutDto> tools;
private List<AthrItemOutDto> knwls;
}

View File

@@ -1,36 +0,0 @@
package io.shinhanlife.dap.biz.so.atm.dto;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* @package io.shinhanlife.dap.biz.so.atm.dto
* @className AthrSaveInDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AthrSaveInDto {
private String systId;
private String roleNo;
private List<String> grantedToolIds;
private List<String> grantedKnwlIds;
}

View File

@@ -1,32 +0,0 @@
package io.shinhanlife.dap.biz.so.atm.dto;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
* @package io.shinhanlife.dap.biz.so.atm.dto
* @className AthrSearchInDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AthrSearchInDto {
private String systId;
private String roleNo;
}

View File

@@ -1,48 +0,0 @@
package io.shinhanlife.dap.biz.so.atm.dto;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.util.Date;
/**
* @package io.shinhanlife.dap.biz.so.atm.dto
* @className RoleKnwlAthrInDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RoleKnwlAthrInDto {
private String roleKnwlAthrId;
private String systId;
private String roleNo;
private String knwlId;
private String puseYn;
private Date systRgiDt;
private String systRgiPrafNo;
private String systRgiOgnzNo;
private String systRgiSystCd;
private String systRgiPrgrId;
private Date systChgDt;
private String systChgPrafNo;
private String systChgOgnzNo;
private String systChgSystCd;
private String systChgPrgrId;
}

View File

@@ -1,33 +0,0 @@
package io.shinhanlife.dap.biz.so.atm.dto;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
* @package io.shinhanlife.dap.biz.so.atm.dto
* @className RoleListInDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RoleListInDto {
private String systId;
private String puseYn;
private String keyword;
}

View File

@@ -1,35 +0,0 @@
package io.shinhanlife.dap.biz.so.atm.dto;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
* @package io.shinhanlife.dap.biz.so.atm.dto
* @className RoleListOutDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RoleListOutDto {
private String systId;
private String roleNo;
private String roleNm;
private String roleDs;
private String puseYn;
}

View File

@@ -1,48 +0,0 @@
package io.shinhanlife.dap.biz.so.atm.dto;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.util.Date;
/**
* @package io.shinhanlife.dap.biz.so.atm.dto
* @className RoleToolAthrInDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RoleToolAthrInDto {
private String roleToolAthrId;
private String systId;
private String roleNo;
private String toolId;
private String puseYn;
private Date systRgiDt;
private String systRgiPrafNo;
private String systRgiOgnzNo;
private String systRgiSystCd;
private String systRgiPrgrId;
private Date systChgDt;
private String systChgPrafNo;
private String systChgOgnzNo;
private String systChgSystCd;
private String systChgPrgrId;
}

View File

@@ -1,59 +0,0 @@
package io.shinhanlife.dap.biz.so.atm.presentation;
import io.shinhanlife.dap.biz.so.atm.dto.RoleListOutDto;
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSaveRequest;
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSearchRequest;
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSearchResponse;
import io.shinhanlife.dap.biz.so.atm.presentation.io.RoleListRequest;
import io.shinhanlife.dap.biz.so.atm.usecase.AccessMgmtUseCase;
import io.shinhanlife.glow.BaseResponse;
import io.shinhanlife.glow.ResponseUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @package io.shinhanlife.dap.biz.so.atm.presentation
* @className SOATM0100Controller
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@RestController
@RequestMapping("/so/atm")
@RequiredArgsConstructor
public class SOATM0100Controller {
private final AccessMgmtUseCase accessMgmtUseCase;
@PostMapping("/SOATM0100R")
public ResponseEntity<BaseResponse<List<RoleListOutDto>>> getRoles(
@RequestBody RoleListRequest request) {
return ResponseUtil.ok(accessMgmtUseCase.getRoles(request));
}
@PostMapping("/SOATM0101R")
public ResponseEntity<BaseResponse<AthrSearchResponse>> getAthr(
@RequestBody AthrSearchRequest request) {
return ResponseUtil.ok(accessMgmtUseCase.getAthr(request));
}
@PostMapping("/SOATM0100U")
public ResponseEntity<BaseResponse<Void>> saveAthr(
@RequestBody AthrSaveRequest request) {
accessMgmtUseCase.saveAthr(request);
return ResponseUtil.ok();
}
}

View File

@@ -1,29 +0,0 @@
package io.shinhanlife.dap.biz.so.atm.presentation.io;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* @package io.shinhanlife.dap.biz.so.atm.presentation.io
* @className AthrSaveRequest
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
public class AthrSaveRequest {
private String systId;
private String roleNo;
private List<String> grantedToolIds;
private List<String> grantedKnwlIds;
}

View File

@@ -1,25 +0,0 @@
package io.shinhanlife.dap.biz.so.atm.presentation.io;
import lombok.Getter;
import lombok.Setter;
/**
* @package io.shinhanlife.dap.biz.so.atm.presentation.io
* @className AthrSearchRequest
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
public class AthrSearchRequest {
private String systId;
private String roleNo;
}

View File

@@ -1,28 +0,0 @@
package io.shinhanlife.dap.biz.so.atm.presentation.io;
import io.shinhanlife.dap.biz.so.atm.dto.AthrItemOutDto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.List;
/**
* @package io.shinhanlife.dap.biz.so.atm.presentation.io
* @className AthrSearchResponse
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@AllArgsConstructor
public class AthrSearchResponse {
private List<AthrItemOutDto> tools;
private List<AthrItemOutDto> knwls;
}

View File

@@ -1,26 +0,0 @@
package io.shinhanlife.dap.biz.so.atm.presentation.io;
import lombok.Getter;
import lombok.Setter;
/**
* @package io.shinhanlife.dap.biz.so.atm.presentation.io
* @className RoleListRequest
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
public class RoleListRequest {
private String systId;
private String puseYn;
private String keyword;
}

View File

@@ -1,32 +0,0 @@
package io.shinhanlife.dap.biz.so.atm.usecase;
import io.shinhanlife.dap.biz.so.atm.dto.RoleListOutDto;
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSaveRequest;
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSearchRequest;
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSearchResponse;
import io.shinhanlife.dap.biz.so.atm.presentation.io.RoleListRequest;
import java.util.List;
/**
* @package io.shinhanlife.dap.biz.so.atm.usecase
* @className AccessMgmtUseCase
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
public interface AccessMgmtUseCase {
List<RoleListOutDto> getRoles(RoleListRequest request);
AthrSearchResponse getAthr(AthrSearchRequest request);
void saveAthr(AthrSaveRequest request);
}

View File

@@ -1,51 +0,0 @@
package io.shinhanlife.dap.biz.so.atm.usecase.impl;
import io.shinhanlife.dap.biz.so.atm.converter.AccessMgmtConverter;
import io.shinhanlife.dap.biz.so.atm.domain.service.AccessMgmtService;
import io.shinhanlife.dap.biz.so.atm.dto.RoleListOutDto;
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSaveRequest;
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSearchRequest;
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSearchResponse;
import io.shinhanlife.dap.biz.so.atm.presentation.io.RoleListRequest;
import io.shinhanlife.dap.biz.so.atm.usecase.AccessMgmtUseCase;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @package io.shinhanlife.dap.biz.so.atm.usecase.impl
* @className AccessMgmtUseCaseImpl
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Service
@RequiredArgsConstructor
public class AccessMgmtUseCaseImpl implements AccessMgmtUseCase {
private final AccessMgmtService accessMgmtService;
private final AccessMgmtConverter converter;
@Override
public List<RoleListOutDto> getRoles(RoleListRequest request) {
return accessMgmtService.getRoles(converter.toInDto(request));
}
@Override
public AthrSearchResponse getAthr(AthrSearchRequest request) {
return converter.toResponse(accessMgmtService.getAthr(converter.toInDto(request)));
}
@Override
public void saveAthr(AthrSaveRequest request) {
accessMgmtService.saveAthr(converter.toInDto(request));
}
}

View File

@@ -0,0 +1,32 @@
package io.shinhanlife.dap.mcg;
/**
* @package io.shinhanlife.dap.mcg
* @className DapGatewayApplication
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication(scanBasePackages = {"io.shinhanlife.dap.mcg", "io.shinhanlife.dap.lib.mcp", "io.shinhanlife.dap.lib.config"})
@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dap.mcg", "io.shinhanlife.dap.lib.mcp", "io.shinhanlife.dap.lib.config"})
@EnableCaching
@EnableScheduling
public class DapGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(DapGatewayApplication.class, args);
}
}

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.dap.biz.mcp.gateway.aop;
package io.shinhanlife.dap.mcg.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
@@ -8,16 +8,16 @@ import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
/**
* @package io.shinhanlife.dap.biz.mcp.gateway.aop
* @package io.shinhanlife.dap.mcg.aop
* @className GatewayLoggingAspect
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@@ -27,7 +27,7 @@ import org.springframework.util.StopWatch;
public class GatewayLoggingAspect {
// gateway의 controller 패키지 하위의 모든 클래스/메서드 실행 작동
@Around("execution(* io.shinhanlife.dap.biz.mcp.gateway.presentation..*(..))")
@Around("execution(* io.shinhanlife.dap.mcg.presentation..*(..))")
public Object logGatewayExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
String targetMethod = joinPoint.getSignature().toShortString();
StopWatch stopWatch = new StopWatch();

View File

@@ -1,8 +1,22 @@
package io.shinhanlife.dap.biz.mcp.gateway.audit;
package io.shinhanlife.dap.mcg.audit;
import io.shinhanlife.dap.biz.mcp.gateway.config.McpGatewayProperties;
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.SensitiveDataMasker;
import io.shinhanlife.dap.biz.mcp.gateway.security.McpRequestContext;
/**
* @package io.shinhanlife.dap.mcg.audit
* @className AuditLogService
* @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 com.fasterxml.jackson.databind.JsonNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -17,11 +31,8 @@ import org.springframework.stereotype.Service;
public class AuditLogService {
private static final Logger audit = LoggerFactory.getLogger("MCP_AUDIT");
private final McpGatewayProperties properties;
private final SensitiveDataMasker masker;
public AuditLogService(McpGatewayProperties properties, SensitiveDataMasker masker) {
public AuditLogService(McpGatewayProperties properties) {
this.properties = properties;
this.masker = masker;
}
/**
@@ -32,7 +43,7 @@ public class AuditLogService {
return;
}
audit.info("event=tool_started requestId={} agentId={} userId={} clientAddress={} tool={} arguments={}",
context.requestId(), context.agentId(), context.userId(), context.clientAddress(), toolName, masker.mask(arguments));
context.requestId(), context.agentId(), context.userId(), context.clientAddress(), toolName, arguments);
}
/**

View File

@@ -1,5 +1,20 @@
package io.shinhanlife.dap.biz.mcp.gateway.config;
package io.shinhanlife.dap.mcg.config;
/**
* @package io.shinhanlife.dap.mcg.config
* @className AgentResponseBudgetProperties
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.dap.biz.mcp.gateway.config;
package io.shinhanlife.dap.mcg.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -8,16 +8,16 @@ import java.util.HashMap;
import java.util.Map;
/**
* @package io.shinhanlife.dap.biz.mcp.gateway.config
* @package io.shinhanlife.dap.mcg.config
* @className GatewayFallbackProperties
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
* 2026.09.01 0986406 최초생성
*
* </pre>
*/

View File

@@ -1,5 +1,20 @@
package io.shinhanlife.dap.biz.mcp.gateway.config;
package io.shinhanlife.dap.mcg.config;
/**
* @package io.shinhanlife.dap.mcg.config
* @className McpGatewayProperties
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@@ -10,7 +25,7 @@ import java.util.stream.Collectors;
@Component
@ConfigurationProperties(prefix = "mcp")
/**
* application.properties mcp.* 설정을 Java 코드에서 사용하기 위한 설정 클래스입니다.
* application.yml mcp.* 설정을 Java 코드에서 사용하기 위한 설정 클래스입니다.
*
* API Key, 허용 Tool, 감사 로그, Redis Trace, Retry, Circuit Breaker,
* EIMS/Tool 서버 주소 같은 MCP Gateway 운영 설정을 곳에서 관리합니다.

View File

@@ -1,6 +1,6 @@
package io.shinhanlife.dap.biz.mcp.gateway.config;
package io.shinhanlife.dap.mcg.config;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
@@ -10,16 +10,16 @@ import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @package io.shinhanlife.dap.biz.mcp.gateway.config
* @package io.shinhanlife.dap.mcg.config
* @className RedisConfig
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
* 2026.09.01 0986406 최초생성
*
* </pre>
*/

View File

@@ -1,8 +1,23 @@
package io.shinhanlife.dap.biz.mcp.gateway.guardrail;
package io.shinhanlife.dap.mcg.guardrail;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.FailureType;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.ToolExecutionException;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
/**
* @package io.shinhanlife.dap.mcg.guardrail
* @className GuardrailService
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.stereotype.Service;

View File

@@ -1,8 +1,23 @@
package io.shinhanlife.dap.biz.mcp.gateway.guardrail;
package io.shinhanlife.dap.mcg.guardrail;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.FailureType;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.ToolExecutionException;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
/**
* @package io.shinhanlife.dap.mcg.guardrail
* @className ToolResponseGuardrailService
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.dap.biz.mcp.gateway.messaging;
package io.shinhanlife.dap.mcg.messaging;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -10,16 +10,16 @@ import java.util.Map;
import java.util.UUID;
/**
* @package io.shinhanlife.dap.biz.mcp.gateway.messaging
* @package io.shinhanlife.dap.mcg.messaging
* @className KafkaProducerService
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
* 2026.09.01 0986406 최초생성
*
* </pre>
*/

View File

@@ -1,8 +1,24 @@
package io.shinhanlife.dap.biz.mcp.gateway.presentation;
package io.shinhanlife.dap.mcg.presentation;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
import io.shinhanlife.dap.biz.mcp.gateway.registry.RedisRegistryService;
import io.shinhanlife.dap.biz.mcp.gateway.service.ExecuteService;
/**
* @package io.shinhanlife.dap.mcg.presentation
* @className ChatController
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.shinhanlife.dap.mcg.service.ExecuteService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
@@ -11,6 +27,7 @@ import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import reactor.core.publisher.Flux;
@@ -26,6 +43,7 @@ public class ChatController {
private final ObjectMapper objectMapper;
private final RedisRegistryService registryService;
private final ChatClient.Builder chatClientBuilder;
private final McpRouterController mcpRouterController;
@PostMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter chatStream(@RequestBody Map<String, String> request,
@@ -39,19 +57,45 @@ public class ChatController {
try {
List<ToolCallback> callbacks = new ArrayList<>();
for (ToolMetadata meta : registryService.getAllTools()) {
if (Boolean.TRUE.equals(meta.getVisible())) {
callbacks.add(new DynamicMcpToolCallback(meta, executeService, objectMapper, effectiveTenantId));
JsonRpcResponse toolsResponse = mcpRouterController.listTools(null).getBody();
if (toolsResponse != null && toolsResponse.getResult() instanceof Map) {
Map<String, Object> resultMap = (Map<String, Object>) toolsResponse.getResult();
if (resultMap.containsKey("tools")) {
List<ToolMetadata> allTools = (List<ToolMetadata>) resultMap.get("tools");
Set<String> addedToolNames = new HashSet<>();
for (ToolMetadata meta : allTools) {
if (Boolean.TRUE.equals(meta.getVisible()) && meta.getName() != null) {
String cleanName = meta.getName().replaceAll("[^a-zA-Z0-9_-]", "_");
if (!addedToolNames.contains(cleanName)) {
callbacks.add(new DynamicMcpToolCallback(meta, executeService, objectMapper, effectiveTenantId));
addedToolNames.add(cleanName);
}
}
}
}
}
ChatClient chatClient = chatClientBuilder
String selectedModel = request.getOrDefault("model", "gemini-flash-latest").trim();
if (selectedModel.isEmpty()) {
selectedModel = "gemini-flash-latest";
}
// 1. gemini-flash-latest 선택 OpenRouter의 Google Gemma 4 모델로 라우팅
if (selectedModel.equals("gemini-flash-latest")) {
selectedModel = "google/gemma-4-31b-it:free";
log.info("[Real AI Chat] Gemini 직접 API → OpenRouter Google Gemma 4 31B 로 라우팅 전환");
}
// 2. OpenRouter 무료 모델 처리 (Spring AI 빌드된 ChatClient MCP 호출 완벽 지원!)
ChatClient activeChatClient = chatClientBuilder
.defaultSystem("You are AX HUB Assistant, a highly capable enterprise AI agent. You must use the provided tools to answer user questions when necessary. Always answer politely in Korean.")
.build();
Flux<String> responseStream = chatClient.prompt()
Flux<String> responseStream = activeChatClient.prompt()
.user(message)
.tools((Object[]) callbacks.toArray(new ToolCallback[0])) // Spring AI 2.0 uses tools()
.options(org.springframework.ai.openai.OpenAiChatOptions.builder()
.model(selectedModel))
.stream()
.content();
@@ -74,7 +118,7 @@ public class ChatController {
} else if (errorMsg != null && errorMsg.contains("503")) {
emitter.send("\n\n⚠ **AI 모델 서버 혼잡 (503)**: 현재 AI 모델을 제공하는 서버에 일시적으로 접속자가 많아 지연이 발생하고 있습니다. 잠시 후 다시 시도해 주세요.");
} else if (errorMsg != null && errorMsg.contains("429")) {
emitter.send("\n\n⚠ **API 사용량 초과 (429)**: 현재 사용 중인 Gemini API(무료 티어)의 일일 또는 분당 요청 한도를 초과했습니다. 잠시 후 다시 시도하시거나 API 플랜을 확인해 주세요.");
emitter.send("\n\n⚠ **API 사용량 초과 (429)**: AI 모델 API(무료 티어 또는 오픈라우터)의 요청 한도를 초과했습니다. 잠시 후 다시 시도하시거나 API 키의 유효 한도를 확인해 주세요.");
} else {
emitter.send("\n[에러 발생: " + errorMsg + "]");
}

View File

@@ -1,6 +1,6 @@
package io.shinhanlife.dap.biz.mcp.gateway.presentation;
package io.shinhanlife.dap.mcg.presentation;
import io.shinhanlife.dap.biz.mcp.gateway.service.KillSwitchService;
import io.shinhanlife.dap.mcg.service.KillSwitchService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@@ -8,16 +8,16 @@ import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* @package io.shinhanlife.dap.biz.mcp.gateway.presentation
* @package io.shinhanlife.dap.mcg.presentation
* @className KillSwitchController
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
* 2026.09.01 0986406 최초생성
*
* </pre>
*/

View File

@@ -1,14 +1,29 @@
package io.shinhanlife.dap.biz.mcp.gateway.presentation;
package io.shinhanlife.dap.mcg.presentation;
/**
* @package io.shinhanlife.dap.mcg.presentation
* @className McpRouterController
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dap.biz.mcp.adapter.dto.JsonRpcRequest;
import io.shinhanlife.dap.biz.mcp.adapter.dto.JsonRpcResponse;
import io.shinhanlife.dap.biz.mcp.adapter.dto.Params;
import io.shinhanlife.dap.biz.mcp.gateway.config.GatewayFallbackProperties;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
import io.shinhanlife.dap.biz.mcp.gateway.registry.RedisRegistryService;
import io.shinhanlife.dap.biz.mcp.gateway.service.ExecuteService;
import io.shinhanlife.dap.common.mcp.security.SecurityProperties;
import io.shinhanlife.dap.lib.adapter.dto.JsonRpcRequest;
import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse;
import io.shinhanlife.dap.lib.adapter.dto.Params;
import io.shinhanlife.dap.mcg.config.GatewayFallbackProperties;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.shinhanlife.dap.mcg.service.ExecuteService;
import io.shinhanlife.dap.lib.mcp.security.SecurityProperties;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.time.LocalDateTime;
@@ -76,7 +91,7 @@ public class McpRouterController {
try {
Object result = executeService.execute(payload, effectiveTenantId);
io.shinhanlife.dap.biz.mcp.adapter.dto.JsonRpcResponse response = new io.shinhanlife.dap.biz.mcp.adapter.dto.JsonRpcResponse();
JsonRpcResponse response = new JsonRpcResponse();
response.setJsonrpc("2.0");
response.setId(payload.containsKey("id") ? String.valueOf(payload.get("id")) : UUID.randomUUID().toString());
response.setResult(result);
@@ -97,9 +112,7 @@ public class McpRouterController {
}
}
@GetMapping("/tools/list")
public ResponseEntity<JsonRpcResponse> listTools(
@RequestParam(value = "categoryKey", required = false) String categoryKey) {
private List<ToolMetadata> fetchAllActiveTools() {
List<ToolMetadata> activeTools = redisRegistryService.getAllTools()
.stream()
.filter(ToolMetadata::getVisible)
@@ -133,6 +146,14 @@ public class McpRouterController {
log.warn("Failed to fetch local tools from fallback URL: {}", url);
}
}
return activeTools;
}
@GetMapping("/tools/list")
public ResponseEntity<JsonRpcResponse> listTools(
@RequestParam(value = "categoryKey", required = false) String categoryKey) {
List<ToolMetadata> activeTools = fetchAllActiveTools();
if (categoryKey != null && !categoryKey.trim().isEmpty()) {
activeTools = activeTools.stream()
@@ -146,12 +167,10 @@ public class McpRouterController {
return ResponseEntity.ok(response);
}
@GetMapping(value = "/tools/docs/markdown", produces = "text/markdown;charset=UTF-8")
public ResponseEntity<String> generateToolsMarkdown() {
List<ToolMetadata> tools = redisRegistryService.getAllTools()
.stream()
.filter(ToolMetadata::getVisible)
.collect(Collectors.toList());
List<ToolMetadata> tools = fetchAllActiveTools();
StringBuilder md = new StringBuilder();
md.append("# \uD83E\uDD16 Shinhan AI Tool Catalog\n\n");

View File

@@ -1,8 +1,23 @@
package io.shinhanlife.dap.biz.mcp.gateway.presentation;
package io.shinhanlife.dap.mcg.presentation;
import io.shinhanlife.dap.common.util.PodScaffolder;
import io.shinhanlife.dap.common.util.ToolScaffolder;
import io.shinhanlife.dap.common.util.ToolSourceUpdater;
/**
* @package io.shinhanlife.dap.mcg.presentation
* @className ScaffoldingController
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.lib.util.PodScaffolder;
import io.shinhanlife.dap.lib.util.ToolScaffolder;
import io.shinhanlife.dap.lib.util.ToolSourceUpdater;
import java.io.File;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
@@ -19,7 +34,7 @@ public class ScaffoldingController {
@PostMapping("/pod")
public String scaffoldPod(@RequestBody Map<String, String> req) {
try {
String moduleName = req.getOrDefault("moduleName", "dap-tool-other");
String moduleName = req.getOrDefault("moduleName", "dap-was-oth");
if (!moduleName.startsWith("dap-tool-")) moduleName = "dap-tool-" + moduleName;
String port = req.getOrDefault("port", "8085");
String shortName = moduleName.replace("dap-tool-", "").replace("-", "");
@@ -42,14 +57,15 @@ public class ScaffoldingController {
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-tool-other");
String moduleName = req.getOrDefault("moduleName", "dap-was-oth");
String author = req.get("author");
if (author == null || author.trim().isEmpty()) author = System.getProperty("user.name");
String date = req.get("date");
if (date == null || date.trim().isEmpty()) date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
boolean register = Boolean.parseBoolean(req.getOrDefault("register", "true"));
String clientSystemCode = req.get("clientSystemCode");
return ToolScaffolder.scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, date, register);
return ToolScaffolder.scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, date, register, clientSystemCode);
} catch (Exception e) {
return "오류 발생: " + e.getMessage();
}
@@ -78,13 +94,15 @@ public class ScaffoldingController {
if (sourceDir == null) sourceDir = System.getProperty("user.dir");
File dir = new File(sourceDir);
File[] files = dir.listFiles(f -> f.isDirectory() && f.getName().startsWith("dap-tool-") && !f.getName().equals("dap-tool-core"));
File[] files = dir.listFiles(f -> f.isDirectory() && f.getName().startsWith("dap-tool-") && !f.getName().equals("dap-was-lib"));
if (files == null) return List.of("dap-tool-other");
if (files == null || files.length == 0) {
return List.of("dap-was-oth", "dap-tool-hr", "dap-was-sms");
}
return Arrays.stream(files).map(File::getName).sorted().collect(Collectors.toList());
} catch (Exception e) {
return List.of("dap-tool-other");
return List.of("dap-was-oth", "dap-tool-hr", "dap-was-sms");
}
}
}

View File

@@ -1,5 +1,20 @@
package io.shinhanlife.dap.biz.mcp.gateway.redis;
package io.shinhanlife.dap.mcg.redis;
/**
* @package io.shinhanlife.dap.mcg.redis
* @className McpMonitorEventService
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

View File

@@ -1,9 +1,23 @@
package io.shinhanlife.dap.biz.mcp.gateway.redis;
package io.shinhanlife.dap.mcg.redis;
import io.shinhanlife.dap.biz.mcp.gateway.config.McpGatewayProperties;
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.SensitiveDataMasker;
import io.shinhanlife.dap.biz.mcp.gateway.security.McpRequestContext;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
/**
* @package io.shinhanlife.dap.mcg.redis
* @className RedisToolTraceService
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dap.mcg.security.McpRequestContext;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
@@ -34,19 +48,16 @@ public class RedisToolTraceService {
private final ObjectProvider<StringRedisTemplate> redisProvider;
private final ObjectMapper json;
private final McpMonitorEventService monitorEvents;
private final SensitiveDataMasker masker;
private final Map<String, AttemptState> attemptStates = new ConcurrentHashMap<>();
public RedisToolTraceService(McpGatewayProperties properties,
ObjectProvider<StringRedisTemplate> redisProvider,
ObjectMapper json,
McpMonitorEventService monitorEvents,
SensitiveDataMasker masker) {
McpMonitorEventService monitorEvents) {
this.properties = properties;
this.redisProvider = redisProvider;
this.json = json;
this.monitorEvents = monitorEvents;
this.masker = masker;
}
/**
@@ -191,7 +202,7 @@ public class RedisToolTraceService {
arguments.fieldNames().forEachRemaining(argNames::add);
trace.put("argumentNames", argNames);
trace.put("arguments", masker.mask(arguments).toString());
trace.put("arguments", arguments.toString());
trace.put("responseSummary", responseSummary(responseText));
trace.put("timestamp", Instant.now().toString());
return json.writeValueAsString(trace);

View File

@@ -1,6 +1,6 @@
package io.shinhanlife.dap.biz.mcp.gateway.registry;
package io.shinhanlife.dap.mcg.registry;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
@@ -13,16 +13,16 @@ import java.util.stream.Collectors;
import java.util.Objects;
/**
* @package io.shinhanlife.dap.biz.mcp.gateway.registry
* @package io.shinhanlife.dap.mcg.registry
* @className RedisRegistryService
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
* 2026.09.01 0986406 최초생성
*
* </pre>
*/

View File

@@ -1,5 +1,20 @@
package io.shinhanlife.dap.biz.mcp.gateway.resilience;
package io.shinhanlife.dap.mcg.resilience;
/**
* @package io.shinhanlife.dap.mcg.resilience
* @className CircuitBreaker
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import java.time.Clock;
import java.time.Instant;

View File

@@ -1,5 +1,20 @@
package io.shinhanlife.dap.biz.mcp.gateway.resilience;
package io.shinhanlife.dap.mcg.resilience;
/**
* @package io.shinhanlife.dap.mcg.resilience
* @className CircuitBreakerMonitorController
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

View File

@@ -1,6 +1,21 @@
package io.shinhanlife.dap.biz.mcp.gateway.resilience;
package io.shinhanlife.dap.mcg.resilience;
import io.shinhanlife.dap.biz.mcp.gateway.config.McpGatewayProperties;
/**
* @package io.shinhanlife.dap.mcg.resilience
* @className CircuitBreakerService
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import org.springframework.stereotype.Service;
import java.time.Clock;

View File

@@ -1,5 +1,20 @@
package io.shinhanlife.dap.biz.mcp.gateway.resilience;
package io.shinhanlife.dap.mcg.resilience;
/**
* @package io.shinhanlife.dap.mcg.resilience
* @className CircuitBreakerSnapshot
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
/**
* MCP Monitor 화면에 내려주는 Circuit Breaker 상태 정보입니다.
*/

View File

@@ -0,0 +1,27 @@
package io.shinhanlife.dap.mcg.resilience;
/**
* @package io.shinhanlife.dap.mcg.resilience
* @className CircuitBreakerState
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
/**
* Circuit Breaker의 현재 상태입니다.
*
* CLOSED는 정상 호출 가능, OPEN은 호출 차단, HALF_OPEN은 복구 확인 상태를 의미합니다.
*/
public enum CircuitBreakerState {
CLOSED,
OPEN,
HALF_OPEN
}

View File

@@ -1,5 +1,20 @@
package io.shinhanlife.dap.biz.mcp.gateway.resilience;
package io.shinhanlife.dap.mcg.resilience;
/**
* @package io.shinhanlife.dap.mcg.resilience
* @className FailureType
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
/**
* Tool/EIMS/Tool 서버 호출 실패를 업무적으로 구분하기 위한 장애 유형입니다.
*

View File

@@ -1,5 +1,20 @@
package io.shinhanlife.dap.biz.mcp.gateway.resilience;
package io.shinhanlife.dap.mcg.resilience;
/**
* @package io.shinhanlife.dap.mcg.resilience
* @className RetryPolicy
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
/**
* Tool 호출 재시도 정책입니다.
*

View File

@@ -1,5 +1,20 @@
package io.shinhanlife.dap.biz.mcp.gateway.resilience;
package io.shinhanlife.dap.mcg.resilience;
/**
* @package io.shinhanlife.dap.mcg.resilience
* @className ToolExecutionException
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
/**
* Tool 실행 발생한 오류를 FailureType과 함께 전달하는 공통 예외입니다.
*

View File

@@ -1,5 +1,20 @@
package io.shinhanlife.dap.biz.mcp.gateway.security;
package io.shinhanlife.dap.mcg.security;
/**
* @package io.shinhanlife.dap.mcg.security
* @className McpRequestContext
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import java.util.Set;
/**

View File

@@ -1,5 +1,20 @@
package io.shinhanlife.dap.biz.mcp.gateway.security;
package io.shinhanlife.dap.mcg.security;
/**
* @package io.shinhanlife.dap.mcg.security
* @className McpRequestContextResolver
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;

View File

@@ -1,10 +1,25 @@
package io.shinhanlife.dap.biz.mcp.gateway.security;
package io.shinhanlife.dap.mcg.security;
import io.shinhanlife.dap.biz.mcp.gateway.config.McpGatewayProperties;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.FailureType;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.ToolExecutionException;
import io.shinhanlife.dap.biz.mcp.gateway.dto.OperationType;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
/**
* @package io.shinhanlife.dap.mcg.security
* @className ToolAuthorizationService
* @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.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dap.lib.dto.OperationType;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import org.springframework.stereotype.Service;
import java.util.Set;

View File

@@ -1,20 +1,36 @@
package io.shinhanlife.dap.biz.mcp.gateway.service;
package io.shinhanlife.dap.mcg.service;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.FailureType;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.RetryPolicy;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.ToolExecutionException;
import io.shinhanlife.dap.biz.mcp.gateway.dto.OperationType;
import io.shinhanlife.dap.biz.mcp.gateway.config.McpGatewayProperties;
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.SensitiveDataMasker;
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.GuardrailService;
import io.shinhanlife.dap.biz.mcp.gateway.security.McpRequestContext;
import io.shinhanlife.dap.biz.mcp.gateway.security.McpRequestContextResolver;
import io.shinhanlife.dap.biz.mcp.gateway.audit.AuditLogService;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.CircuitBreaker;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.CircuitBreakerService;
import io.shinhanlife.dap.biz.mcp.gateway.security.ToolAuthorizationService;
import io.shinhanlife.dap.biz.mcp.gateway.redis.RedisToolTraceService;
/**
* @package io.shinhanlife.dap.mcg.service
* @className ExecuteService
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import java.util.HashMap;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.RetryPolicy;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dap.lib.dto.OperationType;
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dap.mcg.guardrail.GuardrailService;
import io.shinhanlife.dap.mcg.security.McpRequestContext;
import io.shinhanlife.dap.mcg.security.McpRequestContextResolver;
import io.shinhanlife.dap.mcg.audit.AuditLogService;
import io.shinhanlife.dap.mcg.resilience.CircuitBreaker;
import io.shinhanlife.dap.mcg.resilience.CircuitBreakerService;
import io.shinhanlife.dap.mcg.security.ToolAuthorizationService;
import io.shinhanlife.dap.mcg.redis.RedisToolTraceService;
import jakarta.annotation.PreDestroy;
import java.util.Map;
@@ -27,12 +43,12 @@ import org.springframework.web.client.RestClient;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.shinhanlife.dap.biz.mcp.gateway.tool.large.LargeToolResponseService;
import io.shinhanlife.dap.biz.mcp.gateway.tool.large.PaginationRequestValidator;
import io.shinhanlife.dap.biz.mcp.gateway.tool.result.ToolExecutionResultFormatter;
import io.shinhanlife.dap.biz.mcp.gateway.tool.result.ToolExecutionResult;
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.ToolResponseGuardrailService;
import io.shinhanlife.dap.biz.mcp.gateway.transport.ToolInvoker;
import io.shinhanlife.dap.mcg.tool.large.LargeToolResponseService;
import io.shinhanlife.dap.mcg.tool.large.PaginationRequestValidator;
import io.shinhanlife.dap.mcg.tool.result.ToolExecutionResultFormatter;
import io.shinhanlife.dap.mcg.tool.result.ToolExecutionResult;
import io.shinhanlife.dap.mcg.guardrail.ToolResponseGuardrailService;
import io.shinhanlife.dap.mcg.transport.ToolInvoker;
@Slf4j
@Service
@@ -41,7 +57,6 @@ public class ExecuteService {
private final ToolPlanner planner;
private final KillSwitchService killSwitchService;
private final ObjectMapper objectMapper;
private final SensitiveDataMasker dataMasker;
private final GuardrailService guardrailService;
private final McpRequestContextResolver contextResolver;
private final AuditLogService auditLogService;
@@ -59,7 +74,6 @@ public class ExecuteService {
public ExecuteService(ToolPlanner planner,
KillSwitchService killSwitchService,
ObjectMapper objectMapper,
SensitiveDataMasker dataMasker,
GuardrailService guardrailService,
McpRequestContextResolver contextResolver,
AuditLogService auditLogService,
@@ -75,7 +89,6 @@ public class ExecuteService {
this.planner = planner;
this.killSwitchService = killSwitchService;
this.objectMapper = objectMapper;
this.dataMasker = dataMasker;
this.guardrailService = guardrailService;
this.contextResolver = contextResolver;
this.auditLogService = auditLogService;
@@ -145,22 +158,53 @@ public class ExecuteService {
long elapsedMillis = elapsedMillis(startedAt);
String responseText = "";
try { responseText = objectMapper.writeValueAsString(result); } catch (Exception ignore) {}
long originalSize = 0;
try {
responseText = objectMapper.writeValueAsString(result);
originalSize = responseText.getBytes(java.nio.charset.StandardCharsets.UTF_8).length;
} catch (Exception ignore) {}
Map<String, Object> finalResult = new java.util.LinkedHashMap<>();
finalResult.put("status", "ok");
finalResult.put("result", result);
finalResult.put("error_code", null);
finalResult.put("error_message", null);
finalResult.put("elapsed_ms", elapsedMillis);
finalResult.put("truncated", false);
finalResult.put("original_size", originalSize);
auditLogService.toolFinished(context, metadata.getName(), elapsedMillis, true, "");
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, true, "", responseText);
return result;
return finalResult;
} catch (ToolExecutionException error) {
long elapsedMillis = elapsedMillis(startedAt);
auditLogService.toolFinished(context, toolName, elapsedMillis, false, error.failureType().name());
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, false, error.failureType().name(), "");
throw error;
Map<String, Object> errorResult = new java.util.LinkedHashMap<>();
errorResult.put("status", "error");
errorResult.put("result", null);
errorResult.put("error_code", error.failureType().name());
errorResult.put("error_message", error.getMessage());
errorResult.put("elapsed_ms", elapsedMillis);
errorResult.put("truncated", false);
errorResult.put("original_size", 0);
return errorResult;
} catch (Exception error) {
long elapsedMillis = elapsedMillis(startedAt);
auditLogService.toolFinished(context, toolName, elapsedMillis, false, FailureType.INTERNAL_ERROR.name());
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, false, FailureType.INTERNAL_ERROR.name(), "");
throw new ToolExecutionException(FailureType.INTERNAL_ERROR, "Tool execution failed: " + toolName, error);
Map<String, Object> errorResult = new java.util.LinkedHashMap<>();
errorResult.put("status", "error");
errorResult.put("result", null);
errorResult.put("error_code", FailureType.INTERNAL_ERROR.name());
errorResult.put("error_message", error.getMessage());
errorResult.put("elapsed_ms", elapsedMillis);
errorResult.put("truncated", false);
errorResult.put("original_size", 0);
return errorResult;
}
}
@@ -213,34 +257,33 @@ public class ExecuteService {
if (metadata.getPodUrl() != null && !metadata.getPodUrl().isEmpty()) {
targetUrl = metadata.getPodUrl();
}
String executeApiUrl = targetUrl + "/mcp/api/v1/tools/call";
ObjectNode pageArguments = paginationValidator.normalize(arguments);
Map<String, String> headers = new java.util.HashMap<>();
headers.put("trace-id", context.requestId());
headers.put("request-id", java.util.UUID.randomUUID().toString());
ObjectNode pageArguments = paginationValidator.normalize(metadata, arguments);
LargeToolResponseService.Collector collector = largeResponses.newCollector(metadata.getName(), context.requestId());
while (true) {
Map<String, Object> pagePayload = new java.util.HashMap<>(payload);
if (pagePayload.containsKey("params")) {
Map<String, Object> params = new java.util.HashMap<>((Map<String, Object>) pagePayload.get("params"));
params.put("arguments", objectMapper.convertValue(pageArguments, Map.class));
pagePayload.put("params", params);
} else {
pagePayload.put("arguments", objectMapper.convertValue(pageArguments, Map.class));
}
Map<String, Object> pagePayload = objectMapper.convertValue(pageArguments, Map.class);
try {
log.info(" [ExecuteService] 요청 페이로드(마스킹 적용): {}", objectMapper.writeValueAsString(dataMasker.mask(objectMapper.valueToTree(pagePayload))));
log.info(" [ExecuteService] 요청 페이로드: {}", objectMapper.writeValueAsString(pagePayload));
} catch (Exception ignore) {}
JsonNode data = null;
try {
data = toolInvoker.invoke(pagePayload, executeApiUrl);
data = toolInvoker.invoke(metadata.getName(), pagePayload, targetUrl, headers);
} catch (org.springframework.web.client.RestClientResponseException e) {
// HTTP 4xx, 5xx 에러는 연결 오류가 아니라 비즈니스 로직 오류이거나 검증 실패이므로 원본 에러를 그대로 반환
throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod HTTP 에러 (" + e.getStatusCode() + "): " + e.getResponseBodyAsString());
} catch (Exception e) {
if (executeApiUrl.contains("http://tool-")) {
String fallbackUrl = executeApiUrl.replaceAll("http://tool-[a-zA-Z0-9-]+", "http://localhost");
if (targetUrl.contains("http://tool-")) {
String fallbackUrl = targetUrl.replaceAll("http://tool-[a-zA-Z0-9-]+", "http://localhost");
log.warn(" [ExecuteService] 호스트를 찾을 수 없어 localhost로 재시도합니다: {}", fallbackUrl);
try {
data = toolInvoker.invoke(pagePayload, fallbackUrl);
data = toolInvoker.invoke(metadata.getName(), pagePayload, fallbackUrl, headers);
} catch (Exception ex) {
throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod 호출 실패 (localhost 재시도 포함): " + ex.getMessage());
}
@@ -337,4 +380,4 @@ public class ExecuteService {
public void shutdown() {
executor.shutdownNow();
}
}
}

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.dap.biz.mcp.gateway.service;
package io.shinhanlife.dap.mcg.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -6,16 +6,16 @@ import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
/**
* @package io.shinhanlife.dap.biz.mcp.gateway.service
* @package io.shinhanlife.dap.mcg.service
* @className KillSwitchService
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
* 2026.09.01 0986406 최초생성
*
* </pre>
*/

View File

@@ -1,9 +1,9 @@
package io.shinhanlife.dap.biz.mcp.gateway.service;
package io.shinhanlife.dap.mcg.service;
import io.shinhanlife.dap.common.mcp.security.SecurityProperties;
import io.shinhanlife.dap.biz.mcp.gateway.registry.RedisRegistryService;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
import io.shinhanlife.dap.biz.mcp.gateway.config.GatewayFallbackProperties;
import io.shinhanlife.dap.lib.mcp.security.SecurityProperties;
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.config.GatewayFallbackProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@@ -12,16 +12,16 @@ import java.util.List;
import java.util.Map;
/**
* @package io.shinhanlife.dap.biz.mcp.gateway.service
* @package io.shinhanlife.dap.mcg.service
* @className ToolPlanner
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
* 2026.09.01 0986406 최초생성
*
* </pre>
*/

View File

@@ -1,15 +1,36 @@
package io.shinhanlife.dap.biz.mcp.gateway.sync;
package io.shinhanlife.dap.mcg.sync;
/**
* @package io.shinhanlife.dap.mcg.sync
* @className CustomWebMvcSseServerTransportProvider
* @description AX HUB MCP Gateway SSE 전송 제공자 - SSE 기반의 MCP 서버 트랜스포트를 구현하는 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.json.TypeRef;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage;
import io.modelcontextprotocol.spec.McpSchema.JSONRPCNotification;
import io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest;
import io.modelcontextprotocol.spec.McpSchema.JSONRPCResponse;
import io.modelcontextprotocol.spec.McpServerSession;
import io.modelcontextprotocol.spec.McpServerTransport;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerResponse;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import reactor.core.publisher.Mono;
import java.util.Map;
@@ -28,6 +49,7 @@ public class CustomWebMvcSseServerTransportProvider implements McpServerTranspor
private final String sseEndpoint;
private final String messageEndpoint;
private final Map<String, McpServerSession> sessions = new ConcurrentHashMap<>();
private final Map<String, CustomMcpSessionTransport> customTransports = new ConcurrentHashMap<>();
private final ObjectMapper objectMapper;
public CustomWebMvcSseServerTransportProvider(String sseEndpoint, String messageEndpoint, ObjectMapper objectMapper) {
@@ -50,6 +72,7 @@ public class CustomWebMvcSseServerTransportProvider implements McpServerTranspor
} catch (Exception ignored) {}
});
sessions.clear();
customTransports.clear();
});
}
@@ -60,30 +83,30 @@ public class CustomWebMvcSseServerTransportProvider implements McpServerTranspor
.toList());
}
public org.springframework.web.servlet.mvc.method.annotation.SseEmitter handleSse() {
public SseEmitter handleSse() {
if (sessionFactory == null) {
throw new IllegalStateException("SessionFactory not configured");
}
org.springframework.web.servlet.mvc.method.annotation.SseEmitter emitter = new org.springframework.web.servlet.mvc.method.annotation.SseEmitter(-1L);
SseEmitter emitter = new SseEmitter(-1L);
String sessionId = UUID.randomUUID().toString();
CustomMcpSessionTransport sessionTransport = new CustomMcpSessionTransport(emitter, sessionId);
McpServerSession session = sessionFactory.create(sessionTransport);
sessions.put(sessionId, session);
emitter.onCompletion(() -> sessions.remove(sessionId));
emitter.onTimeout(() -> sessions.remove(sessionId));
// emitter.onCompletion(() -> sessions.remove(sessionId));
// emitter.onTimeout(() -> sessions.remove(sessionId));
new Thread(() -> {
try {
Thread.sleep(100);
emitter.send(org.springframework.web.servlet.mvc.method.annotation.SseEmitter.event().name("endpoint").data(messageEndpoint + "?sessionId=" + sessionId));
emitter.send(SseEmitter.event().name("endpoint").data(messageEndpoint + "?sessionId=" + sessionId));
} catch (Exception e) {
emitter.completeWithError(e);
}
}).start();
return emitter;
}
@@ -94,22 +117,29 @@ public class CustomWebMvcSseServerTransportProvider implements McpServerTranspor
org.springframework.web.servlet.mvc.method.annotation.SseEmitter emitter = new org.springframework.web.servlet.mvc.method.annotation.SseEmitter(-1L);
CustomMcpSessionTransport sessionTransport = new CustomMcpSessionTransport(emitter, sessionId);
McpServerSession session = sessionFactory.create(sessionTransport);
sessions.put(sessionId, session);
boolean isNew = !sessions.containsKey(sessionId);
emitter.onCompletion(() -> sessions.remove(sessionId));
emitter.onTimeout(() -> sessions.remove(sessionId));
if (isNew) {
CustomMcpSessionTransport sessionTransport = new CustomMcpSessionTransport(emitter, sessionId);
customTransports.put(sessionId, sessionTransport);
McpServerSession session = sessionFactory.create(sessionTransport);
sessions.put(sessionId, session);
// 주의: 클라이언트가 단일 POST 응답 연결을 끊더라도,
// (Warm Pool) 스펙상 세션은 살려둬야 하므로 세션 삭제 로직 제외
} else {
// 기존 세션인 경우 Emitter 파이프만 덮어씌움 (Switching)
CustomMcpSessionTransport sessionTransport = customTransports.get(sessionId);
if (sessionTransport != null) {
sessionTransport.setEmitter(emitter);
}
}
new Thread(() -> {
try {
// 커스텀 클라이언트는 endpoint 이벤트를 무시할 있지만, 표준 호환성을 위해 전송
Thread.sleep(100);
emitter.send(org.springframework.web.servlet.mvc.method.annotation.SseEmitter.event().name("endpoint").data(messageEndpoint + "?sessionId=" + sessionId));
// Body로 들어온 initialize 즉시 처리
// Body로 들어온 메시지 즉시 비동기 처리
if (body != null && !body.trim().isEmpty()) {
handleMessage(sessionId, body);
handleMessage(sessionId, body, emitter);
}
} catch (Exception e) {
emitter.completeWithError(e);
@@ -120,38 +150,52 @@ public class CustomWebMvcSseServerTransportProvider implements McpServerTranspor
}
public org.springframework.http.ResponseEntity<String> handleMessage(String sessionId, String body) {
return handleMessage(sessionId, body, null);
}
public org.springframework.http.ResponseEntity<String> handleMessage(String sessionId, String body, org.springframework.web.servlet.mvc.method.annotation.SseEmitter emitter) {
log.info("Received POST message for sessionId: " + sessionId + ", body: " + body);
if (sessionId == null || !sessions.containsKey(sessionId)) {
return org.springframework.http.ResponseEntity.badRequest().body("Missing or invalid sessionId");
return ResponseEntity.badRequest().body("Unexpected request body");
}
McpServerSession session = sessions.get(sessionId);
try {
java.util.Map<String, Object> map = objectMapper.readValue(body, new com.fasterxml.jackson.core.type.TypeReference<java.util.Map<String, Object>>() {});
io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage message;
Map<String, Object> map = objectMapper.readValue(body, new TypeReference<Map<String, Object>>() {});
JSONRPCMessage message;
if (map.containsKey("id")) {
if (map.containsKey("method")) {
message = objectMapper.convertValue(map, io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest.class);
message = objectMapper.convertValue(map, JSONRPCRequest.class);
} else {
message = objectMapper.convertValue(map, io.modelcontextprotocol.spec.McpSchema.JSONRPCResponse.class);
message = objectMapper.convertValue(map, JSONRPCResponse.class);
}
} else {
message = objectMapper.convertValue(map, io.modelcontextprotocol.spec.McpSchema.JSONRPCNotification.class);
message = objectMapper.convertValue(map, JSONRPCNotification.class);
}
log.info("Converted message type: " + message.getClass().getName());
session.handle(message).subscribe();
log.info("Message sent to session handler");
return org.springframework.http.ResponseEntity.ok().build();
if (emitter != null && !map.containsKey("id")) {
// emitter.complete();
log.info("Completed emitter for notification (disabled for keep-alive)");
}
return ResponseEntity.ok().build();
} catch (Exception e) {
log.error("Failed to handle message", e);
return org.springframework.http.ResponseEntity.status(500).body(e.getMessage());
return ResponseEntity.status(500).body(e.getMessage());
}
}
public boolean hasSession(String sessionId) {
return sessionId != null && sessions.containsKey(sessionId);
}
private class CustomMcpSessionTransport implements McpServerTransport {
private final org.springframework.web.servlet.mvc.method.annotation.SseEmitter emitter;
private volatile org.springframework.web.servlet.mvc.method.annotation.SseEmitter emitter;
private final String sessionId;
public CustomMcpSessionTransport(org.springframework.web.servlet.mvc.method.annotation.SseEmitter emitter, String sessionId) {
@@ -159,6 +203,10 @@ public class CustomWebMvcSseServerTransportProvider implements McpServerTranspor
this.sessionId = sessionId;
}
public void setEmitter(org.springframework.web.servlet.mvc.method.annotation.SseEmitter emitter) {
this.emitter = emitter;
}
@Override
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
return Mono.fromRunnable(() -> {
@@ -166,18 +214,30 @@ public class CustomWebMvcSseServerTransportProvider implements McpServerTranspor
try {
String json = objectMapper.writeValueAsString(message);
log.info("Serialized message: " + json);
emitter.send(org.springframework.web.servlet.mvc.method.annotation.SseEmitter.event().name("message").data(json));
log.info("Message successfully sent to SSE emitter");
if (this.emitter != null) {
this.emitter.send(org.springframework.web.servlet.mvc.method.annotation.SseEmitter.event().name("message").data(json));
log.info("Message successfully sent to SSE emitter");
// Custom 프로토콜: 1회 요청당 1응답 종료 (스트림을 닫아버림)
// 클라이언트가 번의 POST 응답을 받고 연결을 끊기 때문
// this.emitter.complete();
}
} catch (Exception e) {
log.error("Error sending message to SSE emitter", e);
emitter.completeWithError(e);
if (this.emitter != null) {
this.emitter.completeWithError(e);
}
}
});
}
@Override
public Mono<Void> closeGracefully() {
return Mono.fromRunnable(emitter::complete);
return Mono.fromRunnable(() -> {
if (this.emitter != null) {
this.emitter.complete();
}
});
}
@Override

View File

@@ -1,4 +1,21 @@
package io.shinhanlife.dap.biz.mcp.gateway.sync;
package io.shinhanlife.dap.mcg.sync;
/**
* @package io.shinhanlife.dap.mcg.sync
* @className DynamicMcpController
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import java.util.UUID;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
@@ -9,6 +26,7 @@ import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import io.shinhanlife.dap.mcg.sync.CustomWebMvcSseServerTransportProvider;
@RestController
public class DynamicMcpController {
@@ -19,24 +37,6 @@ public class DynamicMcpController {
this.manager = manager;
}
@PostMapping("/mcp")
public SseEmitter handleSse1() {
System.out.println("11111111111");
return null;
}
@PostMapping("/mcp/initialize")
public SseEmitter handleSse2() {
System.out.println("2222222222");
return null;
}
@PostMapping("/mcp/sse/initialize")
public SseEmitter handleSse3() {
System.out.println("33333333333333");
return null;
}
@GetMapping("/mcp/sse/{category}")
public SseEmitter handleSse(@PathVariable("category") String category) {
CustomWebMvcSseServerTransportProvider transport = manager.getTransport(category);
@@ -70,18 +70,33 @@ public class DynamicMcpController {
return ResponseEntity.badRequest().body("Unknown category: " + category);
}
if (sessionId == null || sessionId.isEmpty()) {
// 세션 생성 (initialize 요청)
String newSessionId = java.util.UUID.randomUUID().toString();
SseEmitter emitter = transport.handleCustomSse(newSessionId, body);
return ResponseEntity.ok()
.header("Mcp-Session-Id", newSessionId)
.body(emitter);
} else {
// 기존 세션 메시지 전송 (tools/call )
transport.handleMessage(sessionId, body);
return ResponseEntity.accepted().build();
boolean isNew = (sessionId == null || sessionId.isEmpty());
String activeSessionId = isNew ? java.util.UUID.randomUUID().toString() : sessionId;
if (!isNew && !transport.hasSession(activeSessionId)) {
// 클라이언트가 보낸 세션 ID가 만료되었거나 존재하지 않는 경우 (Warm Pool 스펙: 404 Not Found 반환)
return ResponseEntity.notFound().build();
}
org.springframework.web.servlet.mvc.method.annotation.SseEmitter emitter = transport.handleCustomSse(activeSessionId, body);
return ResponseEntity.ok()
.header("Mcp-Session-Id", activeSessionId)
.body(emitter);
}
@org.springframework.web.bind.annotation.DeleteMapping("/mcp/sse/{category}")
public ResponseEntity<Void> handleDeleteSse(@PathVariable("category") String category) {
return ResponseEntity.ok().build();
}
@GetMapping("/.well-known/oauth-protected-resource")
public ResponseEntity<java.util.Map<String, Object>> handleOauthProtectedResource() {
return ResponseEntity.ok().body(java.util.Map.of());
}
@GetMapping("/.well-known/oauth-protected-resource/**")
public ResponseEntity<java.util.Map<String, Object>> handleOauthProtectedResourceWildcard() {
return ResponseEntity.ok().body(java.util.Map.of());
}
}

View File

@@ -1,8 +1,25 @@
package io.shinhanlife.dap.biz.mcp.gateway.sync;
package io.shinhanlife.dap.mcg.sync;
/**
* @package io.shinhanlife.dap.mcg.sync
* @className DynamicMcpServerManager
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpSyncServer;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.sync.CustomWebMvcSseServerTransportProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@@ -36,7 +53,8 @@ public class DynamicMcpServerManager {
getOrCreateServer("payment");
getOrCreateServer("sms");
getOrCreateServer("email");
getOrCreateServer("other");
getOrCreateServer("oth");
getOrCreateServer("sample");
}
private McpSyncServer getOrCreateServer(String categoryKey) {
@@ -50,8 +68,8 @@ public class DynamicMcpServerManager {
CustomWebMvcSseServerTransportProvider transport = new CustomWebMvcSseServerTransportProvider(ssePath, msgPath, objectMapper);
McpSyncServer newServer = McpServer.sync(transport)
.serverInfo("AXHUB-Gateway-" + key, "1.0.0")
.capabilities(io.modelcontextprotocol.spec.McpSchema.ServerCapabilities.builder().tools(true).build())
.serverInfo("DAP-Gateway-" + key, "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.build();
categoryTransports.put(key, transport);

View File

@@ -1,10 +1,25 @@
package io.shinhanlife.dap.biz.mcp.gateway.sync;
package io.shinhanlife.dap.mcg.sync;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
import io.shinhanlife.dap.biz.mcp.gateway.service.ExecuteService;
/**
* @package io.shinhanlife.dap.mcg.sync
* @className RegistryMcpToolSpecificationFactory
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.service.ExecuteService;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;
import io.shinhanlife.dap.biz.mcp.gateway.tool.result.ToolExecutionResult;
import io.shinhanlife.dap.mcg.tool.result.ToolExecutionResult;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -37,6 +52,12 @@ public class RegistryMcpToolSpecificationFactory {
.name(entry.getName())
.description(description(entry))
.inputSchema(inputSchema(entry))
.annotations(McpSchema.ToolAnnotations.builder()
.readOnlyHint(Boolean.TRUE.equals(entry.getReadOnlyHint()))
.destructiveHint(Boolean.TRUE.equals(entry.getDestructiveHint()))
.idempotentHint(Boolean.TRUE.equals(entry.getIdempotentHint()))
.openWorldHint(Boolean.TRUE.equals(entry.getOpenWorldHint()))
.build())
.build();
return McpServerFeatures.SyncToolSpecification.builder()

View File

@@ -1,7 +1,7 @@
package io.shinhanlife.dap.biz.mcp.gateway.sync;
package io.shinhanlife.dap.mcg.sync;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
import io.shinhanlife.dap.biz.mcp.gateway.registry.RedisRegistryService;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.modelcontextprotocol.server.McpSyncServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -19,16 +19,16 @@ import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
/**
* @package io.shinhanlife.dap.biz.mcp.gateway.sync
* @package io.shinhanlife.dap.mcg.sync
* @className RegistryMcpToolSynchronizer
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
* 2026.09.01 0986406 최초생성
*
* </pre>
*/

View File

@@ -1,7 +1,24 @@
package io.shinhanlife.dap.biz.mcp.gateway.tool.large;
package io.shinhanlife.dap.mcg.tool.large;
import io.shinhanlife.dap.biz.mcp.gateway.config.AgentResponseBudgetProperties;
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.SensitiveDataMasker;
/**
* @package io.shinhanlife.dap.mcg.tool.large
* @className AgentResponseBudgetService
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import java.util.Iterator;
import java.util.Map;
import io.shinhanlife.dap.mcg.config.AgentResponseBudgetProperties;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -12,12 +29,9 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
public class AgentResponseBudgetService {
private final AgentResponseBudgetProperties properties;
private final ObjectMapper json;
private final SensitiveDataMasker masker;
public AgentResponseBudgetService(AgentResponseBudgetProperties properties, ObjectMapper json, SensitiveDataMasker masker) {
public AgentResponseBudgetService(AgentResponseBudgetProperties properties, ObjectMapper json) {
this.properties = properties;
this.json = json;
this.masker = masker;
}
public ObjectNode apply(ObjectNode response) {
@@ -91,7 +105,7 @@ public class AgentResponseBudgetService {
}
private JsonNode budgetItem(JsonNode item, BudgetStats stats) {
JsonNode masked = masker.mask(item);
JsonNode masked = item;
if (!masked.isObject()) {
return truncateByBytes(masked, stats);
}
@@ -99,9 +113,9 @@ public class AgentResponseBudgetService {
int copied = 0;
int originalFields = 0;
java.util.Iterator<java.util.Map.Entry<String, JsonNode>> fields = masked.fields();
Iterator<Map.Entry<String, JsonNode>> fields = masked.fields();
while (fields.hasNext()) {
java.util.Map.Entry<String, JsonNode> entry = fields.next();
Map.Entry<String, JsonNode> entry = fields.next();
originalFields++;
if (copied >= properties.maxFieldsPerItem()) {
continue;
@@ -135,9 +149,9 @@ public class AgentResponseBudgetService {
ObjectNode source = (ObjectNode) item;
ObjectNode limited = json.createObjectNode();
java.util.Iterator<java.util.Map.Entry<String, JsonNode>> fields = source.fields();
Iterator<Map.Entry<String, JsonNode>> fields = source.fields();
while (fields.hasNext()) {
java.util.Map.Entry<String, JsonNode> entry = fields.next();
Map.Entry<String, JsonNode> entry = fields.next();
JsonNode value = entry.getValue();
if (value.isTextual()) {
limited.put(entry.getKey(), truncateText(value.asText()));
@@ -164,7 +178,7 @@ public class AgentResponseBudgetService {
fallback.put("maxItemBytes", properties.maxItemBytes());
if (item.isObject()) {
ArrayNode fieldNames = json.createArrayNode();
java.util.Iterator<String> fieldNamesIter = item.fieldNames();
Iterator<String> fieldNamesIter = item.fieldNames();
while (fieldNamesIter.hasNext()) {
fieldNames.add(fieldNamesIter.next());
}
@@ -188,9 +202,9 @@ public class AgentResponseBudgetService {
private ObjectNode copyWithout(ObjectNode source, String... excludedFields) {
ObjectNode copy = json.createObjectNode();
java.util.Iterator<java.util.Map.Entry<String, JsonNode>> fields = source.fields();
Iterator<Map.Entry<String, JsonNode>> fields = source.fields();
while (fields.hasNext()) {
java.util.Map.Entry<String, JsonNode> entry = fields.next();
Map.Entry<String, JsonNode> entry = fields.next();
if (!excluded(entry.getKey(), excludedFields)) {
copy.set(entry.getKey(), entry.getValue());
}

View File

@@ -1,9 +1,25 @@
package io.shinhanlife.dap.biz.mcp.gateway.tool.large;
package io.shinhanlife.dap.mcg.tool.large;
import io.shinhanlife.dap.biz.mcp.gateway.config.McpGatewayProperties;
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.SensitiveDataMasker;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.FailureType;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.ToolExecutionException;
/**
* @package io.shinhanlife.dap.mcg.tool.large
* @className LargeToolResponseService
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import java.util.Iterator;
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -17,16 +33,13 @@ import java.time.Instant;
public class LargeToolResponseService {
private final McpGatewayProperties properties;
private final ObjectMapper json;
private final SensitiveDataMasker masker;
private final AgentResponseBudgetService agentBudget;
public LargeToolResponseService(McpGatewayProperties properties,
ObjectMapper json,
SensitiveDataMasker masker,
AgentResponseBudgetService agentBudget) {
this.properties = properties;
this.json = json;
this.masker = masker;
this.agentBudget = agentBudget;
}
@@ -140,8 +153,7 @@ public class LargeToolResponseService {
}
Page page = pageFrom(data);
if (!page.paginated() && pageCount == 0 && count(page.items()) <= pageSize()) {
JsonNode masked = masker.mask(data);
normalData = masked;
normalData = data;
pageCount = 1;
returnedCount = count(page.items());
totalCount = returnedCount;
@@ -254,19 +266,18 @@ public class LargeToolResponseService {
}
private JsonNode previewItem(JsonNode item) {
JsonNode masked = masker.mask(item);
long itemBytes = jsonBytes(masked);
long itemBytes = jsonBytes(item);
if (itemBytes <= properties.largeResponseMaxItemBytes()) {
return masked;
return item;
}
truncated = true;
ObjectNode preview = json.createObjectNode();
preview.put("truncated", true);
preview.put("originalBytes", itemBytes);
preview.put("maxItemBytes", properties.largeResponseMaxItemBytes());
if (masked.isObject()) {
if (item.isObject()) {
ArrayNode fieldNames = json.createArrayNode();
java.util.Iterator<String> fieldNamesIter = masked.fieldNames();
Iterator<String> fieldNamesIter = item.fieldNames();
while (fieldNamesIter.hasNext()) {
fieldNames.add(fieldNamesIter.next());
}
@@ -309,7 +320,7 @@ public class LargeToolResponseService {
private int count(JsonNode node) {
int count = 0;
java.util.Iterator<JsonNode> iter = node.elements();
Iterator<JsonNode> iter = node.elements();
while(iter.hasNext()) {
iter.next();
count++;

View File

@@ -1,8 +1,24 @@
package io.shinhanlife.dap.biz.mcp.gateway.tool.large;
package io.shinhanlife.dap.mcg.tool.large;
import io.shinhanlife.dap.biz.mcp.gateway.config.McpGatewayProperties;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.FailureType;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.ToolExecutionException;
/**
* @package io.shinhanlife.dap.mcg.tool.large
* @className PaginationRequestValidator
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
@@ -25,12 +41,18 @@ public class PaginationRequestValidator {
/**
* pageSize/cursor를 검증한 Tool 서버에 넘길 안전한 arguments 복사본을 만듭니다.
*/
public ObjectNode normalize(ObjectNode arguments) {
public ObjectNode normalize(ToolMetadata metadata, ObjectNode arguments) {
try {
ObjectNode normalized = arguments == null
? json.createObjectNode()
: (ObjectNode) json.readTree(json.writeValueAsString(arguments));
normalizePageSize(normalized);
if (metadata != null && metadata.allowedArguments().contains("pageSize")) {
normalizePageSize(normalized);
} else if (normalized.has("pageSize")) {
normalizePageSize(normalized);
}
validateCursor(normalized);
return normalized;
} catch (ToolExecutionException error) {

View File

@@ -1,5 +1,20 @@
package io.shinhanlife.dap.biz.mcp.gateway.tool.result;
package io.shinhanlife.dap.mcg.tool.result;
/**
* @package io.shinhanlife.dap.mcg.tool.result
* @className ToolExecutionResult
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import com.fasterxml.jackson.databind.JsonNode;
import java.util.List;

View File

@@ -1,6 +1,20 @@
package io.shinhanlife.dap.biz.mcp.gateway.tool.result;
package io.shinhanlife.dap.mcg.tool.result;
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.SensitiveDataMasker;
/**
* @package io.shinhanlife.dap.mcg.tool.result
* @className ToolExecutionResultFormatter
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -19,11 +33,8 @@ public class ToolExecutionResultFormatter {
private static final int TEXT_PREVIEW_LIMIT = 2_000;
private final ObjectMapper json;
private final SensitiveDataMasker masker;
public ToolExecutionResultFormatter(ObjectMapper json, SensitiveDataMasker masker) {
public ToolExecutionResultFormatter(ObjectMapper json) {
this.json = json;
this.masker = masker;
}
/**
@@ -56,7 +67,7 @@ public class ToolExecutionResultFormatter {
}
public ToolExecutionResult fromJson(String toolName, JsonNode parsed, long sizeBytes) {
JsonNode masked = masker.mask(parsed);
JsonNode masked = parsed;
if (masked.isObject()) {
ObjectNode object = (ObjectNode) masked;
if (object.path("isError").asBoolean(false) || object.has("error") || object.has("failureType")) {

View File

@@ -0,0 +1,81 @@
package io.shinhanlife.dap.mcg.transport;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.TextNode;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
import io.modelcontextprotocol.spec.McpSchema;
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import java.net.http.HttpRequest;
import java.time.Duration;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/** MCP SDK client used for Gateway-to-Tool-Pod calls over Streamable HTTP. */
@Slf4j
@Component
public class HttpToolInvoker implements ToolInvoker {
private final ObjectMapper objectMapper;
public HttpToolInvoker(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public JsonNode invoke(String toolName, Map<String, Object> arguments, String podUrl, Map<String, String> headers) {
String endpoint = podUrl.replaceAll("/+$", "") + "/mcp";
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder();
if (headers != null) {
headers.forEach(requestBuilder::header);
}
HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(endpoint)
.requestBuilder(requestBuilder)
.connectTimeout(Duration.ofSeconds(5))
.build();
try (McpSyncClient client = McpClient.sync(transport)
.clientInfo(new McpSchema.Implementation("dap-gateway", "1.0.0"))
.requestTimeout(Duration.ofSeconds(30))
.build()) {
client.initialize();
McpSchema.CallToolResult result = client.callTool(McpSchema.CallToolRequest.builder()
.name(toolName)
.arguments(arguments)
.build());
if (Boolean.TRUE.equals(result.isError())) {
throw new ToolExecutionException(FailureType.BUSINESS_ERROR, "Tool Pod MCP error: " + textContent(result));
}
return result.structuredContent() != null
? objectMapper.valueToTree(result.structuredContent())
: textContentAsJson(result);
} catch (ToolExecutionException error) {
throw error;
} catch (Exception error) {
throw new ToolExecutionException(FailureType.SERVER_ERROR,
"Tool Pod MCP call failed: " + error.getMessage(), error);
}
}
private JsonNode textContentAsJson(McpSchema.CallToolResult result) {
String text = textContent(result);
try {
return objectMapper.readTree(text);
} catch (Exception ignored) {
return TextNode.valueOf(text);
}
}
private String textContent(McpSchema.CallToolResult result) {
return result.content().stream()
.filter(McpSchema.TextContent.class::isInstance)
.map(McpSchema.TextContent.class::cast)
.map(McpSchema.TextContent::text)
.findFirst()
.orElse("");
}
}

View File

@@ -0,0 +1,26 @@
package io.shinhanlife.dap.mcg.transport;
/**
* @package io.shinhanlife.dap.mcg.transport
* @className ToolInvoker
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import com.fasterxml.jackson.databind.JsonNode;
import java.util.Map;
/**
* Tool 서버 호출 transport의 최소 공통 인터페이스입니다.
*/
public interface ToolInvoker {
JsonNode invoke(String toolName, Map<String, Object> arguments, String podUrl, Map<String, String> headers);
}

View File

@@ -1,37 +0,0 @@
package io.shinhanlife.dap.sample.converter;
import io.shinhanlife.dap.sample.domain.model.AppliSystNtfyPatiModel;
import io.shinhanlife.dap.sample.dto.AppliSystNtfyRgiInDTO;
import io.shinhanlife.dap.sample.dto.StrnTermListInDTO;
import io.shinhanlife.dap.sample.dto.StrnTermListOutDTO;
import io.shinhanlife.dap.sample.presentation.io.AppliSystNtfyPatiRequest;
import io.shinhanlife.dap.sample.presentation.io.StrnTermRequest;
import io.shinhanlife.dap.sample.presentation.io.StrnTermResponse;
import org.mapstruct.Mapper;
/**
* @package io.shinhanlife.dap.sample.converter
* @className SampleConverter
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Mapper(componentModel = "spring")
public abstract class SampleConverter {
public abstract StrnTermListInDTO convertRequestToDto(StrnTermRequest req);
public abstract StrnTermResponse convertDtoToResponse(StrnTermListOutDTO outDto);
public abstract AppliSystNtfyPatiModel convertDtoToModel(AppliSystNtfyRgiInDTO inDto);
public abstract AppliSystNtfyRgiInDTO convertAppliRequestToDto(AppliSystNtfyPatiRequest request);
}

View File

@@ -1,35 +0,0 @@
package io.shinhanlife.dap.sample.domain.model;
import io.shinhanlife.glow.db.dto.AuditInfo;
import lombok.*;
/**
* @package io.shinhanlife.dap.sample.domain.model
* @className AppliSystNtfyPatiModel
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class AppliSystNtfyPatiModel extends AuditInfo {
private String appliSystNtfyPatiId;
private String appliSystNtfyKdCd;
private String appliDutjCd;
private String appliDutjPrjcCd;
private String ntfyMsgCt;
private String ntfyOccDt;
private String ntfyCfmDt;
private String ntfyTrgtPrafNo;
private String shmsPmlYn;
private String ntfyCfmYn;
}

View File

@@ -1,34 +0,0 @@
package io.shinhanlife.dap.sample.domain.repository;
import io.shinhanlife.dap.sample.domain.model.AppliSystNtfyPatiModel;
import io.shinhanlife.dap.sample.dto.StrnTermListInDTO;
import io.shinhanlife.dap.sample.dto.StrnTermListOutDTO;
import io.shinhanlife.glow.GlowIndexPaging;
import io.shinhanlife.glow.GlowMybatisMapper;
import io.shinhanlife.glow.PageInfo;
import java.util.List;
/**
* @package io.shinhanlife.dap.sample.domain.repository
* @className GlowSampleRepository
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@GlowMybatisMapper
public interface GlowSampleRepository {
@GlowIndexPaging
List<StrnTermListOutDTO.StrnTerm> selectStrnTerm(StrnTermListInDTO inDto, PageInfo pageInfo);
int insertAppliSystNtfyPati(AppliSystNtfyPatiModel appliSystNtfyPatiModel);
}

View File

@@ -1,25 +0,0 @@
package io.shinhanlife.dap.sample.domain.service;
import io.shinhanlife.dap.sample.dto.AppliSystNtfyRgiInDTO;
import io.shinhanlife.dap.sample.dto.StrnTermListInDTO;
import io.shinhanlife.dap.sample.dto.StrnTermListOutDTO;
import io.shinhanlife.glow.PageInfo;
/**
* @package io.shinhanlife.dap.sample.domain.service
* @className GlowSampleService
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
public interface GlowSampleService {
StrnTermListOutDTO getStrnTerms(StrnTermListInDTO inDto, PageInfo pageInfo);
int insertAppliSystNtfyPati(AppliSystNtfyRgiInDTO inDto);
}

View File

@@ -1,55 +0,0 @@
package io.shinhanlife.dap.sample.domain.service.impl;
import io.shinhanlife.dap.sample.converter.SampleConverter;
import io.shinhanlife.dap.sample.domain.model.AppliSystNtfyPatiModel;
import io.shinhanlife.dap.sample.domain.repository.GlowSampleRepository;
import io.shinhanlife.dap.sample.domain.service.GlowSampleService;
import io.shinhanlife.dap.sample.dto.AppliSystNtfyRgiInDTO;
import io.shinhanlife.dap.sample.dto.StrnTermListInDTO;
import io.shinhanlife.dap.sample.dto.StrnTermListOutDTO;
import io.shinhanlife.glow.GlowLogger;
import io.shinhanlife.glow.GlowLogTarget;
import io.shinhanlife.glow.PageInfo;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @package io.shinhanlife.dap.sample.domain.service.impl
* @className GlowSampleServiceImpl
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Service
@RequiredArgsConstructor
public class GlowSampleServiceImpl implements GlowSampleService {
@GlowLogTarget(GlowLogTarget.Target.FILE)
private final GlowLogger log;
private final GlowSampleRepository glowSampleRepository;
private final SampleConverter sampleConverter;
@Override
public StrnTermListOutDTO getStrnTerms(StrnTermListInDTO inDto, PageInfo pageInfo) {
List<StrnTermListOutDTO.StrnTerm> out = glowSampleRepository.selectStrnTerm(inDto, pageInfo);
return StrnTermListOutDTO.builder()
.strnTerms(out)
.pageInfo(pageInfo)
.build();
}
@Override
public int insertAppliSystNtfyPati(AppliSystNtfyRgiInDTO inDto) {
AppliSystNtfyPatiModel appliSystNtfyPatiModel = sampleConverter.convertDtoToModel(inDto);
return glowSampleRepository.insertAppliSystNtfyPati(appliSystNtfyPatiModel);
}
}

View File

@@ -1,41 +0,0 @@
package io.shinhanlife.dap.sample.dto;
import lombok.Getter;
import lombok.Setter;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.*;
/**
* @package io.shinhanlife.dap.sample.dto
* @className AppliSystNtfyRgiInDTO
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AppliSystNtfyRgiInDTO {
private String appliSystNtfyPatiId;
private String appliSystNtfyKdCd;
private String appliDutjCd;
private String appliDutjPrjcCd;
private String ntfyMsgCt;
private String ntfyOccDt;
private String ntfyCfmDt;
private String ntfyTrgtPrafNo;
private String shmsPmlYn;
private String ntfyCfmYn;
}

View File

@@ -1,31 +0,0 @@
package io.shinhanlife.dap.sample.dto;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
/**
* @package io.shinhanlife.dap.sample.dto
* @className StrnTermListInDTO
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class StrnTermListInDTO {
private String strnTermHanNm;
}

View File

@@ -1,46 +0,0 @@
package io.shinhanlife.dap.sample.dto;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import io.shinhanlife.glow.PageInfo;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* @package io.shinhanlife.dap.sample.dto
* @className StrnTermListOutDTO
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Builder
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class StrnTermListOutDTO {
private List<StrnTerm> strnTerms;
private PageInfo pageInfo;
@Builder
@Getter
@Setter
public static class StrnTerm{
private String strnTermHanNm;
private String strnTermEngcAbrNm;
private String strnTermEngNm;
private String strnTermScrnTermNm;
}
}

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