112 Commits

Author SHA1 Message Date
jade
115e403497 fix(scaffold): apply target system code naming for MCI converter and abbreviate baseName
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 17s
2026-09-10 14:35:42 +09:00
jade
b20a4edfdd feat: abbreviate MCI scaffold source names
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 17s
2026-09-10 13:38:57 +09:00
jade
6c318ebeae feat: generate pods with Maven library dependency
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 28s
2026-09-10 11:22:27 +09:00
jade
17d9f3cb77 feat: add folder picker to new pod scaffold
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 32s
2026-09-09 17:52:12 +09:00
jade
e1908b4b7a feat: add standalone pod module scaffolder 2026-09-09 17:49:34 +09:00
jade
d45720d3e4 feat: HTTP Tool 생성 시 dat-was-lib의 application-glow-local.yml 탐색 및 append 로직 적용
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 28s
2026-09-08 14:11:29 +09:00
jade
9a8f716ec7 feat: MCI Request DTO example 자동 생성 및 Tool Scaffold UseCaseImpl/Client 생성 템플릿 개선
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 31s
2026-09-08 09:49:00 +09:00
jade
88d94e8b83 feat(scaffold): add guided AI tool proposal sample
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 18s
2026-09-07 18:17:03 +09:00
jade
c9701a5a94 feat: MCI 변환 AI 병렬 청킹 및 Converter 생성 개선
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 28s
- ScaffoldingController: AI 분석 시 20개 단위 병렬 청킹(CompletableFuture) 적용
- ScaffoldingController: reserved 자동 제외 로직 제거, AI 명시적 include=false만 제외
- ScaffoldingController: AI 프롬프트 개선 - 모든 필드(List/reserved 포함) 번역 지시
- MciResponseScaffolder: Request Converter 메서드명 toLegacyRequest -> toRequest 변경
- scaffold.html: collectMciResponseMappings validation 방어 처리 (자동 fallback)
2026-09-07 15:41:57 +09:00
jade
91c34a2fb3 fix(gateway): load AI API keys from application properties instead of hardcoding
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 16s
2026-09-07 11:08:08 +09:00
jade
bb782e1d38 chore: clarify MCI DTO transform labels
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 17s
2026-09-07 09:25:10 +09:00
jade
ac432a95dd feat: add MCI request DTO scaffold conversion
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 31s
2026-09-07 09:18:43 +09:00
jade
a58af8178a 업데이트
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 16s
2026-09-04 18:09:10 +09:00
jade
a9d2f920e0 업데이트
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 16s
2026-09-04 17:44:53 +09:00
jade
459895ed40 업데이트
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-09-04 17:44:21 +09:00
jade
072a31d883 key 값 업데이트
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 23s
2026-09-03 14:19:50 +09:00
jade
69204cb6dd fix: tolerate AI manifest YAML colons
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 37s
2026-09-03 14:16:57 +09:00
Boram
82c087e2da dto다운로드
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 22s
2026-09-03 10:46:10 +09:00
jade
693ddea18d fix: normalize scaffold pod and tool generation
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 35s
2026-09-03 10:10:21 +09:00
jade
e6bcfe8934 fix: align pod manifest scaffold schema
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 25s
2026-09-02 14:34:56 +09:00
jade
2dc0c19c5a feat: add AI manifest generation to pod scaffold
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 1m6s
2026-09-02 13:29:42 +09:00
jade
a85a5e8d33 fix: add explicit parameter name to ToolReportController
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 12s
2026-09-01 17:00:14 +09:00
jade
a6eb4c4fe5 fix: correct placeholder text in usecase dropdown to '새 UseCase 생성' instead of '새 도메인 생성'
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 10s
2026-09-01 16:49:53 +09:00
jade
f9b96bf7ab fix: add explicit names to all @PathVariable and @RequestParam to support legacy compilers without -parameters flag
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 10s
2026-09-01 16:42:19 +09:00
jade
1efd8617a7 fix: explicitly name @RequestParam to support environments compiled without -parameters flag
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 11s
2026-09-01 16:34:07 +09:00
jade
cfee4e381e fix: robust handling of non-array responses in scaffold.html dropdowns
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 26s
2026-09-01 16:18:54 +09:00
jade
cfd074163b fix: use inputSchema for MCP tool definitions in tester.html
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 14s
2026-08-31 17:55:16 +09:00
jade
cd9f597297 fix: enforce pascal case for MCI client generation
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 1m25s
2026-08-31 17:06:07 +09:00
jade
1e9b8506d0 fix: restore missing syntax characters and encoding in scaffold.html
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 30s
2026-08-31 16:16:01 +09:00
jade
2809f1fd3a refactor: remove retryEnabled from tool metadata 2026-08-31 10:12:31 +09:00
jade
14cb280b0e feat: configure tool timeout and retry metadata in gateway scaffold
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 26s
2026-08-31 09:40:27 +09:00
jade
c791929c9e Revert "fix: resolve 401 Unauthorized for Gemma by using the valid common API key"
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 13s
This reverts commit 35ec9d0730.
2026-08-28 17:25:25 +09:00
jade
35ec9d0730 fix: resolve 401 Unauthorized for Gemma by using the valid common API key
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 13s
2026-08-28 17:20:51 +09:00
jade
f7ed8e8788 fix: route Scaffolder AI requests to internal litellm and fix UseCase update bug
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 29s
2026-08-28 17:11:39 +09:00
jade
94b7c1a395 fix: restore missing OpenRouter free models in HTML dropdowns 2026-08-28 16:06:10 +09:00
jade
5894b23095 Revert "fix: remove mcp-annotations and use local annotations"
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 36s
This reverts commit dc8eaaa17b.
2026-08-28 15:36:21 +09:00
jade
dc8eaaa17b fix: remove mcp-annotations and use local annotations
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
2026-08-28 15:10:24 +09:00
jade
d857069a3c feat: Add support for internal LiteLLM models (Qwen3, Gemma-4)
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
- Add dynamic routing in ChatController for Qwen3-Coder and Gemma-4-31B
- Update scaffold.html and chat.html model selection dropdowns
- Maintain existing OpenRouter fallback via application.yml
2026-08-28 13:55:34 +09:00
jade
658b761778 소스 업데이트
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
2026-08-28 10:04:23 +09:00
jade
b66a708757 chore: add src/main/generated/ to gitignore
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
2026-08-28 10:04:21 +09:00
jade
d5f644e666 fix: 로컬 환경(Windows)에서 Folder Picker(AWT)가 작동하도록 Spring Boot headless 모드 비활성화
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
2026-08-28 09:59:42 +09:00
jade
afd9d57523 fix: Scaffold 생성 시 execute() 대신 Tool 이름 기반 카멜케이스 함수명 사용하도록 변경
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
2026-08-28 09:53:56 +09:00
jade
69ac0ab1df fix: restore corrupted UTF-8 encoding in HTML files
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 11s
2026-08-27 14:57:03 +09:00
jade
23cafbf8e2 fix: scaffold.html 절대경로를 상대경로로 완전 변경 (Context Path 배포 지원)
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 12s
2026-08-27 14:51:29 +09:00
jade
ba65417119 fix: tester.html 에서 Tool 실행 시 JSON Schema 타입(integer, number, boolean)에 맞춰 입력값을 자동 형변환하도록 수정
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 11s
2026-08-27 14:16:42 +09:00
jade
e73a006bac fix: static html relative paths
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 11s
2026-08-27 14:04:56 +09:00
jade
309614a319 fix: scaffold.html typo
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 12s
2026-08-27 14:03:13 +09:00
jade
c5a36814e2 feat: Scaffolder 생성 시 MCI UseCaseImpl 템플릿을 실제 요청(image) 구조(BizException, resTransfer null 체크, IO DTO)와 완전히 일치하도록 수정
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 25s
2026-08-27 13:53:08 +09:00
jade
561520778e refactor: 폐쇄망 환경을 위해 UI html의 외부 CDN(Tailwind, Google Fonts)을 로컬 정적 리소스로 완전 대체
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 11s
2026-08-27 10:34:23 +09:00
jade
a362fe69d9 fix: tester.html 파라미터 스키마 렌더링 오류 수정 및 객체 재귀 탐색 기능 추가
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 12s
2026-08-27 09:53:10 +09:00
jade
8810126185 소스 업데이트
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 12s
2026-08-27 09:14:30 +09:00
jade
ffe5777fea fix: ObjectMapper 주입 시 단일 매칭 실패(NoUniqueBeanDefinitionException) 해결을 위해 jsonMapper를 objectMapper로 변경하고 Primary 복구
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 13s
2026-08-27 09:05:14 +09:00
jade
2fbf805afb fix: 스프링 필터 AOP 프록시 충돌(NPE) 해결 및 사내 로컬 AI 모델(Gemma-4-31B, Qwen3-Coder) 추가 지원
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 29s
2026-08-27 08:54:09 +09:00
jade
bb8020b99c feat: Scaffold UI 및 로직 9자리 Target System Code 지원 업데이트
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 31s
- 폐쇄망 환경을 위한 Bootstrap 등 정적 자원 로컬화 (cdn -> vendor) 적용

- Target System Code를 9자리(예: ONBTI2370)까지 입력받을 수 있도록 수정

- 9자리 입력 시, 2~5번째 문자를 추출하여 패키지 경로 및 Client 클래스 명명에 사용하도록 ToolScaffolder 로직 변경

- IO DTO 이름 생성 규칙(interfaceId -> ioPrefix) 수정
2026-08-25 17:50:55 +09:00
jade
182d50a373 gradle 수정
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 15s
2026-08-20 15:35:15 +09:00
jade
9fb3d28d52 gradle 및 불필요한 소스 수정
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 17s
2026-08-20 15:26:15 +09:00
0feb3e9d26 Merge pull request 'juheelee' (#6) from juheelee into main
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 17s
Reviewed-on: #6
2026-08-20 15:12:19 +09:00
juheelee
a19f1d75b9 Merge branch 'main' of https://git.devjun.net/kimhyungsik/ax_hub_mcp_tool into juheelee 2026-08-20 15:02:41 +09:00
juheelee
fa5335131e fix: dap-was-dapmt 바라보도록 수정 2026-08-20 15:00:10 +09:00
juheelee
52ae31d853 fix: dap-was-dapmt 바라보도록 수정 2026-08-20 14:58:50 +09:00
jade
d73e597543 fix(gateway): restore broken Korean characters in ToolPlanner caused by encoding mismatch during rename
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 16s
2026-08-20 14:54:24 +09:00
jade
4986f9801c fix: resolve PodScaffolder missing workspace path by reading AXHUB_SOURCE_DIR system property instead of just env var
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 23s
2026-08-20 13:31:51 +09:00
jade
400fa3b6c1 fix: resolve remaining baseName placeholders for MCI IO files in grouped append and converter generators
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 14s
2026-08-20 13:30:32 +09:00
jade
90829432c4 fix: replace missed baseName placeholders for MCI IO files and UseCaseImpl integration packages
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 16s
2026-08-20 13:15:50 +09:00
jade
bce603c3b6 fix: resolve mci package splitting and IO class naming issues in ToolScaffolder
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 17s
2026-08-20 13:02:30 +09:00
jade
40ca961239 fix: restore static html files that were dropped during massive rename
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 17s
2026-08-20 12:51:21 +09:00
jade
0085ec6919 refactor: massive rename dap -> dat and fix scaffold syntax error
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 14s
2026-08-20 10:41:37 +09:00
jade
64c422378f feat(scaffold): add native folder picker for Workspace Path selection
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 39s
- Added /api/v1/scaffold/browse-folder endpoint in ScaffoldingController to open native JFileChooser.
- Updated scaffold.html to use the new API for both Pod Module and Tool Function.
- Fixed JS event listener bug where workspacePath changes only affected Pod Module.
2026-08-20 09:22:03 +09:00
jade
d2fb804e5c fix: make AxhubSessionService optional to prevent Gateway crash
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 21s
2026-08-19 17:21:10 +09:00
jade
fdd6c6b99a fix: add DRTM_CD and USER_ID to HeaderDefaults
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 30s
2026-08-19 17:13:43 +09:00
jade
f25b6252d1 feat: align AxhubMciComponent with TmMciComponent (DRTM_CD, USER_ID, BizException)
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 15s
2026-08-19 16:53:02 +09:00
jade
69c1cb30af fix: import path for GlowTrgmField
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 44s
2026-08-19 16:45:25 +09:00
jade
e998127fa5 feat: implement MCI session fetching using employee number
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 1m3s
2026-08-19 16:41:13 +09:00
jade
9cdf313a77 fix: refactor ToolScaffolder to use generic MciClient for MCI tools
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 41s
2026-08-19 14:06:37 +09:00
jade
870412f0c7 fix: resolve workspace path resolution and enhance GrowToolHint generation in ToolScaffolder
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 1m8s
2026-08-19 13:50:49 +09:00
jade
d5a07cd846 agent 내용 업데이트
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 16s
2026-08-19 10:37:00 +09:00
jade
6d6eaab742 feat(ui): Export tools respecting main filter and add OpenAPI/Markdown exports
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 17s
2026-08-19 09:45:53 +09:00
jade
17524c5887 feat(ui): Enhance Tools List JSON modal with search and Postman export
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 15s
2026-08-19 09:40:51 +09:00
jade
ebef388893 feat(ui): Display Tools List JSON in a modal screen instead of direct download
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 17s
2026-08-19 09:35:32 +09:00
jade
eac481da41 feat(ui): Add Export JSON button to Tester UI
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 38s
2026-08-19 09:30:42 +09:00
jade
0122de84aa feat(was-sys): add target parameter to TeamContactUseCase to filter by team
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 14s
2026-08-18 21:21:45 +09:00
jade
bb98eac44c feat(was-sys): update team contact info with full roster
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 14s
2026-08-18 21:20:27 +09:00
jade
6060eb9f93 feat(was-sys): add TeamContactUseCase for AX team and MCP managers
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 16s
2026-08-18 21:13:27 +09:00
jade
7737ac8905 fix(gateway): resolve ClassCastException when parsing tools list in ChatController
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m37s
2026-08-18 21:04:41 +09:00
jade
de2d1061aa refactor(was-lib): use bundleId to dynamically match module-wide tool manifest requests
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-08-18 21:00:09 +09:00
jade
35fa033a1c feat(was-lib): map sys category to iam and pct tools
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-08-18 20:57:13 +09:00
jade
3216148ee2 feat(was-lib): add /tool-manifest/{categoryKey} endpoint for filtered manifest
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 3m19s
2026-08-18 20:51:41 +09:00
jade
fbae7618eb fix(gateway): fix routing prefix matching and cache fallback tools
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 3m9s
2026-08-18 20:46:27 +09:00
jade
c390b89250 컴파일 오류 삭제
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-08-18 20:46:04 +09:00
jade
b4ffac0cd5 fix(gateway): accept registered tools from fallback to recover after gateway restart
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 3m22s
2026-08-18 20:30:49 +09:00
jade
6ec809fb65 fix(gateway): inject X-Tool-Server-API-Key header to tools/local fetch restClient
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-08-18 20:28:18 +09:00
jade
ebaff567c6 툴 검색 시 interface -> 구현체
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 5m20s
2026-08-18 18:12:37 +09:00
jade
c0069c2419 Refactor: apiKey to match dapms
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 3m27s
2026-08-18 16:58:25 +09:00
jade
9f347ef9de fix(security): Set tool-server-api-key property and revert header name
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 3m10s
2026-08-18 14:46:39 +09:00
jade
91b2b67d85 refactor(security): Change X-Tool-Server-API-Key header to apiKey
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 3m44s
2026-08-18 14:41:12 +09:00
jade
b8fd9492c6 Refactor: Switch schema validator to MCP SDK DefaultJsonSchemaValidator (JSON Schema 2020-12) to match dapms
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 3m23s
2026-08-18 11:05:47 +09:00
jade
406e3c79b7 fix: align tool headers
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 3m51s
2026-08-18 09:52:54 +09:00
jade
8bd9b9e922 feat(manifest): expose tool usage guidance
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 3m17s
2026-08-17 15:11:46 +09:00
jade
0816c0ff0c feat(mcp): expose tool guidance in metadata
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 5m4s
2026-08-17 14:48:09 +09:00
jade
59f9a8eaad fix(mci): Fix ClassCastException in GlowMciComponent mock by instantiating resBodyClass
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 3m19s
2026-08-14 17:58:05 +09:00
jade
a420c46c89 fix(gateway): Add fallback routing rule for cmm category
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 3m7s
2026-08-14 17:34:52 +09:00
jade
f70ee854ec fix(sync): Ignore AsyncRequestNotUsableException in GlobalExceptionHandler
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 3m14s
2026-08-14 17:15:06 +09:00
윤희준
6e65228fb9 fix(deploy): install docker-cli and docker-cli-compose inside alpine runner container natively
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 4m42s
2026-08-14 16:55:54 +09:00
윤희준
a8ca65657a fix(deploy): use correct absolute path for host docker binary in volumes
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 2m3s
2026-08-14 16:52:08 +09:00
윤희준
e66e97ecc3 fix: restore repository runner token and implement missing syncToolSource method
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 53s
2026-08-14 16:49:43 +09:00
jade
ac73a86979 fix(sync): clear dead emitter and ignore IllegalStateException
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 1m18s
2026-08-14 16:35:34 +09:00
jade
1411f39ffc fix(sync): downgrade noisy SSE transmission logs to DEBUG
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-08-14 16:33:53 +09:00
jade
1d69bd914e fix(sync): Ignore Broken Pipe on SSE emitter in warm pool and optimize SyncRunner
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-08-14 16:27:09 +09:00
binzz
d8a8501c4e sal 50개추가
Some checks are pending
Deploy to OCIWP / deploy (push) Waiting to run
2026-08-14 16:12:56 +09:00
juheelee
f38633fa9e feat: cus에 툴 50개 추가
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m15s
2026-08-14 14:53:23 +09:00
Boram
ead0cb4375 Merge remote-tracking branch 'origin/main'
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m16s
2026-08-14 14:09:36 +09:00
Boram
b0a0f38691 pro에 mcptool 50개 생성 2026-08-14 14:09:23 +09:00
651 changed files with 17136 additions and 13070 deletions

View File

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

View File

@@ -18,7 +18,7 @@ jobs:
apt-get update
apt-get install -y nodejs git openjdk-21-jdk rsync
elif command -v apk &> /dev/null; then
apk add --no-cache nodejs git openjdk21 rsync
apk add --no-cache nodejs git openjdk21 rsync docker-cli docker-cli-compose
fi
- name: Checkout Code

3
.gitignore vendored
View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

Binary file not shown.

View File

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

Binary file not shown.

BIN
.gradle/file-system.probe Normal file

Binary file not shown.

View File

10
.idea/.gitignore generated vendored Normal file
View File

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

1
.idea/.name generated Normal file
View File

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

153
.idea/compiler.xml generated Normal file
View File

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

16
.idea/dataSources.local.xml generated Normal file
View File

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

17
.idea/dataSources.xml generated Normal file
View File

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

View File

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

6
.idea/encodings.xml generated Normal file
View File

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

23
.idea/gradle.xml generated Normal file
View File

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

20
.idea/jarRepositories.xml generated Normal file
View File

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

17
.idea/misc.xml generated Normal file
View File

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

17
.idea/modules.xml generated Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

6
.idea/vcs.xml generated Normal file
View File

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

868
.idea/workspace.xml generated Normal file
View File

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

View File

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

View File

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

144
README.md
View File

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

7
TestClientTransport.java Normal file
View File

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

22
TestMcpCall.java Normal file
View File

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

39
TestMcpClient.java Normal file
View File

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

BIN
TestReflection.class Normal file

Binary file not shown.

11
TestReflection.java Normal file
View File

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

20
TestSseEndpoint.java Normal file
View File

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

View File

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

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@@ -1,432 +0,0 @@
package io.shinhanlife.dap.mcg.presentation;
/**
* @package io.shinhanlife.dap.mcg.presentation
* @className ScaffoldingController
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.lib.util.PodScaffolder;
import io.shinhanlife.dap.lib.util.ToolScaffolder;
import io.shinhanlife.dap.lib.util.ToolSourceUpdater;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/scaffold")
public class ScaffoldingController {
private static final Set<String> SUPPORTED_FIELD_TYPES = Set.of(
"String", "Integer", "Long", "Double", "Boolean", "BigDecimal", "List");
private static final Set<String> SUPPORTED_AI_MODELS = Set.of(
"inclusionai/ling-3.0-flash:free",
"openai/gpt-oss-20b:free",
"google/gemma-4-31b-it:free",
"nvidia/nemotron-3-nano-30b-a3b:free",
"cohere/north-mini-code:free");
private final ChatClient.Builder chatClientBuilder;
private final ObjectMapper objectMapper;
public ScaffoldingController(ChatClient.Builder chatClientBuilder, ObjectMapper objectMapper) {
this.chatClientBuilder = chatClientBuilder;
this.objectMapper = objectMapper;
}
@PostMapping("/pod")
public String scaffoldPod(@RequestBody Map<String, String> req) {
try {
String moduleName = req.getOrDefault("moduleName", "dap-was-cus");
if (!moduleName.startsWith("dap-was-")) moduleName = "dap-was-" + moduleName;
String port = req.getOrDefault("port", "8085");
String shortName = moduleName.replace("dap-was-", "").replace("-", "");
String author = req.get("author");
if (author == null || author.trim().isEmpty()) author = System.getProperty("user.name");
String date = req.get("date");
if (date == null || date.trim().isEmpty()) date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
return PodScaffolder.scaffoldPod(moduleName, port, shortName, author, date);
} catch (Exception e) {
return "오류 발생: " + e.getMessage();
}
}
@PostMapping("/tool")
public String scaffoldTool(@RequestBody Map<String, String> req) {
try {
String baseName = req.get("baseName");
String interfaceId = req.get("interfaceId");
String title = req.get("title");
if (title == null || title.isBlank()) title = baseName;
String description = req.get("description");
String group = req.getOrDefault("categoryKey", req.getOrDefault("group", "COMMON"));
String routingType = req.getOrDefault("routingType", "HTTP");
String moduleName = req.getOrDefault("moduleName", "dap-was-cus");
String author = req.get("author");
if (author == null || author.trim().isEmpty()) author = System.getProperty("user.name");
String date = req.get("date");
if (date == null || date.trim().isEmpty()) date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
boolean register = Boolean.parseBoolean(req.getOrDefault("register", "true"));
String clientSystemCode = req.get("clientSystemCode");
String httpApiName = req.get("httpApiName");
String inputSchemaResource = req.get("inputSchemaResource");
String outputSchemaResource = req.get("outputSchemaResource");
List<ToolScaffolder.FieldDefinition> inputFields = parseFields(req.get("inputFields"));
List<ToolScaffolder.FieldDefinition> outputFields = parseFields(req.get("outputFields"));
if (inputFields.isEmpty()) {
inputFields = List.of(new ToolScaffolder.FieldDefinition("query", "String", "Search query", List.of("example"), "", false));
}
ToolScaffolder.ToolDefinitionOptions definitionOptions = new ToolScaffolder.ToolDefinitionOptions(
req.get("functionDescription"),
req.get("whenToUse"),
req.get("whenNotToUse"),
req.get("ioLimits"),
req.get("displayDescription"),
parseDelimited(req.get("exampleQueries")),
parseDelimited(req.get("tags")),
req.get("ownerOrg"));
return ToolScaffolder.scaffold(baseName, interfaceId, title, description, group, routingType,
moduleName, author, date, register, clientSystemCode, inputSchemaResource,
outputSchemaResource, inputFields, outputFields, httpApiName, definitionOptions);
} catch (Exception e) {
return "오류 발생: " + e.getMessage();
}
}
@PostMapping("/tool-group")
public String scaffoldToolGroup(@RequestBody ToolGroupRequest request) {
try {
if (request == null || request.useCaseName() == null
|| !request.useCaseName().trim().matches("^[A-Z][A-Za-z0-9]*$")) {
throw new IllegalArgumentException("UseCase name must be PascalCase.");
}
String moduleName = request.moduleName() == null || request.moduleName().isBlank()
? "dap-was-cus" : request.moduleName().trim();
String author = request.author() == null || request.author().isBlank()
? System.getProperty("user.name") : request.author().trim();
String date = request.date() == null || request.date().isBlank()
? LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd")) : request.date().trim();
return ToolScaffolder.scaffoldUseCase(request.useCaseName().trim(), moduleName, author, date,
request.tools() == null ? List.of() : request.tools());
} catch (Exception e) {
return "Error: " + safeMessage(e);
}
}
@GetMapping("/usecases")
public List<String> listUseCases(@RequestParam String moduleName, @RequestParam String categoryKey) {
if (moduleName == null || !moduleName.matches("^dap-was-[a-z0-9-]+$")) {
throw new IllegalArgumentException("Invalid target module.");
}
if (categoryKey == null || !categoryKey.matches("^[a-z0-9]{3}$")) {
throw new IllegalArgumentException("Invalid domain category.");
}
String sourceDir = System.getenv("AXHUB_SOURCE_DIR");
if (sourceDir == null || sourceDir.isBlank()) sourceDir = System.getProperty("user.dir");
File useCaseDir = new File(sourceDir, moduleName + "/src/main/java/io/shinhanlife/dap/mcc/biz/"
+ categoryKey + "/usecase");
File[] files = useCaseDir.listFiles(file -> file.isFile() && file.getName().endsWith("UseCase.java"));
if (files == null) return List.of();
return Arrays.stream(files).map(File::getName)
.map(name -> name.substring(0, name.length() - ".java".length()))
.sorted().toList();
}
@GetMapping("/categories")
public List<String> listCategories(@RequestParam String moduleName) {
if (moduleName == null || !moduleName.matches("^dap-was-[a-z0-9-]+$")) {
throw new IllegalArgumentException("Invalid target module.");
}
String sourceDir = System.getenv("AXHUB_SOURCE_DIR");
if (sourceDir == null || sourceDir.isBlank()) sourceDir = System.getProperty("user.dir");
File bizDir = new File(sourceDir, moduleName + "/src/main/java/io/shinhanlife/dap/mcc/biz");
File[] dirs = bizDir.listFiles(File::isDirectory);
if (dirs == null) return List.of();
return Arrays.stream(dirs).map(File::getName)
.filter(name -> name.matches("^[a-z0-9]{3}$"))
.sorted().toList();
}
@PostMapping("/field-draft")
public ResponseEntity<?> generateFieldDraft(@RequestBody Map<String, String> req) {
String description = req.getOrDefault("description", "").trim();
String target = req.getOrDefault("target", "inputFields");
if (description.isBlank()) {
return ResponseEntity.badRequest().body(Map.of("error", "설명을 입력해주세요."));
}
if (!"inputFields".equals(target) && !"outputFields".equals(target)) {
return ResponseEntity.badRequest().body(Map.of("error", "지원하지 않는 필드 대상입니다."));
}
try {
String targetLabel = "inputFields".equals(target) ? "INPUT request DTO" : "OUTPUT response DTO";
String prompt = """
Generate Java DTO fields for an MCP tool.
Return JSON only. Do not add Markdown, explanations, or code fences.
The response must have this exact shape:
{"fields":[{"name":"camelCaseName","type":"String","description":"short description","examples":["example1","example2"],"pattern":"^regex$","required":true,"enumValues":[],"itemType":null,"itemFields":[]}]}
Allowed type values: String, Integer, Long, Double, Boolean, BigDecimal, List.
Finite values should be enforced by populating enumValues on standard types. List fields must include itemType; use Object plus itemFields for object lists.
If applicable, provide a regex for pattern.
Generate fields only for the requested target: %s.
For OUTPUT fields, include resultCode and resultMessage when appropriate.
Keep field names valid Java camelCase identifiers. Generate at most 10 fields.
User description: %s
""".formatted(targetLabel, description);
String response = generateAiContent(prompt, req.get("model"));
FieldDraft draft = objectMapper.readValue(stripCodeFence(response), FieldDraft.class);
List<ToolScaffolder.FieldDefinition> fields = validateFields(draft.fields());
return ResponseEntity.ok(Map.of("fields", fields));
} catch (Exception e) {
return ResponseEntity.internalServerError().body(Map.of("error", "AI 초안 생성 실패: " + safeMessage(e)));
}
}
@PostMapping("/tool-draft")
public ResponseEntity<?> generateToolDraft(@RequestBody Map<String, String> req) {
String description = req.getOrDefault("description", "").trim();
if (description.isBlank()) {
return ResponseEntity.badRequest().body(Map.of("error", "Tool 설명을 입력해주세요."));
}
try {
String prompt = """
Generate an MCP Tool scaffold from the user request.
Return JSON only. Do not add Markdown, explanations, or code fences.
The response must have this exact shape:
{"baseName":"PascalCaseName","title":"short Korean title","description":"clear Korean LLM tool guidance","categoryKey":"cmm","routingType":"HTTP","httpApiName":"simple-api-name","functionDescription":"core business function","displayDescription":"short portal description","whenToUse":"specific user requests that should select this tool","whenNotToUse":"requests or conditions that must not select this tool","ioLimits":"allowed input and output scope and limits","exampleQueries":["query 1","query 2","query 3"],"tags":["domain","action"],"ownerOrg":"MCP_TOOL","inputFields":[{"name":"camelCaseName","type":"String","description":"short description","examples":["example1","example2"],"pattern":"^regex$","required":true,"enumValues":[],"itemType":null,"itemFields":[]}],"outputFields":[{"name":"resultCode","type":"String","description":"result code","examples":["SUCCESS"],"pattern":"","required":true,"enumValues":[],"itemType":null,"itemFields":[]}]}
categoryKey must be exactly three lowercase letters or digits.
routingType must be either HTTP or MCI. httpApiName can contain only letters, digits, hyphens, and underscores.
Write every V17 metadata field for its distinct purpose; do not copy the same sentence into all fields.
Generate 3 to 10 realistic exampleQueries and concise search tags. Use MCP_TOOL for ownerOrg unless the user names an owner.
Allowed field type values: String, Integer, Long, Double, Boolean, BigDecimal, List. Finite values should be enforced by populating enumValues. List must include itemType and object lists include itemFields.
If applicable, provide a regex for pattern.
Keep all field names valid Java camelCase identifiers. Generate at most 10 fields per list.
Do not generate interfaceId or clientSystemCode; those must come from a real integration contract.
User request: %s
""".formatted(description);
String response = generateAiContent(prompt, req.get("model"));
ToolDraft draft = objectMapper.readValue(stripCodeFence(response), ToolDraft.class);
ToolDraft validatedDraft = validateToolDraft(draft);
return ResponseEntity.ok(validatedDraft);
} catch (Exception e) {
return ResponseEntity.internalServerError().body(Map.of("error", "AI Tool 초안 생성 실패: " + safeMessage(e)));
}
}
@PostMapping("/tool/update")
public String updateTool(@RequestBody Map<String, String> req) {
try {
String toolName = req.get("toolName");
String domainGroup = req.getOrDefault("categoryKey", req.get("domainGroup"));
String description = req.get("description");
boolean register = Boolean.parseBoolean(req.getOrDefault("register", "true"));
Boolean requiresApproval = req.containsKey("requiresApproval") ? Boolean.parseBoolean(req.get("requiresApproval")) : null;
ToolSourceUpdater.updateToolSource(toolName, domainGroup, description, register, requiresApproval);
return "성공";
} catch (Exception e) {
return "오류 발생: " + e.getMessage();
}
}
@GetMapping("/modules")
public List<String> listModules() {
try {
String sourceDir = System.getenv("AXHUB_SOURCE_DIR");
if (sourceDir == null) sourceDir = System.getProperty("user.dir");
File dir = new File(sourceDir);
File[] files = dir.listFiles(f -> f.isDirectory() && f.getName().startsWith("dap-was-") && !f.getName().equals("dap-was-lib"));
if (files == null || files.length == 0) {
return List.of("dap-was-cus", "dap-was-sal", "dap-was-pro", "dap-was-sys");
}
return Arrays.stream(files).map(File::getName).sorted().collect(Collectors.toList());
} catch (Exception e) {
return List.of("dap-was-cus", "dap-was-sal", "dap-was-pro", "dap-was-sys");
}
}
private List<ToolScaffolder.FieldDefinition> parseFields(String source) throws Exception {
if (source == null || source.isBlank()) {
return List.of();
}
return objectMapper.readValue(source, new TypeReference<List<ToolScaffolder.FieldDefinition>>() { });
}
private List<String> parseDelimited(String source) {
if (source == null || source.isBlank()) {
return List.of();
}
return Arrays.stream(source.split("[\\r\\n,]+"))
.map(String::trim)
.filter(value -> !value.isBlank())
.distinct()
.toList();
}
private List<ToolScaffolder.FieldDefinition> validateFields(List<ToolScaffolder.FieldDefinition> source) {
if (source == null || source.isEmpty()) {
throw new IllegalArgumentException("AI가 필드를 생성하지 않았습니다.");
}
Set<String> names = new LinkedHashSet<>();
return source.stream()
.limit(10)
.filter(field -> field != null && field.name() != null && !field.name().isBlank())
.map(field -> new ToolScaffolder.FieldDefinition(
field.name().trim(),
field.type() == null ? "String" : field.type().trim(),
field.description() == null ? "" : field.description().trim(),
field.examples() == null ? List.of() : field.examples().stream().map(String::trim).toList(),
field.pattern() == null ? "" : field.pattern().trim(),
field.required(),
field.enumValues() == null ? List.of() : field.enumValues(),
field.itemType(),
field.itemFields() == null ? List.of() : field.itemFields()))
.peek(field -> {
if (!field.name().matches("^[A-Za-z_$][A-Za-z0-9_$]*$")) {
throw new IllegalArgumentException("AI가 올바르지 않은 필드명을 생성했습니다: " + field.name());
}
if (!SUPPORTED_FIELD_TYPES.contains(field.type())) {
throw new IllegalArgumentException("AI가 지원하지 않는 Type을 생성했습니다: " + field.type());
}
validateStructuredField(field);
if (!names.add(field.name())) {
throw new IllegalArgumentException("AI가 중복 필드명을 생성했습니다: " + field.name());
}
})
.toList();
}
private void validateStructuredField(ToolScaffolder.FieldDefinition field) {
if ("List".equals(field.type()) && (field.itemType() == null || field.itemType().isBlank())) {
throw new IllegalArgumentException("List field needs itemType: " + field.name());
}
if ("List".equals(field.type()) && "Object".equals(field.itemType()) && field.itemFields().isEmpty()) {
throw new IllegalArgumentException("Object List field needs itemFields: " + field.name());
}
}
private ToolDraft validateToolDraft(ToolDraft draft) {
if (draft == null || draft.baseName() == null || !draft.baseName().trim().matches("^[A-Z][A-Za-z0-9]*$")) {
throw new IllegalArgumentException("AI가 올바르지 않은 Base Name을 생성했습니다.");
}
String categoryKey = draft.categoryKey() == null ? "" : draft.categoryKey().trim().toLowerCase(Locale.ROOT);
if (!categoryKey.matches("^[a-z0-9]{3}$")) {
throw new IllegalArgumentException("AI가 올바르지 않은 Category Key를 생성했습니다.");
}
String routingType = draft.routingType() == null ? "" : draft.routingType().trim().toUpperCase(Locale.ROOT);
if (!Set.of("HTTP", "MCI").contains(routingType)) {
throw new IllegalArgumentException("AI가 지원하지 않는 Protocol을 생성했습니다.");
}
String httpApiName = draft.httpApiName() == null ? "http-api" : draft.httpApiName().trim();
if (!httpApiName.matches("^[A-Za-z0-9_-]+$")) {
throw new IllegalArgumentException("AI가 올바르지 않은 HTTP API Name을 생성했습니다.");
}
String title = draft.title() == null ? "" : draft.title().trim();
String description = draft.description() == null ? "" : draft.description().trim();
if (title.isBlank() || description.isBlank()) {
throw new IllegalArgumentException("AI가 Tool 제목 또는 설명을 생성하지 않았습니다.");
}
String functionDescription = textOrDefault(draft.functionDescription(), description);
String displayDescription = textOrDefault(draft.displayDescription(), title);
String whenToUse = textOrDefault(draft.whenToUse(), description + " 요청을 처리할 때 사용한다.");
String whenNotToUse = textOrDefault(draft.whenNotToUse(), "필수 입력값이 없거나 다른 업무 요청에는 사용하지 않는다.");
String ioLimits = textOrDefault(draft.ioLimits(), "정의된 입력 필드만 허용하며 정의된 응답 DTO 범위만 반환한다.");
List<String> exampleQueries = normalizedDraftList(draft.exampleQueries(), List.of(
title + " 해줘", title + " 정보를 알려줘", title + " 결과를 확인해줘"));
List<String> tags = normalizedDraftList(draft.tags(), List.of(categoryKey));
String ownerOrg = textOrDefault(draft.ownerOrg(), "MCP_TOOL");
return new ToolDraft(draft.baseName().trim(), title, description, categoryKey, routingType, httpApiName,
functionDescription, displayDescription, whenToUse, whenNotToUse, ioLimits,
exampleQueries, tags, ownerOrg,
validateFields(draft.inputFields()), validateFields(draft.outputFields()));
}
private String textOrDefault(String value, String fallback) {
return value == null || value.isBlank() ? fallback : value.trim();
}
private List<String> normalizedDraftList(List<String> values, List<String> fallback) {
if (values == null) {
return fallback;
}
List<String> normalized = values.stream()
.filter(value -> value != null && !value.isBlank())
.map(String::trim)
.distinct()
.toList();
return normalized.isEmpty() ? fallback : normalized;
}
private String generateAiContent(String prompt, String requestedModel) {
return chatClientBuilder.build().prompt()
.user(prompt)
.options(org.springframework.ai.openai.OpenAiChatOptions.builder()
.model(resolveModel(requestedModel)).build())
.call()
.content();
}
private String resolveModel(String requestedModel) {
String model = requestedModel == null || requestedModel.isBlank()
? "cohere/north-mini-code:free" : requestedModel.trim();
if (!SUPPORTED_AI_MODELS.contains(model)) {
throw new IllegalArgumentException("지원하지 않는 AI 모델입니다.");
}
return model;
}
private String stripCodeFence(String response) {
if (response == null) {
throw new IllegalArgumentException("AI 응답이 비어 있습니다.");
}
return response.trim().replaceFirst("^```(?:json)?\\s*", "").replaceFirst("\\s*```$", "").trim();
}
private String safeMessage(Exception e) {
return e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage();
}
private record FieldDraft(List<ToolScaffolder.FieldDefinition> fields) {
}
private record ToolDraft(String baseName, String title, String description, String categoryKey, String routingType,
String httpApiName, String functionDescription, String displayDescription,
String whenToUse, String whenNotToUse, String ioLimits,
List<String> exampleQueries, List<String> tags, String ownerOrg,
List<ToolScaffolder.FieldDefinition> inputFields,
List<ToolScaffolder.FieldDefinition> outputFields) {
}
private record ToolGroupRequest(String useCaseName, String moduleName, String author, String date,
List<ToolScaffolder.ToolMethodDefinition> tools) {
}
}

View File

@@ -1,124 +0,0 @@
package io.shinhanlife.dap.mcg.presentation;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClient;
import org.springframework.web.server.ResponseStatusException;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** Gateway 내부 MCP 목록 조회와 Report 서비스의 Excel API 프록시를 담당한다. */
@RestController
public class ToolReportProxyController {
private final RestClient restClient;
private final String reportServiceUrl;
private final McpRouterController mcpRouterController;
public ToolReportProxyController(
RestClient.Builder restClientBuilder,
McpRouterController mcpRouterController,
@Value("${report.service-url}") String reportServiceUrl) {
this.restClient = restClientBuilder.build();
this.mcpRouterController = mcpRouterController;
this.reportServiceUrl = reportServiceUrl.replaceAll("/+$", "");
}
/** Tool 목록은 Report 서비스나 로컬 소스가 아니라 Gateway의 MCP Registry에서만 조회한다. */
@GetMapping(value = "/report/api/report-tools", produces = MediaType.APPLICATION_JSON_VALUE)
public List<ReportToolSummary> tools() {
return activeTools().stream().map(ReportToolSummary::from).toList();
}
@PostMapping(value = "/report/api/tool-reports/excel",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
public ResponseEntity<byte[]> excel(@RequestBody Map<String, Object> request) {
validateMcpSelection(request);
ResponseEntity<byte[]> response = restClient.post()
.uri(reportServiceUrl + "/api/tool-reports/excel")
.contentType(MediaType.APPLICATION_JSON)
.body(request)
.retrieve()
.toEntity(byte[].class);
return copy(response);
}
private void validateMcpSelection(Map<String, Object> request) {
Object rawNames = request.get("toolNames");
if (!(rawNames instanceof List<?> names) || names.isEmpty()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "toolNames must contain at least one tool");
}
Set<String> registeredNames = activeTools().stream()
.map(ToolMetadata::getName)
.collect(java.util.stream.Collectors.toSet());
Set<String> unknown = new LinkedHashSet<>();
names.stream().map(String::valueOf)
.filter(name -> !registeredNames.contains(name))
.forEach(unknown::add);
if (!unknown.isEmpty()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"MCP에 등록되지 않은 툴이 포함되어 있습니다: " + unknown);
}
}
private List<ToolMetadata> activeTools() {
return mcpRouterController.activeToolsForReport();
}
private ResponseEntity<byte[]> copy(ResponseEntity<byte[]> response) {
ResponseEntity.BodyBuilder builder = ResponseEntity.status(response.getStatusCode());
MediaType contentType = response.getHeaders().getContentType();
if (contentType != null) builder.contentType(contentType);
String disposition = response.getHeaders().getFirst(HttpHeaders.CONTENT_DISPOSITION);
if (disposition != null) builder.header(HttpHeaders.CONTENT_DISPOSITION, disposition);
return builder.body(response.getBody());
}
public record ReportToolSummary(
String name,
String title,
String description,
String categoryKey,
String mappingId,
boolean register,
boolean requiresApproval,
boolean readOnlyHint,
boolean destructiveHint,
boolean idempotentHint,
boolean openWorldHint,
String requestType,
String responseType,
String useCaseClass,
String sourceFile,
String inputSchemaResource,
String outputSchemaResource) {
static ReportToolSummary from(ToolMetadata tool) {
String name = tool.getName() == null || tool.getName().isBlank() ? tool.getUid() : tool.getName();
String title = tool.getDisplayName() == null || tool.getDisplayName().isBlank()
? name : tool.getDisplayName();
return new ReportToolSummary(
name, title, value(tool.getDescription()), value(tool.getCategoryKey()),
value(tool.getMciServiceId()), Boolean.TRUE.equals(tool.getIsRegistered()),
Boolean.TRUE.equals(tool.getRequiresApproval()), Boolean.TRUE.equals(tool.getReadOnlyHint()),
Boolean.TRUE.equals(tool.getDestructiveHint()), Boolean.TRUE.equals(tool.getIdempotentHint()),
Boolean.TRUE.equals(tool.getOpenWorldHint()), "", "", "", "", "", "");
}
private static String value(String value) {
return value == null ? "" : value;
}
}
}

View File

@@ -1,41 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 1. 로그 패턴 설정 (MDC traceId 포함) -->
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n" />
<!-- 2. 콘솔(Console) 출력 설정 (로컬 개발용) -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- 3. 파일(File) 출력 설정 (서버 운영용) -->
<!-- Logback에서 시스템 Hostname을 가져오기 위한 설정 -->
<property name="HOSTNAME" value="${HOSTNAME}" />
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/swlog/dap-gateway/A01/${HOSTNAME}_A01.log</file> <!-- 현재 로그가 쌓이는 파일 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 매일 자정에 날짜를 붙여서 지난 로그를 분리 저장 -->
<fileNamePattern>/swlog/dap-gateway/A01/${HOSTNAME}_A01_%d{yyyyMMdd}.%i.log</fileNamePattern>
<!-- 개별 로그 파일 크기를 100MB로 제한하고, 전체 보관 일수는 30일, 총 로그 누적 용량은 1GB로 제한 -->
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- 4. 기본 로깅 레벨 설정 -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
<!-- 5. 우리 프로젝트 패키지는 디버그 레벨까지 상세히 보기 -->
<logger name="io.shinhanlife" level="DEBUG" />
</configuration>

View File

@@ -31,7 +31,7 @@
.subtitle { margin: 0; color: #a1a1aa; font-size: 14px; line-height: 1.65; }
.count-badge { padding: 7px 11px; border: 1px solid #27272a; border-radius: 8px; background: #18181b; color: #a1a1aa; font-family: "Geist Mono", Consolas, monospace; font-size: 12px; white-space: nowrap; }
.panel { overflow: hidden; border: 1px solid #27272a; border-radius: 12px; background: #111113; box-shadow: 0 18px 50px rgba(0,0,0,.22); }
.toolbar { display: grid; grid-template-columns: minmax(260px,1fr) 190px auto auto; gap: 10px; padding: 16px; border-bottom: 1px solid #27272a; background: #111113; }
.toolbar { display: grid; grid-template-columns: 210px minmax(240px,1fr) 170px auto auto; gap: 10px; padding: 16px; border-bottom: 1px solid #27272a; background: #111113; }
input, select, button { min-height: 40px; border: 1px solid #3f3f46; border-radius: 7px; background: #18181b; color: #e4e4e7; padding: 8px 12px; font: inherit; font-size: 13px; outline: none; }
input::placeholder { color: #71717a; }
input:focus, select:focus { border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59,130,246,.12); }
@@ -95,6 +95,10 @@
<section class="panel">
<div class="toolbar">
<select id="sourceType" aria-label="Tool 소스 프로젝트">
<option value="DAP_WAS_DAPMT">dap-was-dapmt 소스</option>
<option value="DAP_ADMIN">dap-admin 소스</option>
</select>
<input id="keyword" type="search" autocomplete="off" placeholder="툴명, 제목 또는 설명 검색">
<select id="category"><option value="">전체 카테고리</option></select>
<button id="selectAll" type="button">전체 선택</button>
@@ -114,7 +118,7 @@
</main>
<script>
const state = { tools: [], selected: new Set() };
const state = { tools: [], selected: new Set(), sourceType: 'DAP_WAS_DAPMT' };
const el = id => document.getElementById(id);
const gateway = location.origin;
@@ -155,7 +159,11 @@
async function load() {
try {
const response = await fetch('/report/api/report-tools');
state.sourceType = el('sourceType').value;
state.selected.clear();
state.tools = [];
el('tools').innerHTML = '<tr><td class="loading-row" colspan="6">선택한 프로젝트의 Tool 소스를 분석하고 있습니다.</td></tr>';
const response = await fetch(`/report/api/report-tools?sourceType=${encodeURIComponent(state.sourceType)}`);
const body = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(body?.error || body?.message || `툴 목록 조회에 실패했습니다. (${response.status})`);
@@ -178,6 +186,10 @@
}
el('keyword').addEventListener('input', render);
el('sourceType').addEventListener('change', () => {
el('category').innerHTML = '<option value="">전체 카테고리</option>';
load();
});
el('category').addEventListener('change', render);
el('selectAll').addEventListener('click', () => { filtered().forEach(tool => state.selected.add(tool.name)); render(); });
el('clearAll').addEventListener('click', () => { filtered().forEach(tool => state.selected.delete(tool.name)); render(); });
@@ -185,7 +197,8 @@
el('download').disabled = true; updateStatus('소스를 분석하고 Excel을 생성하는 중입니다.');
try {
const response = await fetch('/report/api/tool-reports/excel', {
method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({toolNames: [...state.selected]})
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({toolNames: [...state.selected], sourceType: state.sourceType})
});
if (!response.ok) {
const body = await response.json().catch(() => ({}));

View File

@@ -1,38 +0,0 @@
package io.shinhanlife.dap.mcg.presentation;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dap.lib.mcp.security.SecurityProperties;
import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse;
import io.shinhanlife.dap.mcg.config.GatewayFallbackProperties;
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.shinhanlife.dap.mcg.service.ExecuteService;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.http.ResponseEntity;
class McpRouterControllerTest {
@Test
void returnsEmptyToolListWhenRedisIsUnavailableAndNoFallbackIsConfigured() {
RedisRegistryService registryService = org.mockito.Mockito.mock(RedisRegistryService.class);
when(registryService.getAllTools()).thenThrow(new RedisConnectionFailureException("Redis unavailable"));
McpRouterController controller = new McpRouterController(
registryService,
org.mockito.Mockito.mock(ExecuteService.class),
org.mockito.Mockito.mock(SecurityProperties.class),
new ObjectMapper(),
new GatewayFallbackProperties());
ResponseEntity<JsonRpcResponse> response = assertDoesNotThrow(() -> controller.listTools(null));
Map<?, ?> result = (Map<?, ?>) response.getBody().getResult();
assertEquals(List.of(), result.get("tools"));
}
}

View File

@@ -1,102 +0,0 @@
package io.shinhanlife.dap.mcg.presentation;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Path;
import java.nio.file.Files;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
class ScaffoldingControllerToolDraftTest {
@TempDir
Path root;
@Test
void listsExistingUseCasesForSelectedModuleAndCategory() throws Exception {
Path useCaseDir = root.resolve("dap-was-sample/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase");
Files.createDirectories(useCaseDir);
Files.writeString(useCaseDir.resolve("EmployeeSearchUseCase.java"), "interface EmployeeSearchUseCase {}\n");
Files.writeString(useCaseDir.resolve("Ignored.java"), "class Ignored {}\n");
String previousUserDir = System.getProperty("user.dir");
System.setProperty("user.dir", root.toString());
try {
ScaffoldingController controller = new ScaffoldingController(mock(ChatClient.Builder.class), new ObjectMapper());
assertEquals(java.util.List.of("EmployeeSearchUseCase"), controller.listUseCases("dap-was-sample", "smp"));
} finally {
System.setProperty("user.dir", previousUserDir);
}
}
@Test
void groupedToolRequestGeneratesOneUseCase() throws Exception {
ChatClient.Builder builder = mock(ChatClient.Builder.class);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(
new ScaffoldingController(builder, new ObjectMapper()))
.setMessageConverters(new MappingJackson2HttpMessageConverter())
.build();
String moduleName = root.resolve("dap-was-customer").toString().replace("\\", "\\\\");
String request = """
{"useCaseName":"Customer","moduleName":"%s","author":"tester","date":"2026.08.12","tools":[
{"baseName":"CustomerGuidance","methodName":"searchGuidance","interfaceId":"CTMNILO00007","title":"Customer guidance","description":"Search guidance","group":"cmm","routingType":"MCI","register":false,"clientSystemCode":"NILD","inputFields":[{"name":"customerId","type":"String","description":"Customer ID","example":"C001","required":true}],"outputFields":[]}
]}
""".formatted(moduleName);
mockMvc.perform(post("/api/v1/scaffold/tool-group")
.contentType(MediaType.APPLICATION_JSON).content(request))
.andExpect(status().isOk())
.andExpect(content().string(org.hamcrest.Matchers.containsString("CustomerUseCase.java")));
}
@Test
void toolDraftEndpointIsAvailable() throws Exception {
ChatClient.Builder builder = mock(ChatClient.Builder.class);
ChatClient chatClient = mock(ChatClient.class);
ChatClient.ChatClientRequestSpec requestSpec = mock(ChatClient.ChatClientRequestSpec.class);
ChatClient.CallResponseSpec responseSpec = mock(ChatClient.CallResponseSpec.class);
when(builder.build()).thenReturn(chatClient);
when(chatClient.prompt()).thenReturn(requestSpec);
when(requestSpec.user(anyString())).thenReturn(requestSpec);
when(requestSpec.options(any(ChatOptions.class))).thenReturn(requestSpec);
when(requestSpec.call()).thenReturn(responseSpec);
when(responseSpec.content()).thenReturn("""
{"baseName":"CustomerContractStatus","title":"계약 상태 조회","description":"고객번호로 계약 상태를 조회합니다.","categoryKey":"cmm","routingType":"HTTP","httpApiName":"contract-status","functionDescription":"고객 계약의 현재 상태를 조회한다.","displayDescription":"고객 계약 상태 조회","whenToUse":"고객번호로 계약 상태 확인을 요청할 때 사용한다.","whenNotToUse":"계약 변경 또는 해지를 요청할 때는 사용하지 않는다.","ioLimits":"고객번호 한 건을 입력받아 계약 상태 한 건을 반환한다.","exampleQueries":["고객 C123의 계약 상태를 알려줘","C123 계약이 정상인지 확인해줘","고객번호 C123 계약 조회해줘"],"tags":["contract","status","search"],"ownerOrg":"MCP_TOOL","inputFields":[{"name":"customerId","type":"String","description":"고객번호","example":"C123","required":true}],"outputFields":[{"name":"resultCode","type":"String","description":"결과 코드","example":"SUCCESS","required":true}]}
""");
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(
new ScaffoldingController(builder, new ObjectMapper()))
.setMessageConverters(new MappingJackson2HttpMessageConverter())
.build();
mockMvc.perform(post("/api/v1/scaffold/tool-draft")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"description\":\"고객번호로 계약 상태를 조회\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.baseName").value("CustomerContractStatus"))
.andExpect(jsonPath("$.functionDescription").value("고객 계약의 현재 상태를 조회한다."))
.andExpect(jsonPath("$.displayDescription").value("고객 계약 상태 조회"))
.andExpect(jsonPath("$.whenToUse").value("고객번호로 계약 상태 확인을 요청할 때 사용한다."))
.andExpect(jsonPath("$.whenNotToUse").value("계약 변경 또는 해지를 요청할 때는 사용하지 않는다."))
.andExpect(jsonPath("$.ioLimits").value("고객번호 한 건을 입력받아 계약 상태 한 건을 반환한다."))
.andExpect(jsonPath("$.exampleQueries[0]").value("고객 C123의 계약 상태를 알려줘"))
.andExpect(jsonPath("$.tags[0]").value("contract"))
.andExpect(jsonPath("$.ownerOrg").value("MCP_TOOL"))
.andExpect(jsonPath("$.inputFields[0].name").value("customerId"));
}
}

View File

@@ -4,10 +4,10 @@ plugins {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.1'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.4'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.19.1'
implementation 'com.github.javaparser:javaparser-core:3.26.3'
implementation 'org.apache.poi:poi-ooxml:5.3.0'
implementation 'org.apache.poi:poi-ooxml:5.4.1'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

View File

@@ -1,8 +0,0 @@
package io.shinhanlife.dap.report.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
/** 보고서 소스 위치와 출력 정책 설정. */
@ConfigurationProperties(prefix = "report")
public record ReportProperties(String sourceRoot, String outputFilenamePrefix) {
}

View File

@@ -1,7 +0,0 @@
package io.shinhanlife.dap.report.model;
import java.util.List;
/** 사용자가 선택한 툴 보고서 생성 요청. */
public record ToolReportRequest(List<String> toolNames) {
}

View File

@@ -1,194 +0,0 @@
package io.shinhanlife.dap.report.source;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.body.TypeDeclaration;
import com.github.javaparser.ast.expr.AnnotationExpr;
import io.shinhanlife.dap.report.model.FieldDefinition;
import io.shinhanlife.dap.report.model.ToolReportModel;
import io.shinhanlife.dap.report.model.ToolSummary;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Component;
/** 선택된 툴의 DTO, JSON Schema와 원천 전문 필드를 수집한다. */
@Component
public class ToolDetailAnalyzer {
private final ToolSourceDiscovery discovery;
private final ObjectMapper objectMapper;
private final ObjectMapper yamlMapper;
public ToolDetailAnalyzer(ToolSourceDiscovery discovery) {
this.discovery = discovery;
this.objectMapper = new ObjectMapper();
this.yamlMapper = new ObjectMapper(new YAMLFactory());
}
public ToolReportModel analyze(ToolSummary tool) {
List<FieldDefinition> fields = new ArrayList<>();
List<String> diagnostics = new ArrayList<>();
Map<String, Path> javaFiles = indexJavaFiles();
collectSchema(tool, tool.inputSchemaResource(), "INPUT", fields, diagnostics);
collectSchema(tool, tool.outputSchemaResource(), "OUTPUT", fields, diagnostics);
boolean definitionInput = collectDefinitionSchema(tool, "parameters_schema", "INPUT", fields, diagnostics);
boolean definitionOutput = collectDefinitionSchema(tool, "output_schema", "OUTPUT", fields, diagnostics);
if (tool.inputSchemaResource().isBlank() && !definitionInput) {
collectJavaType(tool, javaFiles.get(tool.requestType()), "DTO", "INPUT", fields, diagnostics);
}
if (tool.outputSchemaResource().isBlank() && !definitionOutput) {
collectJavaType(tool, javaFiles.get(tool.responseType()), "DTO", "OUTPUT", fields, diagnostics);
}
if (!tool.mappingId().isBlank()) {
collectJavaType(tool, javaFiles.get(tool.mappingId() + "_I"), "TELEGRAM", "INPUT", fields, diagnostics);
collectJavaType(tool, javaFiles.get(tool.mappingId() + "_O"), "TELEGRAM", "OUTPUT", fields, diagnostics);
}
return new ToolReportModel(tool, List.copyOf(fields), List.copyOf(diagnostics));
}
private boolean collectDefinitionSchema(ToolSummary tool, String schemaKey, String direction,
List<FieldDefinition> fields, List<String> diagnostics) {
if (tool.definitionFile() == null || tool.definitionFile().isBlank()) return false;
Path path = discovery.sourceRoot().resolve(tool.definitionFile()).normalize();
try {
JsonNode schema = yamlMapper.readTree(path.toFile()).path(schemaKey);
if (schema.isMissingNode() || schema.isNull() || schema.isEmpty()) return false;
collectSchemaFields(tool, schema, "TOOL_DEFINITION", direction, "", "", relative(path), fields);
return true;
} catch (Exception exception) {
diagnostics.add("Tool definition schema 분석 실패: " + relative(path) + " (" + schemaKey + ")");
return false;
}
}
private Map<String, Path> indexJavaFiles() {
Map<String, Path> result = new LinkedHashMap<>();
try (var paths = Files.walk(discovery.sourceRoot())) {
paths.filter(path -> Files.isRegularFile(path) && path.getFileName().toString().endsWith(".java"))
.forEach(path -> result.putIfAbsent(path.getFileName().toString().replace(".java", ""), path));
} catch (IOException exception) {
throw new IllegalStateException("Failed to index Java sources", exception);
}
return result;
}
private void collectSchema(ToolSummary tool, String resource, String direction,
List<FieldDefinition> fields, List<String> diagnostics) {
if (resource == null || resource.isBlank()) return;
String relative = resource.replaceFirst("^classpath:", "").replaceFirst("^/", "");
List<Path> matches = new ArrayList<>();
try (var paths = Files.walk(discovery.sourceRoot())) {
paths.filter(path -> Files.isRegularFile(path))
.filter(path -> path.toString().replace('\\', '/').endsWith("/src/main/resources/" + relative))
.forEach(matches::add);
} catch (IOException exception) {
diagnostics.add("Schema 검색 실패: " + resource);
return;
}
if (matches.isEmpty()) {
diagnostics.add("Schema를 찾을 수 없음: " + resource);
return;
}
Path path = matches.get(0);
try {
JsonNode root = objectMapper.readTree(path.toFile());
collectSchemaFields(tool, root, "JSON_SCHEMA", direction, "", "", relative(path), fields);
} catch (Exception exception) {
diagnostics.add("Schema 분석 실패: " + relative(path));
}
}
private void collectSchemaFields(ToolSummary tool, JsonNode schema, String sourceKind, String direction,
String ownerPath, String fieldPrefix, String sourceFile,
List<FieldDefinition> fields) {
List<String> required = new ArrayList<>();
schema.path("required").forEach(node -> required.add(node.asText()));
Iterator<Map.Entry<String, JsonNode>> iterator = schema.path("properties").fields();
while (iterator.hasNext()) {
Map.Entry<String, JsonNode> entry = iterator.next();
JsonNode definition = entry.getValue();
String fieldName = fieldPrefix.isBlank() ? entry.getKey() : fieldPrefix + "." + entry.getKey();
String type = schemaType(definition);
fields.add(new FieldDefinition(tool.name(), sourceKind, direction, ownerPath, fieldName, type,
required.contains(entry.getKey()), definition.path("description").asText(""),
constraints(definition), sourceFile));
JsonNode nested = definition.path("properties").isObject() ? definition : definition.path("items");
if (nested.path("properties").isObject()) {
collectSchemaFields(tool, nested, sourceKind, direction, fieldName, fieldName, sourceFile, fields);
}
}
}
private String schemaType(JsonNode definition) {
String type = definition.path("type").asText("object");
if ("array".equals(type)) {
return "array<" + definition.path("items").path("type").asText("object") + ">";
}
return type;
}
private void collectJavaType(ToolSummary tool, Path path, String sourceKind, String direction,
List<FieldDefinition> fields, List<String> diagnostics) {
if (path == null) {
diagnostics.add(sourceKind + " " + direction + " 타입을 찾을 수 없음");
return;
}
try {
CompilationUnit unit = StaticJavaParser.parse(path);
for (TypeDeclaration<?> type : unit.getTypes()) {
collectFields(tool, path, type, sourceKind, direction, fields);
type.getMembers().stream().filter(TypeDeclaration.class::isInstance)
.map(TypeDeclaration.class::cast)
.forEach(nested -> collectFields(tool, path, nested, sourceKind, direction, fields));
}
} catch (Exception exception) {
diagnostics.add(sourceKind + " 분석 실패: " + relative(path));
}
}
private void collectFields(ToolSummary tool, Path path, TypeDeclaration<?> owner, String sourceKind,
String direction, List<FieldDefinition> fields) {
for (FieldDeclaration declaration : owner.getFields()) {
AnnotationExpr param = JavaAnnotationReader.find(declaration.getAnnotations(), "McpToolParam").orElse(null);
AnnotationExpr schema = JavaAnnotationReader.find(declaration.getAnnotations(), "Schema").orElse(null);
AnnotationExpr telegram = JavaAnnotationReader.find(declaration.getAnnotations(), "GlowTrgmField").orElse(null);
declaration.getVariables().forEach(variable -> fields.add(new FieldDefinition(
tool.name(), sourceKind, direction, owner.getNameAsString(), variable.getNameAsString(),
variable.getTypeAsString(),
param == null ? null : JavaAnnotationReader.bool(param, "required", false),
param == null ? "" : JavaAnnotationReader.string(param, "description", ""),
annotationConstraints(schema, telegram), relative(path))));
}
}
private String annotationConstraints(AnnotationExpr schema, AnnotationExpr telegram) {
List<String> values = new ArrayList<>();
if (schema != null) values.add("Schema=" + schema);
if (telegram != null) values.add("GlowTrgmField=" + telegram);
return String.join("; ", values);
}
private String constraints(JsonNode node) {
List<String> values = new ArrayList<>();
for (String key : List.of("format", "pattern", "minimum", "maximum", "minLength", "maxLength", "example")) {
if (node.has(key)) values.add(key + "=" + node.get(key).asText());
}
if (node.has("enum")) values.add("enum=" + node.get("enum"));
return String.join("; ", values);
}
private String relative(Path path) {
return discovery.sourceRoot().relativize(path.toAbsolutePath().normalize()).toString().replace('\\', '/');
}
}

View File

@@ -1,196 +0,0 @@
package io.shinhanlife.dap.report.source;
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.expr.AnnotationExpr;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import io.shinhanlife.dap.report.config.ReportProperties;
import io.shinhanlife.dap.report.model.ToolSummary;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.stereotype.Component;
/** 프로젝트의 @McpTool 선언을 읽기 전용으로 탐색한다. */
@Component
public class ToolSourceDiscovery {
private final Path sourceRoot;
private final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
public ToolSourceDiscovery(ReportProperties properties) {
String configuredRoot = properties.sourceRoot();
if (configuredRoot == null || configuredRoot.isBlank()) {
throw new IllegalArgumentException("report.source-root must not be blank");
}
this.sourceRoot = resolveProjectRoot(Path.of(configuredRoot).toAbsolutePath().normalize());
}
public Path sourceRoot() {
return sourceRoot;
}
public List<ToolSummary> discover() {
if (!Files.isDirectory(sourceRoot)) {
throw new IllegalStateException("Report source root does not exist: " + sourceRoot);
}
List<ToolSummary> tools = new ArrayList<>();
try (var paths = Files.walk(sourceRoot)) {
paths.filter(this::isUseCaseSource).forEach(path -> parse(path, tools));
} catch (IOException exception) {
throw new IllegalStateException("Failed to scan tool sources: " + sourceRoot, exception);
}
Map<String, ToolSummary> discovered = tools.stream()
.sorted(Comparator.comparing(ToolSummary::sourceFile))
.collect(Collectors.toMap(ToolSummary::name, Function.identity(),
(first, duplicate) -> first, LinkedHashMap::new));
discoverDefinitions().forEach((name, definition) ->
discovered.merge(name, definition, this::mergeDefinition));
return discovered.values().stream()
.sorted(Comparator.comparing(ToolSummary::name))
.toList();
}
private Map<String, ToolSummary> discoverDefinitions() {
Map<String, ToolSummary> definitions = new LinkedHashMap<>();
try (var paths = Files.walk(sourceRoot)) {
paths.filter(this::isToolDefinition).sorted().forEach(path -> {
try {
JsonNode root = yamlMapper.readTree(path.toFile());
String name = text(root, "name");
if (name.isBlank()) return;
JsonNode description = root.path("description");
String function = text(description, "function");
definitions.putIfAbsent(name, new ToolSummary(
name, text(root, "display_name"), function, text(root, "category_key"),
text(root, "legacy_interface_id"), true, false,
bool(root, "read_only"), bool(root, "destructive"), bool(root, "idempotent"), false,
"", "", "", "", "", "", text(root, "version"), function,
text(description, "when_to_use"), text(description, "when_not_to_use"),
text(description, "io_limits"), text(root, "display_description"),
joined(root.path("example_queries")), joined(root.path("tags")),
joined(root.path("required_env_keys")), text(root, "owner_org"), relative(path)));
} catch (IOException exception) {
throw new IllegalStateException("Failed to parse tool definition: " + path, exception);
}
});
} catch (IOException exception) {
throw new IllegalStateException("Failed to scan tool definitions: " + sourceRoot, exception);
}
return definitions;
}
private ToolSummary mergeDefinition(ToolSummary source, ToolSummary definition) {
return new ToolSummary(source.name(), prefer(definition.title(), source.title()),
prefer(definition.description(), source.description()),
prefer(definition.categoryKey(), source.categoryKey()),
prefer(definition.mappingId(), source.mappingId()), source.register(), source.requiresApproval(),
definition.readOnlyHint(), definition.destructiveHint(), definition.idempotentHint(),
source.openWorldHint(), source.requestType(), source.responseType(), source.useCaseClass(),
source.sourceFile(), source.inputSchemaResource(), source.outputSchemaResource(), definition.version(),
definition.functionDescription(), definition.whenToUse(), definition.whenNotToUse(),
definition.ioLimits(), definition.displayDescription(), definition.exampleQueries(), definition.tags(),
definition.requiredEnvKeys(), definition.ownerOrg(), definition.definitionFile());
}
private boolean isToolDefinition(Path path) {
String normalized = path.toString().replace('\\', '/');
String fileName = path.getFileName().toString().toLowerCase();
return Files.isRegularFile(path) && (fileName.endsWith(".yml") || fileName.endsWith(".yaml"))
&& normalized.contains("/src/main/resources/tool-definitions/");
}
private String text(JsonNode node, String field) {
return node.path(field).asText("");
}
private boolean bool(JsonNode node, String field) {
return node.path(field).asBoolean(false);
}
private String joined(JsonNode node) {
if (!node.isArray()) return node.asText("");
List<String> values = new ArrayList<>();
node.forEach(value -> values.add(value.asText()));
return String.join(" | ", values);
}
private String prefer(String primary, String fallback) {
return primary == null || primary.isBlank() ? fallback : primary;
}
private String relative(Path path) {
return sourceRoot.relativize(path.toAbsolutePath().normalize()).toString().replace('\\', '/');
}
private Path resolveProjectRoot(Path configuredPath) {
Path candidate = configuredPath;
while (candidate != null) {
if (Files.isRegularFile(candidate.resolve("settings.gradle"))
&& Files.isDirectory(candidate.resolve("dap-was-lib"))) {
return candidate;
}
candidate = candidate.getParent();
}
return configuredPath;
}
private boolean isUseCaseSource(Path path) {
String normalized = path.toString().replace('\\', '/');
return Files.isRegularFile(path)
&& path.getFileName().toString().endsWith("UseCase.java")
&& normalized.contains("/src/main/java/")
&& !normalized.contains("/dap-tool-report/");
}
private void parse(Path path, List<ToolSummary> tools) {
try {
CompilationUnit unit = StaticJavaParser.parse(path);
String owner = unit.getPrimaryTypeName().orElse(path.getFileName().toString().replace(".java", ""));
for (MethodDeclaration method : unit.findAll(MethodDeclaration.class)) {
JavaAnnotationReader.find(method.getAnnotations(), "McpTool")
.ifPresent(annotation -> tools.add(toSummary(path, owner, method, annotation)));
}
} catch (Exception exception) {
throw new IllegalStateException("Failed to parse tool source: " + path, exception);
}
}
private ToolSummary toSummary(Path path, String owner, MethodDeclaration method, AnnotationExpr tool) {
AnnotationExpr hint = JavaAnnotationReader.find(method.getAnnotations(), "GrowToolHint")
.or(() -> JavaAnnotationReader.find(method.getAnnotations(), "ToolHint")).orElse(null);
AnnotationExpr annotations = JavaAnnotationReader.nested(tool, "annotations").orElse(null);
String name = JavaAnnotationReader.string(tool, "name", method.getNameAsString());
String title = JavaAnnotationReader.string(tool, "title", name);
String description = JavaAnnotationReader.string(tool, "description", "");
String requestType = method.getParameters().isEmpty() ? "" : method.getParameter(0).getTypeAsString();
return new ToolSummary(
name,
title,
description,
hint == null ? "common" : JavaAnnotationReader.string(hint, "categoryKey", "com"),
hint == null ? "" : JavaAnnotationReader.string(hint, "mappingId", ""),
hint != null && JavaAnnotationReader.bool(hint, "register", false),
hint != null && JavaAnnotationReader.bool(hint, "requiresApproval", false),
annotations != null && JavaAnnotationReader.bool(annotations, "readOnlyHint", false),
annotations != null && JavaAnnotationReader.bool(annotations, "destructiveHint", false),
annotations != null && JavaAnnotationReader.bool(annotations, "idempotentHint", false),
annotations != null && JavaAnnotationReader.bool(annotations, "openWorldHint", false),
requestType,
method.getTypeAsString(),
owner,
sourceRoot.relativize(path.toAbsolutePath().normalize()).toString().replace('\\', '/'),
hint == null ? "" : JavaAnnotationReader.string(hint, "inputSchemaResource", ""),
hint == null ? "" : JavaAnnotationReader.string(hint, "outputSchemaResource", ""));
}
}

View File

@@ -1,10 +1,10 @@
package io.shinhanlife.dap.report;
package io.shinhanlife.dat.report;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @package io.shinhanlife.dap.report
* @package io.shinhanlife.dat.report
* @className ToolReportApplication
* @description 소스를 읽기 전용으로 분석하여 Excel 보고서를 생성하는 독립 애플리케이션
* @author 0986406

View File

@@ -1,10 +1,11 @@
package io.shinhanlife.dap.report.application;
package io.shinhanlife.dat.report.application;
import io.shinhanlife.dap.report.excel.ToolReportExcelWriter;
import io.shinhanlife.dap.report.model.ToolReportModel;
import io.shinhanlife.dap.report.model.ToolSummary;
import io.shinhanlife.dap.report.source.ToolDetailAnalyzer;
import io.shinhanlife.dap.report.source.ToolSourceDiscovery;
import io.shinhanlife.dat.report.excel.ToolReportExcelWriter;
import io.shinhanlife.dat.report.model.ToolReportModel;
import io.shinhanlife.dat.report.model.ToolSummary;
import io.shinhanlife.dat.report.model.ToolSourceType;
import io.shinhanlife.dat.report.source.ToolDetailAnalyzer;
import io.shinhanlife.dat.report.source.ToolSourceDiscovery;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -26,13 +27,18 @@ public class ToolReportService {
this.excelWriter = excelWriter;
}
public byte[] createExcel(List<String> selectedNames) {
public List<ToolSummary> findTools(String sourceType) {
return discovery.discover(ToolSourceType.from(sourceType));
}
public byte[] createExcel(List<String> selectedNames, String sourceTypeValue) {
if (selectedNames == null || selectedNames.isEmpty()) {
throw new IllegalArgumentException("보고서에 포함할 툴을 하나 이상 선택해야 합니다.");
}
ToolSourceType sourceType = ToolSourceType.from(sourceTypeValue);
Map<String, ToolSummary> sourceTools = new LinkedHashMap<>();
discovery.discover().forEach(tool -> sourceTools.put(tool.name(), tool));
discovery.discover(sourceType).forEach(tool -> sourceTools.put(tool.name(), tool));
List<String> missingSources = selectedNames.stream()
.filter(name -> !sourceTools.containsKey(name)).distinct().toList();
if (!missingSources.isEmpty()) {
@@ -41,7 +47,7 @@ public class ToolReportService {
List<ToolReportModel> reports = selectedNames.stream().distinct()
.map(sourceTools::get)
.map(analyzer::analyze)
.map(tool -> analyzer.analyze(tool, sourceType))
.toList();
return excelWriter.write(reports);
}

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.dap.report.config;
package io.shinhanlife.dat.report.config;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

View File

@@ -0,0 +1,10 @@
package io.shinhanlife.dat.report.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
/** 보고서 소스 위치와 출력 정책 설정. */
@ConfigurationProperties(prefix = "report")
public record ReportProperties(String dapWasDapmtSourceRoot,
String dapAdminSourceRoot,
String outputFilenamePrefix) {
}

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dap.report.excel;
package io.shinhanlife.dat.report.excel;
import io.shinhanlife.dap.report.model.FieldDefinition;
import io.shinhanlife.dap.report.model.ToolReportModel;
import io.shinhanlife.dap.report.model.ToolSummary;
import io.shinhanlife.dat.report.model.FieldDefinition;
import io.shinhanlife.dat.report.model.ToolReportModel;
import io.shinhanlife.dat.report.model.ToolSummary;
import java.awt.Color;
import java.io.ByteArrayOutputStream;
import java.time.LocalDateTime;

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.dap.report.model;
package io.shinhanlife.dat.report.model;
/** DTO, JSON Schema 또는 원천 전문에서 수집한 필드 정의. */
public record FieldDefinition(

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.dap.report.model;
package io.shinhanlife.dat.report.model;
import java.util.List;

View File

@@ -0,0 +1,7 @@
package io.shinhanlife.dat.report.model;
import java.util.List;
/** 사용자가 선택한 툴 보고서 생성 요청. */
public record ToolReportRequest(List<String> toolNames, String sourceType) {
}

View File

@@ -0,0 +1,18 @@
package io.shinhanlife.dat.report.model;
import java.util.Locale;
/** Report 생성 시 참조할 Tool 소스 프로젝트. */
public enum ToolSourceType {
DAP_WAS_DAPMT,
DAP_ADMIN;
public static ToolSourceType from(String value) {
if (value == null || value.isBlank()) return DAP_WAS_DAPMT;
try {
return valueOf(value.trim().replace('-', '_').toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException exception) {
throw new IllegalArgumentException("지원하지 않는 Tool 소스 유형입니다: " + value);
}
}
}

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.dap.report.model;
package io.shinhanlife.dat.report.model;
/** UI 선택 목록과 보고서 기본정보에 사용하는 툴 요약. */
public record ToolSummary(

View File

@@ -1,8 +1,10 @@
package io.shinhanlife.dap.report.presentation;
package io.shinhanlife.dat.report.presentation;
import io.shinhanlife.dap.report.application.ToolReportService;
import io.shinhanlife.dap.report.config.ReportProperties;
import io.shinhanlife.dap.report.model.ToolReportRequest;
import io.shinhanlife.dat.report.application.ToolReportService;
import io.shinhanlife.dat.report.config.ReportProperties;
import io.shinhanlife.dat.report.model.ToolReportRequest;
import io.shinhanlife.dat.report.model.ToolSummary;
import java.util.List;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@@ -12,10 +14,12 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
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 org.springframework.web.bind.annotation.RequestParam;
/** 툴 선택 목록과 Excel 다운로드 API. */
@RestController
@@ -33,7 +37,7 @@ public class ToolReportController {
@PostMapping(value = "/tool-reports/excel",
produces = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
public ResponseEntity<byte[]> excel(@RequestBody ToolReportRequest request) {
byte[] content = service.createExcel(request.toolNames());
byte[] content = service.createExcel(request.toolNames(), request.sourceType());
String prefix = properties.outputFilenamePrefix() == null || properties.outputFilenamePrefix().isBlank()
? "tool-report" : properties.outputFilenamePrefix();
String filename = prefix + "-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")) + ".xlsx";
@@ -47,6 +51,11 @@ public class ToolReportController {
.body(content);
}
@GetMapping("/report-tools")
public List<ToolSummary> tools(@RequestParam(value = "sourceType", required = false) String sourceType) {
return service.findTools(sourceType);
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Map<String, String>> badRequest(IllegalArgumentException exception) {
return ResponseEntity.badRequest().body(Map.of("error", exception.getMessage()));

View File

@@ -1,7 +1,8 @@
package io.shinhanlife.dap.report.source;
package io.shinhanlife.dat.report.source;
import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.expr.AnnotationExpr;
import com.github.javaparser.ast.expr.ArrayInitializerExpr;
import com.github.javaparser.ast.expr.BooleanLiteralExpr;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.MemberValuePair;
@@ -9,6 +10,7 @@ import com.github.javaparser.ast.expr.NormalAnnotationExpr;
import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr;
import com.github.javaparser.ast.expr.StringLiteralExpr;
import java.util.Optional;
import java.util.stream.Collectors;
/** JavaParser AST에서 어노테이션 값을 안전하게 읽는 도우미. */
final class JavaAnnotationReader {
@@ -49,6 +51,18 @@ final class JavaAnnotationReader {
.orElse(fallback);
}
static String strings(AnnotationExpr annotation, String key) {
return value(annotation, key)
.filter(ArrayInitializerExpr.class::isInstance)
.map(ArrayInitializerExpr.class::cast)
.map(array -> array.getValues().stream()
.filter(StringLiteralExpr.class::isInstance)
.map(StringLiteralExpr.class::cast)
.map(StringLiteralExpr::asString)
.collect(Collectors.joining(" | ")))
.orElse("");
}
static Optional<AnnotationExpr> nested(AnnotationExpr annotation, String key) {
return value(annotation, key).filter(AnnotationExpr.class::isInstance).map(AnnotationExpr.class::cast);
}

View File

@@ -0,0 +1,103 @@
package io.shinhanlife.dat.report.source;
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.body.TypeDeclaration;
import com.github.javaparser.ast.expr.AnnotationExpr;
import io.shinhanlife.dat.report.model.FieldDefinition;
import io.shinhanlife.dat.report.model.ToolReportModel;
import io.shinhanlife.dat.report.model.ToolSourceType;
import io.shinhanlife.dat.report.model.ToolSummary;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Component;
/** 선택한 프로젝트의 DTO와 전문 Java 어노테이션에서 Report 파라미터를 수집한다. */
@Component
public class ToolDetailAnalyzer {
private final ToolSourceDiscovery discovery;
public ToolDetailAnalyzer(ToolSourceDiscovery discovery) {
this.discovery = discovery;
}
public ToolReportModel analyze(ToolSummary tool, ToolSourceType sourceType) {
List<FieldDefinition> fields = new ArrayList<>();
List<String> diagnostics = new ArrayList<>();
Path sourceRoot = discovery.sourceRoot(sourceType);
Map<String, Path> javaFiles = indexJavaFiles(sourceRoot);
collectJavaType(sourceRoot, tool, javaFiles.get(tool.requestType()), "DTO", "INPUT", fields, diagnostics);
collectJavaType(sourceRoot, tool, javaFiles.get(tool.responseType()), "DTO", "OUTPUT", fields, diagnostics);
if (!tool.mappingId().isBlank()) {
collectJavaType(sourceRoot, tool, javaFiles.get(tool.mappingId() + "_I"),
"TELEGRAM", "INPUT", fields, diagnostics);
collectJavaType(sourceRoot, tool, javaFiles.get(tool.mappingId() + "_O"),
"TELEGRAM", "OUTPUT", fields, diagnostics);
}
return new ToolReportModel(tool, List.copyOf(fields), List.copyOf(diagnostics));
}
private Map<String, Path> indexJavaFiles(Path sourceRoot) {
Map<String, Path> result = new LinkedHashMap<>();
try (var paths = Files.walk(sourceRoot)) {
paths.filter(path -> Files.isRegularFile(path) && path.getFileName().toString().endsWith(".java"))
.forEach(path -> result.putIfAbsent(path.getFileName().toString().replace(".java", ""), path));
} catch (IOException exception) {
throw new IllegalStateException("Failed to index Java sources: " + sourceRoot, exception);
}
return result;
}
private void collectJavaType(Path sourceRoot, ToolSummary tool, Path path, String sourceKind, String direction,
List<FieldDefinition> fields, List<String> diagnostics) {
if (path == null) {
diagnostics.add(sourceKind + " " + direction + " 타입을 찾을 수 없음");
return;
}
try {
CompilationUnit unit = StaticJavaParser.parse(path);
for (TypeDeclaration<?> type : unit.getTypes()) {
collectFields(sourceRoot, tool, path, type, sourceKind, direction, fields);
type.getMembers().stream().filter(TypeDeclaration.class::isInstance)
.map(TypeDeclaration.class::cast)
.forEach(nested -> collectFields(sourceRoot, tool, path, nested, sourceKind, direction, fields));
}
} catch (Exception exception) {
diagnostics.add(sourceKind + " 분석 실패: " + relative(sourceRoot, path));
}
}
private void collectFields(Path sourceRoot, ToolSummary tool, Path path, TypeDeclaration<?> owner,
String sourceKind, String direction, List<FieldDefinition> fields) {
for (FieldDeclaration declaration : owner.getFields()) {
AnnotationExpr param = JavaAnnotationReader.find(declaration.getAnnotations(), "McpToolParam").orElse(null);
AnnotationExpr schema = JavaAnnotationReader.find(declaration.getAnnotations(), "Schema").orElse(null);
AnnotationExpr telegram = JavaAnnotationReader.find(declaration.getAnnotations(), "GlowTrgmField").orElse(null);
declaration.getVariables().forEach(variable -> fields.add(new FieldDefinition(
tool.name(), sourceKind, direction, owner.getNameAsString(), variable.getNameAsString(),
variable.getTypeAsString(),
param == null ? null : JavaAnnotationReader.bool(param, "required", false),
param == null ? "" : JavaAnnotationReader.string(param, "description", ""),
annotationConstraints(schema, telegram), relative(sourceRoot, path))));
}
}
private String annotationConstraints(AnnotationExpr schema, AnnotationExpr telegram) {
List<String> values = new ArrayList<>();
if (schema != null) values.add("Schema=" + schema);
if (telegram != null) values.add("GlowTrgmField=" + telegram);
return String.join("; ", values);
}
private String relative(Path sourceRoot, Path path) {
return sourceRoot.relativize(path.toAbsolutePath().normalize()).toString().replace('\\', '/');
}
}

View File

@@ -0,0 +1,140 @@
package io.shinhanlife.dat.report.source;
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.expr.AnnotationExpr;
import io.shinhanlife.dat.report.config.ReportProperties;
import io.shinhanlife.dat.report.model.ToolSummary;
import io.shinhanlife.dat.report.model.ToolSourceType;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.EnumMap;
import org.springframework.stereotype.Component;
/** 프로젝트의 @McpTool 선언을 읽기 전용으로 탐색한다. */
@Component
public class ToolSourceDiscovery {
private final Map<ToolSourceType, Path> sourceRoots = new EnumMap<>(ToolSourceType.class);
public ToolSourceDiscovery(ReportProperties properties) {
sourceRoots.put(ToolSourceType.DAP_WAS_DAPMT,
resolveConfiguredRoot(properties.dapWasDapmtSourceRoot(), "report.dap-was-dapmt-source-root"));
sourceRoots.put(ToolSourceType.DAP_ADMIN,
resolveConfiguredRoot(properties.dapAdminSourceRoot(), "report.dap-admin-source-root"));
}
public Path sourceRoot(ToolSourceType sourceType) {
return sourceRoots.get(sourceType);
}
public List<ToolSummary> discover(ToolSourceType sourceType) {
Path sourceRoot = sourceRoot(sourceType);
if (!Files.isDirectory(sourceRoot)) {
throw new IllegalStateException("Report source root does not exist: " + sourceRoot);
}
List<ToolSummary> tools = new ArrayList<>();
try (var paths = Files.walk(sourceRoot)) {
paths.filter(this::isUseCaseSource).forEach(path -> parse(sourceRoot, path, tools));
} catch (IOException exception) {
throw new IllegalStateException("Failed to scan tool sources: " + sourceRoot, exception);
}
return tools.stream()
.sorted(Comparator.comparing(ToolSummary::sourceFile))
.collect(java.util.stream.Collectors.toMap(ToolSummary::name, java.util.function.Function.identity(),
(first, duplicate) -> first, LinkedHashMap::new)).values().stream()
.sorted(Comparator.comparing(ToolSummary::name))
.toList();
}
private Path resolveConfiguredRoot(String configuredRoot, String propertyName) {
if (configuredRoot == null || configuredRoot.isBlank()) {
throw new IllegalArgumentException(propertyName + " must not be blank");
}
return resolveProjectRoot(Path.of(configuredRoot).toAbsolutePath().normalize());
}
private Path resolveProjectRoot(Path configuredPath) {
Path candidate = configuredPath;
while (candidate != null) {
if (Files.isRegularFile(candidate.resolve("settings.gradle"))
&& Files.isDirectory(candidate.resolve("dat-was-lib"))) {
return candidate;
}
candidate = candidate.getParent();
}
return configuredPath;
}
private boolean isUseCaseSource(Path path) {
String normalized = path.toString().replace('\\', '/');
return Files.isRegularFile(path)
&& path.getFileName().toString().endsWith("UseCase.java")
&& normalized.contains("/src/main/java/")
&& !normalized.contains("/dap-tool-report/");
}
private void parse(Path sourceRoot, Path path, List<ToolSummary> tools) {
try {
CompilationUnit unit = StaticJavaParser.parse(path);
String owner = unit.getPrimaryTypeName().orElse(path.getFileName().toString().replace(".java", ""));
for (MethodDeclaration method : unit.findAll(MethodDeclaration.class)) {
JavaAnnotationReader.find(method.getAnnotations(), "McpTool")
.ifPresent(annotation -> tools.add(toSummary(sourceRoot, path, owner, method, annotation)));
}
} catch (Exception exception) {
throw new IllegalStateException("Failed to parse tool source: " + path, exception);
}
}
private ToolSummary toSummary(Path sourceRoot, Path path, String owner, MethodDeclaration method, AnnotationExpr tool) {
AnnotationExpr hint = JavaAnnotationReader.find(method.getAnnotations(), "GrowToolHint")
.or(() -> JavaAnnotationReader.find(method.getAnnotations(), "ToolHint")).orElse(null);
AnnotationExpr annotations = JavaAnnotationReader.nested(tool, "annotations").orElse(null);
String name = JavaAnnotationReader.string(tool, "name", method.getNameAsString());
String title = JavaAnnotationReader.string(tool, "title", name);
String description = JavaAnnotationReader.string(tool, "description", "");
String requestType = method.getParameters().isEmpty() ? "" : method.getParameter(0).getTypeAsString();
return new ToolSummary(
name,
title,
description,
hint == null ? "common" : JavaAnnotationReader.string(hint, "categoryKey", "com"),
hint == null ? "" : JavaAnnotationReader.string(hint, "mappingId", ""),
hint != null && JavaAnnotationReader.bool(hint, "register", false),
hint != null && JavaAnnotationReader.bool(hint, "requiresApproval", false),
annotations != null && JavaAnnotationReader.bool(annotations, "readOnlyHint", false),
annotations != null
? JavaAnnotationReader.bool(annotations, "destructiveHint",
hint != null && JavaAnnotationReader.bool(hint, "destructive", false))
: hint != null && JavaAnnotationReader.bool(hint, "destructive", false),
annotations != null
? JavaAnnotationReader.bool(annotations, "idempotentHint",
hint != null && JavaAnnotationReader.bool(hint, "idempotent", false))
: hint != null && JavaAnnotationReader.bool(hint, "idempotent", false),
annotations != null && JavaAnnotationReader.bool(annotations, "openWorldHint", false),
requestType,
method.getTypeAsString(),
owner,
sourceRoot.relativize(path.toAbsolutePath().normalize()).toString().replace('\\', '/'),
hint == null ? "" : JavaAnnotationReader.string(hint, "inputSchemaResource", ""),
hint == null ? "" : JavaAnnotationReader.string(hint, "outputSchemaResource", ""),
hint == null ? "" : JavaAnnotationReader.string(hint, "version", ""),
hint == null ? "" : JavaAnnotationReader.string(hint, "functionDescription", ""),
hint == null ? "" : JavaAnnotationReader.string(hint, "whenToUse", ""),
hint == null ? "" : JavaAnnotationReader.string(hint, "whenNotToUse", ""),
hint == null ? "" : JavaAnnotationReader.string(hint, "ioLimits", ""),
hint == null ? "" : JavaAnnotationReader.string(hint, "displayDescription", ""),
hint == null ? "" : JavaAnnotationReader.strings(hint, "exampleQueries"),
hint == null ? "" : JavaAnnotationReader.strings(hint, "tags"),
hint == null ? "" : JavaAnnotationReader.strings(hint, "requiredEnvKeys"),
hint == null ? "" : JavaAnnotationReader.string(hint, "ownerOrg", ""), "");
}
}

View File

@@ -7,5 +7,6 @@ spring:
name: dap-tool-report
report:
source-root: ${REPORT_SOURCE_ROOT:.}
dap-was-dapmt-source-root: ${REPORT_DAP_WAS_DAPMT_SOURCE_ROOT:../dap-was-dapmt}
dap-admin-source-root: ${REPORT_DAP_ADMIN_SOURCE_ROOT:.}
output-filename-prefix: tool-report

View File

@@ -1,51 +0,0 @@
package io.shinhanlife.dap.report.source;
import static org.assertj.core.api.Assertions.assertThat;
import io.shinhanlife.dap.report.config.ReportProperties;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
class ToolSourceDiscoveryTest {
@Test
void discoversMcpToolsFromProjectSources() {
Path projectRoot = Path.of("..").toAbsolutePath().normalize();
ToolSourceDiscovery discovery = new ToolSourceDiscovery(
new ReportProperties(projectRoot.toString(), "tool-report"));
assertThat(discovery.discover())
.extracting(tool -> tool.name())
.contains("cmm_customer_tool", "cmm_claim_search", "iam_system_status");
assertThat(discovery.discover().stream()
.filter(tool -> tool.name().equals("cmm_customer_tool"))
.findFirst().orElseThrow())
.satisfies(tool -> {
assertThat(tool.version()).isEqualTo("1.0.0");
assertThat(tool.mappingId()).isEqualTo("ONILD0320");
assertThat(tool.definitionFile()).endsWith("tool-definitions/cmm/cmm_customer_tool.yml");
assertThat(tool.whenToUse()).contains("고객 ID");
});
}
@Test
void collectsJsonSchemaFieldsEmbeddedInToolDefinitionYaml() {
Path projectRoot = Path.of("..").toAbsolutePath().normalize();
ToolSourceDiscovery discovery = new ToolSourceDiscovery(
new ReportProperties(projectRoot.toString(), "tool-report"));
var tool = discovery.discover().stream()
.filter(candidate -> candidate.name().equals("cmm_customer_tool"))
.findFirst().orElseThrow();
var report = new ToolDetailAnalyzer(discovery).analyze(tool);
assertThat(report.fields())
.anySatisfy(field -> {
assertThat(field.sourceKind()).isEqualTo("TOOL_DEFINITION");
assertThat(field.direction()).isEqualTo("INPUT");
assertThat(field.fieldName()).isEqualTo("csNo");
assertThat(field.required()).isTrue();
});
}
}

View File

@@ -1,10 +1,10 @@
package io.shinhanlife.dap.report.excel;
package io.shinhanlife.dat.report.excel;
import static org.assertj.core.api.Assertions.assertThat;
import io.shinhanlife.dap.report.model.FieldDefinition;
import io.shinhanlife.dap.report.model.ToolReportModel;
import io.shinhanlife.dap.report.model.ToolSummary;
import io.shinhanlife.dat.report.model.FieldDefinition;
import io.shinhanlife.dat.report.model.ToolReportModel;
import io.shinhanlife.dat.report.model.ToolSummary;
import java.io.ByteArrayInputStream;
import java.util.List;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

View File

@@ -0,0 +1,55 @@
package io.shinhanlife.dat.report.source;
import static org.assertj.core.api.Assertions.assertThat;
import io.shinhanlife.dat.report.config.ReportProperties;
import io.shinhanlife.dat.report.model.ToolSourceType;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
class ToolSourceDiscoveryTest {
private final Path adminRoot = projectRoot();
private final Path dapmtRoot = adminRoot.resolve("../dap-was-dapmt").normalize();
@Test
void discoversToolsFromBothSelectableProjects() {
ToolSourceDiscovery discovery = discovery();
assertThat(discovery.discover(ToolSourceType.DAP_ADMIN))
.extracting(tool -> tool.name())
.contains("cmm_claim_search");
assertThat(discovery.discover(ToolSourceType.DAP_WAS_DAPMT))
.extracting(tool -> tool.name())
.contains("spr_field_inquiry_list");
}
@Test
void collectsParametersFromDtoAnnotationsInsteadOfYamlDefinitions() {
ToolSourceDiscovery discovery = discovery();
var tool = discovery.discover(ToolSourceType.DAP_WAS_DAPMT).stream()
.filter(candidate -> candidate.name().equals("spr_field_inquiry_list"))
.findFirst().orElseThrow();
var report = new ToolDetailAnalyzer(discovery).analyze(tool, ToolSourceType.DAP_WAS_DAPMT);
assertThat(report.fields()).anySatisfy(field -> {
assertThat(field.sourceKind()).isEqualTo("DTO");
assertThat(field.direction()).isEqualTo("INPUT");
assertThat(field.fieldName()).isEqualTo("inquiryId");
assertThat(field.constraints()).contains("Schema=");
});
assertThat(report.fields()).noneMatch(field -> field.sourceKind().equals("TOOL_DEFINITION"));
}
private ToolSourceDiscovery discovery() {
return new ToolSourceDiscovery(new ReportProperties(
dapmtRoot.toString(), adminRoot.toString(), "tool-report"));
}
private Path projectRoot() {
Path current = Path.of("").toAbsolutePath().normalize();
return java.nio.file.Files.isRegularFile(current.resolve("settings.gradle"))
? current : current.getParent();
}
}

View File

@@ -1,8 +0,0 @@
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
RUN apk add --no-cache tzdata
ENV TZ=Asia/Seoul
COPY dap-was-cus/build/libs/*-SNAPSHOT.jar app.jar
EXPOSE 8084
ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@@ -1,10 +0,0 @@
plugins {
// OTH Tool Pod를 독립 실행 가능한 Spring Boot JAR로 생성합니다.
id 'org.springframework.boot'
}
dependencies {
// MCP Server, Tool 공통 처리, MCI/EAI 연동 기반은 dap-was-lib에서 상속합니다.
implementation project(':dap-was-lib')
implementation 'org.apache.poi:poi-ooxml:5.3.0'
}

View File

@@ -1,29 +0,0 @@
package io.shinhanlife.dap.mcc.biz.cmm.converter;
import io.shinhanlife.dap.mcc.biz.cmm.dto.CcCstUnfcNotiCarhDto;
import io.shinhanlife.dap.mcc.biz.cmm.dto.CustomerGuidanceToolRequest;
import io.shinhanlife.dap.mcc.infra.itrf.mci.nil.d.io.ONILD0320_I;
import io.shinhanlife.dap.mcc.infra.itrf.mci.nil.d.io.ONILD0320_O.CstUnfcNotiCarhInqrOutDto;
import org.mapstruct.*;
import java.util.List;
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface CustomerGuidanceToolConverter {
/**
* 고객통합이력조회 전사 Request -> I/F
*/
@Mapping(target = "cstUnfcNotiCarhInqrInDto.inqrEndYmd", source = "inqrEndYmd")
@Mapping(target = "cstUnfcNotiCarhInqrInDto.inqrStrtYmd", source = "inqrStrYmd")
@Mapping(target = "cstUnfcNotiCarhInqrInDto.notiPmlMdCd", source = "notiPmlMdCd")
@Mapping(target = "cstUnfcNotiCarhInqrInDto.ntleCd", source = "ntleCd")
@Mapping(target = "cstUnfcNotiCarhInqrInDto.csNo", source = "csNo")
public abstract ONILD0320_I toONILD0320_I(CustomerGuidanceToolRequest req);
@Named("toCcCstUnfcNotiCarhDto")
public abstract CcCstUnfcNotiCarhDto toCcCstUnfcNotiCarhDto(CstUnfcNotiCarhInqrOutDto cstUnfcNotiCarhInqrOutDto);
@IterableMapping(qualifiedByName = "toCcCstUnfcNotiCarhDto")
public abstract List<CcCstUnfcNotiCarhDto> toCcCstUnfcNotiCarhDtoList(List<CstUnfcNotiCarhInqrOutDto> cstUnfcNotiCarhInqrOutDto);
}

View File

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

View File

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

View File

@@ -1,29 +0,0 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class AnalysisDataQueryRequest {
@Schema(description = "데이터 조회를 시작할 날짜", example = "2024-01-01")
private String startDate;
@Schema(description = "데이터 조회를 종료할 날짜", example = "2024-12-31")
private String endDate;
@Schema(description = "분석할 데이터 카테고리", example = "sales")
private String category;
@Schema(description = "지역 필터", example = "seoul")
private String region;
@Schema(description = "조회할 결과 수 제한", example = "100")
private Integer limit;
@Schema(description = "조회할 결과 시작 위치", example = "0")
private Integer offset;
}

View File

@@ -1,97 +0,0 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
@Data
public class CcCstUnfcNotiCarhDto {
@Schema(description = "고객번호", example = "CUST123")
private String csNo;
@Schema(description = "안내일시", example = "20260812093000")
private String notiDt;
@Schema(description = "발송일련번호", example = "1")
private int pmlSriaNo;
@Schema(description = "안내신청상세일시", example = "20260812092500")
private String notiPetDtptDt;
@Schema(description = "안내장코드", example = "NT001")
private String ntleCd;
@Schema(description = "안내장명", example = "카드대금 결제일 안내")
private String ntleNm;
@Schema(description = "안내발송일련번호", example = "10")
private int notiPmlSriaNo;
@Schema(description = "안내발송방법코드", example = "SMS")
private String notiPmlMdCd;
@Schema(description = "안내수신정보암호화내용", example = "AbC123EncRypTed==")
private String notiRcvInfoEncrCt;
@Schema(description = "메시지ID", example = "MSG20260812000001")
private String msgId;
@Schema(description = "안내내용", example = "8월 카드대금 결제일은 8월 25일입니다.")
private String notiCt;
@Schema(description = "통합안내발송결과코드", example = "00")
private String unfcNotiPmlRsltCd;
@Schema(description = "통합안내발송결과상세코드", example = "0001")
private String unfcNotiPmlRsltDtptCd;
@Schema(description = "발송시스템코드", example = "SYS01")
private String pmlSystCd;
@Schema(description = "발송조직번호", example = "1001")
private String pmlOgnzNo;
@Schema(description = "발송조직명", example = "고객지원팀")
private String pmlOgnzNm;
@Schema(description = "발신기관조직명", example = "카드사업본부")
private String msdilttOgnzNm;
@Schema(description = "조회구분명", example = "전체")
private String inqrDvsnNm;
@Schema(description = "등록일시", example = "20260812090000")
private String rgiDt;
@Schema(description = "등록자명", example = "홍길동")
private String rgstNm;
@Schema(description = "등록자조직명", example = "고객지원팀")
private String rgstOgnzNm;
@Schema(description = "최종변경자명", example = "김철수")
private String lstPrwpNm;
@Schema(description = "최종변경일시", example = "20260812101500")
private String lstChgDt;
@Schema(description = "최종변경자조직명", example = "운영지원팀")
private String lstPrwpOgnzNm;
@Schema(description = "원천회사구분코드", example = "01")
private String oriCpnyDvsnCd;
@Schema(description = "고객한글명", example = "이보람")
private String cstHanNm;
@Schema(description = "시스템등록인사번호", example = "20260001")
private String systRgiPrafNo;
@Schema(description = "시스템변경인사번호", example = "20260002")
private String systChgPrafNo;
}

View File

@@ -1,27 +0,0 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
@Data
@Getter
@Builder
public class CustomerGuidanceToolRequest {
@Schema(description = "고객번호", example = "CUST123")
private String csNo;
@Schema(description = "안내장코드", example = "ab12321")
private String ntleCd;
@Schema(description = "안내발송방법코드", example = "aa")
private String notiPmlMdCd;
@Schema(description = "조회시작일자", example = "20260101")
private String inqrStrYmd;
@Schema(description = "조회종료일자", example = "20260101")
private String inqrEndYmd;
}

View File

@@ -1,21 +0,0 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.List;
@Getter
@Builder
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CustomerGuidanceToolResponse {
private String resultCode;
private String resultMessage;
private List<CcCstUnfcNotiCarhDto> ccCstUnfcNotiCarhDtoList;
}

View File

@@ -1,32 +0,0 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.shinhanlife.glow.GlowMciFieldInfo;
import lombok.Data;
import java.util.List;
/**
* @package io.shinhanlife.dap.mcc.dto
* @className MciSampleStringResponse
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class MciSampleStringResponse {
@GlowMciFieldInfo(order = 1, length = 10, description = "이름")
private String name;
@GlowMciFieldInfo(order = 2, length = 3, description = "나이")
private int age;
@GlowMciFieldInfo(order = 3, length = 8, description = "가입일자(YYYYMMDD)")
private String joinDate;
@GlowMciFieldInfo(order = 4, length = 2, description = "상태코드")
private String statusCode;
@GlowMciFieldInfo(order = 5, length = 30, description = "타겟 리스트", target = MciSampleTargetDto.class)
private List<MciSampleTargetDto> targetList;
}

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