From b698c0e8f848c26158b3fd83e7d418b9e99906ad Mon Sep 17 00:00:00 2001 From: jade Date: Tue, 4 Aug 2026 21:59:08 +0900 Subject: [PATCH] Initial commit --- .agents/AGENTS.md | 39 + .agents/mcp.json | 7 + .agents/skills/chaos-tool-tester/SKILL.md | 16 + .../skills/gateway-routing-tracer/SKILL.md | 12 + .../skills/gateway-routing-tracer/trace.sh | 18 + .agents/skills/mcp-schema-autogen/SKILL.md | 16 + .agents/skills/mcp_best_practices/SKILL.md | 249 ++++++ .agents/skills/mcp_evaluation/SKILL.md | 602 +++++++++++++++ .dockerignore | 10 + .gitattributes | 25 + .gitea/workflows/deploy.yml | 67 ++ .gitignore | 124 +++ AddJavadoc.java | 103 +++ Dockerfile | 31 + HELP.md | 31 + McpBridge.java | 57 ++ README.md | 606 +++++++++++++++ build.gradle | 68 ++ dap-tool-core/build.gradle | 40 + .../lib/adapter/aop/EimsMonitoringAspect.java | 63 ++ .../connector/ExternalApiConnector.java | 72 ++ .../connector/InternalSystemConnector.java | 74 ++ .../adapter/connector/LegacyDbConnector.java | 58 ++ .../connector/LegacyEimsConnector.java | 151 ++++ .../ThirdPartySecurityConnector.java | 71 ++ .../dap/lib/adapter/dto/ErrorDetail.java | 32 + .../dap/lib/adapter/dto/JsonRpcRequest.java | 27 + .../dap/lib/adapter/dto/JsonRpcResponse.java | 30 + .../dap/lib/adapter/dto/Params.java | 38 + .../exception/MciCommunicationException.java | 26 + .../dap/lib/adapter/sender/EaiEimsSender.java | 104 +++ .../dap/lib/adapter/sender/EimsSender.java | 20 + .../lib/adapter/sender/HttpEimsSender.java | 81 ++ .../lib/adapter/sender/JspFormEimsSender.java | 58 ++ .../lib/adapter/sender/JspJsonEimsSender.java | 59 ++ .../dap/lib/adapter/sender/MciEimsSender.java | 86 +++ .../adapter/sender/MciStringEimsSender.java | 68 ++ .../lib/adapter/sender/ShinhanMciSender.java | 109 +++ .../dap/lib/adapter/sender/TcpEimsSender.java | 71 ++ .../support/DynamicPayloadBuilder.java | 59 ++ .../support/DynamicSchemaValidator.java | 47 ++ .../dap/lib/adapter/support/MciTemplate.java | 67 ++ .../adapter/support/ResultStandardizer.java | 53 ++ .../lib/adapter/support/TicketManager.java | 96 +++ .../adapter/support/ToolExecutionService.java | 90 +++ .../lib/adapter/test/MockEimsHttpServer.java | 85 +++ .../lib/adapter/test/MockEimsTcpServer.java | 128 ++++ .../dap/lib/adapter/test/MockJspServer.java | 58 ++ .../adapter/util/LegacyDataTransformer.java | 92 +++ .../util/PiiMaskingLogbackConverter.java | 35 + .../dap/lib/adapter/util/PiiMaskingUtils.java | 77 ++ .../dap/lib/annotation/McpAnyOf.java | 19 + .../dap/lib/annotation/McpFunction.java | 76 ++ .../dap/lib/annotation/McpOutputSchema.java | 17 + .../dap/lib/annotation/McpParameter.java | 26 + .../dap/lib/annotation/McpTool.java | 33 + .../dap/lib/annotation/McpValidation.java | 36 + .../dap/lib/aop/ToolSlaMonitoringAspect.java | 83 ++ .../dap/lib/config/CorsConfig.java | 34 + .../config/GlowCommunicationProperties.java | 85 +++ .../dap/lib/config/McpProperties.java | 37 + .../dap/lib/config/MybatisConfig.java | 33 + .../dap/lib/config/P6SpySqlFormatter.java | 54 ++ .../lib/config/ToolSchemaConfiguration.java | 18 + .../lib/integration/GlowIntegrationCall.java | 105 +++ .../integration/dto/SampleGlowMessage.java | 84 +++ .../eai/component/AxhubEaiComponent.java | 99 +++ .../mci/component/AxhubMciComponent.java | 168 +++++ .../mci/config/GlowMockConfig.java | 40 + .../config/ShinhanIntegrationProperties.java | 40 + .../mci/dto/MciRequestWrapper.java | 34 + .../mci/dto/MciResponseWrapper.java | 32 + .../mci/dto/OlCommonHeaderDto.java | 90 +++ .../mci/dto/ShinhanCommonHeaderDto.java | 72 ++ .../mci/dto/ShinhanMessageDto.java | 51 ++ .../mci/dto/ShinhanTelegramWrapper.java | 38 + .../mci/dto/SlCommonHeaderDto.java | 84 +++ .../mci/enums/IndvCtinRoleTyp.java | 27 + .../dap/lib/mcp/config/CacheConfig.java | 32 + .../dap/lib/mcp/config/JacksonConfig.java | 41 + .../dap/lib/mcp/config/KafkaLocalConfig.java | 58 ++ .../dap/lib/mcp/config/SwaggerConfig.java | 47 ++ .../dap/lib/mcp/config/WebConfig.java | 54 ++ .../mcp/exception/GlobalExceptionHandler.java | 60 ++ .../dap/lib/mcp/filter/MdcLoggingFilter.java | 56 ++ .../lib/mcp/security/ApiKeyInterceptor.java | 80 ++ .../lib/mcp/security/SecurityProperties.java | 40 + .../session/converter/ZtUsacConverter.java | 29 + .../lib/session/domain/model/ZtUsacModel.java | 51 ++ .../domain/repository/ZtUsacRepository.java | 33 + .../session/domain/usecase/ZtUsacUseCase.java | 29 + .../usecase/impl/ZtUsacUseCaseImpl.java | 42 ++ .../dap/lib/session/dto/SessionDto.java | 90 +++ .../dap/lib/session/dto/ZtUsacInDto.java | 37 + .../dap/lib/session/dto/ZtUsacOutDto.java | 85 +++ .../presentation/SsoRestController.java | 91 +++ .../session/presentation/io/SsoResponse.java | 35 + .../dap/lib/util/JsonSchemaGenerator.java | 205 +++++ .../dap/lib/util/PodScaffolder.java | 309 ++++++++ .../shinhanlife/dap/lib/util/SessionUtil.java | 45 ++ .../dap/lib/util/ToolScaffolder.java | 714 ++++++++++++++++++ .../dap/lib/util/ToolSchemaResolver.java | 78 ++ .../dap/lib/util/ToolSourceUpdater.java | 123 +++ .../McpToolNameValidationRunner.java | 21 + .../lib/validation/McpToolNameValidator.java | 173 +++++ .../shinhanlife/dap/mcc/dto/ToolMetadata.java | 76 ++ .../mcc/manifest/ToolManifestAnnotations.java | 10 + .../dap/mcc/manifest/ToolManifestItem.java | 15 + .../dap/mcc/manifest/ToolManifestMeta.java | 5 + .../mcc/manifest/ToolManifestResponse.java | 7 + .../dap/mcc/manifest/ToolManifestService.java | 131 ++++ .../dap/mcc/mcp/McpRequestHeaderContext.java | 21 + .../dap/mcc/mcp/McpRequestHeaderFilter.java | 34 + .../dap/mcc/mcp/McpRequestHeaders.java | 9 + .../mcc/mcp/ToolMcpServerConfiguration.java | 26 + .../mcc/mcp/ToolPodMcpToolSynchronizer.java | 81 ++ .../presentation/BusinessToolController.java | 243 ++++++ .../ToolArgumentSchemaValidator.java | 32 + .../presentation/ToolManifestController.java | 32 + .../mcc/usecase/AbstractMcpToolUseCase.java | 95 +++ .../usecase/ToolRegistryHeartbeatSender.java | 189 +++++ .../shinhanlife/dap/mcg/dto/ToolMetadata.java | 87 +++ .../io/shinhanlife/glow/BaseException.java | 43 ++ .../io/shinhanlife/glow/BaseResponse.java | 39 + .../io/shinhanlife/glow/BizException.java | 20 + .../io/shinhanlife/glow/GlowAppServiceId.java | 28 + .../io/shinhanlife/glow/GlowControllerId.java | 20 + .../io/shinhanlife/glow/GlowIndexPaging.java | 24 + .../io/shinhanlife/glow/GlowLogTarget.java | 29 + .../java/io/shinhanlife/glow/GlowLogger.java | 33 + .../io/shinhanlife/glow/GlowMciFieldInfo.java | 44 ++ .../shinhanlife/glow/GlowMybatisMapper.java | 33 + .../shinhanlife/glow/GlowServiceGroupId.java | 22 + .../io/shinhanlife/glow/GlowTrgmField.java | 41 + .../java/io/shinhanlife/glow/PageInfo.java | 83 ++ .../io/shinhanlife/glow/ResponseCode.java | 45 ++ .../io/shinhanlife/glow/ResponseUtil.java | 125 +++ .../annotation/GlowTrgmField.java | 26 + .../glow/communication/dto/CommonHeader.java | 8 + .../communication/dto/HeaderDefaults.java | 6 + .../glow/communication/dto/Transfer.java | 20 + .../exception/ItrfException.java | 10 + .../eai/component/GlowEaiComponent.java | 26 + .../mci/component/GlowMciComponent.java | 18 + .../util/CommonHeaderFactory.java | 18 + .../io/shinhanlife/glow/db/dto/AuditInfo.java | 40 + .../shinhanlife/glow/util/GlowMciParser.java | 105 +++ .../shinhanlife/glow/util/GlowTrgmParser.java | 92 +++ .../resources/glow/application-glow-dev.yml | 5 + .../resources/glow/application-glow-local.yml | 5 + .../resources/glow/application-glow-prod.yml | 5 + .../resources/glow/application-glow-test.yml | 5 + .../main/resources/glow/application-glow.yml | 54 ++ .../src/main/resources/mock-responses.json | 106 +++ .../resources/static/tool-test-console.html | 260 +++++++ .../adapter/sender/ShinhanMciSenderTest.java | 60 ++ .../config/ToolSchemaConfigurationTest.java | 16 + .../dap/lib/util/JsonSchemaGeneratorTest.java | 126 ++++ .../dap/lib/util/ToolScaffolderTest.java | 28 + .../dap/lib/util/ToolSchemaResolverTest.java | 119 +++ .../validation/McpToolNameValidatorTest.java | 110 +++ .../mcc/manifest/ToolManifestServiceTest.java | 100 +++ .../mcc/mcp/McpRequestHeaderFilterTest.java | 62 ++ .../mcp/ToolMcpServerConfigurationTest.java | 22 + ...inessToolControllerHeaderContractTest.java | 32 + .../ToolArgumentSchemaValidatorTest.java | 25 + .../ToolTestConsoleResourceTest.java | 24 + .../annotation/GlowTrgmFieldContractTest.java | 27 + .../glow/util/GlowMciParserTest.java | 65 ++ dap-tool-oth/Dockerfile | 8 + dap-tool-oth/build.gradle | 12 + .../converter/MetaCommonCodeConverter.java | 31 + .../biz/cmm/converter/MetaTableConverter.java | 31 + .../dap/mcc/biz/cmm/dto/BalanceRequest.java | 27 + .../dap/mcc/biz/cmm/dto/BalanceResponse.java | 26 + .../biz/cmm/dto/BillingProcessRequest.java | 40 + .../mcc/biz/cmm/dto/BillingStatusRequest.java | 34 + .../dap/mcc/biz/cmm/dto/BondCheckRequest.java | 34 + .../dap/mcc/biz/cmm/dto/BondIssueRequest.java | 40 + .../mcc/biz/cmm/dto/ClaimSearchRequest.java | 57 ++ .../mcc/biz/cmm/dto/ClaimSearchResponse.java | 88 +++ .../biz/cmm/dto/ContractDetailRequest.java | 37 + .../biz/cmm/dto/ContractStatusRequest.java | 37 + .../biz/cmm/dto/CustomerDetailRequest.java | 37 + .../mcc/biz/cmm/dto/CustomerGradeRequest.java | 37 + .../mcc/biz/cmm/dto/LeaveCountRequest.java | 34 + .../biz/cmm/dto/MciSampleStringResponse.java | 32 + .../mcc/biz/cmm/dto/MciSampleTargetDto.java | 20 + .../biz/cmm/dto/MetaCommonCodeRequest.java | 32 + .../biz/cmm/dto/MetaCommonCodeResponse.java | 37 + .../dap/mcc/biz/cmm/dto/MetaTableRequest.java | 32 + .../mcc/biz/cmm/dto/MetaTableResponse.java | 37 + .../mcc/biz/cmm/dto/SampleStringRequest.java | 24 + .../mcc/biz/cmm/dto/SampleStringResponse.java | 38 + .../biz/cmm/dto/TemplateDownloadRequest.java | 35 + .../biz/cmm/dto/VacationRegisterRequest.java | 40 + .../mcc/biz/cmm/usecase/BalanceUseCase.java | 15 + .../cmm/usecase/BillingProcessUseCase.java | 16 + .../mcc/biz/cmm/usecase/BondIssueUseCase.java | 16 + .../ClaimSearchSchemaSampleUseCase.java | 26 + .../biz/cmm/usecase/CommonUtilityUseCase.java | 19 + .../cmm/usecase/ContractInquiryUseCase.java | 16 + .../biz/cmm/usecase/CustomerInfoUseCase.java | 16 + .../cmm/usecase/MetaCommonCodeUseCase.java | 37 + .../mcc/biz/cmm/usecase/MetaTableUseCase.java | 37 + .../cmm/usecase/TemplateUtilityUseCase.java | 21 + .../cmm/usecase/impl/BalanceUseCaseImpl.java | 48 ++ .../impl/BillingProcessUseCaseImpl.java | 68 ++ .../usecase/impl/BondIssueUseCaseImpl.java | 37 + .../ClaimSearchSchemaSampleUseCaseImpl.java | 36 + .../impl/CommonUtilityUseCaseImpl.java | 43 ++ .../impl/ContractInquiryUseCaseImpl.java | 37 + .../usecase/impl/CustomerInfoUseCaseImpl.java | 37 + .../impl/MetaCommonCodeUseCaseImpl.java | 89 +++ .../usecase/impl/MetaTableUseCaseImpl.java | 89 +++ .../impl/TemplateUtilityUseCaseImpl.java | 58 ++ .../biz/oth/converter/Onnba3011Converter.java | 29 + .../dap/mcc/biz/oth/dto/Onnba3011Request.java | 119 +++ .../mcc/biz/oth/usecase/Onnba3011UseCase.java | 10 + .../usecase/impl/Onnba3011UseCaseImpl.java | 60 ++ .../mcc/biz/smp/dto/DailyQuoteRequest.java | 3 + .../mcc/biz/smp/dto/DailyQuoteResponse.java | 3 + .../mcc/biz/smp/dto/ExchangeRateRequest.java | 3 + .../mcc/biz/smp/dto/ExchangeRateResponse.java | 3 + .../mcc/biz/smp/dto/TeamMemberRequest.java | 26 + .../mcc/biz/smp/dto/TeamMemberResponse.java | 24 + .../dap/mcc/biz/smp/dto/WeatherRequest.java | 25 + .../dap/mcc/biz/smp/dto/WeatherResponse.java | 24 + .../smp/usecase/DailyQuoteToolUseCase.java | 19 + .../smp/usecase/ExchangeRateToolUseCase.java | 21 + .../biz/smp/usecase/TeamMemberUseCase.java | 23 + .../biz/smp/usecase/WeatherToolUseCase.java | 18 + .../impl/DailyQuoteToolUseCaseImpl.java | 46 ++ .../impl/ExchangeRateToolUseCaseImpl.java | 50 ++ .../usecase/impl/TeamMemberUseCaseImpl.java | 54 ++ .../usecase/impl/WeatherToolUseCaseImpl.java | 105 +++ .../sol/converter/SolReqDetailConverter.java | 31 + .../sol/converter/SolReqListConverter.java | 33 + .../mcc/biz/sol/dto/SolReqDetailRequest.java | 27 + .../mcc/biz/sol/dto/SolReqDetailResponse.java | 33 + .../mcc/biz/sol/dto/SolReqListRequest.java | 32 + .../mcc/biz/sol/dto/SolReqListResponse.java | 37 + .../biz/sol/usecase/SolReqDetailUseCase.java | 39 + .../biz/sol/usecase/SolReqListUseCase.java | 23 + .../usecase/impl/SolReqDetailUseCaseImpl.java | 101 +++ .../usecase/impl/SolReqListUseCaseImpl.java | 80 ++ .../infra/itrf/mci/cfp/a/MciCfpaClient.java | 72 ++ .../itrf/mci/cfp/a/io/CLCNNB00001_I.java | 91 +++ .../itrf/mci/cfp/a/io/CLCNNB00001_O.java | 16 + .../infra/itrf/mci/ncl/g/MciNclgClient.java | 30 + .../itrf/mci/ncl/g/io/SOLG00000001_I.java | 24 + .../itrf/mci/ncl/g/io/SOLG00000001_O.java | 34 + .../itrf/mci/ncl/g/io/SOLG00000002_I.java | 23 + .../itrf/mci/ncl/g/io/SOLG00000002_O.java | 31 + .../dap/mcc/oth/DapToolOthApplication.java | 30 + .../src/main/resources/application-dev.yml | 28 + .../src/main/resources/application-local.yml | 43 ++ .../src/main/resources/application.yml | 19 + .../src/main/resources/logback-spring.xml | 39 + .../claim-search-resource-input-schema.json | 37 + .../claim-search-resource-output-schema.json | 81 ++ .../cmm/dto/ClaimSearchRequestSchemaTest.java | 88 +++ .../impl/Onnba3011UseCaseImplTest.java | 46 ++ .../impl/SolReqDetailUseCaseImplTest.java | 45 ++ .../impl/SolReqListUseCaseImplTest.java | 39 + .../Onnba3011MciRequestConverterTest.java | 55 ++ dap-tool-sms/Dockerfile | 8 + dap-tool-sms/build.gradle | 12 + .../biz/sms/converter/SmsLegacyConverter.java | 29 + .../dap/mcc/biz/sms/dto/SmsSendRequest.java | 42 ++ .../mcc/biz/sms/legacy/SmsLegacyRequest.java | 33 + .../mcc/biz/sms/usecase/SmsToolUseCase.java | 10 + .../sms/usecase/impl/SmsToolUseCaseImpl.java | 53 ++ .../dap/mcc/sms/DapToolSmsApplication.java | 30 + .../src/main/resources/application-dev.yml | 28 + .../src/main/resources/application-local.yml | 39 + .../src/main/resources/application.yml | 19 + .../src/main/resources/logback-spring.xml | 39 + docker-compose.yml | 92 +++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43764 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 251 ++++++ gradlew.bat | 94 +++ mci-mock/mappings/extmci.json | 21 + mci-mock/mappings/mci-send.json | 21 + mci_sample_test.http | 35 + scratch_rewrite.py | 66 ++ settings.gradle | 5 + sql.txt | 283 +++++++ sse_out.txt | 2 + 290 files changed, 16715 insertions(+) create mode 100644 .agents/AGENTS.md create mode 100644 .agents/mcp.json create mode 100644 .agents/skills/chaos-tool-tester/SKILL.md create mode 100644 .agents/skills/gateway-routing-tracer/SKILL.md create mode 100644 .agents/skills/gateway-routing-tracer/trace.sh create mode 100644 .agents/skills/mcp-schema-autogen/SKILL.md create mode 100644 .agents/skills/mcp_best_practices/SKILL.md create mode 100644 .agents/skills/mcp_evaluation/SKILL.md create mode 100644 .dockerignore create mode 100644 .gitattributes create mode 100644 .gitea/workflows/deploy.yml create mode 100644 .gitignore create mode 100644 AddJavadoc.java create mode 100644 Dockerfile create mode 100644 HELP.md create mode 100644 McpBridge.java create mode 100644 README.md create mode 100644 build.gradle create mode 100644 dap-tool-core/build.gradle create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/aop/EimsMonitoringAspect.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/connector/ExternalApiConnector.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/connector/InternalSystemConnector.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/connector/LegacyDbConnector.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/connector/LegacyEimsConnector.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/connector/ThirdPartySecurityConnector.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/dto/ErrorDetail.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/dto/JsonRpcRequest.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/dto/JsonRpcResponse.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/dto/Params.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/exception/MciCommunicationException.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/EaiEimsSender.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/EimsSender.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/HttpEimsSender.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/JspFormEimsSender.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/JspJsonEimsSender.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/MciEimsSender.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/MciStringEimsSender.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/ShinhanMciSender.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/TcpEimsSender.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/support/DynamicPayloadBuilder.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/support/DynamicSchemaValidator.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/support/MciTemplate.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/support/ResultStandardizer.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/support/TicketManager.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/support/ToolExecutionService.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/test/MockEimsHttpServer.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/test/MockEimsTcpServer.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/test/MockJspServer.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/util/LegacyDataTransformer.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/util/PiiMaskingLogbackConverter.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/util/PiiMaskingUtils.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpAnyOf.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpOutputSchema.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpParameter.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpTool.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/aop/ToolSlaMonitoringAspect.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/CorsConfig.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/GlowCommunicationProperties.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/McpProperties.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/MybatisConfig.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/P6SpySqlFormatter.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/ToolSchemaConfiguration.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/GlowIntegrationCall.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/dto/SampleGlowMessage.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/eai/component/AxhubEaiComponent.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/component/AxhubMciComponent.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/config/GlowMockConfig.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/config/ShinhanIntegrationProperties.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/MciRequestWrapper.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/MciResponseWrapper.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/OlCommonHeaderDto.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/ShinhanCommonHeaderDto.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/ShinhanMessageDto.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/ShinhanTelegramWrapper.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/SlCommonHeaderDto.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/enums/IndvCtinRoleTyp.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/config/CacheConfig.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/config/JacksonConfig.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/config/KafkaLocalConfig.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/config/SwaggerConfig.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/config/WebConfig.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/exception/GlobalExceptionHandler.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/filter/MdcLoggingFilter.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/security/ApiKeyInterceptor.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/security/SecurityProperties.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/converter/ZtUsacConverter.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/domain/model/ZtUsacModel.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/domain/repository/ZtUsacRepository.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/domain/usecase/ZtUsacUseCase.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/domain/usecase/impl/ZtUsacUseCaseImpl.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/dto/SessionDto.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/dto/ZtUsacInDto.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/dto/ZtUsacOutDto.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/presentation/SsoRestController.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/presentation/io/SsoResponse.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/PodScaffolder.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/SessionUtil.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolSourceUpdater.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidationRunner.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidator.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/dto/ToolMetadata.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/manifest/ToolManifestAnnotations.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/manifest/ToolManifestItem.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/manifest/ToolManifestMeta.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/manifest/ToolManifestResponse.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/manifest/ToolManifestService.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/mcp/McpRequestHeaderContext.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/mcp/McpRequestHeaderFilter.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/mcp/McpRequestHeaders.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/mcp/ToolMcpServerConfiguration.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/mcp/ToolPodMcpToolSynchronizer.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolArgumentSchemaValidator.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolManifestController.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/usecase/AbstractMcpToolUseCase.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/usecase/ToolRegistryHeartbeatSender.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/mcg/dto/ToolMetadata.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/glow/BaseException.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/glow/BaseResponse.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/glow/BizException.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/glow/GlowAppServiceId.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/glow/GlowControllerId.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/glow/GlowIndexPaging.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/glow/GlowLogTarget.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/glow/GlowLogger.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/glow/GlowMciFieldInfo.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/glow/GlowMybatisMapper.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/glow/GlowServiceGroupId.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/glow/GlowTrgmField.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/glow/PageInfo.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/glow/ResponseCode.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/glow/ResponseUtil.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/glow/communication/annotation/GlowTrgmField.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/glow/communication/dto/CommonHeader.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/glow/communication/dto/HeaderDefaults.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/glow/communication/dto/Transfer.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/glow/communication/exception/ItrfException.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/glow/communication/module/eai/component/GlowEaiComponent.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/glow/communication/module/mci/component/GlowMciComponent.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/glow/communication/util/CommonHeaderFactory.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/glow/db/dto/AuditInfo.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/glow/util/GlowMciParser.java create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/glow/util/GlowTrgmParser.java create mode 100644 dap-tool-core/src/main/resources/glow/application-glow-dev.yml create mode 100644 dap-tool-core/src/main/resources/glow/application-glow-local.yml create mode 100644 dap-tool-core/src/main/resources/glow/application-glow-prod.yml create mode 100644 dap-tool-core/src/main/resources/glow/application-glow-test.yml create mode 100644 dap-tool-core/src/main/resources/glow/application-glow.yml create mode 100644 dap-tool-core/src/main/resources/mock-responses.json create mode 100644 dap-tool-core/src/main/resources/static/tool-test-console.html create mode 100644 dap-tool-core/src/test/java/io/shinhanlife/dap/lib/common/adapter/sender/ShinhanMciSenderTest.java create mode 100644 dap-tool-core/src/test/java/io/shinhanlife/dap/lib/config/ToolSchemaConfigurationTest.java create mode 100644 dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/JsonSchemaGeneratorTest.java create mode 100644 dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java create mode 100644 dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java create mode 100644 dap-tool-core/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java create mode 100644 dap-tool-core/src/test/java/io/shinhanlife/dap/mcc/manifest/ToolManifestServiceTest.java create mode 100644 dap-tool-core/src/test/java/io/shinhanlife/dap/mcc/mcp/McpRequestHeaderFilterTest.java create mode 100644 dap-tool-core/src/test/java/io/shinhanlife/dap/mcc/mcp/ToolMcpServerConfigurationTest.java create mode 100644 dap-tool-core/src/test/java/io/shinhanlife/dap/mcc/presentation/BusinessToolControllerHeaderContractTest.java create mode 100644 dap-tool-core/src/test/java/io/shinhanlife/dap/mcc/presentation/ToolArgumentSchemaValidatorTest.java create mode 100644 dap-tool-core/src/test/java/io/shinhanlife/dap/mcc/presentation/ToolTestConsoleResourceTest.java create mode 100644 dap-tool-core/src/test/java/io/shinhanlife/glow/communication/annotation/GlowTrgmFieldContractTest.java create mode 100644 dap-tool-core/src/test/java/io/shinhanlife/glow/util/GlowMciParserTest.java create mode 100644 dap-tool-oth/Dockerfile create mode 100644 dap-tool-oth/build.gradle create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/converter/MetaCommonCodeConverter.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/converter/MetaTableConverter.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BalanceRequest.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BalanceResponse.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BillingProcessRequest.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BillingStatusRequest.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BondCheckRequest.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BondIssueRequest.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequest.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchResponse.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ContractDetailRequest.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ContractStatusRequest.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/CustomerDetailRequest.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/CustomerGradeRequest.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/LeaveCountRequest.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MciSampleStringResponse.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MciSampleTargetDto.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MetaCommonCodeRequest.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MetaCommonCodeResponse.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MetaTableRequest.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MetaTableResponse.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/SampleStringRequest.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/SampleStringResponse.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/TemplateDownloadRequest.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/VacationRegisterRequest.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BalanceUseCase.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BillingProcessUseCase.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BondIssueUseCase.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CommonUtilityUseCase.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ContractInquiryUseCase.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CustomerInfoUseCase.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaCommonCodeUseCase.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaTableUseCase.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/TemplateUtilityUseCase.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/BalanceUseCaseImpl.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/BillingProcessUseCaseImpl.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/BondIssueUseCaseImpl.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchSchemaSampleUseCaseImpl.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/CommonUtilityUseCaseImpl.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ContractInquiryUseCaseImpl.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/CustomerInfoUseCaseImpl.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/MetaCommonCodeUseCaseImpl.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/MetaTableUseCaseImpl.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/TemplateUtilityUseCaseImpl.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/converter/Onnba3011Converter.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/dto/Onnba3011Request.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/Onnba3011UseCase.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/impl/Onnba3011UseCaseImpl.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/DailyQuoteRequest.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/DailyQuoteResponse.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/ExchangeRateRequest.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/ExchangeRateResponse.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/TeamMemberRequest.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/TeamMemberResponse.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/WeatherRequest.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/WeatherResponse.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/DailyQuoteToolUseCase.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/ExchangeRateToolUseCase.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/TeamMemberUseCase.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/WeatherToolUseCase.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/impl/DailyQuoteToolUseCaseImpl.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/impl/ExchangeRateToolUseCaseImpl.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/impl/TeamMemberUseCaseImpl.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/impl/WeatherToolUseCaseImpl.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/converter/SolReqDetailConverter.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/converter/SolReqListConverter.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/dto/SolReqDetailRequest.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/dto/SolReqDetailResponse.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/dto/SolReqListRequest.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/dto/SolReqListResponse.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqDetailUseCase.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqListUseCase.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/impl/SolReqDetailUseCaseImpl.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/impl/SolReqListUseCaseImpl.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/cfp/a/MciCfpaClient.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/cfp/a/io/CLCNNB00001_I.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/cfp/a/io/CLCNNB00001_O.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncl/g/MciNclgClient.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncl/g/io/SOLG00000001_I.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncl/g/io/SOLG00000001_O.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncl/g/io/SOLG00000002_I.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncl/g/io/SOLG00000002_O.java create mode 100644 dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/oth/DapToolOthApplication.java create mode 100644 dap-tool-oth/src/main/resources/application-dev.yml create mode 100644 dap-tool-oth/src/main/resources/application-local.yml create mode 100644 dap-tool-oth/src/main/resources/application.yml create mode 100644 dap-tool-oth/src/main/resources/logback-spring.xml create mode 100644 dap-tool-oth/src/main/resources/tool-schemas/cmm/claim-search-resource-input-schema.json create mode 100644 dap-tool-oth/src/main/resources/tool-schemas/cmm/claim-search-resource-output-schema.json create mode 100644 dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java create mode 100644 dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/oth/usecase/impl/Onnba3011UseCaseImplTest.java create mode 100644 dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/sol/usecase/impl/SolReqDetailUseCaseImplTest.java create mode 100644 dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/sol/usecase/impl/SolReqListUseCaseImplTest.java create mode 100644 dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/infra/itrf/mci/cfp/a/converter/Onnba3011MciRequestConverterTest.java create mode 100644 dap-tool-sms/Dockerfile create mode 100644 dap-tool-sms/build.gradle create mode 100644 dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/converter/SmsLegacyConverter.java create mode 100644 dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/dto/SmsSendRequest.java create mode 100644 dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/legacy/SmsLegacyRequest.java create mode 100644 dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/usecase/SmsToolUseCase.java create mode 100644 dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/usecase/impl/SmsToolUseCaseImpl.java create mode 100644 dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/sms/DapToolSmsApplication.java create mode 100644 dap-tool-sms/src/main/resources/application-dev.yml create mode 100644 dap-tool-sms/src/main/resources/application-local.yml create mode 100644 dap-tool-sms/src/main/resources/application.yml create mode 100644 dap-tool-sms/src/main/resources/logback-spring.xml create mode 100644 docker-compose.yml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 mci-mock/mappings/extmci.json create mode 100644 mci-mock/mappings/mci-send.json create mode 100644 mci_sample_test.http create mode 100644 scratch_rewrite.py create mode 100644 settings.gradle create mode 100644 sql.txt create mode 100644 sse_out.txt diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md new file mode 100644 index 000000000..37f6daf02 --- /dev/null +++ b/.agents/AGENTS.md @@ -0,0 +1,39 @@ +# AX HUB 프로젝트 개발 규칙 (AGENTS.md) + +이 파일은 AI Agent(Antigravity)가 이 프로젝트에서 작업할 때 항상 명심하고 지켜야 할 규칙을 정의하는 파일입니다. +작업 시 유의해야 할 사항이 생기면 언제든지 이 아래에 자유롭게 내용을 추가해 주세요! + +## 개발 가이드라인 +* 패키지명은 `controller` 대신 `presentation`을 사용합니다. +* MapStruct 사용 시, Spring DI를 활용하여 의존성 주입(`private final Converter converter;`)을 받는 방식을 권장합니다. (단위 테스트 시에는 `@MockBean` 또는 직접 구현체를 주입하여 테스트) + +## 명심해야 할 규칙 추가란 +* `application.yml` 등 설정 파일 수정 시 한글이 깨지지 않도록 항상 UTF-8 인코딩을 유지하고, 깨진 문자열(`?\uFFFD` 등)이 발생하지 않도록 각별히 주의한다. +* 이모지는 무조건 넣지 않는다 +* import 할것 무조건 한다 +* Git commit과 push는 사용자의 명시적인 허락(지시) 없이는 절대 수행하지 않는다. +* 자바 만들때는 무조건 아래 내용을 넣는다 +/** + * @package io.shinhanlife.axhub.biz.mcp.tool.sms + * @className AxHubToolSmsApplication + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +* 신한라이프 인터페이스 통신 프로토콜 및 메시지 포맷 가이드 (준버티컬 Tool 대상): + * 현재 개발 대상은 **대내MCI / EAI (JSON)** 연동으로 한정한다. (대외MCI FixedLength 연동은 범위에서 제외) + * 통신 노선, 데이터 변환 규격, 시스템별 연계 방식 등은 MCI/EIMS 상에서 관리되므로 코드 레벨에서 식별하거나 분기 처리하지 않는다. (단순 통합 JSON 요청만 수행) + * 처리계 UI ↔ 처리계 AP 구간: `HTTPS` / `SSV`를 사용하며, FW에서 x-api를 통해 SSV↔DTO 변환을 수행한다. + +* 신한라이프 표준 로그 기준 (Logback 설정): + * **로그 생성 경로:** `/swlog/어플리케이션명(모듈명)/코드명/` (예: `/swlog/dap-gateway/A01/`) + * **로그 네이밍 규칙:** `${HOSTNAME}_코드명_yyyyMMdd.log` (예: `${HOSTNAME}_A01_20260722.log`) + * **기본 로그 구분 코드:** 시스템 운영기록 가동기록의 경우 `A01`을 기본으로 사용한다. + * **호스트명 동적 할당:** Logback 설정 시 `` 를 선언하여 사용한다. diff --git a/.agents/mcp.json b/.agents/mcp.json new file mode 100644 index 000000000..891203db6 --- /dev/null +++ b/.agents/mcp.json @@ -0,0 +1,7 @@ +{ + "mcpServers": { + "axhub-gateway": { + "url": "http://localhost:8281/mcp/sse" + } + } +} diff --git a/.agents/skills/chaos-tool-tester/SKILL.md b/.agents/skills/chaos-tool-tester/SKILL.md new file mode 100644 index 000000000..154d167f5 --- /dev/null +++ b/.agents/skills/chaos-tool-tester/SKILL.md @@ -0,0 +1,16 @@ +--- +name: chaos-tool-tester +description: 현재 연결된 MCP 도구들에 대해 엣지 케이스(Null, 타입 오류, SQL 인젝션 문자열 등)를 고의로 주입하여 안정성을 테스트합니다. +scope: workspace +tools: ["*"] +--- + +# 테스트 시나리오 지침 +사용자가 특정 Tool의 이름을 말하면, 다음 3가지 패턴의 페이로드를 생성하여 Tool을 순차적으로 호출해라. + +1. **타입 브레이커:** Integer가 들어가야 할 곳에 긴 String 넣기 +2. **누락 테스트:** 필수(Required) 파라미터를 누락시키고 호출하기 +3. **악의적 페이로드:** 파라미터 값에 `` 또는 `' OR 1=1 --` 삽입하기 + +# 결과 리포팅 +각 호출 시도 후, Spring Boot 서버가 500 에러를 뱉고 죽었는지, 아니면 안전하게 Validation 에러 메시지를 반환했는지 마크다운 표로 정리해서 보고할 것. diff --git a/.agents/skills/gateway-routing-tracer/SKILL.md b/.agents/skills/gateway-routing-tracer/SKILL.md new file mode 100644 index 000000000..f5eb891c6 --- /dev/null +++ b/.agents/skills/gateway-routing-tracer/SKILL.md @@ -0,0 +1,12 @@ +--- +name: gateway-routing-tracer +description: 게이트웨이 로그를 분석하여 특정 Tool Call이나 Request ID의 라우팅 경로와 소요 시간을 추적합니다. +scope: workspace +--- + +# 실행 스크립트 +이 스킬은 같은 폴더에 위치한 `trace.sh` 스크립트를 실행하여 로그를 파싱합니다. + +# 에이전트 지침 +1. 사용자가 조회하고자 하는 `Request ID` 또는 `Tool 이름`을 감지하면, 백그라운드 터미널에서 `trace.sh <인자>`를 실행해라. +2. 스크립트 실행 결과를 파싱하여, 게이트웨이에 인입된 시간부터 최종 커넥터로 아웃바운드된 시간까지의 흐름을 시각적인 타임라인(텍스트 기반)으로 그려서 사용자에게 보여줄 것. diff --git a/.agents/skills/gateway-routing-tracer/trace.sh b/.agents/skills/gateway-routing-tracer/trace.sh new file mode 100644 index 000000000..41af8631c --- /dev/null +++ b/.agents/skills/gateway-routing-tracer/trace.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# Spring Boot 로그 파일 경로 (기본 설정: ./logs/spring-boot-logger.log) +LOG_FILE="./logs/spring-boot-logger.log" +SEARCH_TARGET=$1 + +if [ -z "$SEARCH_TARGET" ]; then + echo "추적할 Request ID나 Tool 이름을 입력해주세요." + exit 1 +fi + +if [ ! -f "$LOG_FILE" ]; then + echo "오류: 로그 파일($LOG_FILE)을 찾을 수 없습니다. 경로를 확인해주세요." + exit 1 +fi + +echo "[$SEARCH_TARGET] 라우팅 추적 시작..." +# Spring Boot 로그에서 대상 검색하여 주요 필드 출력 +grep -i "$SEARCH_TARGET" "$LOG_FILE" | awk '{print $1, $2, $3, $5, $NF}' diff --git a/.agents/skills/mcp-schema-autogen/SKILL.md b/.agents/skills/mcp-schema-autogen/SKILL.md new file mode 100644 index 000000000..f83f77f96 --- /dev/null +++ b/.agents/skills/mcp-schema-autogen/SKILL.md @@ -0,0 +1,16 @@ +--- +name: mcp-schema-autogen +description: Java 21 Record 클래스나 Spring Controller 코드를 분석하여 MCP 표준 JSON Schema(Tool 명세)를 자동 생성합니다. +scope: workspace +--- + +# 시스템 지침 (System Prompt) +너는 MCP(Model Context Protocol) Tool 스키마 전문 엔지니어다. +사용자가 Java 코드를 제공하면 다음 규칙에 따라 JSON Schema를 작성해라. + +1. **타입 매핑:** Java 21 `Record`의 필드 타입을 완벽하게 JSON Schema 타입(string, integer, boolean, object, array 등)으로 매핑할 것. +2. **필수값 추출:** `@NotNull`, `@NotBlank` 또는 Java 21 Record의 Non-null 필드는 반드시 `required` 배열에 포함시킬 것. +3. **설명 추가:** JavaDoc 주석이 있거나 필드 위에 주석이 있다면 스키마의 `description` 필드로 적극 반영할 것. + +# 출력 형식 +결과물은 반드시 순수한 JSON 형태로만 출력하고, 불필요한 부연 설명은 생략한다. diff --git a/.agents/skills/mcp_best_practices/SKILL.md b/.agents/skills/mcp_best_practices/SKILL.md new file mode 100644 index 000000000..b8e949cf4 --- /dev/null +++ b/.agents/skills/mcp_best_practices/SKILL.md @@ -0,0 +1,249 @@ +# MCP Server Best Practices + +## Quick Reference + +### Server Naming +- **Python**: `{service}_mcp` (e.g., `slack_mcp`) +- **Node/TypeScript**: `{service}-mcp-server` (e.g., `slack-mcp-server`) + +### Tool Naming +- Use snake_case with service prefix +- Format: `{service}_{action}_{resource}` +- Example: `slack_send_message`, `github_create_issue` + +### Response Formats +- Support both JSON and Markdown formats +- JSON for programmatic processing +- Markdown for human readability + +### Pagination +- Always respect `limit` parameter +- Return `has_more`, `next_offset`, `total_count` +- Default to 20-50 items + +### Transport +- **Streamable HTTP**: For remote servers, multi-client scenarios +- **stdio**: For local integrations, command-line tools +- Avoid SSE (deprecated in favor of streamable HTTP) + +--- + +## Server Naming Conventions + +Follow these standardized naming patterns: + +**Python**: Use format `{service}_mcp` (lowercase with underscores) +- Examples: `slack_mcp`, `github_mcp`, `jira_mcp` + +**Node/TypeScript**: Use format `{service}-mcp-server` (lowercase with hyphens) +- Examples: `slack-mcp-server`, `github-mcp-server`, `jira-mcp-server` + +The name should be general, descriptive of the service being integrated, easy to infer from the task description, and without version numbers. + +--- + +## Tool Naming and Design + +### Tool Naming + +1. **Use snake_case**: `search_users`, `create_project`, `get_channel_info` +2. **Include service prefix**: Anticipate that your MCP server may be used alongside other MCP servers + - Use `slack_send_message` instead of just `send_message` + - Use `github_create_issue` instead of just `create_issue` +3. **Be action-oriented**: Start with verbs (get, list, search, create, etc.) +4. **Be specific**: Avoid generic names that could conflict with other servers + +### Tool Design + +- Tool descriptions must narrowly and unambiguously describe functionality +- Descriptions must precisely match actual functionality +- Provide tool annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) +- Keep tool operations focused and atomic + +--- + +## Response Formats + +All tools that return data should support multiple formats: + +### JSON Format (`response_format="json"`) +- Machine-readable structured data +- Include all available fields and metadata +- Consistent field names and types +- Use for programmatic processing + +### Markdown Format (`response_format="markdown"`, typically default) +- Human-readable formatted text +- Use headers, lists, and formatting for clarity +- Convert timestamps to human-readable format +- Show display names with IDs in parentheses +- Omit verbose metadata + +--- + +## Pagination + +For tools that list resources: + +- **Always respect the `limit` parameter** +- **Implement pagination**: Use `offset` or cursor-based pagination +- **Return pagination metadata**: Include `has_more`, `next_offset`/`next_cursor`, `total_count` +- **Never load all results into memory**: Especially important for large datasets +- **Default to reasonable limits**: 20-50 items is typical + +Example pagination response: +```json +{ + "total": 150, + "count": 20, + "offset": 0, + "items": [...], + "has_more": true, + "next_offset": 20 +} +``` + +--- + +## Transport Options + +### Streamable HTTP + +**Best for**: Remote servers, web services, multi-client scenarios + +**Characteristics**: +- Bidirectional communication over HTTP +- Supports multiple simultaneous clients +- Can be deployed as a web service +- Enables server-to-client notifications + +**Use when**: +- Serving multiple clients simultaneously +- Deploying as a cloud service +- Integration with web applications + +### stdio + +**Best for**: Local integrations, command-line tools + +**Characteristics**: +- Standard input/output stream communication +- Simple setup, no network configuration needed +- Runs as a subprocess of the client + +**Use when**: +- Building tools for local development environments +- Integrating with desktop applications +- Single-user, single-session scenarios + +**Note**: stdio servers should NOT log to stdout (use stderr for logging) + +### Transport Selection + +| Criterion | stdio | Streamable HTTP | +|-----------|-------|-----------------| +| **Deployment** | Local | Remote | +| **Clients** | Single | Multiple | +| **Complexity** | Low | Medium | +| **Real-time** | No | Yes | + +--- + +## Security Best Practices + +### Authentication and Authorization + +**OAuth 2.1**: +- Use secure OAuth 2.1 with certificates from recognized authorities +- Validate access tokens before processing requests +- Only accept tokens specifically intended for your server + +**API Keys**: +- Store API keys in environment variables, never in code +- Validate keys on server startup +- Provide clear error messages when authentication fails + +### Input Validation + +- Sanitize file paths to prevent directory traversal +- Validate URLs and external identifiers +- Check parameter sizes and ranges +- Prevent command injection in system calls +- Use schema validation (Pydantic/Zod) for all inputs + +### Error Handling + +- Don't expose internal errors to clients +- Log security-relevant errors server-side +- Provide helpful but not revealing error messages +- Clean up resources after errors + +### DNS Rebinding Protection + +For streamable HTTP servers running locally: +- Enable DNS rebinding protection +- Validate the `Origin` header on all incoming connections +- Bind to `127.0.0.1` rather than `0.0.0.0` + +--- + +## Tool Annotations + +Provide annotations to help clients understand tool behavior: + +| Annotation | Type | Default | Description | +|-----------|------|---------|-------------| +| `readOnlyHint` | boolean | false | Tool does not modify its environment | +| `destructiveHint` | boolean | true | Tool may perform destructive updates | +| `idempotentHint` | boolean | false | Repeated calls with same args have no additional effect | +| `openWorldHint` | boolean | true | Tool interacts with external entities | + +**Important**: Annotations are hints, not security guarantees. Clients should not make security-critical decisions based solely on annotations. + +--- + +## Error Handling + +- Use standard JSON-RPC error codes +- Report tool errors within result objects (not protocol-level errors) +- Provide helpful, specific error messages with suggested next steps +- Don't expose internal implementation details +- Clean up resources properly on errors + +Example error handling: +```typescript +try { + const result = performOperation(); + return { content: [{ type: "text", text: result }] }; +} catch (error) { + return { + isError: true, + content: [{ + type: "text", + text: `Error: ${error.message}. Try using filter='active_only' to reduce results.` + }] + }; +} +``` + +--- + +## Testing Requirements + +Comprehensive testing should cover: + +- **Functional testing**: Verify correct execution with valid/invalid inputs +- **Integration testing**: Test interaction with external systems +- **Security testing**: Validate auth, input sanitization, rate limiting +- **Performance testing**: Check behavior under load, timeouts +- **Error handling**: Ensure proper error reporting and cleanup + +--- + +## Documentation Requirements + +- Provide clear documentation of all tools and capabilities +- Include working examples (at least 3 per major feature) +- Document security considerations +- Specify required permissions and access levels +- Document rate limits and performance characteristics diff --git a/.agents/skills/mcp_evaluation/SKILL.md b/.agents/skills/mcp_evaluation/SKILL.md new file mode 100644 index 000000000..e2c502ecc --- /dev/null +++ b/.agents/skills/mcp_evaluation/SKILL.md @@ -0,0 +1,602 @@ +# MCP Server Evaluation Guide + +## Overview + +This document provides guidance on creating comprehensive evaluations for MCP servers. Evaluations test whether LLMs can effectively use your MCP server to answer realistic, complex questions using only the tools provided. + +--- + +## Quick Reference + +### Evaluation Requirements +- Create 10 human-readable questions +- Questions must be READ-ONLY, INDEPENDENT, NON-DESTRUCTIVE +- Each question requires multiple tool calls (potentially dozens) +- Answers must be single, verifiable values +- Answers must be STABLE (won't change over time) + +### Output Format +```xml + + + Your question here + Single verifiable answer + + +``` + +--- + +## Purpose of Evaluations + +The measure of quality of an MCP server is NOT how well or comprehensively the server implements tools, but how well these implementations (input/output schemas, docstrings/descriptions, functionality) enable LLMs with no other context and access ONLY to the MCP servers to answer realistic and difficult questions. + +## Evaluation Overview + +Create 10 human-readable questions requiring ONLY READ-ONLY, INDEPENDENT, NON-DESTRUCTIVE, and IDEMPOTENT operations to answer. Each question should be: +- Realistic +- Clear and concise +- Unambiguous +- Complex, requiring potentially dozens of tool calls or steps +- Answerable with a single, verifiable value that you identify in advance + +## Question Guidelines + +### Core Requirements + +1. **Questions MUST be independent** + - Each question should NOT depend on the answer to any other question + - Should not assume prior write operations from processing another question + +2. **Questions MUST require ONLY NON-DESTRUCTIVE AND IDEMPOTENT tool use** + - Should not instruct or require modifying state to arrive at the correct answer + +3. **Questions must be REALISTIC, CLEAR, CONCISE, and COMPLEX** + - Must require another LLM to use multiple (potentially dozens of) tools or steps to answer + +### Complexity and Depth + +4. **Questions must require deep exploration** + - Consider multi-hop questions requiring multiple sub-questions and sequential tool calls + - Each step should benefit from information found in previous questions + +5. **Questions may require extensive paging** + - May need paging through multiple pages of results + - May require querying old data (1-2 years out-of-date) to find niche information + - The questions must be DIFFICULT + +6. **Questions must require deep understanding** + - Rather than surface-level knowledge + - May pose complex ideas as True/False questions requiring evidence + - May use multiple-choice format where LLM must search different hypotheses + +7. **Questions must not be solvable with straightforward keyword search** + - Do not include specific keywords from the target content + - Use synonyms, related concepts, or paraphrases + - Require multiple searches, analyzing multiple related items, extracting context, then deriving the answer + +### Tool Testing + +8. **Questions should stress-test tool return values** + - May elicit tools returning large JSON objects or lists, overwhelming the LLM + - Should require understanding multiple modalities of data: + - IDs and names + - Timestamps and datetimes (months, days, years, seconds) + - File IDs, names, extensions, and mimetypes + - URLs, GIDs, etc. + - Should probe the tool's ability to return all useful forms of data + +9. **Questions should MOSTLY reflect real human use cases** + - The kinds of information retrieval tasks that HUMANS assisted by an LLM would care about + +10. **Questions may require dozens of tool calls** + - This challenges LLMs with limited context + - Encourages MCP server tools to reduce information returned + +11. **Include ambiguous questions** + - May be ambiguous OR require difficult decisions on which tools to call + - Force the LLM to potentially make mistakes or misinterpret + - Ensure that despite AMBIGUITY, there is STILL A SINGLE VERIFIABLE ANSWER + +### Stability + +12. **Questions must be designed so the answer DOES NOT CHANGE** + - Do not ask questions that rely on "current state" which is dynamic + - For example, do not count: + - Number of reactions to a post + - Number of replies to a thread + - Number of members in a channel + +13. **DO NOT let the MCP server RESTRICT the kinds of questions you create** + - Create challenging and complex questions + - Some may not be solvable with the available MCP server tools + - Questions may require specific output formats (datetime vs. epoch time, JSON vs. MARKDOWN) + - Questions may require dozens of tool calls to complete + +## Answer Guidelines + +### Verification + +1. **Answers must be VERIFIABLE via direct string comparison** + - If the answer can be re-written in many formats, clearly specify the output format in the QUESTION + - Examples: "Use YYYY/MM/DD.", "Respond True or False.", "Answer A, B, C, or D and nothing else." + - Answer should be a single VERIFIABLE value such as: + - User ID, user name, display name, first name, last name + - Channel ID, channel name + - Message ID, string + - URL, title + - Numerical quantity + - Timestamp, datetime + - Boolean (for True/False questions) + - Email address, phone number + - File ID, file name, file extension + - Multiple choice answer + - Answers must not require special formatting or complex, structured output + - Answer will be verified using DIRECT STRING COMPARISON + +### Readability + +2. **Answers should generally prefer HUMAN-READABLE formats** + - Examples: names, first name, last name, datetime, file name, message string, URL, yes/no, true/false, a/b/c/d + - Rather than opaque IDs (though IDs are acceptable) + - The VAST MAJORITY of answers should be human-readable + +### Stability + +3. **Answers must be STABLE/STATIONARY** + - Look at old content (e.g., conversations that have ended, projects that have launched, questions answered) + - Create QUESTIONS based on "closed" concepts that will always return the same answer + - Questions may ask to consider a fixed time window to insulate from non-stationary answers + - Rely on context UNLIKELY to change + - Example: if finding a paper name, be SPECIFIC enough so answer is not confused with papers published later + +4. **Answers must be CLEAR and UNAMBIGUOUS** + - Questions must be designed so there is a single, clear answer + - Answer can be derived from using the MCP server tools + +### Diversity + +5. **Answers must be DIVERSE** + - Answer should be a single VERIFIABLE value in diverse modalities and formats + - User concept: user ID, user name, display name, first name, last name, email address, phone number + - Channel concept: channel ID, channel name, channel topic + - Message concept: message ID, message string, timestamp, month, day, year + +6. **Answers must NOT be complex structures** + - Not a list of values + - Not a complex object + - Not a list of IDs or strings + - Not natural language text + - UNLESS the answer can be straightforwardly verified using DIRECT STRING COMPARISON + - And can be realistically reproduced + - It should be unlikely that an LLM would return the same list in any other order or format + +## Evaluation Process + +### Step 1: Documentation Inspection + +Read the documentation of the target API to understand: +- Available endpoints and functionality +- If ambiguity exists, fetch additional information from the web +- Parallelize this step AS MUCH AS POSSIBLE +- Ensure each subagent is ONLY examining documentation from the file system or on the web + +### Step 2: Tool Inspection + +List the tools available in the MCP server: +- Inspect the MCP server directly +- Understand input/output schemas, docstrings, and descriptions +- WITHOUT calling the tools themselves at this stage + +### Step 3: Developing Understanding + +Repeat steps 1 & 2 until you have a good understanding: +- Iterate multiple times +- Think about the kinds of tasks you want to create +- Refine your understanding +- At NO stage should you READ the code of the MCP server implementation itself +- Use your intuition and understanding to create reasonable, realistic, but VERY challenging tasks + +### Step 4: Read-Only Content Inspection + +After understanding the API and tools, USE the MCP server tools: +- Inspect content using READ-ONLY and NON-DESTRUCTIVE operations ONLY +- Goal: identify specific content (e.g., users, channels, messages, projects, tasks) for creating realistic questions +- Should NOT call any tools that modify state +- Will NOT read the code of the MCP server implementation itself +- Parallelize this step with individual sub-agents pursuing independent explorations +- Ensure each subagent is only performing READ-ONLY, NON-DESTRUCTIVE, and IDEMPOTENT operations +- BE CAREFUL: SOME TOOLS may return LOTS OF DATA which would cause you to run out of CONTEXT +- Make INCREMENTAL, SMALL, AND TARGETED tool calls for exploration +- In all tool call requests, use the `limit` parameter to limit results (<10) +- Use pagination + +### Step 5: Task Generation + +After inspecting the content, create 10 human-readable questions: +- An LLM should be able to answer these with the MCP server +- Follow all question and answer guidelines above + +## Output Format + +Each QA pair consists of a question and an answer. The output should be an XML file with this structure: + +```xml + + + Find the project created in Q2 2024 with the highest number of completed tasks. What is the project name? + Website Redesign + + + Search for issues labeled as "bug" that were closed in March 2024. Which user closed the most issues? Provide their username. + sarah_dev + + + Look for pull requests that modified files in the /api directory and were merged between January 1 and January 31, 2024. How many different contributors worked on these PRs? + 7 + + + Find the repository with the most stars that was created before 2023. What is the repository name? + data-pipeline + + +``` + +## Evaluation Examples + +### Good Questions + +**Example 1: Multi-hop question requiring deep exploration (GitHub MCP)** +```xml + + Find the repository that was archived in Q3 2023 and had previously been the most forked project in the organization. What was the primary programming language used in that repository? + Python + +``` + +This question is good because: +- Requires multiple searches to find archived repositories +- Needs to identify which had the most forks before archival +- Requires examining repository details for the language +- Answer is a simple, verifiable value +- Based on historical (closed) data that won't change + +**Example 2: Requires understanding context without keyword matching (Project Management MCP)** +```xml + + Locate the initiative focused on improving customer onboarding that was completed in late 2023. The project lead created a retrospective document after completion. What was the lead's role title at that time? + Product Manager + +``` + +This question is good because: +- Doesn't use specific project name ("initiative focused on improving customer onboarding") +- Requires finding completed projects from specific timeframe +- Needs to identify the project lead and their role +- Requires understanding context from retrospective documents +- Answer is human-readable and stable +- Based on completed work (won't change) + +**Example 3: Complex aggregation requiring multiple steps (Issue Tracker MCP)** +```xml + + Among all bugs reported in January 2024 that were marked as critical priority, which assignee resolved the highest percentage of their assigned bugs within 48 hours? Provide the assignee's username. + alex_eng + +``` + +This question is good because: +- Requires filtering bugs by date, priority, and status +- Needs to group by assignee and calculate resolution rates +- Requires understanding timestamps to determine 48-hour windows +- Tests pagination (potentially many bugs to process) +- Answer is a single username +- Based on historical data from specific time period + +**Example 4: Requires synthesis across multiple data types (CRM MCP)** +```xml + + Find the account that upgraded from the Starter to Enterprise plan in Q4 2023 and had the highest annual contract value. What industry does this account operate in? + Healthcare + +``` + +This question is good because: +- Requires understanding subscription tier changes +- Needs to identify upgrade events in specific timeframe +- Requires comparing contract values +- Must access account industry information +- Answer is simple and verifiable +- Based on completed historical transactions + +### Poor Questions + +**Example 1: Answer changes over time** +```xml + + How many open issues are currently assigned to the engineering team? + 47 + +``` + +This question is poor because: +- The answer will change as issues are created, closed, or reassigned +- Not based on stable/stationary data +- Relies on "current state" which is dynamic + +**Example 2: Too easy with keyword search** +```xml + + Find the pull request with title "Add authentication feature" and tell me who created it. + developer123 + +``` + +This question is poor because: +- Can be solved with a straightforward keyword search for exact title +- Doesn't require deep exploration or understanding +- No synthesis or analysis needed + +**Example 3: Ambiguous answer format** +```xml + + List all the repositories that have Python as their primary language. + repo1, repo2, repo3, data-pipeline, ml-tools + +``` + +This question is poor because: +- Answer is a list that could be returned in any order +- Difficult to verify with direct string comparison +- LLM might format differently (JSON array, comma-separated, newline-separated) +- Better to ask for a specific aggregate (count) or superlative (most stars) + +## Verification Process + +After creating evaluations: + +1. **Examine the XML file** to understand the schema +2. **Load each task instruction** and in parallel using the MCP server and tools, identify the correct answer by attempting to solve the task YOURSELF +3. **Flag any operations** that require WRITE or DESTRUCTIVE operations +4. **Accumulate all CORRECT answers** and replace any incorrect answers in the document +5. **Remove any ``** that require WRITE or DESTRUCTIVE operations + +Remember to parallelize solving tasks to avoid running out of context, then accumulate all answers and make changes to the file at the end. + +## Tips for Creating Quality Evaluations + +1. **Think Hard and Plan Ahead** before generating tasks +2. **Parallelize Where Opportunity Arises** to speed up the process and manage context +3. **Focus on Realistic Use Cases** that humans would actually want to accomplish +4. **Create Challenging Questions** that test the limits of the MCP server's capabilities +5. **Ensure Stability** by using historical data and closed concepts +6. **Verify Answers** by solving the questions yourself using the MCP server tools +7. **Iterate and Refine** based on what you learn during the process + +--- + +# Running Evaluations + +After creating your evaluation file, you can use the provided evaluation harness to test your MCP server. + +## Setup + +1. **Install Dependencies** + + ```bash + pip install -r scripts/requirements.txt + ``` + + Or install manually: + ```bash + pip install anthropic mcp + ``` + +2. **Set API Key** + + ```bash + export ANTHROPIC_API_KEY=your_api_key_here + ``` + +## Evaluation File Format + +Evaluation files use XML format with `` elements: + +```xml + + + Find the project created in Q2 2024 with the highest number of completed tasks. What is the project name? + Website Redesign + + + Search for issues labeled as "bug" that were closed in March 2024. Which user closed the most issues? Provide their username. + sarah_dev + + +``` + +## Running Evaluations + +The evaluation script (`scripts/evaluation.py`) supports three transport types: + +**Important:** +- **stdio transport**: The evaluation script automatically launches and manages the MCP server process for you. Do not run the server manually. +- **sse/http transports**: You must start the MCP server separately before running the evaluation. The script connects to the already-running server at the specified URL. + +### 1. Local STDIO Server + +For locally-run MCP servers (script launches the server automatically): + +```bash +python scripts/evaluation.py \ + -t stdio \ + -c python \ + -a my_mcp_server.py \ + evaluation.xml +``` + +With environment variables: +```bash +python scripts/evaluation.py \ + -t stdio \ + -c python \ + -a my_mcp_server.py \ + -e API_KEY=abc123 \ + -e DEBUG=true \ + evaluation.xml +``` + +### 2. Server-Sent Events (SSE) + +For SSE-based MCP servers (you must start the server first): + +```bash +python scripts/evaluation.py \ + -t sse \ + -u https://example.com/mcp \ + -H "Authorization: Bearer token123" \ + -H "X-Custom-Header: value" \ + evaluation.xml +``` + +### 3. HTTP (Streamable HTTP) + +For HTTP-based MCP servers (you must start the server first): + +```bash +python scripts/evaluation.py \ + -t http \ + -u https://example.com/mcp \ + -H "Authorization: Bearer token123" \ + evaluation.xml +``` + +## Command-Line Options + +``` +usage: evaluation.py [-h] [-t {stdio,sse,http}] [-m MODEL] [-c COMMAND] + [-a ARGS [ARGS ...]] [-e ENV [ENV ...]] [-u URL] + [-H HEADERS [HEADERS ...]] [-o OUTPUT] + eval_file + +positional arguments: + eval_file Path to evaluation XML file + +optional arguments: + -h, --help Show help message + -t, --transport Transport type: stdio, sse, or http (default: stdio) + -m, --model Claude model to use (default: claude-3-7-sonnet-20250219) + -o, --output Output file for report (default: print to stdout) + +stdio options: + -c, --command Command to run MCP server (e.g., python, node) + -a, --args Arguments for the command (e.g., server.py) + -e, --env Environment variables in KEY=VALUE format + +sse/http options: + -u, --url MCP server URL + -H, --header HTTP headers in 'Key: Value' format +``` + +## Output + +The evaluation script generates a detailed report including: + +- **Summary Statistics**: + - Accuracy (correct/total) + - Average task duration + - Average tool calls per task + - Total tool calls + +- **Per-Task Results**: + - Prompt and expected response + - Actual response from the agent + - Whether the answer was correct (✅/❌) + - Duration and tool call details + - Agent's summary of its approach + - Agent's feedback on the tools + +### Save Report to File + +```bash +python scripts/evaluation.py \ + -t stdio \ + -c python \ + -a my_server.py \ + -o evaluation_report.md \ + evaluation.xml +``` + +## Complete Example Workflow + +Here's a complete example of creating and running an evaluation: + +1. **Create your evaluation file** (`my_evaluation.xml`): + +```xml + + + Find the user who created the most issues in January 2024. What is their username? + alice_developer + + + Among all pull requests merged in Q1 2024, which repository had the highest number? Provide the repository name. + backend-api + + + Find the project that was completed in December 2023 and had the longest duration from start to finish. How many days did it take? + 127 + + +``` + +2. **Install dependencies**: + +```bash +pip install -r scripts/requirements.txt +export ANTHROPIC_API_KEY=your_api_key +``` + +3. **Run evaluation**: + +```bash +python scripts/evaluation.py \ + -t stdio \ + -c python \ + -a github_mcp_server.py \ + -e GITHUB_TOKEN=ghp_xxx \ + -o github_eval_report.md \ + my_evaluation.xml +``` + +4. **Review the report** in `github_eval_report.md` to: + - See which questions passed/failed + - Read the agent's feedback on your tools + - Identify areas for improvement + - Iterate on your MCP server design + +## Troubleshooting + +### Connection Errors + +If you get connection errors: +- **STDIO**: Verify the command and arguments are correct +- **SSE/HTTP**: Check the URL is accessible and headers are correct +- Ensure any required API keys are set in environment variables or headers + +### Low Accuracy + +If many evaluations fail: +- Review the agent's feedback for each task +- Check if tool descriptions are clear and comprehensive +- Verify input parameters are well-documented +- Consider whether tools return too much or too little data +- Ensure error messages are actionable + +### Timeout Issues + +If tasks are timing out: +- Use a more capable model (e.g., `claude-3-7-sonnet-20250219`) +- Check if tools are returning too much data +- Verify pagination is working correctly +- Consider simplifying complex questions \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..a351ee1bf --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +.git +.gradle +.idea +/build/ +target/ +*/target/ +bin/ +*/bin/ +out/ +*/out/ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..3ef0642dc --- /dev/null +++ b/.gitattributes @@ -0,0 +1,25 @@ +# 湲곕낯媛? 紐⑤뱺 ?띿뒪???뚯씪 CRLF (Windows 媛쒕컻 ?섍꼍) +* text=auto eol=crlf + +# Shell ?ㅽ겕由쏀듃??LF 怨좎젙 (Linux CI/CD ?ㅽ뻾 ?섍꼍) +*.sh text eol=lf +mvnw text eol=lf +gradlew text eol=lf + +# 諛붿씠?덈━ ?뚯씪 (以꾨컮轅?蹂€???쒖쇅) +*.jar binary +*.war binary +*.ear binary +*.class binary +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.svg binary +*.ttf binary +*.woff binary +*.woff2binary +*.pdf binary +*.zip binary +*.tar.gz binary diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 000000000..42a363a05 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,67 @@ +name: Deploy to OCIWP + +on: + push: + branches: + - main + - master + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + + - name: Prepare Environment (Install Node.js & Git & Java 21) + run: | + if command -v apt-get &> /dev/null; then + apt-get update + apt-get install -y nodejs git openjdk-21-jdk rsync + elif command -v apk &> /dev/null; then + apk add --no-cache nodejs git openjdk21 rsync + fi + + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Sync Code to Host Volume + run: | + echo "Copying latest code to /app (Host Volume) and removing stale files..." + if command -v rsync &> /dev/null; then + rsync -a --delete --exclude='.git' . /app/ + else + rm -rf /app/dap-* /app/src /app/build.gradle /app/settings.gradle + cp -a . /app/ + fi + + - name: Deploy Task on Host + run: | + echo "Starting Standard CI/CD Deploy pipeline..." + + # 1. runner에 설치된 java로 직접 빌드! (격리된 환경 문제 해결) + cd /app + chmod +x gradlew + ./gradlew bootJar -x test + + # 2. 빌드 결과 plain jar 아카이브 정리 + find /app -name "*-plain.jar" -delete + + # 3. 좀비 컨테이너 청소 (현재 우리가 쓸 포트를 점유 중인 옛날 컨테이너들만 정확히 색출해서 사살) + echo "Finding and killing zombie containers holding our ports..." + for port in 6379 8080 8089 8281 8282 8283 8284 8285 8288; do + cid=$(docker ps -q --filter "publish=$port") + if [ ! -z "$cid" ]; then + echo "Force killing container $cid using port $port" + docker rm -f $cid + fi + done + + # 4. 마운트된 /app 디렉토리로 이동하여 호스트의 도커 컴포즈 제어! + cd /app + docker system prune -f + ACTIVE_PROFILE=dev docker compose up -d --build --remove-orphans gateway redis mci-mock dozzle tool-sms tool-oth + + # 5. 배포 후 대롱대롱 매달려 있는 가비지 이미지 자동 소거 청소! + docker image prune -f + + echo "CI/CD Deploy Success!" diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..6f25a3424 --- /dev/null +++ b/.gitignore @@ -0,0 +1,124 @@ +# ===== 鍮뚮뱶 寃곌낵臾?===== +target/ +!**/src/main/**/target/ +!**/src/test/**/target/ +*.class +*.jar +!gradle/wrapper/gradle-wrapper.jar +*.war +*.ear +*.nar + +# ===== 濡쒓렇 ===== +*.log +logs/ +spring-shell.log + +# ===== Maven ===== +.mvn/wrapper/maven-wrapper.jar +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties + +# ===== IntelliJ IDEA ===== +.idea/ +*.iws +*.iml +*.ipr +out/ + +# ===== Eclipse / STS ===== +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ + +# ===== NetBeans ===== +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +.gradle/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +# ===== VS Code ===== +.vscode/ + +# ===== 鍮뚮뱶 寃곌낵臾?===== +target/ +!**/src/main/**/target/ +!**/src/test/**/target/ +*.class +*.jar +!gradle/wrapper/gradle-wrapper.jar +*.war +*.ear +*.nar + +# ===== 濡쒓렇 ===== +*.log +logs/ +spring-shell.log + +# ===== Maven ===== +.mvn/wrapper/maven-wrapper.jar +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties + +# ===== IntelliJ IDEA ===== +.idea/ +*.iws +*.iml +*.ipr +out/ + +# ===== Eclipse / STS ===== +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ + +# ===== NetBeans ===== +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +.gradle/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +# ===== VS Code ===== +.vscode/ + +# ===== OS ===== +.DS_Store +Thumbs.db +ehthumbs.db + +# ===== ?섍꼍?ㅼ젙 (誘쇨컧?뺣낫 遺꾨━ ?? ===== +# application-local.yml +# application-secret.yml diff --git a/AddJavadoc.java b/AddJavadoc.java new file mode 100644 index 000000000..d97be0009 --- /dev/null +++ b/AddJavadoc.java @@ -0,0 +1,103 @@ +import java.io.*; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.*; +import java.util.regex.*; + +public class AddJavadoc { + + static final String ROOT_DIR = "."; + + static final String TEMPLATE = + "/**\n" + + " * @package %s\n" + + " * @className %s\n" + + " * @description AX HUB ?쒖뒪??泥섎━ ?대옒??n" + + " * @author 源€?뺤떇\n" + + " * @create 2026.09.01\n" + + " *
\n" +
+        " * ---------- 媛쒖젙?대젰 ----------\n" +
+        " * ?섏젙??     ?섏젙??   ?섏젙?댁슜\n" +
+        " * ---------- -------- ---------------------------\n" +
+        " * 2026.09.01  源€?뺤떇    理쒖큹?앹꽦\n" +
+        " * \n" +
+        " * 
\n" + + " */"; + + public static void main(String[] args) throws Exception { + Files.walkFileTree(Paths.get(ROOT_DIR), new SimpleFileVisitor() { + @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 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); + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..07ff5ffa0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +# 1. ??���???�꼍 (JDK 21) +FROM eclipse-temurin:21-jdk-alpine AS builder +WORKDIR /app + +# Gradle ??�띁?? ???�� 蹂듭�? +COPY gradlew . +COPY gradle gradle +COPY build.gradle settings.gradle ./ +COPY src src + +# 沅뚰�??�€??�???���?(???��????�쇅) +RUN chmod +x gradlew +RUN ./gradlew clean build -x test + +# 2. ??�뻾 ??�꼍 (JRE 21) +FROM eclipse-temurin:21-jre-alpine +WORKDIR /app + +# ???꾩�???�젙 (??�뎅 ??�컙) +RUN apk add --no-cache tzdata +ENV TZ=Asia/Seoul + +# ??���??JAR ???�� 蹂듭�? +COPY --from=builder /app/build/libs/*.jar app.jar + +# 湲곕???????몄텧 +EXPOSE 8081 + +# ?�⑦???��???�뻾 ??JAR ??�뻾 +ENTRYPOINT ["java", "-jar", "app.jar"] + diff --git a/HELP.md b/HELP.md new file mode 100644 index 000000000..52734b7ba --- /dev/null +++ b/HELP.md @@ -0,0 +1,31 @@ +# Read Me First +The following was discovered as part of building this project: + +* The original package name 'io.shinhanlife.chat-backend' is invalid and this project uses 'io.shinhanlife.chatbackend' instead. + +# Getting Started + +### Reference Documentation +For further reference, please consider the following sections: + +* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html) +* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/3.5.3/maven-plugin) +* [Create an OCI image](https://docs.spring.io/spring-boot/3.5.3/maven-plugin/build-image.html) +* [Spring Web](https://docs.spring.io/spring-boot/3.5.3/reference/web/servlet.html) +* [Spring Session for Spring Data Redis](https://docs.spring.io/spring-session/reference/) +* [Spring Data JPA](https://docs.spring.io/spring-boot/3.5.3/reference/data/sql.html#data.sql.jpa-and-spring-data) + +### Guides +The following guides illustrate how to use some features concretely: + +* [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/) +* [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/) +* [Building REST services with Spring](https://spring.io/guides/tutorials/rest/) +* [Accessing Data with JPA](https://spring.io/guides/gs/accessing-data-jpa/) + +### Maven Parent overrides + +Due to Maven's design, elements are inherited from the parent POM to the project POM. +While most of the inheritance is fine, it also inherits unwanted elements like `` and `` from the parent. +To prevent this, the project POM contains empty overrides for these elements. +If you manually switch to a different parent and actually want the inheritance, you need to remove those overrides. diff --git a/McpBridge.java b/McpBridge.java new file mode 100644 index 000000000..165421592 --- /dev/null +++ b/McpBridge.java @@ -0,0 +1,57 @@ +/** + * @package io.shinhanlife + * @className McpBridge + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +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 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(); + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 000000000..650cddc66 --- /dev/null +++ b/README.md @@ -0,0 +1,606 @@ +# AX HUB MCP Tool + +신한라이프 업무 시스템과 AI Agent를 연결하는 MCP(Model Context Protocol) Gateway 및 Tool 서버 프로젝트입니다. + +Agent는 Gateway에서 Tool 목록과 입력 명세를 받고, Gateway는 권한과 정책을 확인한 뒤 Tool 서버로 요청을 전달합니다. 업무 Tool은 `DTO → UseCase → Converter → MCI/EAI Client` 구조로 레거시 시스템을 호출합니다. + +## 전체 흐름 + +```text +AI Agent / MCP Client + │ + ▼ +MCP Gateway (dap-gateway) + ├─ Tool 목록·스키마 제공 + ├─ Tool 권한·승인·가드레일 확인 + ├─ Redis Registry 및 실행 추적 + └─ 대상 Tool 서버로 라우팅 + │ + ▼ +Tool Server (dap-tool-sms / dap-tool-oth) + └─ BusinessToolController + │ + ▼ +UseCase → Converter → MCI/EAI Client → 레거시 시스템 +``` + +## 현재 구조와 목표 구조 + +현재는 Gateway와 두 개의 Tool 애플리케이션으로 구성됩니다. + +```text +현재: Gateway + SMS Tool Pod + OTH Tool Pod +목표: Gateway + 고객 Pod + 영업 Pod + 지급/납입 Pod + 알림 Pod + 인사 Pod + 공통 Pod +``` + +`dap-tool-oth`에는 여러 업무 카테고리가 함께 있습니다. AA 협의 후에는 부서·업무 소유권 단위로 Tool 서버, 이미지, Pod, 배포 파이프라인을 분리합니다. 이 목표 구조는 향후 전환 방향이며 현재 구현 완료 상태가 아닙니다. + +## Gradle 멀티모듈 + +| 모듈 | 역할 | 실행 포트 | +|---|---|---:| +| `dap-gateway` | MCP 진입점, Tool Registry, 라우팅, 권한·가드레일, Chat API | 8081 | +| `dap-tool-core` | 공통 어노테이션, Controller, JSON Schema, MCI/EAI 지원, 보안·로깅 공통 기능 | 라이브러리 | +| `dap-tool-sms` | SMS/알림 Tool 서버 | 8082 | +| `dap-tool-oth` | 공통·업무·샘플·MCI Tool 서버 | 8084 | + +기술 기준은 Java 21, Spring Boot 4, Gradle, Spring AI MCP Server, Redis, MapStruct, MyBatis, Resilience4j입니다. + +## 환경 (Environment) + +| 항목 | 버전 / 기준 | +|---|---| +| Java | 21 | +| Spring Boot | 4.0.5 | +| Gradle Wrapper | 8.14.3 | +| Spring AI BOM | 2.0.0 | +| Spring AI MCP Server | `spring-ai-starter-mcp-server-webmvc` | +| Redis Client | Lettuce 6.6.0.RELEASE | +| Resilience | Resilience4j 2.2.0 | +| MyBatis Spring Boot Starter | 3.0.3 | +| MapStruct | 1.5.5.Final | +| Lombok | 1.18.32 | +| JSON Schema Validator | networknt 1.4.0 | +| OpenAPI UI | springdoc 2.5.0 | +| 컨테이너 실행 | Docker Compose | + +프로젝트는 JDK 21을 기준으로 컴파일됩니다. IntelliJ에서는 Project SDK, Gradle JVM, Run Configuration JRE를 모두 JDK 21로 맞춰야 합니다. +## 5분 빠른 시작 + +Docker와 JDK 21이 준비된 로컬 개발 환경 기준입니다. + +```powershell +# 1. Redis와 MCI Mock만 먼저 실행 +$env:ACTIVE_PROFILE = 'local' +docker compose up -d redis mci-mock + +# 2. Gateway 실행 (새 PowerShell) +$env:SPRING_PROFILES_ACTIVE = 'local' +$env:OPENROUTER_API_KEY = '<개발용 비밀 저장소의 키>' +.\gradlew.bat :dap-gateway:bootRun + +# 3. OTH Tool 실행 (또 다른 PowerShell) +$env:SPRING_PROFILES_ACTIVE = 'local' +$env:AXHUB_GATEWAY_URL = 'http://localhost:8081' +$env:AXHUB_TOOL_URL = 'http://localhost:8084' +.\gradlew.bat :dap-tool-oth:bootRun +``` + +Tool 서버가 기동된 뒤 아래 URL로 등록된 Tool 목록을 확인합니다. + +```text +http://localhost:8081/mcp/api/v1/tools/list +``` + +SMS Tool도 함께 확인하려면 별도 PowerShell에서 아래 명령을 실행합니다. + +```powershell +$env:SPRING_PROFILES_ACTIVE = 'local' +$env:AXHUB_GATEWAY_URL = 'http://localhost:8081' +$env:AXHUB_TOOL_URL = 'http://localhost:8082' +.\gradlew.bat :dap-tool-sms:bootRun +``` + +전체 컨테이너 환경이 필요하면 개별 실행 대신 다음 한 줄을 사용합니다. + +```powershell +$env:ACTIVE_PROFILE = 'local' +docker compose up -d --build +``` +## Tool 등록과 실행 + +### 등록과 목록 제공 + +1. Tool 서버 기동 시 `ToolRegistryHeartbeatSender`가 `@McpTool`, `@McpFunction`을 스캔합니다. +2. Tool 이름, 설명, 입력 JSON Schema, `categoryKey`, 연동 방식, 실행 URL을 메타데이터로 생성합니다. +3. Gateway Redis Registry에 등록·Heartbeat 정보를 전송합니다. +4. Agent와 관리 화면은 Gateway에서 Tool 목록과 명세를 조회합니다. + +### 실행 + +1. Agent가 Gateway에 Tool 이름과 입력값을 보냅니다. +2. Gateway가 Tool 존재 여부, 허용 Tool, 쓰기 승인, 가드레일을 확인합니다. +3. Gateway가 Tool 서버의 `/mcp/{toolName}`으로 요청을 전달합니다. +4. `BusinessToolController`가 Tool 메서드를 찾아 DTO로 변환하고 JSON Schema를 검증합니다. +5. UseCase가 업무 흐름을 수행합니다. +6. Converter가 업무 DTO를 인터페이스 ID 기반 MCI 요청 DTO로 변환합니다. +7. MCI/EAI Client가 레거시를 호출하고 결과를 Tool 응답으로 반환합니다. + +## 주요 URL + +로컬에서 Gateway를 직접 실행할 때의 기준입니다. Docker Compose를 사용하면 Gateway 호스트 포트는 `8281`입니다. + +| 용도 | 메서드 | URL | +|---|---|---| +| Tool 목록 | `GET` | `http://localhost:8081/mcp/api/v1/tools/list` | +| Tool 실행 | `POST` | `http://localhost:8081/mcp/api/v1/tools/call` | +| Tool Markdown 문서 | `GET` | `http://localhost:8081/mcp/api/v1/tools/docs/markdown` | +| 카테고리별 SSE MCP 연결 | `GET` | `http://localhost:8081/mcp/sse/{categoryKey}` | +| 카테고리별 MCP 호출 채널 | `POST` | `http://localhost:8081/mcp/custom/{categoryKey}` | +| Chat 스트리밍 API | `POST` | `http://localhost:8081/api/chat` | +| Scaffold API | `POST` | `http://localhost:8081/api/v1/scaffold/pod` 또는 `/tool` | + +`/mcp/sse/{categoryKey}`는 SSE 연결을 여는 전송 경로이고, `/mcp/custom/{categoryKey}`는 같은 카테고리의 MCP 요청을 처리하는 호출 경로입니다. 두 URL은 역할이 다릅니다. + +## Tool 개발 표준 + +| 구성 요소 | 책임 | +|---|---| +| `XxxRequest`, `XxxResponse` | Agent/Tool 관점의 입력·응답 DTO | +| `XxxUseCase` | Tool 계약과 MCP 메타데이터 선언 | +| `XxxUseCaseImpl` | 업무 흐름 조합과 Client 호출 | +| `XxxConverter` | 업무 DTO와 레거시 인터페이스 DTO 사이 변환 | +| `MciXxxClient` | Glow/MCI 또는 EAI 통신 호출 | +| `INTERFACE_ID_I`, `INTERFACE_ID_O` | 인터페이스 ID 기준 MCI 요청·응답 DTO | + +### Tool 선언 + +Tool 그룹에는 `@McpTool`, Agent가 호출하는 함수에는 `@McpFunction`을 사용합니다. + +```java +@McpTool(routingType = "MCI", categoryKey = "claim") +public interface ClaimInquiryUseCase { + + @McpFunction( + name = "claim_inquiry", + displayName = "보험금 청구 조회", + description = "청구 번호로 보험금 청구 상태를 조회합니다.", + mappingId = "CLM00000001" + ) + ClaimInquiryResponse inquire(ClaimInquiryRequest request); +} +``` + +`categoryKey`는 Tool의 업무 그룹입니다. Gateway의 목록 필터링, Agent 권한, 동적 MCP 서버 구분에 사용하므로 합의된 업무 키를 사용합니다. + +- `MCI`: 사내 MCI 인터페이스 호출 +- `EAI`: EAI 연동 +- `DIRECT`: 외부 HTTP 또는 내부 직접 연동 + +### 변환 원칙 + +UseCase 구현체는 Tool 요청을 레거시 요청과 섞어 쓰지 않습니다. Converter에서 변환한 뒤 Client에 전달합니다. + +```java +@Override +public ClaimInquiryResponse inquire(ClaimInquiryRequest request) { + CLM00000001_I mciRequest = converter.toMciRequest(request); + CLM00000001_O mciResponse = mciClmClient.callClm00000001(mciRequest); + return converter.toResponse(mciResponse); +} +``` + +MCI 입출력 객체는 업무 이름이 아니라 인터페이스 ID를 기준으로 둡니다. + +```text +CLCNNB00001_I : CLCNNB00001 요청 DTO +CLCNNB00001_O : CLCNNB00001 응답 DTO +``` + +예를 들어 `Onnba3011Request`와 `CLCNNB00001_I`는 같은 업무 데이터를 담을 수 있지만 같은 객체가 아닙니다. 둘 사이의 변환 책임은 `Onnba3011Converter`에 둡니다. + +### 새 Tool 추가 체크리스트 + +새 업무 Tool을 추가할 때는 아래 순서로 확인합니다. + +- [ ] 소속 모듈과 `categoryKey`를 업무 소유 조직 기준으로 결정한다. +- [ ] Agent 입력·응답 DTO인 `XxxRequest`, `XxxResponse`를 만든다. +- [ ] `XxxUseCase`에 `@McpTool`을 선언하고, 호출 메서드에 `@McpFunction`의 이름·설명·연동 ID를 선언한다. +- [ ] `XxxUseCaseImpl`에서 업무 흐름만 조합한다. +- [ ] `XxxConverter`에 업무 DTO ↔ 인터페이스 ID DTO 변환을 둔다. +- [ ] `MciXxxClient`와 `INTERFACE_ID_I`, `INTERFACE_ID_O`를 인터페이스 ID 기준으로 만든다. +- [ ] DTO에 Bean Validation을 선언하고, 중첩 DTO가 있으면 입력 Schema와 검증 대상에 포함되는지 확인한다. +- [ ] 조회·변경 작업 특성에 따라 `readOnlyHint`, `requiresApproval`, `idempotentHint`를 설정한다. +- [ ] 단위 테스트를 작성하고 `:dap-tool-core:test` 또는 대상 모듈 테스트를 실행한다. +- [ ] Tool 서버 기동 후 `/mcp/api/v1/tools/list`에서 이름, 설명, category, JSON Schema가 맞는지 확인한다. +- [ ] 요청·응답 로그에 개인정보나 인증값이 남지 않는지 확인한다. +### 등록·노출 제어 + +| 속성 | 의미 | +|---|---| +| `register` | Redis Registry와 Gateway 카탈로그에 등록할지 여부 | +| `visible` | Agent/클라이언트 목록에 표시할지 여부 | +| `requiresApproval` | 쓰기·고위험 작업의 승인 요구 여부 | +| `readOnlyHint`, `destructiveHint`, `idempotentHint` | Agent 호출 특성 힌트 | + +`register = false`인 Tool은 자동 카탈로그 등록 대상이 아닙니다. 별도 실행 목적이 있는 경우에만 사용하고, 필요한 fallback 경로를 운영 설정으로 확인합니다. + +## 빌드·테스트·로컬 실행 + +### 사전 조건 + +- JDK 21 +- Gradle Wrapper 사용 권장 +- 로컬 Redis 또는 Docker Compose 환경 +- 필요 시 MCI Mock 또는 사내 MCI/EAI 접근 환경 + +### 전체 빌드와 대표 검증 + +```powershell +.\gradlew.bat clean build +.\gradlew.bat :dap-tool-core:test +.\gradlew.bat :dap-tool-core:compileJava +``` + +### 애플리케이션 실행 + +각 애플리케이션은 별도 PowerShell에서 실행합니다. + +```powershell +# Gateway +.\gradlew.bat :dap-gateway:bootRun + +# SMS Tool +.\gradlew.bat :dap-tool-sms:bootRun + +# 기타 업무 Tool +.\gradlew.bat :dap-tool-oth:bootRun +``` + +기본 프로필은 `local`입니다. 개발 서버 설정이 필요하면 실행 환경에 프로필을 지정합니다. + +```powershell +$env:SPRING_PROFILES_ACTIVE = 'dev' +.\gradlew.bat :dap-tool-oth:bootRun +``` + +## Docker Compose 실행 + +Docker Compose는 Redis, Gateway, MCI Mock, SMS Tool, OTH Tool을 함께 기동합니다. + +```powershell +$env:ACTIVE_PROFILE = 'local' +docker compose up -d --build +docker compose ps +``` + +| 서비스 | 컨테이너 포트 | 호스트 포트 | +|---|---:|---:| +| Redis | 6379 | 6379 | +| Gateway | 8081 | 8281 | +| MCI Mock | 8080 | 8089 | +| SMS Tool | 8082 | 8282 | +| OTH Tool | 8084 | 8284 | + +컨테이너 내부와 PC 브라우저의 접속 주소는 다릅니다. + +```text +컨테이너 내부: http://gateway:8081, http://tool-oth:8084 +PC 브라우저: http://localhost:8281, http://localhost:8284 +``` + +## 환경 설정 + +### 프로필 + +기본 프로필은 `local`입니다. Tool 서버와 Gateway 모두 `local`, `dev` 프로필 파일을 사용합니다. + +| 프로필 | 목적 | 주요 설정 파일 | +|---|---|---| +| `local` | PC 개발·MCI Mock·H2 메모리 DB 기반 실행 | `application-local.yml` | +| `dev` | 개발 서버 배포 실행 | `application-dev.yml` | + +프로필은 환경 변수로 지정합니다. + +```powershell +# 로컬 개발 +$env:SPRING_PROFILES_ACTIVE = 'local' + +# 개발 서버 설정으로 실행 +$env:SPRING_PROFILES_ACTIVE = 'dev' +``` + +### 환경별 연결 주소 비교 + +| 항목 | `local` 프로세스 실행 | Docker Compose | `dev` 프로필 | +|---|---|---|---| +| Gateway 접근 주소 | `http://localhost:8081` | 컨테이너 내부 `http://gateway:8081` | 배포 환경 Gateway URL | +| SMS Tool 주소 | `http://localhost:8082` | `http://tool-sms:8082` | `PORT` 기본 8082 | +| OTH Tool 주소 | `http://localhost:8084` | `http://tool-oth:8084` | `PORT` 기본 8084 | +| Redis 주소 | 로컬 Redis 또는 `localhost:6379` | `redis:6379` | 운영/개발 Redis 설정 | +| MCI/EAI 대상 | MCI Mock 또는 로컬 설정 | `mci-mock:8080` | 개발망 연동 설정 | +| 브라우저 Gateway 접속 | `http://localhost:8081` | `http://localhost:8281` | 운영·개발 도메인 | + +`local`에서 프로세스를 직접 실행하면 Tool의 `AXHUB_GATEWAY_URL`은 `localhost`를 사용합니다. Docker에서는 각 컨테이너가 서로 다른 네트워크 공간에 있으므로 반드시 Compose 서비스 이름을 사용합니다. +### Gateway 환경 변수 + +| 변수 | 적용 대상 | 설명 | 로컬 기본값/예시 | +|---|---|---|---| +| `SPRING_PROFILES_ACTIVE` | Gateway, 모든 Tool | 활성 Spring 프로필 | `local` | +| `OPENROUTER_API_KEY` | Gateway | Chat/LLM 호출 API 키 | 운영·개발 환경의 비밀 저장소에서 주입 | +| `SPRING_DATA_REDIS_HOST` | Gateway, 모든 Tool | Redis 호스트 | Docker: `redis` | +| `SPRING_DATA_REDIS_PORT` | Gateway, 모든 Tool | Redis 포트 | `6379` | +| `MCP_GATEWAY_FALLBACK_DEFAULT_URL` | Gateway | Registry에 없는 Tool의 기본 fallback URL | Docker: `http://tool-oth:8084` | +| `MCP_GATEWAY_FALLBACK_ROUTES_SMS` | Gateway | SMS 계열 fallback URL | Docker: `http://tool-sms:8082` | +| `ACTIVE_PROFILE` | Docker Compose | Compose가 `SPRING_PROFILES_ACTIVE`에 전달할 프로필 | `local` | + +`OPENROUTER_API_KEY` 같은 인증값은 `application.yml`, README, Git 커밋에 직접 넣지 않습니다. 개발·운영 환경의 Secret, CI/CD 변수 또는 안전한 환경 변수로 주입합니다. + +### Tool 서버 환경 변수 + +| 변수 | 적용 대상 | 설명 | 로컬 기본값/예시 | +|---|---|---|---| +| `PORT` | `dap-tool-sms`, `dap-tool-oth` | 개발 프로필에서 Tool 서버 포트 변경 | SMS `8082`, OTH `8084` | +| `AXHUB_GATEWAY_URL` | 모든 Tool | Tool 등록·Heartbeat 대상 Gateway 주소 | 로컬 `http://localhost:8081`, Docker `http://gateway:8081` | +| `AXHUB_TOOL_URL` | 모든 Tool | Gateway가 해당 Tool Pod를 호출할 주소 | 로컬 `http://localhost:{server.port}` | +| `GLOW_COMMUNICATION_MCI_HOMT` | Docker Tool 컨테이너 | MCI 대상 호스트 | 로컬 Compose는 `mci-mock` | +| `GLOW_COMMUNICATION_MCI_PORT` | Docker Tool 컨테이너 | MCI 대상 포트 | 로컬 Compose는 `8080` | +| `GLOW_COMMUNICATION_EAI_HOMT` | Docker Tool 컨테이너 | EAI 대상 호스트 | 로컬 Compose는 `mci-mock` | +| `GLOW_COMMUNICATION_EAI_PORT` | Docker Tool 컨테이너 | EAI 대상 포트 | 로컬 Compose는 `8080` | + +`AXHUB_TOOL_URL`은 반드시 Gateway가 실제로 접근 가능한 주소여야 합니다. PC에서 각각 실행할 때는 `localhost`를 사용하고, Docker 컨테이너 안에서는 `tool-sms`, `tool-oth` 같은 Compose 서비스 이름을 사용합니다. + +### 로컬 실행용 권장 설정 + +아래는 키 값 없이 로컬 프로세스를 실행하는 예시입니다. Redis를 Docker로 먼저 기동하거나 전체 Docker Compose를 사용합니다. + +```powershell +# 선택 1: Redis만 기동 +$env:ACTIVE_PROFILE = 'local' +docker compose up -d redis mci-mock + +# 선택 2: 각 프로세스를 로컬에서 기동 +$env:SPRING_PROFILES_ACTIVE = 'local' +$env:OPENROUTER_API_KEY = '<개인 또는 개발용 비밀 저장소의 키>' +.\gradlew.bat :dap-gateway:bootRun +``` + +다른 PowerShell에서 Tool을 실행합니다. + +```powershell +$env:SPRING_PROFILES_ACTIVE = 'local' +$env:AXHUB_GATEWAY_URL = 'http://localhost:8081' +$env:AXHUB_TOOL_URL = 'http://localhost:8084' +.\gradlew.bat :dap-tool-oth:bootRun +``` + +### 권한 도메인 설정 + +`mcp.security.tenant-domains`는 테넌트 또는 호출 주체가 접근할 수 있는 `categoryKey`를 정의합니다. 운영 환경에서는 `ALL`을 무분별하게 사용하지 않고, Agent·조직별 허용 도메인을 최소 권한으로 설정합니다. + +```yaml +mcp: + security: + tenant-domains: + claims-agent: claim, customer + notification-agent: notification +``` + +## 공통 기능과 운영 기준 + +### Redis + +Redis는 Tool Registry의 등록 상태와 Heartbeat, Gateway 실행 추적 정보에 사용됩니다. 현재 Redis는 PII 원문 보관소가 아닙니다. + +### 권한과 승인 + +Gateway는 Agent가 전달한 허용 Tool 목록, 서버 정책, 신뢰된 Claim 여부, 쓰기 승인 여부를 확인합니다. Tool마다 권한 로직을 중복 구현하지 말고 Gateway 공통 정책과 `@McpFunction` 메타데이터를 사용합니다. + +### Trace ID와 Request ID + +```text +trace-id : 사용자 요청 전체에서 유지되는 상관관계 ID +request-id : Gateway → Tool, Tool → MCI 등 HTTP 호출마다 새로 생성되는 ID +``` + +Gateway와 Tool에는 관련 헤더 및 MDC 기반 로그 처리가 있습니다. 신규 HTTP Client도 공통 전파 정책을 따르며, Tool별로 임의의 헤더 이름을 추가하지 않습니다. + +### 로그와 개인정보 + +Gateway에는 민감 키와 일부 형식을 마스킹하는 공통 기능이 있습니다. 마스킹은 원문을 Agent에서 분리하는 PII 토큰화와는 다릅니다. + +- 요청·응답 전문을 로그에 남길 때는 반드시 마스킹합니다. +- 운영 로그에 주민번호, 계좌번호, 전화번호, 이메일, 인증값을 남기지 않습니다. +- Tool 서버의 신규 로그도 같은 마스킹 정책을 적용합니다. +- 민감정보 복원 필요 여부는 Tool 개발자가 임의로 결정하지 않고 보안·AA 정책을 따릅니다. + +## AA 협의 기반 향후 전환 과제 + +아래 항목은 현재 구현 완료 기능이 아니라 회의에서 합의한 목표 구조입니다. + +### 부서별 Tool Pod와 저장소 경계 + +현재 `dap-tool-oth`에 함께 있는 업무 Tool을 부서·업무 소유권 단위로 분리합니다. 각 Pod는 독립 이미지, 독립 배포, 독립 장애 범위를 갖도록 구성합니다. 실제 분리는 AA가 확정한 Tool 소유 부서와 운영 책임 매핑을 기준으로 수행합니다. + +### Agent별 Tool 노출 수 제한 + +Agent에게 모든 Tool을 한 번에 제공하지 않고, 업무 도메인과 권한에 따라 약 10~20개 수준의 Tool 그룹을 제공합니다. `categoryKey`는 이를 위한 기초 메타데이터이며, 향후 Agent-Tool Group 정책으로 확장합니다. + +### PII 토큰화 + +```text +원문 개인정보 + → PII Gateway가 Redis에 짧은 TTL로 보관 + → Agent에는 PII 토큰 또는 안전한 식별자만 전달 + → 인가된 Tool이 MCI 호출 직전에 필요한 항목만 복원 +``` + +이 전환 전까지는 현행 마스킹 기능을 PII 분리 구현으로 오해하지 않아야 합니다. + +### MCP SDK와 Glow Framework + +Gateway에는 Spring AI MCP Server 의존성이 포함되어 있습니다. MCP SDK/Glow 표준 적용 시에는 업무 Tool을 재작성하지 않고, 기존 `@McpTool`·`@McpFunction`과 UseCase를 표준 MCP Tool 명세·호출 콜백으로 연결하는 Adapter 계층을 공통 Core에 추가합니다. + +```text +MCP SDK 표준 tools/list, tools/call + ↓ +공통 Adapter + ↓ +기존 UseCase → Converter → MCI Client +``` + +따라서 업무 DTO, Converter, MCI Client의 책임은 유지됩니다. + +## 참고 소스 + +| 주제 | 대표 위치 | +|---|---| +| Gateway Tool API | `dap-gateway/.../presentation/McpRouterController.java` | +| 동적 MCP SSE/호출 경로 | `dap-gateway/.../sync/DynamicMcpController.java` | +| Tool 실행 Controller | `dap-tool-core/.../presentation/BusinessToolController.java` | +| Tool 자동 등록 | `dap-tool-core/.../usecase/ToolRegistryHeartbeatSender.java` | +| Tool 어노테이션 | `dap-tool-core/.../annotation/McpTool.java`, `McpFunction.java` | +| Tool 예시 | `dap-tool-oth/.../biz/oth`, `biz/sol`, `biz/smp` | +| SMS Tool 예시 | `dap-tool-sms/.../biz/sms` | +| Docker 환경 | `docker-compose.yml` | + +--- + +문서에 없는 업무·보안·배포 기준은 임의로 추가하지 말고 AA 및 플랫폼 운영 기준과 먼저 합의합니다. +## Input/Output Schema 작성 가이드 + +Tool Schema는 Agent가 Tool을 정확히 호출하고, 반환값의 의미를 일관되게 해석하도록 하는 계약입니다. 인증 정보·사번·주민번호 등 민감정보(PII)는 Input/Output Schema와 Tool 응답에 포함하지 않습니다. + +### Input Schema + +Input Schema는 Agent가 Tool에 전달하는 파라미터의 이름, 타입, 필수 여부, 허용값, 형식 등을 정의합니다. + +적용 우선순위는 다음과 같습니다. + +1. `inputSchemaResource` — 복잡한 규칙을 담은 JSON Schema 리소스 +2. `inputSchema` — 어노테이션에 직접 선언한 JSON Schema +3. 요청 DTO 필드의 `@McpValidation` — 자동 JSON Schema 생성 + +단순한 요청 DTO는 `@McpValidation`만으로 관리합니다. + +```java +public class ClaimSearchRequest { + + @McpValidation(required = true, pattern = "^CLM[0-9]{13}$") + private String claimNo; + + @McpValidation(minimum = 1, maximum = 100) + private Integer size; +} +``` + +### Output Schema + +Output Schema는 Tool이 반환하는 결과의 타입과 의미를 정의합니다. `BusinessToolController`는 Tool 실행 후 반환값을 Output Schema 기준으로 검증합니다. + +적용 우선순위는 다음과 같습니다. + +1. `outputSchemaResource` — 조건부 필드·중첩 배열 등 복잡한 규칙을 담은 JSON Schema 리소스 +2. `outputSchema` — 어노테이션에 직접 선언한 JSON Schema +3. 반환 DTO의 `@McpOutputSchema`와 필드 `@McpValidation` — 자동 JSON Schema 생성 +4. 위 설정이 모두 없으면 Output Schema 검증을 수행하지 않음 + +따라서 단순한 응답은 별도 `outputSchemaResource` 없이 반환 DTO에 `@McpOutputSchema`를 선언하면 됩니다. `null`이 정상 값일 수 있는 필드는 `nullable = true`를 반드시 지정합니다. + +```java +@McpOutputSchema +public class ClaimSearchResponse { + + @McpValidation(required = true, allowedValues = {"SUCCESS", "FAILURE"}) + private String resultCode; + + @McpValidation(nullable = true, minimum = 0) + private Long approvedAmount; +} +``` + +### 복잡한 Schema는 Tool 모듈별 리소스로 관리 + +조건부 응답, 중첩 DTO, 배열 정렬 기준처럼 어노테이션만으로 표현하기 어려운 규칙은 Tool Core가 아니라 각 Tool 모듈의 리소스에 JSON Schema로 둡니다. + +```text +src/main/resources/ +└─ tool-schemas/ + └─ {categoryKey}/ + ├─ claim-search-resource-input-schema.json + └─ claim-search-resource-output-schema.json +``` + +예를 들어 `categoryKey`가 `cmm`이면 아래와 같이 선언합니다. + +```java +@McpFunction( + name = "oth.cmm.claim.search", + inputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-input-schema.json", + outputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-output-schema.json" +) +public ClaimSearchResponse search(ClaimSearchRequest request) { + // ... +} +``` + +`inputSchemaResource`와 `outputSchemaResource`는 복잡한 경우에만 선언합니다. 단순한 Tool까지 JSON 파일을 별도 생성할 필요는 없습니다. + +### Output 설계 규칙 + +- 코드와 표시용 라벨을 함께 반환합니다. 예: `status` + `statusLabel` +- `null`이 정상인 값은 의미를 설명에 명시하고 DTO에는 `nullable = true`를 설정합니다. +- 조건부 필드는 어떤 조건에서 값이 존재하는지 JSON Schema에 명시합니다. +- 배열은 정렬 기준을 설명에 명시합니다. 예: `접수일 내림차순` +- 목록 응답에는 추가 조회 여부를 나타내는 `hasMore`를 포함합니다. +- 민감정보는 마스킹보다 **응답에서 제외**하는 것을 우선합니다. + +### 실행 로그 및 확인 + +Tool 실행이 끝나면 아래 로그는 Schema 정의가 아니라 **검증을 통과한 실제 최종 응답값**을 출력합니다. + +```text +[Tool -> MCP Gateway] Output Schema Result: { ... } +``` + +따라서 로그에도 실제 응답이 남으므로, 응답 DTO와 Output Schema에 민감정보가 포함되지 않도록 설계해야 합니다. + +스키마 리소스와 DTO 기반 자동 Schema는 아래 테스트로 함께 검증할 수 있습니다. + +```powershell +.\gradlew.bat :dap-tool-oth:test --tests "io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequestSchemaTest" +``` + +### Tool Naming Convention + +All Tool names use the four-level lowercase format `pod.domain.service.action`. Do not use underscores or CamelCase; use a hyphen (`-`) only when a single level has multiple words. + +- `pod`: deployment Tool Pod/module (`dap-tool-oth` → `oth`, `dap-tool-sms` → `sms`) +- `domain`: business-domain package (`cmm`, `smp`, `sol`, etc.) +- `service`: business service or resource +- `action`: the requested operation (`search`, `list`, `detail`, `issue`, `inquiry`, etc.) + +```text +oth.cmm.bond.issue +oth.cmm.claim.search +oth.sol.request.list +oth.smp.weather.inquiry +``` + +When Scaffold receives `dap-tool-oth`, `cmm`, and `ClaimSearch`, it generates `oth.cmm.claim.search`. The `validateMcpToolNames` Gradle task rejects both a duplicate name and any name outside this format before packaging, including its source file and line number. + +### Tool Test Console + +각 Tool Pod는 공통 테스트 화면을 제공합니다. + +```text +http://localhost:8084/tool-test-console.html +``` + +화면은 현재 Pod의 `/tool-manifest`에서 Tool 목록과 `inputSchema`를 읽습니다. Tool을 선택한 뒤 `Schema 샘플 채우기`로 요청 JSON을 만들고 실행할 수 있습니다. 업무에 맞게 보정한 요청은 `현재 요청 저장`으로 브라우저의 `localStorage`에 보관합니다. + +`Run saved cases`는 저장된 테스트 케이스를 순차 실행해 성공/실패, HTTP 상태, 소요 시간을 보여줍니다. 따라서 Tool이 수백 개여도 각 Tool마다 테스트 화면을 만들 필요 없이, 유효한 업무 테스트 데이터만 한 번 저장하면 이후에는 몇 번의 클릭으로 회귀 테스트할 수 있습니다. + +- Tool 호출은 현재 Pod의 `/mcp/{toolName}`로 수행합니다. +- 매 실행마다 `trace-id`, `request-id`를 새로 생성하여 응답과 함께 표시합니다. +- 외부 MCI/EAI Tool은 샘플값 대신 개발계에서 허용된 테스트 데이터를 저장해서 사용해야 합니다. diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..4c0bcc3e6 --- /dev/null +++ b/build.gradle @@ -0,0 +1,68 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '4.0.5' apply false + id 'io.spring.dependency-management' version '1.1.6' apply false +} + +allprojects { + group = 'io.shinhanlife' + version = '0.0.1-SNAPSHOT' +} + +subprojects { + apply plugin: 'java' + apply plugin: 'io.spring.dependency-management' + + java { + sourceCompatibility = '21' + } + + repositories { + mavenCentral() + maven { url 'https://repo.spring.io/milestone' } + maven { url 'https://repo.spring.io/snapshot' } + } + + dependencyManagement { + imports { + mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES + mavenBom "org.springframework.ai:spring-ai-bom:2.0.0" + } + } + + dependencies { + compileOnly 'org.projectlombok:lombok:1.18.32' + annotationProcessor 'org.projectlombok:lombok:1.18.32' + annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0' + implementation 'org.mapstruct:mapstruct:1.5.5.Final' + annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + } + + tasks.withType(JavaCompile) { + options.compilerArgs << '-parameters' + options.compilerArgs << '-Amapstruct.defaultComponentModel=spring' + } + + tasks.withType(Test) { + useJUnitPlatform() + } +} + +def toolCoreProject = project(':dap-tool-core') + +tasks.register('validateMcpToolNames', JavaExec) { + group = 'verification' + description = 'Checks duplicate @McpFunction names across all Tool modules before packaging.' + dependsOn toolCoreProject.tasks.named('classes') + classpath = toolCoreProject.sourceSets.main.runtimeClasspath + mainClass.set('io.shinhanlife.dap.lib.validation.McpToolNameValidationRunner') + args rootProject.projectDir.absolutePath +} + +subprojects { + tasks.matching { it.name == 'bootJar' }.configureEach { + dependsOn rootProject.tasks.named('validateMcpToolNames') + } +} \ No newline at end of file diff --git a/dap-tool-core/build.gradle b/dap-tool-core/build.gradle new file mode 100644 index 000000000..2c803d59f --- /dev/null +++ b/dap-tool-core/build.gradle @@ -0,0 +1,40 @@ +plugins { + id 'java-library' +} + +dependencies { + api 'org.springframework.boot:spring-boot-starter-web' + api 'org.springframework.boot:spring-boot-starter-validation' + api 'org.springframework.boot:spring-boot-starter-data-redis' + + api 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0' + api 'io.github.resilience4j:resilience4j-core:2.2.0' + api 'io.github.resilience4j:resilience4j-circuitbreaker:2.2.0' + api 'io.github.resilience4j:resilience4j-ratelimiter' + api 'io.github.resilience4j:resilience4j-retry:2.2.0' + api 'org.springframework.boot:spring-boot-starter-aop:3.3.0' + + api 'org.springframework.boot:spring-boot-starter-jdbc' + api 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3' + api 'com.h2database:h2' + api 'p6spy:p6spy:3.9.1' + api 'io.lettuce:lettuce-core:6.6.0.RELEASE' + + api "org.mapstruct:mapstruct:1.5.5.Final" + + api 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.17.1' + api 'com.fasterxml.jackson.core:jackson-databind:2.17.1' + api 'com.networknt:json-schema-validator:3.0.0' + api 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc' + api 'org.springframework.kafka:spring-kafka:3.2.0' + api 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' +} + +dependencies { + compileOnly 'org.projectlombok:lombok:1.18.32' + annotationProcessor 'org.projectlombok:lombok:1.18.32' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testCompileOnly 'org.projectlombok:lombok:1.18.32' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.32' +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/aop/EimsMonitoringAspect.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/aop/EimsMonitoringAspect.java new file mode 100644 index 000000000..bc2ff8a74 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/aop/EimsMonitoringAspect.java @@ -0,0 +1,63 @@ +package io.shinhanlife.dap.lib.adapter.aop; + +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; + +import java.util.Arrays; + +/** + * @package io.shinhanlife.dap.lib.adapter.aop + * @className EimsMonitoringAspect + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +@Aspect +@Component +public class EimsMonitoringAspect { + + // 포인트컷: io.shinhanlife.dap.lib.adapter.sender 패키지 내의 EimsSender를 구현한 모든 클래스의 메서드를 타겟으로 지정합니다. + @Around("execution(* io.shinhanlife.dap.lib.adapter.sender.*EimsSender.*(..))") + public Object monitorEimsCommunication(ProceedingJoinPoint joinPoint) throws Throwable { + + // 1. 호출되는 클래스와 메서드 이름 추출 + String className = joinPoint.getTarget().getClass().getSimpleName(); + String methodName = joinPoint.getSignature().getName(); + Object[] args = joinPoint.getArgs(); // 파라미터 + + long startTime = System.currentTimeMillis(); + + // 2. [요청 로깅] EIMS망으로 요청이 나가기 직전 + log.info(" [EIMS 요청] {} - {}() | Params: {}", className, methodName, Arrays.toString(args)); + + try { + // 실제 레거시 통신 로직 실행 (이 코드가 없으면 통신이 진행되지 않습니다) + Object result = joinPoint.proceed(); + + // 3. [응답 로깅] 정상적으로 통신이 완료된 후 + long executionTime = System.currentTimeMillis() - startTime; + log.info("◀ [EIMS 응답] {} - {}() | 소요시간: {}ms | Result: {}", className, methodName, executionTime, result); + + return result; + + } catch (Exception e) { + // 4. [에러 로깅] 레거시 통신 중 장애(타임아웃 등) 발생 시 + long executionTime = System.currentTimeMillis() - startTime; + log.error(" [EIMS 에러] {} - {}() | 소요시간: {}ms | Error: {}", className, methodName, executionTime, e.getMessage()); + + // 에러를 삼키지 않고 다시 던져서 기존 예외 처리 로직(서킷 브레이커 등)이 작동하게 합니다. + throw e; + } + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/connector/ExternalApiConnector.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/connector/ExternalApiConnector.java new file mode 100644 index 000000000..cf8b0bb0a --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/connector/ExternalApiConnector.java @@ -0,0 +1,72 @@ +package io.shinhanlife.dap.lib.adapter.connector; + +import io.shinhanlife.dap.lib.adapter.support.DynamicPayloadBuilder; +import io.shinhanlife.dap.lib.adapter.support.DynamicSchemaValidator; +import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import io.github.resilience4j.ratelimiter.annotation.RateLimiter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; +import java.util.Map; +import java.util.List; + +/** + * @package io.shinhanlife.dap.lib.adapter.connector + * @className ExternalApiConnector + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +@Service +public class ExternalApiConnector { + + private final RestClient restClient; + private final DynamicSchemaValidator schemaValidator; + private final DynamicPayloadBuilder payloadBuilder; + + public ExternalApiConnector(DynamicSchemaValidator schemaValidator, DynamicPayloadBuilder payloadBuilder) { + this.restClient = RestClient.create(); + this.schemaValidator = schemaValidator; + this.payloadBuilder = payloadBuilder; + } + + @RateLimiter(name = "externalApi", fallbackMethod = "fallbackForExternalApi") + @CircuitBreaker(name = "externalApi", fallbackMethod = "fallbackForExternalApi") + public String callExternalApi(String apiName, String endpoint, Map data, List> spec, boolean isFixedLength) throws Exception { + + // 1. 요청 데이터 검증 (errorLog 전달하여 구체적 에러 포착) + StringBuilder errorLog = new StringBuilder(); + if (!schemaValidator.validate(spec, data, errorLog)) { + String errorMsg = "API 요청 데이터 스키마 불일치 [" + apiName + "]: " + errorLog.toString(); + log.error(" {}", errorMsg); + throw new IllegalArgumentException(errorMsg); + } + + // 2. 페이로드 빌드 + Object payload = isFixedLength ? payloadBuilder.buildFixedLengthString(spec, data) : data; + String contentType = isFixedLength ? "application/x-www-form-urlencoded;charset=EUC-KR" : "application/json"; + + log.info(" 외부 API 호출 [{}] 시작 (FixedLength: {})", apiName, isFixedLength); + + return restClient.post() + .uri(endpoint) + .contentType(MediaType.parseMediaType(contentType)) + .body(payload) + .retrieve() + .body(String.class); + } + + public String fallbackForExternalApi(String apiName, String endpoint, Map data, List> spec, boolean isFixedLength, Throwable t) { + log.error(" [외부 API 장애] {} 호출 실패: {}", apiName, t.getMessage()); + return String.format("{\"status\":\"EXTERNAL_API_ERROR\", \"message\":\"외부 서비스 연동 중 오류 발생: %s\"}", t.getMessage()); + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/connector/InternalSystemConnector.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/connector/InternalSystemConnector.java new file mode 100644 index 000000000..17a7144d8 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/connector/InternalSystemConnector.java @@ -0,0 +1,74 @@ +package io.shinhanlife.dap.lib.adapter.connector; + +import io.shinhanlife.dap.lib.adapter.support.DynamicPayloadBuilder; +import io.shinhanlife.dap.lib.adapter.support.DynamicSchemaValidator; +import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import io.github.resilience4j.ratelimiter.annotation.RateLimiter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; +import java.util.Map; +import java.util.List; + +/** + * @package io.shinhanlife.dap.lib.adapter.connector + * @className InternalSystemConnector + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +@Service +public class InternalSystemConnector { + + private final RestClient restClient; + private final DynamicSchemaValidator schemaValidator; + private final DynamicPayloadBuilder payloadBuilder; + + // 생성자를 통한 의존성 주입 (Spring이 알아서 Validator와 Builder를 넣어줍니다) + public InternalSystemConnector(DynamicSchemaValidator schemaValidator, DynamicPayloadBuilder payloadBuilder) { + this.restClient = RestClient.create(); + this.schemaValidator = schemaValidator; + this.payloadBuilder = payloadBuilder; + } + + @RateLimiter(name = "internalSystem", fallbackMethod = "fallbackForInternal") + @CircuitBreaker(name = "internalSystem", fallbackMethod = "fallbackForInternal") + public String callInternalSystem(String targetName, String endpoint, Map data, List> spec, boolean isFixedLength) throws Exception { + + // 1. 요청 데이터 검증 (errorLog 전달하여 구체적 에러 포착) + StringBuilder errorLog = new StringBuilder(); + if (!schemaValidator.validate(spec, data, errorLog)) { + String errorMsg = "대내외 시스템 연계 데이터 스키마 불일치 [" + targetName + "]: " + errorLog.toString(); + log.error(" {}", errorMsg); + throw new IllegalArgumentException(errorMsg); + } + + // 2. 페이로드 빌드 (고정장 vs JSON) + Object payload = isFixedLength ? payloadBuilder.buildFixedLengthString(spec, data) : data; + String contentType = isFixedLength ? "application/x-www-form-urlencoded;charset=EUC-KR" : "application/json"; + + log.info(" 대내외 시스템 호출 [{}] 시작 (FixedLength: {})", targetName, isFixedLength); + + return restClient.post() + .uri(endpoint) + .contentType(MediaType.parseMediaType(contentType)) + .body(payload) + .retrieve() + .body(String.class); + } + + // 통신 장애(CircuitBreaker) 또는 허용량 초과(RateLimiter) 시 처리 로직 + public String fallbackForInternal(String targetName, String endpoint, Map data, List> spec, boolean isFixedLength, Throwable t) { + log.error(" [대내외 시스템 장애/지연] {} 연계 실패: {}", targetName, t.getMessage()); + return String.format("{\"status\":\"INTERNAL_SYSTEM_ERROR\", \"message\":\"대내외 연계 시스템 호출 중 오류가 발생했거나 요청이 지연되었습니다. 사유: %s\"}", t.getMessage()); + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/connector/LegacyDbConnector.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/connector/LegacyDbConnector.java new file mode 100644 index 000000000..d572c8c6c --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/connector/LegacyDbConnector.java @@ -0,0 +1,58 @@ +package io.shinhanlife.dap.lib.adapter.connector; + +import io.shinhanlife.dap.lib.adapter.support.ResultStandardizer; +import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import io.github.resilience4j.ratelimiter.annotation.RateLimiter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @package io.shinhanlife.dap.lib.adapter.connector + * @className LegacyDbConnector + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +@Service +@RequiredArgsConstructor +public class LegacyDbConnector { + + private final NamedParameterJdbcTemplate jdbcTemplate; // 동적 파라미터 바인딩을 위한 템플릿 + private final ResultStandardizer resultStandardizer; + + @RateLimiter(name = "legacyDb", fallbackMethod = "fallbackForDb") + @CircuitBreaker(name = "legacyDb", fallbackMethod = "fallbackForDb") + public List> executeDynamicQuery(String queryId, String sql, Map params) { + log.info(" Legacy DB 조회 시작 [QueryID: {}]", queryId); + + // 1. SQL 쿼리 실행 (오라클 등) + List> rawResults = jdbcTemplate.queryForList(sql, params); + + // 2. 스키마 매핑 및 결과 정형화 (대문자 -> 카멜케이스 변환) + List> standardResults = rawResults.stream() + .map(resultStandardizer::standardize) + .collect(Collectors.toList()); + + log.info(" Legacy DB 조회 완료 ({}건 반환)", standardResults.size()); + return standardResults; + } + + public List> fallbackForDb(String queryId, String sql, Map params, Throwable t) { + log.error(" [Legacy DB 장애/지연] 쿼리 실행 실패 [{}]: {}", queryId, t.getMessage()); + throw new RuntimeException("레거시 DB 연동 중 오류가 발생했습니다. (QueryID: " + queryId + ")"); + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/connector/LegacyEimsConnector.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/connector/LegacyEimsConnector.java new file mode 100644 index 000000000..915756967 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/connector/LegacyEimsConnector.java @@ -0,0 +1,151 @@ +package io.shinhanlife.dap.lib.adapter.connector; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.shinhanlife.dap.lib.adapter.sender.EimsSender; +import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import io.github.resilience4j.ratelimiter.RequestNotPermitted; +import io.github.resilience4j.ratelimiter.annotation.RateLimiter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import io.shinhanlife.dap.lib.adapter.util.LegacyDataTransformer; + +/** + * @package io.shinhanlife.dap.lib.adapter.connector + * @className LegacyEimsConnector + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +@Service +@RequiredArgsConstructor +public class LegacyEimsConnector { + + // 4가지 방식의 Sender를 모두 주입받습니다. (변수명이 아주 중요합니다!) + private final EimsSender httpEimsSender; // 1~20 (EIMS API) + private final EimsSender tcpEimsSender; // 21~30 (EIMS Socket) + private final EimsSender jspFormEimsSender; // 31~40 (JSP Form) + private final EimsSender jspJsonEimsSender; // 41~50 (JSP JSON) + + private final EimsSender mciEimsSender; // 실시간 연계 (기존 HTTP/TCP 대체, 동기식 API) + private final EimsSender mciStringEimsSender; // 실시간 연계 (String 전문 버전) + private final EimsSender eaiEimsSender; // 비동기/대용량 연계 (배치 통신 등) + + private final ObjectMapper jsonMapper; + + + // 방어막 적용: 예외가 발생하거나 차단기가 열리면 fallbackMethod를 즉시 실행합니다! + // 서킷 브레이커와 Rate Limiter를 동시에 적용 (둘 중 하나라도 걸리면 fallbackForEims 실행) + @RateLimiter(name = "eims", fallbackMethod = "fallbackForEims") + @CircuitBreaker(name = "eims", fallbackMethod = "fallbackForEims") + // 2번 업그레이드 적용: 파라미터 기반의 스마트 캐싱 (고객의 요청 데이터가 다르면 캐시도 다르게 적용) + @Cacheable(value = "eimsData", key = "#routingType + '-' + #interfaceId + '-' + (#data != null ? #data.hashCode() : 0)") + public String executeByTool(String routingType, String interfaceId, Map data, List> spec) throws Exception { + + // 1. 데이터 조립 (방어 로직 포함) + String payload = buildPayload(data, spec); + + log.info("\n [Adapter -> Legacy] EIMS 통신 요청 - RoutingType: {}, Interface: {}, Payload: {}", routingType, interfaceId, payload); + + // 2. 4단계 라우팅 분기 처리 (Switch Expression 활용) + String upperRoutingType = routingType != null ? routingType.toUpperCase() : ""; + + String legacyResponse = switch (upperRoutingType) { + case "HTTP" -> { + log.info("[라우팅] EIMS API (HTTP) 통신으로 전달"); + yield httpEimsSender.send(interfaceId, payload); + } + case "TCP" -> { + log.info("[라우팅] EIMS 소켓 (TCP) 통신으로 전달"); + yield tcpEimsSender.send(interfaceId, payload); + } + case "JSP_FORM" -> { + log.info("[라우팅] JSP Form 통신으로 전달"); + yield jspFormEimsSender.send(interfaceId, payload); + } + case "JSP_JSON" -> { + log.info("[라우팅] JSP JSON 통신으로 전달"); + yield jspJsonEimsSender.send(interfaceId, payload); + } + + case "MCI" -> { + if (isStringMci(spec)) { + log.info("[라우팅] 스펙 자동 판별 (String 포맷 감지) -> MCI 연계 어댑터(String)를 통해 EIMS 전달"); + yield mciStringEimsSender.send(interfaceId, payload); + } else { + log.info("[라우팅] 실시간 AI 요청 -> MCI 연계 어댑터를 통해 EIMS 전달"); + yield mciEimsSender.send(interfaceId, payload); + } + } + case "EAI" -> { + log.info("[라우팅] 비동기/대용량 요청 -> EAI 연계 어댑터를 통해 EIMS 전달"); + yield eaiEimsSender.send(interfaceId, payload); + } + default -> { + log.error("[라우팅] 알 수 없는 라우팅 타입: {}", routingType); + throw new IllegalArgumentException("지원하지 않는 라우팅 타입입니다: " + routingType); + } + }; + + log.info("\n [Legacy -> Adapter] EIMS 통신 응답 수신: {}", legacyResponse); + return legacyResponse; + } + + + // 비상용 응답 메서드 (차단기가 열려있거나, 타임아웃/에러가 났을 때 실행됨) + // 주의: 파라미터는 원본 메서드와 100% 똑같이 맞추고, 마지막에 Throwable을 받아야 합니다. + public String fallbackForEims(String routingType, String interfaceId, Map data, List> spec, Throwable t) { + + // 1. Rate Limiter에 의해 차단된 경우 (트래픽 폭주) + if (t instanceof RequestNotPermitted) { + log.warn(" [Rate Limiter 발동] 트래픽 폭주로 요청 차단! interfaceId: {}", interfaceId); + return String.format( + "{\"status\":\"TOO_MANY_REQUESTS\", \"message\":\"순간적인 요청 폭주로 인해 일시적으로 제한되었습니다. 잠시 후 시도해 주세요.\", \"interfaceId\":\"%s\"}", + interfaceId + ); + } + + // 2. Circuit Breaker에 의해 차단된 경우 (레거시 시스템 장애/지연) + log.error(" [서킷 브레이커 발동] 레거시 통신 차단! 원인: {}", t.getMessage()); + return String.format( + "{\"status\":\"CIRCUIT_OPEN\", \"message\":\"신한라이프 내부 시스템 장애로 인해 일시적으로 차단되었습니다. 복구 후 재시도 부탁드립니다.\", \"interfaceId\":\"%s\"}", + interfaceId + ); + } + + private String buildPayload(Map data, List> spec) { + // 3번 항목 적용: 원본 데이터를 스펙에 맞게 엄격히 정제(변환, 형변환, 잘라내기, 기본값 등) + Map transformedData = LegacyDataTransformer.transform(data, spec); + + if (transformedData == null || transformedData.isEmpty()) return "{}"; + + try { + return jsonMapper.writeValueAsString(transformedData); + } catch (Exception e) { + log.error(" JSON 변환 에러: {}", e.getMessage()); + return "{}"; + } + } + + private boolean isStringMci(List> spec) { + if (spec == null || spec.isEmpty()) return false; + // 스펙 내에 maxLength 등 고정길이 전문 관련 속성이 하나라도 존재하거나 명시적으로 STRING 힌트가 있으면 String 전문으로 간주 + return spec.stream().anyMatch(field -> + field.containsKey("maxLength") || + field.containsKey("byteSize") || + "STRING".equalsIgnoreCase(String.valueOf(field.get("mciFormat"))) + ); + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/connector/ThirdPartySecurityConnector.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/connector/ThirdPartySecurityConnector.java new file mode 100644 index 000000000..3c70a697d --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/connector/ThirdPartySecurityConnector.java @@ -0,0 +1,71 @@ +package io.shinhanlife.dap.lib.adapter.connector; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Map; + +/** + * @package io.shinhanlife.dap.lib.adapter.connector + * @className ThirdPartySecurityConnector + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ThirdPartySecurityConnector { + + private final ObjectMapper jsonMapper; + + /** + * 3rd Party 보안 모듈(DRM, BM 등)과 연동하기 위한 전용 메서드입니다. + */ + public String executeSecurityModule(String interfaceId, Map data) throws Exception { + log.info(" [3rd Party Security] 보안 모듈 연동 시작 - Interface: {}", interfaceId); + + // 보안 모듈 통신을 위한 특수 페이로드 조립 (예시) + // 실제로는 RestClient나 WebClient를 통해 보안 VM의 전용 엔드포인트로 호출합니다. + + String resultJson; + + if (interfaceId.startsWith("DRM_")) { + log.info(" [DRM 처리] 내부 문서 암/복호화 모듈과 통신 중..."); + resultJson = jsonMapper.writeValueAsString(Map.of( + "status", "SUCCESS", + "module", "DRM", + "message", "문서 보안 처리가 완료되었습니다.", + "interfaceId", interfaceId, + "data", data != null ? data : Map.of() + )); + } else if (interfaceId.startsWith("BM_")) { + log.info(" [BM 처리] 바이오 인증 모듈과 통신 중..."); + resultJson = jsonMapper.writeValueAsString(Map.of( + "status", "SUCCESS", + "module", "Bio-Metric", + "message", "바이오 인증이 완료되었습니다.", + "interfaceId", interfaceId, + "data", data != null ? data : Map.of() + )); + } else { + log.warn(" 알 수 없는 보안 모듈 연동 요청: {}", interfaceId); + resultJson = jsonMapper.writeValueAsString(Map.of( + "status", "UNKNOWN_MODULE", + "message", "알 수 없는 보안 모듈 인터페이스입니다." + )); + } + + log.info(" [3rd Party Security] 보안 모듈 처리 완료"); + return resultJson; + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/dto/ErrorDetail.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/dto/ErrorDetail.java new file mode 100644 index 000000000..e85d3169d --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/dto/ErrorDetail.java @@ -0,0 +1,32 @@ +package io.shinhanlife.dap.lib.adapter.dto; + +import lombok.Builder; +import lombok.NoArgsConstructor; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +/** + * @package io.shinhanlife.dap.lib.adapter.dto + * @className ErrorDetail + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@Setter +@AllArgsConstructor +@Builder +@NoArgsConstructor +public class ErrorDetail { + private int code; + private String message; +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/dto/JsonRpcRequest.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/dto/JsonRpcRequest.java new file mode 100644 index 000000000..3f4f105e7 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/dto/JsonRpcRequest.java @@ -0,0 +1,27 @@ +package io.shinhanlife.dap.lib.adapter.dto; + +import lombok.Getter; +import lombok.Setter; + +/** + * @package io.shinhanlife.dap.lib.adapter.dto + * @className JsonRpcRequest + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@Setter +public class JsonRpcRequest { + private String jsonrpc; + private String method; + private Params params; + private String id; +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/dto/JsonRpcResponse.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/dto/JsonRpcResponse.java new file mode 100644 index 000000000..cc79e10d1 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/dto/JsonRpcResponse.java @@ -0,0 +1,30 @@ +package io.shinhanlife.dap.lib.adapter.dto; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.Getter; +import lombok.Setter; + +// 2. 응답 DTO +/** + * @package io.shinhanlife.dap.lib.adapter.dto + * @className JsonRpcResponse + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@Setter +@JsonPropertyOrder({"jsonrpc", "result", "error", "id"}) +public class JsonRpcResponse { + public String jsonrpc = "2.0"; + public Object result; + public Object error; + public String id; +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/dto/Params.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/dto/Params.java new file mode 100644 index 000000000..e4931eb49 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/dto/Params.java @@ -0,0 +1,38 @@ +package io.shinhanlife.dap.lib.adapter.dto; + +import lombok.Builder; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.util.List; +import java.util.Map; + +import lombok.Getter; +import lombok.Setter; + +/** + * @package io.shinhanlife.dap.lib.adapter.dto + * @className Params + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Params { + private String routingType; + private String name; + private String interfaceId; + private Map data; + private List> spec; +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/exception/MciCommunicationException.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/exception/MciCommunicationException.java new file mode 100644 index 000000000..2e45f94a1 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/exception/MciCommunicationException.java @@ -0,0 +1,26 @@ +package io.shinhanlife.dap.lib.adapter.exception; + +/** + * @package io.shinhanlife.dap.lib.adapter.exception + * @className MciCommunicationException + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +public class MciCommunicationException extends RuntimeException { + + public MciCommunicationException(String message) { + super(message); + } + + public MciCommunicationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/EaiEimsSender.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/EaiEimsSender.java new file mode 100644 index 000000000..2ddbc1c25 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/EaiEimsSender.java @@ -0,0 +1,104 @@ +package io.shinhanlife.dap.lib.adapter.sender; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import io.shinhanlife.dap.lib.adapter.support.TicketManager; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; +import org.springframework.util.StopWatch; + +/** + * @package io.shinhanlife.dap.lib.adapter.sender + * @className EaiEimsSender + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +@Service("eaiEimsSender") +@RequiredArgsConstructor +public class EaiEimsSender implements EimsSender { + + private final ObjectMapper jsonMapper; + private final XmlMapper xmlMapper; + + // 실전 코드: 스프링이 제공하는 카프카 템플릿 주입 + private final KafkaTemplate kafkaTemplate; + + // 비동기 티켓 매니저 + private final TicketManager ticketManager; + + @Override + public String send(String interfaceId, String jsonPayload) throws Exception { + StopWatch stopWatch = new StopWatch(); stopWatch.start(); + + try { + JsonNode jsonNode = jsonMapper.readTree(jsonPayload); + String xmlData = xmlMapper.writeValueAsString(jsonNode); + String esbStandardXml = wrapWithEaiHeader(interfaceId, xmlData); + + log.info(" [EAI 어댑터] Kafka 토픽(eai-topic)으로 전송 시도..."); + + try { + // 실전 코드 적용: Kafka로 메시지 발행 + kafkaTemplate.send("eai-topic", esbStandardXml); + log.info(" [EAI 어댑터] Kafka 전송 완료!"); + } catch (Exception e) { + // 로컬 환경에는 카프카가 없으므로 에러가 날 수 있습니다. 테스트를 위해 로깅만 하고 넘깁니다. + log.warn(" 로컬 환경이거나 Ka\n" + + "\n" + + "
\n" + + " MCP_GATEWAY_ASYNC\n" + + " EAI_BATCH_JOB\n" + + " 1782705901850\n" + + " ASYNC\n" + + "
\n" + + " \n" + + " \n" + + " BATCH_20260629_001\n" + + " GLOBAL_MINIMUM_TAX_SYS\n" + + " 50000\n" + + " \n" + + " \n" + + "
fka 서버에 연결할 수 없습니다. (메시지 출력으로 대체합니다) \n전송하려던 메시지: {}", esbStandardXml); + } + + // 비동기 폴링을 위한 티켓 발급 + String ticketId = ticketManager.issueTicket(interfaceId, jsonPayload); + + return String.format( + "{\"status\":\"PROCESSING\", \"interfaceId\":\"%s\", \"ticketId\":\"%s\", \"message\":\"비동기 작업이 접수되었습니다. 상태 조회 API를 통해 결과를 확인하세요.\"}", + interfaceId, ticketId); + + } finally { + stopWatch.stop(); + log.info(" [SLA 모니터링 - EAI] 소요시간: {} ms", stopWatch.getTotalTimeMillis()); + } + } + + private String wrapWithEaiHeader(String interfaceId, String xmlData) { + // 비동기 EAI는 추적을 위해 TransferType이나 Batch ID 같은 속성이 추가로 들어가는 경우가 많습니다. + return String.format( + "" + + "
" + + "MCP_GATEWAY_ASYNC" + + "%s" + + "%d" + + "ASYNC" + + "
" + + "%s" + + "
", + interfaceId, System.currentTimeMillis(), xmlData + ); + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/EimsSender.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/EimsSender.java new file mode 100644 index 000000000..ee6afe1bf --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/EimsSender.java @@ -0,0 +1,20 @@ +package io.shinhanlife.dap.lib.adapter.sender; + +/** + * @package io.shinhanlife.dap.lib.adapter.sender + * @className EimsSender + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +public interface EimsSender { + // 프로토콜에 상관없이 이 메서드 하나로 통일합니다. + String send(String interfaceId, String payload) throws Exception; +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/HttpEimsSender.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/HttpEimsSender.java new file mode 100644 index 000000000..9ba08a23d --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/HttpEimsSender.java @@ -0,0 +1,81 @@ +package io.shinhanlife.dap.lib.adapter.sender; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; +import org.springframework.http.client.SimpleClientHttpRequestFactory; + +import java.util.Map; +import java.util.UUID; + +import org.slf4j.MDC; + +import io.shinhanlife.dap.lib.config.GlowCommunicationProperties; + +/** + * @package io.shinhanlife.dap.lib.adapter.sender + * @className HttpEimsSender + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +@Component +public class HttpEimsSender implements EimsSender { + + private final RestClient restClient; + private final String eimsUrl; + private final GlowCommunicationProperties glowProps; + + public HttpEimsSender(GlowCommunicationProperties glowProps) { + this.glowProps = glowProps; + // yml의 대내 MCI host, port, uri를 조합하여 EIMS 호출 주소 생성 + this.eimsUrl = glowProps.getMci().getHost() + ":" + glowProps.getMci().getPort() + glowProps.getMci().getUri(); + + /* + *********************************************** 중요 ************************************************** + this.restClient = RestClient.create(); + 보통 금융권(신한라이프 등 은행/보험사)의 내부 레거시 시스템이나 MCI(Message Channel Integration) 솔루션은 HTTP/2를 기본으로 지원하지 않는 경우가 훨씬 많습니다. + *********************************************** 중요 ************************************************** + */ + + // HTTP/2 통신 시 Stream Cancelled(RST_STREAM) 에러 방지를 위해 HTTP/1.1 전용 Factory 사용 + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(3000); + factory.setReadTimeout(5000); + + this.restClient = RestClient.builder().requestFactory(factory).build(); + } + + @Override + public String send(String interfaceId, String payload) { + log.info(" [HTTP 모드] EIMS API 호출 중... URL: {}", eimsUrl); + + // EIMS가 요구하는 JSON 포맷으로 래핑해서 전송 (EIMS 규격에 따라 수정 가능) + Map requestBody = Map.of( + "interfaceId", interfaceId, + "data", payload + ); + + // 4번 항목 적용: MDC에 저장된 traceId를 추출하여 HTTP Header(X-Trace-Id)로 전파 + String traceId = MDC.get("traceId"); + if (traceId == null) traceId = "SYSTEM-GENERATED-" + UUID.randomUUID().toString(); + + return restClient.post() + .uri(eimsUrl) + .header("X-Trace-Id", traceId) + .header("X-Shinhan-Global-ID", traceId) + .body(requestBody) + .retrieve() + .body(String.class); // 응답 결과를 String으로 받음 + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/JspFormEimsSender.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/JspFormEimsSender.java new file mode 100644 index 000000000..c05cfbb12 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/JspFormEimsSender.java @@ -0,0 +1,58 @@ +package io.shinhanlife.dap.lib.adapter.sender; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClient; + +/** + * @package io.shinhanlife.dap.lib.adapter.sender + * @className JspFormEimsSender + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +@Component +public class JspFormEimsSender implements EimsSender { + + private final RestClient restClient; + private final String jspUrl; + + public JspFormEimsSender(@Value("${eims.jsp.form.url}") String jspUrl) { + this.jspUrl = jspUrl; + this.restClient = RestClient.builder().build(); + } + + @Override + public String send(String interfaceId, String payload) { + log.info(" [JSP Form 모드] 레거시 폼 데이터 전송 중... URL: {}", jspUrl); + + // 1. Form Data 조립 (HTML
태그 전송과 동일한 효과) + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("interfaceId", interfaceId); + formData.add("data", payload); + + // 2. HTTP 전송 (Content-Type: application/x-www-form-urlencoded) + String rawResponse = restClient.post() + .uri(jspUrl) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(formData) + .retrieve() + .body(String.class); + + // 3. JSP 특유의 앞뒤 공백 및 엔터 제거 + return rawResponse != null ? rawResponse.trim() : ""; + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/JspJsonEimsSender.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/JspJsonEimsSender.java new file mode 100644 index 000000000..a89dd8f1e --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/JspJsonEimsSender.java @@ -0,0 +1,59 @@ +package io.shinhanlife.dap.lib.adapter.sender; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +import java.util.Map; + +/** + * @package io.shinhanlife.dap.lib.adapter.sender + * @className JspJsonEimsSender + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +@Component +public class JspJsonEimsSender implements EimsSender { + + private final RestClient restClient; + private final String jspUrl; + + // 여기서 eims.jsp.json.url 딱 하나만 깔끔하게 받아옵니다! + public JspJsonEimsSender(@Value("${eims.jsp.json.url}") String jspUrl) { + this.jspUrl = jspUrl; + this.restClient = RestClient.builder().build(); + } + + @Override + public String send(String interfaceId, String payload) { + log.info(" [JSP JSON 모드] JSON 페이로드 전송 중... URL: {}", jspUrl); + + // 1. JSON 객체로 조립 (스프링이 알아서 JSON String으로 변환해 줌) + Map jsonBody = Map.of( + "interfaceId", interfaceId, + "data", payload + ); + + // 2. HTTP 전송 (Content-Type: application/json) + String rawResponse = restClient.post() + .uri(jspUrl) + .contentType(MediaType.APPLICATION_JSON) + .body(jsonBody) + .retrieve() + .body(String.class); + + // 3. JSP 응답 정제 + return rawResponse != null ? rawResponse.trim() : ""; + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/MciEimsSender.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/MciEimsSender.java new file mode 100644 index 000000000..71acbfefe --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/MciEimsSender.java @@ -0,0 +1,86 @@ +package io.shinhanlife.dap.lib.adapter.sender; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.util.StopWatch; +import org.springframework.web.client.RestClient; + +/** + * @package io.shinhanlife.dap.lib.adapter.sender + * @className MciEimsSender + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +@Service("mciEimsSender") +public class MciEimsSender implements EimsSender { + + private final ObjectMapper jsonMapper; + private final XmlMapper xmlMapper; + private final RestClient restClient; // Spring Boot 3.2+ 최신 HTTP 클라이언트 + private final String mciUrl; + + public MciEimsSender(ObjectMapper jsonMapper, XmlMapper xmlMapper, @Value("${eims.mci.url}") String mciUrl) { + this.jsonMapper = jsonMapper; + this.xmlMapper = xmlMapper; + this.mciUrl = mciUrl; + this.restClient = RestClient.create(); // 클라이언트 초기화 + + /* + *********************************************** 중요 ************************************************** + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(3000); + factory.setReadTimeout(5000); + this.restClient = RestClient.builder().requestFactory(factory).build(); + 보통 금융권(신한라이프 등 은행/보험사)의 내부 레거시 시스템이나 MCI(Message Channel Integration) 솔루션은 HTTP/2를 기본으로 지원하지 않는 경우가 훨씬 많습니다. + *********************************************** 중요 ************************************************** + */ + } + + @Override + public String send(String interfaceId, String jsonPayload) throws Exception { + StopWatch stopWatch = new StopWatch(); stopWatch.start(); + + try { + JsonNode jsonNode = jsonMapper.readTree(jsonPayload); + String xmlData = xmlMapper.writer().withRootName("Body").writeValueAsString(jsonNode); + String esbStandardXml = wrapWithEsbHeader(interfaceId, xmlData); + + log.info(" [ESB 어댑터] 전송 준비 완료 - RestClient 호출 시작"); + + String responseXml = restClient.post() + .uri(mciUrl) + .contentType(MediaType.APPLICATION_XML) + .body(esbStandardXml) + .retrieve() + .body(String.class); + + log.info(" [ESB 어댑터] 응답 수신 완료: {}", responseXml); + + JsonNode responseNode = xmlMapper.readTree(responseXml); + return jsonMapper.writeValueAsString(responseNode); + + } finally { + stopWatch.stop(); + log.info(" [SLA 모니터링 - MCI] 소요시간: {} ms", stopWatch.getTotalTimeMillis()); + } + } + + private String wrapWithEsbHeader(String interfaceId, String xmlData) { + return String.format("
%s
%s
", interfaceId, xmlData); + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/MciStringEimsSender.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/MciStringEimsSender.java new file mode 100644 index 000000000..c6bd3ae09 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/MciStringEimsSender.java @@ -0,0 +1,68 @@ +package io.shinhanlife.dap.lib.adapter.sender; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.util.StopWatch; +import org.springframework.web.client.RestClient; + +/** + * @package io.shinhanlife.dap.lib.adapter.sender + * @className MciStringEimsSender + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +@Service("mciStringEimsSender") +public class MciStringEimsSender implements EimsSender { + + private final RestClient restClient; + private final String mciUrl; + + public MciStringEimsSender(@Value("${eims.mcistring.url}") String mciUrl) { + this.mciUrl = mciUrl; + this.restClient = RestClient.create(); + /* + *********************************************** 중요 ************************************************** + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(3000); + factory.setReadTimeout(5000); + this.restClient = RestClient.builder().requestFactory(factory).build(); + 보통 금융권(신한라이프 등 은행/보험사)의 내부 레거시 시스템이나 MCI(Message Channel Integration) 솔루션은 HTTP/2를 기본으로 지원하지 않는 경우가 훨씬 많습니다. + *********************************************** 중요 ************************************************** + */ + + } + + @Override + public String send(String interfaceId, String payload) throws Exception { + StopWatch stopWatch = new StopWatch(); stopWatch.start(); + + try { + log.info(" [ESB 어댑터(String)] 전송 준비 완료 - RestClient 호출 시작 (Interface: {})", interfaceId); + + String response = restClient.post() + .uri(mciUrl) + .contentType(MediaType.TEXT_PLAIN) + .body(payload != null ? payload : "") + .retrieve() + .body(String.class); + + log.info(" [ESB 어댑터(String)] 응답 수신 완료: {}", response); + return response != null ? response : ""; + + } finally { + stopWatch.stop(); + log.info(" [SLA 모니터링 - MCI(String)] 소요시간: {} ms", stopWatch.getTotalTimeMillis()); + } + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/ShinhanMciSender.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/ShinhanMciSender.java new file mode 100644 index 000000000..599ba442a --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/ShinhanMciSender.java @@ -0,0 +1,109 @@ +package io.shinhanlife.dap.lib.adapter.sender; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.shinhanlife.dap.lib.integration.mci.config.ShinhanIntegrationProperties; +import io.shinhanlife.dap.lib.integration.mci.dto.MciRequestWrapper; +import io.shinhanlife.dap.lib.integration.mci.dto.ShinhanCommonHeaderDto; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.util.StopWatch; +import org.springframework.web.client.RestClient; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @package io.shinhanlife.dap.lib.adapter.sender + * @className ShinhanMciSender + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +@Slf4j +@Service +@EnableConfigurationProperties(ShinhanIntegrationProperties.class) +public class ShinhanMciSender { + + private final ObjectMapper jsonMapper; + private final RestClient restClient; + private final ShinhanIntegrationProperties properties; + private final AtomicInteger sequenceGenerator = new AtomicInteger(1); + + public ShinhanMciSender(ObjectMapper jsonMapper, ShinhanIntegrationProperties properties) { + this.jsonMapper = jsonMapper; + this.properties = properties; + this.restClient = RestClient.create(); + } + + public String send(String targetUrl, MciRequestWrapper requestWrapper) throws Exception { + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + + try { + ShinhanCommonHeaderDto header = requestWrapper.getTgrmCmnnhddValu(); + if (header == null) { + header = new ShinhanCommonHeaderDto(); + requestWrapper.setTgrmCmnnhddValu(header); + } + + // 필수 헤더 자동 세팅 로직 + header.setEnvrTypeCd(properties.getEnvrTypeCd()); + header.setReqRspnScCd("S"); // S: 요청 + header.setAppliDutjCd("DAP"); // 어플리케이션업무코드 + + if (header.getGlbId() == null || header.getGlbId().isEmpty()) { + header.setGlbId(generateGlbId()); + } + + int currentSeq = sequenceGenerator.getAndIncrement(); + header.setPgrsSriaNo(String.format("%03d", currentSeq)); // 3자리 + + if (header.getReqTgrmTnsmDtptDt() == null) { + header.setReqTgrmTnsmDtptDt(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"))); + } + + String jsonPayload = jsonMapper.writeValueAsString(requestWrapper); + + log.info(" [신한 통합 MCI 어댑터] 전송 준비 완료 - URL: {}", targetUrl); + log.debug(" [신한 통합 MCI 어댑터] 요청 Payload: {}", jsonPayload); + + String responseJson = restClient.post() + .uri(targetUrl) + .contentType(MediaType.APPLICATION_JSON) + .body(jsonPayload) + .retrieve() + .body(String.class); + + log.info(" [신한 통합 MCI 어댑터] 응답 수신 완료"); + log.debug(" [신한 통합 MCI 어댑터] 응답 Payload: {}", responseJson); + + return responseJson; + + } finally { + stopWatch.stop(); + log.info(" [SLA 모니터링 - MCI 연계] 소요시간: {} ms", stopWatch.getTotalTimeMillis()); + } + } + + private String generateGlbId() { + // 전사공통키 (37 Byte) + // 전문생성상세일시(17) + 전문생성시스템명(9) + 어플리케이션업무코드(3) + 전문세션번호(8) + String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")); // 17 byte + String systemName = String.format("%-9s", "AXHUB"); // 9 byte, left-aligned padded with spaces + String appCode = "DAP"; // 3 byte + String sessionNo = UUID.randomUUID().toString().substring(0, 8).toUpperCase(); // 8 byte + return timestamp + systemName + appCode + sessionNo; + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/TcpEimsSender.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/TcpEimsSender.java new file mode 100644 index 000000000..a6f9a1a7c --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/sender/TcpEimsSender.java @@ -0,0 +1,71 @@ +package io.shinhanlife.dap.lib.adapter.sender; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.Socket; + +/** + * @package io.shinhanlife.dap.lib.adapter.sender + * @className TcpEimsSender + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +@Component +public class TcpEimsSender implements EimsSender { + + @Value("${eims.tcp.host}") + private String host; + + @Value("${eims.tcp.port}") + private int port; + + @Value("${eims.tcp.timeout}") + private int timeout; + + @Override + public String send(String interfaceId, String payload) throws Exception { + log.info(" [TCP 소켓 모드] EIMS 접속 중... {}:{}", host, port); + + // TCP 소켓 자원을 사용 후 안전하게 닫아주는 try-with-resources 구문 + try (Socket socket = new Socket()) { + // 1. 타임아웃 및 연결 설정 + socket.connect(new InetSocketAddress(host, port), timeout); + socket.setSoTimeout(timeout); // 읽기 타임아웃 + + OutputStream os = socket.getOutputStream(); + InputStream is = socket.getInputStream(); + + // 2. 데이터 송신 (EUC-KR 인코딩 필수) + // 보통 금융권 TCP 통신은 맨 앞에 전체 길이나 인터페이스 ID를 헤더로 붙입니다. + String sendData = interfaceId + payload; + os.write(sendData.getBytes("EUC-KR")); + os.flush(); + + // 3. 데이터 수신 + byte[] buffer = new byte[4096]; + int readByte = is.read(buffer); + + if (readByte == -1) { + throw new RuntimeException("EIMS 서버가 응답 없이 연결을 종료했습니다."); + } + + // 받은 바이트를 다시 한글(EUC-KR) 문자열로 복원 + return new String(buffer, 0, readByte, "EUC-KR"); + } + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/support/DynamicPayloadBuilder.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/support/DynamicPayloadBuilder.java new file mode 100644 index 000000000..62f4090be --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/support/DynamicPayloadBuilder.java @@ -0,0 +1,59 @@ +package io.shinhanlife.dap.lib.adapter.support; + +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; + +/** + * @package io.shinhanlife.dap.lib.adapter.support + * @className DynamicPayloadBuilder + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Service +public class DynamicPayloadBuilder { + + public String buildFixedLengthString(List> specList, Map data) throws Exception { + StringBuilder sb = new StringBuilder(); + + for (Map spec : specList) { + String name = (String) spec.get("name"); + // EIMS가 고정장을 요구할 경우를 대비한 len 파라미터 체크 (기본값 0 방어) + int len = spec.get("len") != null ? (Integer) spec.get("len") : 0; + String type = (String) spec.get("type"); + String rawValue = String.valueOf(data.getOrDefault(name, "")); + + // len 값이 없으면 변환 없이 바로 이어붙임 (JSON 통신용) + if (len == 0) { + sb.append(rawValue); + continue; + } + + // len 값이 있으면 고정장 통신 규칙 적용 + byte[] rawBytes = rawValue.getBytes("EUC-KR"); + if (rawBytes.length > len) { + throw new IllegalArgumentException(name + " 길이가 " + len + " 바이트를 초과합니다."); + } + + int padLength = len - rawBytes.length; + StringBuilder paddedValue = new StringBuilder(rawValue); + + if ("NUMBER".equalsIgnoreCase(type)) { + for (int i = 0; i < padLength; i++) paddedValue.insert(0, "0"); + } else { + for (int i = 0; i < padLength; i++) paddedValue.append(" "); + } + sb.append(paddedValue.toString()); + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/support/DynamicSchemaValidator.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/support/DynamicSchemaValidator.java new file mode 100644 index 000000000..711674832 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/support/DynamicSchemaValidator.java @@ -0,0 +1,47 @@ +package io.shinhanlife.dap.lib.adapter.support; + +import org.springframework.stereotype.Service; +import java.util.List; +import java.util.Map; + +/** + * @package io.shinhanlife.dap.lib.adapter.support + * @className DynamicSchemaValidator + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Service +public class DynamicSchemaValidator { + + public boolean validate(List> specList, Map data, StringBuilder errorLog) { + for (Map spec : specList) { + String name = (String) spec.get("name"); + String type = (String) spec.get("type"); + boolean isRequired = spec.get("required") != null && (Boolean) spec.get("required"); + Object value = data.get(name); + + // 1. 필수값 체크 + if (isRequired && (value == null || String.valueOf(value).trim().isEmpty())) { + errorLog.append(String.format("[%s] 필드는 필수 입력 항목입니다. ", name)); + return false; + } + + // 2. 타입 체크 (값이 있을 때만) + if (value != null && !String.valueOf(value).trim().isEmpty()) { + if ("NUMBER".equalsIgnoreCase(type) && !String.valueOf(value).matches("-?\\d+(\\.\\d+)?")) { + errorLog.append(String.format("[%s] 필드는 숫자여야 합니다. ", name)); + return false; + } + } + } + return true; + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/support/MciTemplate.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/support/MciTemplate.java new file mode 100644 index 000000000..a808d9f0a --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/support/MciTemplate.java @@ -0,0 +1,67 @@ +package io.shinhanlife.dap.lib.adapter.support; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.shinhanlife.dap.lib.adapter.exception.MciCommunicationException; +import io.shinhanlife.dap.lib.adapter.sender.EimsSender; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * @package io.shinhanlife.dap.lib.adapter.support + * @className MciTemplate + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ *
+ * 
+ */ +@Slf4j +@Component +@RequiredArgsConstructor +public class MciTemplate { + + private final EimsSender httpEimsSender; + private final ObjectMapper objectMapper; + + /** + * MCI 인터페이스를 호출하고 결과를 지정된 타입으로 반환합니다. + * + * @param interfaceId 호출할 MCI 인터페이스 ID + * @param requestDto 요청 데이터 객체 + * @param responseType 응답을 매핑할 클래스 타입 + * @param 요청 객체 타입 + * @param 응답 객체 타입 + * @return 매핑된 응답 객체 + * @throws MciCommunicationException 통신 또는 파싱 실패 시 예외 발생 + */ + public R call(String interfaceId, T requestDto, Class responseType) { + log.info(" [MciTemplate] 시작 - Interface ID: {}", interfaceId); + + try { + String payload = objectMapper.writeValueAsString(requestDto); + log.debug(" [MciTemplate] 전송 페이로드: {}", payload); + + String responseJson = httpEimsSender.send(interfaceId, payload); + log.debug(" [MciTemplate] 수신 응답 JSON: {}", responseJson); + + R response = objectMapper.readValue(responseJson, responseType); + log.info(" [MciTemplate] 완료 - Interface ID: {}", interfaceId); + + return response; + + } catch (JsonProcessingException e) { + log.error(" [MciTemplate] JSON 변환 중 오류 발생", e); + throw new MciCommunicationException("MCI 통신 중 JSON 파싱 오류", e); + } catch (Exception e) { + log.error(" [MciTemplate] 통신 중 오류 발생", e); + throw new MciCommunicationException("MCI 통신 실패", e); + } + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/support/ResultStandardizer.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/support/ResultStandardizer.java new file mode 100644 index 000000000..f3105928a --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/support/ResultStandardizer.java @@ -0,0 +1,53 @@ +package io.shinhanlife.dap.lib.adapter.support; + +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +/** + * @package io.shinhanlife.dap.lib.adapter.support + * @className ResultStandardizer + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Component +public class ResultStandardizer { + + // 오라클 조회 결과(Map)의 키 값을 카멜 케이스로 변환 (스키마 매핑) + public Map standardize(Map rawData) { + Map standardMap = new HashMap<>(); + + rawData.forEach((key, value) -> { + String camelKey = convertToCamelCase(key); + standardMap.put(camelKey, value); + }); + + return standardMap; + } + + private String convertToCamelCase(String snakeCase) { + if (snakeCase == null || snakeCase.isEmpty()) return snakeCase; + + StringBuilder result = new StringBuilder(); + boolean nextIsUpper = false; + + for (char c : snakeCase.toLowerCase().toCharArray()) { + if (c == '_') { + nextIsUpper = true; + } else { + result.append(nextIsUpper ? Character.toUpperCase(c) : c); + nextIsUpper = false; + } + } + return result.toString(); + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/support/TicketManager.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/support/TicketManager.java new file mode 100644 index 000000000..4339f9c91 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/support/TicketManager.java @@ -0,0 +1,96 @@ +package io.shinhanlife.dap.lib.adapter.support; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * @package io.shinhanlife.dap.lib.adapter.support + * @className TicketManager + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +@Component +public class TicketManager { + + // 로컬 시뮬레이션을 위한 인메모리 저장소 (운영 환경에서는 Redis 등을 사용) + private final Map ticketStore = new ConcurrentHashMap<>(); + + // 가짜 EAI 작업(10초 대기)을 실행할 백그라운드 쓰레드 풀 + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(5); + + /** + * 새로운 비동기 작업을 접수하고 티켓을 발급합니다. + */ + public String issueTicket(String interfaceId, String payload) { + String ticketId = "TICKET-" + UUID.randomUUID().toString(); + + Ticket ticket = new Ticket(); + ticket.setTicketId(ticketId); + ticket.setStatus("PROCESSING"); + ticket.setMessage("EAI 시스템에서 데이터 처리 중입니다..."); + + ticketStore.put(ticketId, ticket); + + log.info(" [TicketManager] 비동기 작업 티켓 발급 완료: {}", ticketId); + + // 10초 뒤에 자동으로 작업을 완료 상태로 변경하는 백그라운드 시뮬레이터 실행 + simulateEaiProcessing(ticketId, interfaceId); + + return ticketId; + } + + /** + * 특정 티켓의 현재 상태를 조회합니다. + */ + public Ticket getTicketStatus(String ticketId) { + return ticketStore.getOrDefault(ticketId, new Ticket("NOT_FOUND", "해당 티켓을 찾을 수 없습니다.")); + } + + /** + * 10초 뒤에 상태를 COMPLETED로 변경하여 진짜 EAI 배치가 끝난 것처럼 흉내냅니다. + */ + private void simulateEaiProcessing(String ticketId, String interfaceId) { + scheduler.schedule(() -> { + Ticket ticket = ticketStore.get(ticketId); + if (ticket != null) { + ticket.setStatus("COMPLETED"); + ticket.setMessage("EAI 배치가 정상적으로 완료되었습니다."); + // 가짜 최종 결과 데이터 주입 + ticket.setResultData("{\"resultCode\":\"0000\", \"interfaceId\":\"" + interfaceId + "\", \"processedRecords\":50000}"); + ticketStore.put(ticketId, ticket); + log.info(" [TicketManager] EAI 비동기 작업 시뮬레이션 완료! (Ticket: {})", ticketId); + } + }, 10, TimeUnit.SECONDS); // 10초 지연 + } + + // 티켓 상태를 담을 내부 DTO 클래스 + @lombok.Data + public static class Ticket { + private String ticketId; + private String status; // PROCESSING, COMPLETED, FAILED, NOT_FOUND + private String message; + private String resultData; // 최종 완료 시 담길 데이터 + + public Ticket() {} + public Ticket(String status, String message) { + this.status = status; + this.message = message; + } + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/support/ToolExecutionService.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/support/ToolExecutionService.java new file mode 100644 index 000000000..cd0bb3006 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/support/ToolExecutionService.java @@ -0,0 +1,90 @@ +package io.shinhanlife.dap.lib.adapter.support; + +import io.shinhanlife.dap.lib.adapter.connector.LegacyEimsConnector; +import io.shinhanlife.dap.lib.adapter.dto.ErrorDetail; +import io.shinhanlife.dap.lib.adapter.dto.JsonRpcRequest; +import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse; +import io.shinhanlife.dap.lib.adapter.dto.Params; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +/** + * @package io.shinhanlife.dap.lib.adapter.support + * @className ToolExecutionService + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ToolExecutionService { + + // 방어막(서킷/캐시)이 적용된 커넥터 주입 + private final LegacyEimsConnector legacyEimsConnector; + + /** + * AI Agent가 호출한 툴을 실제 레거시 커넥터로 전달합니다. + */ + public JsonRpcResponse executeTool(JsonRpcRequest request) { + Params params = request.getParams(); + + // 1. 필수 파라미터 검증 + if (params == null || params.getName() == null) { + return createErrorResponse(request.getId(), -32602, "Invalid params: 'name' is required"); + } + + try { + // 2. Connector의 executeByTool 메서드 호출 + // [참고] connector에 선언된 파라미터 구조에 맞게 매핑합니다. + String result = legacyEimsConnector.executeByTool( + params.getRoutingType(), + params.getInterfaceId(), + params.getData(), + params.getSpec() + ); + + // 3. 성공 응답 생성 (result가 JSON 문자열일 경우, 실제 DTO로 변환하여 반환하면 더 좋습니다) + return createSuccessResponse(request.getId(), result); + + } catch (Exception e) { + log.error(" [ToolExecution] 커넥터 호출 실패: RoutingType={}, Error={}", params.getRoutingType(), e.getMessage()); + return createErrorResponse(request.getId(), -32000, "Connector error: " + e.getMessage()); + } + } + + /** + * AI Agent를 위한 툴 목록 스키마 반환 + */ + public JsonRpcResponse getToolList(String requestId) { + // 기존에 정의한 Tool 스키마 리스트 반환 로직... + Map result = new HashMap<>(); + // ... (Tool 명세 내용) + return createSuccessResponse(requestId, result); + } + + private JsonRpcResponse createSuccessResponse(String id, Object result) { + JsonRpcResponse response = new JsonRpcResponse(); + response.setId(id); + response.setResult(result); + return response; + } + + private JsonRpcResponse createErrorResponse(String id, int code, String message) { + JsonRpcResponse response = new JsonRpcResponse(); + response.setId(id); + response.setError(new ErrorDetail(code, message)); + return response; + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/test/MockEimsHttpServer.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/test/MockEimsHttpServer.java new file mode 100644 index 000000000..5fa174ced --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/test/MockEimsHttpServer.java @@ -0,0 +1,85 @@ +package io.shinhanlife.dap.lib.adapter.test; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.core.io.ClassPathResource; +import java.util.Map; + +/** + * @package io.shinhanlife.dap.lib.adapter.test + * @className MockEimsHttpServer + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +@RestController +@RequestMapping("/api") +public class MockEimsHttpServer { + + private final ObjectMapper objectMapper; + + public MockEimsHttpServer(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @PostMapping("/gateway") + public Map mockEimsReceiver( + @RequestHeader(value = "X-Trace-Id", required = false) String traceId, + @RequestBody Map request) { + + String interfaceId = (String) request.get("interfaceId"); + String data = (String) request.get("data"); + + log.info(" [가짜 EIMS 서버] HTTP 요청 수신 완료!"); + log.info(" 수신된 Trace-ID (거래고유번호): {}", traceId != null ? traceId : "없음"); + log.info(" 인터페이스ID: {}, 데이터: [{}]", interfaceId, data); + + // No-Code Mock 응답 생성 (JSON 설정 파일 기반) + try { + ClassPathResource resource = new ClassPathResource("mock-responses.json"); + Map mockDataMap = objectMapper.readValue(resource.getInputStream(), Map.class); + + if (mockDataMap.containsKey(interfaceId)) { + return (Map) mockDataMap.get(interfaceId); + } + } catch (Exception e) { + log.warn("JSON 파싱 에러 또는 mock 파일 읽기 실패 (기본 응답 반환)", e); + } + + // 기본 응답 + return Map.of( + "status", "404", + "message", "MOCK 데이터가 정의되지 않았습니다.", + "receivedLength", data.length() + ); + } + + @PostMapping("/mock/esb/api") + public String mockEsbReceiver(@RequestBody String xmlPayload) { + log.info(" [가짜 ESB 서버] MCI/ESB 요청 수신 완료!"); + log.info(" 수신된 XML 전문: {}", xmlPayload); + + // MciEimsSender가 기대하는 JSON 변환용 XML 포맷 응답 + return "SUCCESSMOCK_MCI_EIMS_RECEIVE_SUCCESS정상 처리되었습니다."; + } + + @PostMapping("/mock/esb/string") + public String mockEsbStringReceiver(@RequestBody(required = false) String payload) { + log.info(" [가짜 ESB 서버] MCI String 요청 수신 완료!"); + log.info(" 수신된 String 전문: {}", payload); + + // MciSampleStringResponse 에 맞게 고정 길이 응답 생성 + // name (10), age (3), joinDate (8), statusCode (2) + // targetList (30) -> MciSampleTargetDto (itemCode 5, itemValue 5) x 3 + return "홍길동 03020260901OKA0001B0001A0002B0002A0003B0003"; + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/test/MockEimsTcpServer.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/test/MockEimsTcpServer.java new file mode 100644 index 000000000..32f9d3429 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/test/MockEimsTcpServer.java @@ -0,0 +1,128 @@ +package io.shinhanlife.dap.lib.adapter.test; + +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.core.io.ClassPathResource; +import java.util.Map; + +@Slf4j +@Component +// 대신 "local" 환경(application-local.properties)에서만 가짜 소켓 서버가 켜지도록 보장합니다. +/** + * @package io.shinhanlife.dap.lib.adapter.test + * @className MockEimsTcpServer + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Profile("local") +public class MockEimsTcpServer { + + private final ObjectMapper objectMapper; + + public MockEimsTcpServer(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Value("${eims.tcp.port:8090}") + private int port; + + @Value("${server.port:8081}") + private int serverPort; + + private ServerSocket serverSocket; + private boolean running = true; + + @EventListener(ApplicationReadyEvent.class) + public void startTcpServer() { + // MSA 환경에서는 각 툴 Pod 내부에서 독립적인 가짜 EIMS 서버가 실행되도록 허용 + log.info(" [가짜 EIMS 서버] 로컬 테스트 환경용 EIMS TCP 서버 가동을 준비합니다."); + + // Java 21 가상 스레드를 사용하여 메인 서버 가동에 방해 없이 백그라운드에서 실행 + Thread.ofVirtual().start(() -> { + try { + serverSocket = new ServerSocket(port); + log.info(" [가짜 EIMS 서버] 로컬 TCP 소켓 서버 가동 완료 (Port: {})", port); + + while (running) { + Socket clientSocket = serverSocket.accept(); + + // 연결된 요청을 별도 스레드로 처리 + Thread.ofVirtual().start(() -> handleClient(clientSocket)); + } + } catch (Exception e) { + if (running) log.error(" 가짜 TCP 서버 에러: {}", e.getMessage()); + } + }); + } + + private void handleClient(Socket socket) { + try (socket) { + InputStream is = socket.getInputStream(); + OutputStream os = socket.getOutputStream(); + + byte[] buffer = new byte[4096]; + int readByte = is.read(buffer); + + if (readByte != -1) { + String receivedData = new String(buffer, 0, readByte, "EUC-KR"); + log.info(" [가짜 EIMS 서버] TCP 데이터 수신 완료!"); + log.info(" 수신된 전문: [{}]", receivedData); + + String responseData = "{\"status\":\"404\",\"message\":\"MOCK 데이터가 정의되지 않았습니다.\"}"; + try { + int braceIndex = receivedData.indexOf('{'); + String interfaceId = ""; + if (braceIndex > 0) { + interfaceId = receivedData.substring(0, braceIndex).trim(); + } else if (braceIndex == -1) { + interfaceId = receivedData.trim(); + } + + ClassPathResource resource = new ClassPathResource("mock-responses.json"); + Map mockDataMap = objectMapper.readValue(resource.getInputStream(), Map.class); + + if (mockDataMap.containsKey(interfaceId)) { + responseData = objectMapper.writeValueAsString(mockDataMap.get(interfaceId)); + } + } catch (Exception e) { + log.warn("TCP JSON 파싱 에러 또는 파일 읽기 실패", e); + } + + // 응답 데이터 송신 + os.write(responseData.getBytes("EUC-KR")); + os.flush(); + } + } catch (Exception e) { + log.error(" 가짜 TCP 클라이언트 처리 에러: {}", e.getMessage()); + } + } + + @PreDestroy + public void stopTcpServer() { + this.running = false; + try { + if (serverSocket != null) serverSocket.close(); + } catch (Exception ignored) {} + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/test/MockJspServer.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/test/MockJspServer.java new file mode 100644 index 000000000..f1939d1e5 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/test/MockJspServer.java @@ -0,0 +1,58 @@ +package io.shinhanlife.dap.lib.adapter.test; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +/** + * @package io.shinhanlife.dap.lib.adapter.test + * @className MockJspServer + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +@RestController +@RequestMapping("/mock") +public class MockJspServer { + + // ========================================== + // 1. Form Data 방식 테스트 수신부 (jsp-form) + // ========================================== + @PostMapping(value = "/jsp-form", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + public String mockJspFormReceiver( + @RequestParam("interfaceId") String interfaceId, + @RequestParam("data") String data) { + + log.info(" [가짜 JSP 서버] 폼 데이터(Form) 수신 완료!"); + log.info(" 파라미터 파싱 확인 - ID: {}, DATA: [{}]", interfaceId, data); + + // 실제 JSP 서버처럼 앞뒤에 의미 없는 줄바꿈(엔터)과 공백을 잔뜩 넣어서 리턴합니다. + return "\n\n SUCCESS_FROM_MOCK_JSP_FORM \n\n"; + } + + // ========================================== + // 2. JSON 방식 테스트 수신부 (jsp-json) + // ========================================== + @PostMapping(value = "/jsp-json", consumes = MediaType.APPLICATION_JSON_VALUE) + public String mockJspJsonReceiver(@RequestBody Map request) { + + String interfaceId = request.get("interfaceId"); + String data = request.get("data"); + + log.info(" [가짜 JSP 서버] 제이슨(JSON) 수신 완료!"); + log.info(" JSON 파싱 확인 - ID: {}, DATA: [{}]", interfaceId, data); + + // 여기도 마찬가지로 쓰레기 여백을 넣어줍니다. + return "\n\n SUCCESS_FROM_MOCK_JSP_JSON \n\n"; + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/util/LegacyDataTransformer.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/util/LegacyDataTransformer.java new file mode 100644 index 000000000..88aef4e05 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/util/LegacyDataTransformer.java @@ -0,0 +1,92 @@ +package io.shinhanlife.dap.lib.adapter.util; + +import lombok.extern.slf4j.Slf4j; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @package io.shinhanlife.dap.lib.adapter.util + * @className LegacyDataTransformer + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +public class LegacyDataTransformer { + + /** + * AI 모델이 보낸 자유로운 형태의 data를 레거시 시스템 규격(spec)에 맞게 강제 변환합니다. + * + * @param data AI가 보낸 파라미터 맵 + * @param spec 레거시 시스템이 요구하는 파라미터 스펙 (name, type, maxLength, defaultValue, required 등) + * @return 엄격하게 정제된 파라미터 맵 + */ + public static Map transform(Map data, List> spec) { + if (spec == null || spec.isEmpty()) { + return data != null ? data : new HashMap<>(); + } + + Map transformedData = new HashMap<>(); + + for (Map fieldSpec : spec) { + String fieldName = (String) fieldSpec.get("name"); + if (fieldName == null) continue; + + Object rawValue = data != null ? data.get(fieldName) : null; + Object finalValue = rawValue; + + // 1. 기본값(Default Value) 주입 + if (finalValue == null && fieldSpec.containsKey("defaultValue")) { + finalValue = fieldSpec.get("defaultValue"); + log.debug(" [DataTransformer] '{}' 필드 누락 -> 기본값 '{}' 주입", fieldName, finalValue); + } + + // 2. 강제 형변환 (Type Coercion) + String type = (String) fieldSpec.getOrDefault("type", "string"); + if (finalValue != null) { + if ("string".equalsIgnoreCase(type) && !(finalValue instanceof String)) { + finalValue = String.valueOf(finalValue); + log.debug(" [DataTransformer] '{}' 필드 강제 String 형변환", fieldName); + } else if ("number".equalsIgnoreCase(type) && finalValue instanceof String) { + try { + finalValue = Long.parseLong((String) finalValue); + log.debug(" [DataTransformer] '{}' 필드 강제 Number 형변환", fieldName); + } catch (NumberFormatException e) { + log.warn(" [DataTransformer] '{}' 필드 Number 형변환 실패. 기존 값 유지", fieldName); + } + } + } + + // 3. 길이 제한 (Truncation / MaxLength) + if (finalValue instanceof String && fieldSpec.containsKey("maxLength")) { + int maxLength = (Integer) fieldSpec.get("maxLength"); + String strVal = (String) finalValue; + if (strVal.length() > maxLength) { + finalValue = strVal.substring(0, maxLength); + log.warn(" [DataTransformer] '{}' 필드 길이 초과! {}자로 강제 자름 (Truncated)", fieldName, maxLength); + } + } + + // 4. 필수값(Required) 누락 체크 (에러를 던지지 않고 빈 문자열 강제 주입하여 레거시 팅김 방지) + boolean isRequired = (Boolean) fieldSpec.getOrDefault("required", false); + if (isRequired && finalValue == null) { + log.error(" [DataTransformer] 필수 필드 '{}' 누락! 강제 공백 주입하여 시스템 장애 방지", fieldName); + finalValue = "string".equalsIgnoreCase(type) ? "" : 0; + } + + if (finalValue != null) { + transformedData.put(fieldName, finalValue); + } + } + + return transformedData; + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/util/PiiMaskingLogbackConverter.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/util/PiiMaskingLogbackConverter.java new file mode 100644 index 000000000..50bb7d7e8 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/util/PiiMaskingLogbackConverter.java @@ -0,0 +1,35 @@ +package io.shinhanlife.dap.lib.adapter.util; + +import ch.qos.logback.classic.pattern.MessageConverter; +import ch.qos.logback.classic.spi.ILoggingEvent; + +/** + * Logback 커스텀 컨버터 + * 모든 로그 메시지(%msg)가 파일이나 콘솔에 찍히기 직전에 이 클래스를 거쳐가게 됩니다. + * 여기서 PiiMaskingUtils.mask()를 호출하여 PII(주민번호, 계좌번호 등)를 안전하게 별표(*) 처리합니다. + */ +/** + * @package io.shinhanlife.dap.lib.adapter.util + * @className PiiMaskingLogbackConverter + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +public class PiiMaskingLogbackConverter extends MessageConverter { + + @Override + public String convert(ILoggingEvent event) { + // 원본 로그 메시지를 가져옵니다. + String originalMessage = super.convert(event); + + // 정규식을 이용하여 개인정보가 포함되어 있으면 마스킹 처리하여 반환합니다. + return PiiMaskingUtils.mask(originalMessage); + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/util/PiiMaskingUtils.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/util/PiiMaskingUtils.java new file mode 100644 index 000000000..d98c57046 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/adapter/util/PiiMaskingUtils.java @@ -0,0 +1,77 @@ +package io.shinhanlife.dap.lib.adapter.util; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @package io.shinhanlife.dap.lib.adapter.util + * @className PiiMaskingUtils + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +public class PiiMaskingUtils { + + // 1. 주민등록번호 패턴 (ex: 900101-1234567 또는 9001011234567) + private static final Pattern RRN_PATTERN = Pattern.compile("(\\d{6})[-]?([1-4]\\d{6})"); + + // 2. 휴대전화번호 패턴 (ex: 010-1234-5678) + private static final Pattern PHONE_PATTERN = Pattern.compile("(01[016789])[-]?(\\d{3,4})[-]?(\\d{4})"); + + // 3. 신한라이프 계좌/증권번호 패턴 (단순 예시용 계좌번호 11~14자리) + private static final Pattern ACCOUNT_PATTERN = Pattern.compile("(\\d{3})-?(\\d{3})-?(\\d{5,8})"); + + public static String mask(String input) { + if (input == null || input.isEmpty()) { + return input; + } + + String masked = input; + + // [1] 주민번호 뒷자리 마스킹 (첫자리 성별 식별자는 남기고 마스킹: 900101-1******) + Matcher rrnMatcher = RRN_PATTERN.matcher(masked); + StringBuffer rrnBuffer = new StringBuffer(); + while (rrnMatcher.find()) { + String firstPart = rrnMatcher.group(1); + String secondPart = rrnMatcher.group(2); + rrnMatcher.appendReplacement(rrnBuffer, firstPart + "-" + secondPart.charAt(0) + "******"); + } + rrnMatcher.appendTail(rrnBuffer); + masked = rrnBuffer.toString(); + + // [2] 전화번호 중간자리 마스킹 (010-****-5678) + Matcher phoneMatcher = PHONE_PATTERN.matcher(masked); + StringBuffer phoneBuffer = new StringBuffer(); + while (phoneMatcher.find()) { + String p1 = phoneMatcher.group(1); + String p2 = phoneMatcher.group(2); + String p3 = phoneMatcher.group(3); + String maskedP2 = p2.replaceAll(".", "*"); + phoneMatcher.appendReplacement(phoneBuffer, p1 + "-" + maskedP2 + "-" + p3); + } + phoneMatcher.appendTail(phoneBuffer); + masked = phoneBuffer.toString(); + + // [3] 계좌번호 뒷자리 마스킹 (110-123-********) + Matcher accMatcher = ACCOUNT_PATTERN.matcher(masked); + StringBuffer accBuffer = new StringBuffer(); + while (accMatcher.find()) { + String a1 = accMatcher.group(1); + String a2 = accMatcher.group(2); + String a3 = accMatcher.group(3); + String maskedA3 = a3.replaceAll(".", "*"); + accMatcher.appendReplacement(accBuffer, a1 + "-" + a2 + "-" + maskedA3); + } + accMatcher.appendTail(accBuffer); + masked = accBuffer.toString(); + + return masked; + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpAnyOf.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpAnyOf.java new file mode 100644 index 000000000..a781ba3ad --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpAnyOf.java @@ -0,0 +1,19 @@ +package io.shinhanlife.dap.lib.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * MCP 스키마 생성 시 anyOf (해당 필드들 중 최소 1개 이상 필수) 제약을 부여합니다. + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface McpAnyOf { + /** + * anyOf 제약에 포함될 필드명 목록 + * 예: @McpAnyOf({"claimNo", "contractNo"}) + */ + String[] value(); +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java new file mode 100644 index 000000000..aa0d6aeba --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java @@ -0,0 +1,76 @@ +package io.shinhanlife.dap.lib.annotation; + + +/** + * @package io.shinhanlife.dap.lib.annotation + * @className McpFunction + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +import java.lang.annotation.*; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface McpFunction { + String displayName(); // 사람이 읽는 라벨 (예: "고객 조회 툴") + String name(); // MCP 서브툴 명칭 (예: "customer_search") + String description(); + String prompt() default ""; + String mappingId() default ""; + + + /** + * Tool 입력 JSON Schema를 인라인으로 지정한다. 지정하지 않으면 요청 DTO에서 자동 생성한다. + */ + String inputSchema() default "{}"; + + /** + * 복합 조건(anyOf 등)이 필요한 Tool의 입력 JSON Schema 클래스패스 경로다. + * inputSchemaResource가 지정되면 inputSchema 및 DTO 자동 생성보다 우선한다. + */ + // 추가: Redis 자동 등록 및 Heartbeat 대상 여부 제어 + String inputSchemaResource() default ""; + + /** + * Tool response JSON Schema. When unset, output validation is skipped. + */ + String outputSchema() default "{}"; + + /** + * Classpath resource for a complex Tool response JSON Schema. + * This value has priority over outputSchema. + */ + String outputSchemaResource() default ""; + boolean register() default false; + + // 추가: 툴 목록 노출 여부 제어 (false 시 라우팅은 되나 목록에서 숨김) + boolean visible() default true; + + // 추가: HITL 승인 체계 지원 (실행 전 사용자 승인 필요 여부) + boolean requiresApproval() default false; + + boolean readOnlyHint() default false; + boolean destructiveHint() default false; + boolean idempotentHint() default false; + boolean openWorldHint() default false; + + /** Version exposed as _meta.version in the Tool Manifest. */ + String version() default "1.0.0"; + + /** Maximum execution time exposed as _meta.timeoutMillis in the Tool Manifest. */ + long timeoutMillis() default 300000L; + + /** Whether the Tool is available for MCP exposure. */ + boolean enabled() default true; + + // 추가: 툴 별 기본 Timeout 설정 (기본 300초 = 300000ms) +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpOutputSchema.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpOutputSchema.java new file mode 100644 index 000000000..cfb37dca3 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpOutputSchema.java @@ -0,0 +1,17 @@ +package io.shinhanlife.dap.lib.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a Tool response DTO for automatic output JSON Schema generation. + * Field constraints are declared with {@link McpValidation}. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface McpOutputSchema { +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpParameter.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpParameter.java new file mode 100644 index 000000000..0497a02a3 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpParameter.java @@ -0,0 +1,26 @@ +package io.shinhanlife.dap.lib.annotation; + + +/** + * @package io.shinhanlife.dap.lib.annotation + * @className McpParameter + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +import java.lang.annotation.*; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface McpParameter { + String description(); + boolean required() default false; +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpTool.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpTool.java new file mode 100644 index 000000000..340177865 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpTool.java @@ -0,0 +1,33 @@ +package io.shinhanlife.dap.lib.annotation; + + +/** + * @package io.shinhanlife.dap.lib.annotation + * @className McpTool + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +import org.springframework.core.annotation.AliasFor; +import org.springframework.stereotype.Component; + +import java.lang.annotation.*; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface McpTool { + @AliasFor(annotation = Component.class) + String value() default ""; + + String categoryKey() default "common"; + String routingType() default "HTTP"; +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java new file mode 100644 index 000000000..4353edf67 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java @@ -0,0 +1,36 @@ +package io.shinhanlife.dap.lib.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @package io.shinhanlife.dap.lib.annotation + * @className McpValidation + * @description Declares JSON Schema validation constraints for MCP tool input fields + * @author 0986406 + * @create 2026.07.27 + *
+ * ---------- revision history ----------
+ * date       author    description
+ * ---------- --------- ---------------------------
+ * 2026.07.27 0986406    initial creation
+ * 
+ */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface McpValidation { + boolean required() default false; + String pattern() default ""; + long minimum() default Long.MIN_VALUE; + long maximum() default Long.MAX_VALUE; + int minLength() default -1; + int maxLength() default -1; + String[] allowedValues() default {}; + String format() default ""; + boolean nullable() default false; String defaultValue() default ""; + String[] examples() default {}; +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/aop/ToolSlaMonitoringAspect.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/aop/ToolSlaMonitoringAspect.java new file mode 100644 index 000000000..a7aef5eb3 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/aop/ToolSlaMonitoringAspect.java @@ -0,0 +1,83 @@ +package io.shinhanlife.dap.lib.aop; + + +/** + * @package io.shinhanlife.dap.lib.aop + * @className ToolSlaMonitoringAspect + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +import io.shinhanlife.dap.lib.annotation.McpFunction; +import io.shinhanlife.dap.lib.config.McpProperties; +import java.lang.reflect.Method; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; +import org.springframework.util.StopWatch; + +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class ToolSlaMonitoringAspect { + + private final McpProperties mcpProperties; + + // @McpFunction 어노테이션이 붙은 모든 비즈니스 툴 메서드 실행을 가로챕니다. + @Around("@annotation(McpFunction)") + public Object monitorToolSla(ProceedingJoinPoint joinPoint) throws Throwable { + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + McpFunction functionAnnotation = method.getAnnotation(McpFunction.class); + + // 네임스페이스 자동 주입 로직을 반영하여 최종 툴 이름을 산출합니다. + String baseName = functionAnnotation.name(); + String finalName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty() + ? mcpProperties.getNamespace() + "_" + baseName + : baseName; + + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + + try { + // 실제 비즈니스 로직(툴) 실행 + Object result = joinPoint.proceed(); + + stopWatch.stop(); + long timeMillis = stopWatch.getTotalTimeMillis(); + + // SLA 기준을 초과하면 (예: 2초 이상) 경고 로깅 처리 가능 + if (timeMillis > 2000) { + log.warn(" [SLA 경고] Tool: {} | 소요시간: {}ms | 상태: SLOW_RESPONSE", finalName, timeMillis); + } else { + log.info(" [SLA 추적] Tool: {} | 소요시간: {}ms | 상태: SUCCESS", finalName, timeMillis); + } + + return result; + + } catch (Throwable e) { + if (stopWatch.isRunning()) { + stopWatch.stop(); + } + long timeMillis = stopWatch.getTotalTimeMillis(); + + // 에러 발생 시 명확하게 실패 로그 기록 + log.error(" [SLA 장애] Tool: {} | 소요시간: {}ms | 상태: FAILED | 사유: {}", finalName, timeMillis, e.getMessage()); + + // 원래 흐름대로 예외를 던져서 게이트웨이나 상위 로직이 에러를 처리하게 함 + throw e; + } + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/CorsConfig.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/CorsConfig.java new file mode 100644 index 000000000..8fadfa7ce --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/CorsConfig.java @@ -0,0 +1,34 @@ +package io.shinhanlife.dap.lib.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * @package io.shinhanlife.dap.lib.config + * @className CorsConfig + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Configuration +public class CorsConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") // 모든 엔드포인트에 대해 CORS 허용 + .allowedOriginPatterns("*") // 외부 Agent Builder 등 모든 오리진 허용 + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH") // 허용할 HTTP 메서드 + .allowedHeaders("*") // 모든 헤더 허용 + .exposedHeaders("Mcp-Session-Id") // MCP-HTTP 세션 아이디 노출 허용 + .allowCredentials(true) // 쿠키/인증 정보 허용 + .maxAge(3600); // preflight 요청 캐시 시간 (초 단위) + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/GlowCommunicationProperties.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/GlowCommunicationProperties.java new file mode 100644 index 000000000..eacf69e3c --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/GlowCommunicationProperties.java @@ -0,0 +1,85 @@ +package io.shinhanlife.dap.lib.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * [Glow Framework 통신 환경 설정 클래스] + * application-glow-local.yml 의 'glow.communication' 하위 설정값들을 + * 자바 객체(Bean)로 매핑하여 제공합니다. + */ +/** + * @package io.shinhanlife.dap.lib.config + * @className GlowCommunicationProperties + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Component +@Getter +@Setter +@ConfigurationProperties(prefix = "glow.communication") +public class GlowCommunicationProperties { + + private Common common = new Common(); + private Http http = new Http(); + private Mci mci = new Mci(); + private ExtMci extmci = new ExtMci(); + private Eai eai = new Eai(); + private Websocket websocket = new Websocket(); + + @Getter @Setter + public static class Common { + private String envType; // 대내표준 헤더의 환경 타입정보 (D, T, P) + } + + @Getter @Setter + public static class Http { + private int connectionTimeout; // 연결 타임아웃 시간 (초 단위) + private int readTimeout; // 읽기 타임아웃 시간 (초 단위) + } + + @Getter @Setter + public static class Mci { + private String host; + private int port; + private String uri; + private String receiveUri; + private int connectionTimeout; + private int readTimeout; + private String encoding; + } + + @Getter @Setter + public static class ExtMci { + private String host; + private int port; + private String uri; + private String jsonUri; + private String receiveUri; + private int connectionTimeout; + private int readTimeout; + private String encoding; + } + + @Getter @Setter + public static class Eai { + private String host; + private int port; + } + + @Getter @Setter + public static class Websocket { + private String endpoint; + private String allowedOrigins; + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/McpProperties.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/McpProperties.java new file mode 100644 index 000000000..1ff72e5ce --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/McpProperties.java @@ -0,0 +1,37 @@ +package io.shinhanlife.dap.lib.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.Map; + +/** + * @package io.shinhanlife.dap.lib.config + * @className McpProperties + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Data +@Configuration +@ConfigurationProperties(prefix = "mcp") +public class McpProperties { + + private String namespace; + private Manifest manifest = new Manifest(); + + @Data + public static class Manifest { + private String bundleId; + private String namePrefix; + } + +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/MybatisConfig.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/MybatisConfig.java new file mode 100644 index 000000000..2912eb0a9 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/MybatisConfig.java @@ -0,0 +1,33 @@ +package io.shinhanlife.dap.lib.config; + +import javax.sql.DataSource; + +import org.apache.ibatis.session.SqlSessionFactory; +import org.mybatis.spring.SqlSessionFactoryBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @package io.shinhanlife.dap.lib.config + * @className MybatisConfig + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Configuration +public class MybatisConfig { + + @Bean + public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { + SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); + sessionFactory.setDataSource(dataSource); + return sessionFactory.getObject(); + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/P6SpySqlFormatter.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/P6SpySqlFormatter.java new file mode 100644 index 000000000..06cc4c69f --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/P6SpySqlFormatter.java @@ -0,0 +1,54 @@ +package io.shinhanlife.dap.lib.config; + +import com.p6spy.engine.logging.Category; +import com.p6spy.engine.spy.appender.MessageFormattingStrategy; + +/** + * @package io.shinhanlife.dap.lib.config + * @className P6SpySqlFormatter + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +public class P6SpySqlFormatter implements MessageFormattingStrategy { + + @Override + public String formatMessage(int connectionId, String now, long elapsed, + String category, String prepared, String sql, String url) { + + if (sql == null || sql.isBlank()) return ""; + if (Category.STATEMENT.getName().equals(category)) { + + String prettySQL = sql + .replaceAll("(?i)\\bSELECT\\b", "\nSELECT") + .replaceAll("(?i)\\bFROM\\b", "\n FROM") + .replaceAll("(?i)\\bWHERE\\b", "\n WHERE") + .replaceAll("(?i)\\bAND\\b", "\n AND") + .replaceAll("(?i)\\bOR\\b", "\n OR") + .replaceAll("(?i)\\bINNER JOIN\\b", "\n INNER JOIN") + .replaceAll("(?i)\\bLEFT JOIN\\b", "\n LEFT JOIN") + .replaceAll("(?i)\\bORDER BY\\b", "\n ORDER BY") + .replaceAll("(?i)\\bGROUP BY\\b", "\n GROUP BY") + .replaceAll("(?i)\\bINSERT INTO\\b", "\nINSERT INTO") + .replaceAll("(?i)\\bVALUES\\b", "\n VALUES") + .replaceAll("(?i)\\bUPDATE\\b", "\nUPDATE") + .replaceAll("(?i)\\bSET\\b", "\n SET") + .replaceAll("(?i)\\bDELETE FROM\\b", "\nDELETE FROM"); + + return String.format(""" + \n┌───────────────────────────────────────── + │ SQL [%dms] + │%s + └───────────────────────────────────────── + """, elapsed, prettySQL.indent(2).stripTrailing()); + } + return ""; + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/ToolSchemaConfiguration.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/ToolSchemaConfiguration.java new file mode 100644 index 000000000..0364a5e59 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/ToolSchemaConfiguration.java @@ -0,0 +1,18 @@ +package io.shinhanlife.dap.lib.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.shinhanlife.dap.lib.util.ToolSchemaResolver; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Common MCP Tool Schema Bean configuration. + */ +@Configuration +public class ToolSchemaConfiguration { + + @Bean + public ToolSchemaResolver toolSchemaResolver(ObjectMapper objectMapper) { + return new ToolSchemaResolver(objectMapper); + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/GlowIntegrationCall.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/GlowIntegrationCall.java new file mode 100644 index 000000000..ce38c7853 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/GlowIntegrationCall.java @@ -0,0 +1,105 @@ +package io.shinhanlife.dap.lib.integration; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +// TODO: 실제 Glow Framework 의존성이 추가되면 아래 주석들을 풀고 사용하세요! +// import io.shinhanlife.glow.communication.dto.CommonHeader; +// import io.shinhanlife.glow.communication.dto.Transfer; +// import io.shinhanlife.glow.communication.module.eai.component.GlowEaiComponent; +// import io.shinhanlife.glow.communication.module.mci.component.GlowExtMciComponent; +// import io.shinhanlife.glow.communication.module.mci.component.GlowMciComponent; +// import io.shinhanlife.glow.communication.util.CommonHeaderFactory; + +/** + * [MCI / EAI 공통 연동 래퍼(Wrapper) 템플릿] + * 신한라이프 Glow Framework 개발표준정의서를 바탕으로 대내/대외망/EAI 통신을 + * MCP 툴에서 손쉽게 호출할 수 있도록 일원화한 컴포넌트입니다. + */ +/** + * @package io.shinhanlife.dap.lib.integration + * @className GlowIntegrationCall + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Component +@RequiredArgsConstructor +public class GlowIntegrationCall { + + // 1. 실제 의존성이 주입될 프레임워크 컴포넌트들 (임시 주석 처리) + /* + private final GlowMciComponent mci; + private final GlowEaiComponent eai; + private final GlowExtMciComponent extMci; + */ + + /** + * 1. 대외 MCI 호출 (타행, 금융결제원 등 외부 기관) + * 가이드 2.2.2에 명시된 필수 파라미터(기관코드, 종별코드, 업무코드, 거래코드)를 모두 포함합니다. + */ + /* + public Transfer callExtMci(String itrfId, String frbuCd, String cmouDutjCd, String cmouCssfCd, String cmouTraCd, S body, Class resBody) { + + // 1. 공통 헤더 생성 + CommonHeader header = CommonHeaderFactory.createRequestHeader(itrfId); + + // 2. 대외 전용 필수 코드 세팅 로직 (프레임워크 내부 스펙에 맞게 가공) + // (예: 헤더에 해당 속성들을 주입하거나 Transfer 객체에 싣는 과정 추가) + + // 3. Transfer 객체 빌드 + Transfer req = Transfer.builder() + .header(header) + .body(body) + .build(); + + // 4. 대외 MCI 컴포넌트를 통해 최종 전송 + return extMci.call(req, resBody); + } + */ + + /** + * 2. EAI 호출 (대내망 중계기) + * 가이드 2.3에 명시된 대로 수신서비스 ID 없이 인터페이스 ID(itrfId)만 필수로 받습니다. + */ + /* + public Transfer callEai(String itrfId, S body, Class resBody) { + + // 1. EAI는 인터페이스 ID만으로 심플하게 헤더 생성 + CommonHeader header = CommonHeaderFactory.createRequestHeader(itrfId); + + // 2. Transfer 객체 빌드 + Transfer req = Transfer.builder() + .header(header) + .body(body) + .build(); + + // 3. EAI 컴포넌트를 통해 최종 전송 + return eai.call(req, resBody); + } + */ + + /** + * 3. 대내 MCI 호출 (사내 시스템 간 통신) + */ + /* + public Transfer callMci(String itrfId, S body, Class resBody) { + + CommonHeader header = CommonHeaderFactory.createRequestHeader(itrfId); + + Transfer req = Transfer.builder() + .header(header) + .body(body) + .build(); + + return mci.call(req, resBody); + } + */ +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/dto/SampleGlowMessage.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/dto/SampleGlowMessage.java new file mode 100644 index 000000000..e9961c050 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/dto/SampleGlowMessage.java @@ -0,0 +1,84 @@ +package io.shinhanlife.dap.lib.integration.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import java.util.List; + +// TODO: 실제 Glow Framework 의존성이 추가되면 아래 주석을 풀고 사용하세요! +// import io.shinhanlife.glow.communication.annotation.GlowMciFieldInfo; + +/** + * [대외 MCI 연동용 DTO 표준 템플릿] + * Glow Framework 개발표준정의서(2.2.1 IO 작성) 규칙을 100% 준수한 샘플입니다. + * 새로운 대외 통신 전문을 만들 때 이 파일을 복사해서 필드명과 길이만 수정하여 사용하세요. + */ +/** + * @package io.shinhanlife.dap.lib.integration.dto + * @className SampleGlowMessage + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PUBLIC) // [규칙 1] Reflection을 위한 기본 생성자 필수 (public 유지) +public class SampleGlowMessage { + + // [규칙 2] @GlowMciFieldInfo 선언 필수 (order: 순서) + // @GlowMciFieldInfo(order = 1) + private MessageHeader header; + + // [규칙 2] @GlowMciFieldInfo 선언 필수 (order: 순서) + // @GlowMciFieldInfo(order = 2) + private List msgDtdvValu; // 다건(List) 본문 데이터 + + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor(access = AccessLevel.PUBLIC) + public static class MessageHeader { + + // [규칙 2] 단건 필드의 경우 length 필수 입력 (EIMS 길이와 일치해야 함) + // @GlowMciFieldInfo(order = 1, length = 1) + private String msgTnsmTypeCd; + + // @GlowMciFieldInfo(order = 2, length = 8) + private int msdvLen; + + // [규칙 3] 다건(List) 건수 필드의 경우, target 속성에 대상 변수명("msgDtdvValu") 필수 기입! + // @GlowMciFieldInfo(order = 3, length = 2, target = "msgDtdvValu") + private int msgRpttCc; + } + + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor(access = AccessLevel.PUBLIC) + public static class MessageBody { + + // @GlowMciFieldInfo(order = 1, length = 8) + private String msgCd; + + // @GlowMciFieldInfo(order = 2, length = 1) + private String msgPrnAttrCd; + + // @GlowMciFieldInfo(order = 3, length = 200) + private String msgCt; + + // @GlowMciFieldInfo(order = 4, length = 200) + private String anxMsgCt; + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/eai/component/AxhubEaiComponent.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/eai/component/AxhubEaiComponent.java new file mode 100644 index 000000000..8cf2b26e8 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/eai/component/AxhubEaiComponent.java @@ -0,0 +1,99 @@ +package io.shinhanlife.dap.lib.integration.eai.component; + +import io.shinhanlife.dap.lib.config.GlowCommunicationProperties; +import io.shinhanlife.glow.communication.dto.CommonHeader; +import io.shinhanlife.glow.communication.dto.Transfer; +import io.shinhanlife.glow.communication.module.eai.component.GlowEaiComponent; +import io.shinhanlife.glow.communication.util.CommonHeaderFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 신한라이프 내부 Glow 표준 EAI 컴포넌트 어댑터 (AXHUB) + */ +@Slf4j +@Component +@RequiredArgsConstructor + +/** + * @package io.shinhanlife.dap.lib.integration.eai.component + * @className AxhubEaiComponent + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +public class AxhubEaiComponent { + + @SuppressWarnings("rawtypes") + private final GlowEaiComponent eai; + private final GlowCommunicationProperties communicationProperties; + + @SuppressWarnings("unchecked") + private Transfer syncEai(Transfer request) { + // LOG 저장 (AXHUB 방식 로깅) + CommonHeader reqHeader = (CommonHeader) request.getHeader(); + log.info("[AxhubEaiComponent] {} EAI 호출시작 (수신서비스: {})", reqHeader.getItrfId(), reqHeader.getRcvSvcId()); + + Transfer response = (Transfer) eai.sync(request); + + log.info("[AxhubEaiComponent] {} EAI 호출종료 (수신서비스: {})", reqHeader.getItrfId(), reqHeader.getRcvSvcId()); + + return response; + } + + /** + * EAI 호출 + */ + public Transfer call(String itrfId, String rcvSvcId, I inputDto) throws Exception { + CommonHeader header = CommonHeaderFactory.createRequestHeader(itrfId, rcvSvcId); + + Transfer request = Transfer.builder() + .header(header) + .body(inputDto) + .build(); + + return syncEai(request); + } + + /** + * EAI 호출 (응답 타입 명시) + */ + @SuppressWarnings("unchecked") + public Transfer call(String itrfId, String rcvSvcId, I inputDto, Class resBodyClass) throws Exception { + CommonHeader header = CommonHeaderFactory.createRequestHeader(itrfId, rcvSvcId); + + Transfer request = Transfer.builder() + .header(header) + .body(inputDto) + .resBodyClass((Class) (Class) resBodyClass) + .build(); + + return syncEai(request); + } + + /** + * EAI 호출 (rcvSvcId 없는 경우) + */ + public Transfer call(String itrfId, I inputDTO, Class resBodyClass) throws Exception { + String className = inputDTO.getClass().getSimpleName(); + String rcvSvcId = className.replace("_I", ""); + return call(itrfId, rcvSvcId, inputDTO, resBodyClass); + } + + /** + * EAI 호출 (Response body class와 rcvSvcId 없는 경우) + */ + public Transfer call(String itrfId, I inputDTO) throws Exception { + String className = inputDTO.getClass().getSimpleName(); + String rcvSvcId = className.replace("_I", ""); + return call(itrfId, rcvSvcId, inputDTO); + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/component/AxhubMciComponent.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/component/AxhubMciComponent.java new file mode 100644 index 000000000..746d70fb0 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/component/AxhubMciComponent.java @@ -0,0 +1,168 @@ +package io.shinhanlife.dap.lib.integration.mci.component; + +import io.shinhanlife.dap.lib.session.dto.SessionDto; +import io.shinhanlife.dap.lib.util.SessionUtil; +import io.shinhanlife.dap.lib.config.GlowCommunicationProperties; +import io.shinhanlife.glow.communication.dto.CommonHeader; +import io.shinhanlife.glow.communication.dto.HeaderDefaults; +import io.shinhanlife.glow.communication.dto.Transfer; +import io.shinhanlife.glow.communication.module.mci.component.GlowMciComponent; +import io.shinhanlife.glow.communication.util.CommonHeaderFactory; +import io.shinhanlife.dap.lib.integration.mci.enums.IndvCtinRoleTyp; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +/** + * 신한라이프 내부 Glow 표준 컴포넌트 어댑터 (AXHUB) + */ +@Slf4j +@Component +@RequiredArgsConstructor + +/** + * @package io.shinhanlife.dap.lib.integration.mci.component + * @className AxhubMciComponent + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +public class AxhubMciComponent { + + @SuppressWarnings("rawtypes") + private final GlowMciComponent mci; + private final GlowCommunicationProperties communicationProperties; + + /** 전문생성채널유형코드 : 1 (채널계) */ + private final static String TGRM_CREA_CHNN_TYPE_CD_1 = "1"; + private static final String SUCO_UNBL_CODE = "NNB00147"; // 청약불가 + + /** + * 전문 Common Header 생성 + * @param itrfName 인터페이스Id + * @param rcvSvcId 수신서비스Id + * @return Map + */ + private Map createCommonHeaderMap(String itrfName, String rcvSvcId) { + SessionDto sessionDto = SessionUtil.getSession(); + Map commonHeaderMap = new HashMap<>(); + + commonHeaderMap.put(HeaderDefaults.ITRF_ID, itrfName); + commonHeaderMap.put(HeaderDefaults.RCV_SVC_ID, rcvSvcId); + + if (sessionDto != null) { + commonHeaderMap.put(HeaderDefaults.STR_YMD, sessionDto.getStrYmd()); + commonHeaderMap.put(HeaderDefaults.ACNT_OGNZ_NO, sessionDto.getBrafNo()); + commonHeaderMap.put(HeaderDefaults.PSMR_ASRT_CD, sessionDto.getPsmrAsrtCd()); + commonHeaderMap.put(HeaderDefaults.SBSN_RULP_ASRT_CD, sessionDto.getSbsnRulpAsrtCd()); + commonHeaderMap.put(HeaderDefaults.BSDU_CD, sessionDto.getBsduCd()); + commonHeaderMap.put(HeaderDefaults.BSQU_CD, sessionDto.getBsquCd()); + commonHeaderMap.put(HeaderDefaults.OGNZ_ASRT_CD, sessionDto.getOgnzAsrtCd()); + commonHeaderMap.put(HeaderDefaults.OGNZ_LEVE_CD, sessionDto.getOgnzLeveCd()); + commonHeaderMap.put(HeaderDefaults.SCRN_ID, sessionDto.getPrgrId()); + } + + commonHeaderMap.put(HeaderDefaults.INDV_CTIN_ROLE_CD, IndvCtinRoleTyp.CD_Z99.getCode()); + commonHeaderMap.put(HeaderDefaults.TGRM_CREA_CHNN_TYPE_CD, TGRM_CREA_CHNN_TYPE_CD_1); + + if (communicationProperties != null && communicationProperties.getCommon() != null) { + commonHeaderMap.put(HeaderDefaults.ENVR_TYPE_CD, communicationProperties.getCommon().getEnvType()); + } + + return commonHeaderMap; + } + + @SuppressWarnings("unchecked") + private Transfer syncMci(Transfer request) { + // LOG 저장 (AXHUB 방식 로깅) + CommonHeader reqHeader = (CommonHeader) request.getHeader(); + log.info("[AxhubMciComponent] {} MCI 호출시작 (수신서비스: {})", reqHeader.getItrfId(), reqHeader.getRcvSvcId()); + Transfer response = (Transfer) mci.sync(request); + log.info("[AxhubMciComponent] {} MCI 호출종료 (수신서비스: {})", reqHeader.getItrfId(), reqHeader.getRcvSvcId()); + + if (response != null && response.getHeader() != null) { + CommonHeader resHeader = (CommonHeader) response.getHeader(); + String tgrmDalRsltCd = resHeader.getTgrmDalRsltCd(); + // TODO 추가 메시지 처리 및 오류 코드 제어 로직 + } + + return response; + } + + public Transfer callTo(String itrfName, String rcvSvcId, I inputDto) throws Exception { + Map commonHeaderMap = createCommonHeaderMap(itrfName, rcvSvcId); + CommonHeader header = CommonHeaderFactory.createRequestHeader(commonHeaderMap); + + Transfer request = Transfer.builder() + .header(header) + .body(inputDto) + .build(); + + return syncMci(request); + } + + /** + * 대내 mci 호출 + * @param itrfName 인터페이스Id + * @param rcvSvcId 수신서비스Id + * @param inputDto inputDto + * @param resBodyClass resBodyClass + * @return Transfer + * @param resBodyClass 제너릭 + * @param inputDto 제너릭 + */ + @SuppressWarnings("unchecked") + public Transfer callTo(String itrfName, String rcvSvcId, I inputDto, Class resBodyClass) throws Exception { + Map commonHeaderMap = createCommonHeaderMap(itrfName, rcvSvcId); + CommonHeader header = CommonHeaderFactory.createRequestHeader(commonHeaderMap); + + Transfer request = Transfer.builder() + .header(header) + .body(inputDto) + .resBodyClass((Class) (Class) resBodyClass) + .build(); + + return syncMci(request); + } + + /** + * 대내 mci 호출 (rcvSvcId 없는 경우) + * @param itrfName 인터페이스Id + * @param inputDTO 수신서비스Id (클래스명 대체) + * @param resBodyClass resBodyClass + * @return Transfer + * @param resBodyClass 제너릭 + * @param inputDto 제너릭 + * @throws Exception Exception + */ + public Transfer callTo(String itrfName, I inputDTO, Class resBodyClass) throws Exception { + String className = inputDTO.getClass().getSimpleName(); + String rcvSvcId = className.replace("_I", ""); + return callTo(itrfName, rcvSvcId, inputDTO, resBodyClass); + } + + /** + * 대내 mci 호출 (Response body class와 rcvSvcId 없는 경우) + * @param itrfName 인터페이스Id + * @param inputDTO 수신서비스Id (클래스명 대체) + * @return Transfer + * @param resBodyClass 제너릭 + * @param inputDto 제너릭 + * @throws Exception Exception + */ + public Transfer callTo(String itrfName, I inputDTO) throws Exception { + String className = inputDTO.getClass().getSimpleName(); + String rcvSvcId = className.replace("_I", ""); + return callTo(itrfName, rcvSvcId, inputDTO); + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/config/GlowMockConfig.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/config/GlowMockConfig.java new file mode 100644 index 000000000..a46fcd538 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/config/GlowMockConfig.java @@ -0,0 +1,40 @@ +package io.shinhanlife.dap.lib.integration.mci.config; + +import io.shinhanlife.glow.communication.module.mci.component.GlowMciComponent; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * TODO: 실제 Glow Framework 의존성이 추가되어 io.shinhanlife.glow 패키지가 + * ComponentScan에 잡히게 되면 이 설정 클래스는 삭제하세요. + */ +@Configuration + +/** + * @package io.shinhanlife.dap.lib.integration.mci.config + * @className GlowMockConfig + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +public class GlowMockConfig { + + @Bean + @SuppressWarnings("rawtypes") + public GlowMciComponent glowMciComponent() { + return new GlowMciComponent(); + } + + @Bean + @SuppressWarnings("rawtypes") + public io.shinhanlife.glow.communication.module.eai.component.GlowEaiComponent glowEaiComponent() { + return new io.shinhanlife.glow.communication.module.eai.component.GlowEaiComponent(); + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/config/ShinhanIntegrationProperties.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/config/ShinhanIntegrationProperties.java new file mode 100644 index 000000000..d13027460 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/config/ShinhanIntegrationProperties.java @@ -0,0 +1,40 @@ +package io.shinhanlife.dap.lib.integration.mci.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * @package io.shinhanlife.dap.lib.integration.mci.config + * @className ShinhanIntegrationProperties + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@Setter +@ConfigurationProperties(prefix = "shinhan.integration") +public class ShinhanIntegrationProperties { + + /** + * 환경유형코드: 운영(R), 테스트(T), 개발(D) + */ + private String envrTypeCd = "D"; + + private ServerInfo eai = new ServerInfo(); + private ServerInfo internalMci = new ServerInfo(); + + @Getter + @Setter + public static class ServerInfo { + private String url; + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/MciRequestWrapper.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/MciRequestWrapper.java new file mode 100644 index 000000000..23c3bc66b --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/MciRequestWrapper.java @@ -0,0 +1,34 @@ +package io.shinhanlife.dap.lib.integration.mci.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; + +/** + * @package io.shinhanlife.dap.lib.integration.mci.dto + * @className MciRequestWrapper + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MciRequestWrapper { + private ShinhanCommonHeaderDto tgrmCmnnhddValu; + + @JsonUnwrapped + private T body; +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/MciResponseWrapper.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/MciResponseWrapper.java new file mode 100644 index 000000000..4e2b94753 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/MciResponseWrapper.java @@ -0,0 +1,32 @@ +package io.shinhanlife.dap.lib.integration.mci.dto; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @package io.shinhanlife.dap.lib.integration.mci.dto + * @className MciResponseWrapper + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MciResponseWrapper { + private ShinhanCommonHeaderDto tgrmCmnnhddValu; + + @JsonUnwrapped + private T body; +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/OlCommonHeaderDto.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/OlCommonHeaderDto.java new file mode 100644 index 000000000..9eff40b6e --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/OlCommonHeaderDto.java @@ -0,0 +1,90 @@ +package io.shinhanlife.dap.lib.integration.mci.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.util.List; + +/** + * @package io.shinhanlife.dap.lib.integration.mci.dto + * @className OlCommonHeaderDto + * @description AX HUB 시스템 처리 클래스 - OL(구 오렌지라이프) 공통 헤더 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class OlCommonHeaderDto { + private String custNm; // 고객명 + private String custRrn; // 고객 주민등록번호 + private String custNo; // 고객번호 + private String rcevNo; // 접수번호 + private String pono; // 증권번호 + private String scrNm; // 화면명 + private String scrId; // 화면ID + private String lginDttm; // 사용자가 로그인한 접속일시 + private String lginIpAddr; // 사용자가 접속한 IP 주소 + private String userNm; // 사용자 이름(한글) + private String userEngNm; // 사용자 영문이름 + private String userId; // 사용자 ID(AD ID) + private String userNo; // 사용자번호 + private String deptCd; // 사용자조직 코드 + private String salsDvCd; // 영업본부코드 + private String salsBoCd; // 영업지점코드 + private String uppDeptCd; // 상위조직코드 + private String prcsrUserId; // 처리자 ID(AD ID) + private String prcsrUserNo; // 처리지번호 + private String prcsrDeptCd; // 처리지조직 코드 + private String prcsrDvCd; // 처리지 영업본부코드 + private String prcsrBoCd; // 처리지 영업지점코드 + private String prcsrUppDeptCd; // 부서코드 + private String sysCd; // 요청이 들어온 시스템을 표시 + private String reqtSvcNm; // 요청하는 서비스 모듈명 + private String reqtMthdNm; // 요청하는 메소드명 + private String reqtVoNm; // 요청메소드에 전달할 값을 담는 VO명 + private String scrButnFuncClssCd; // 화면에서 버튼 별 이벤트 구분을 위한 구분코드 + private String scrGriCnt; // 화면 그리드 개수 + private List pageList; // 페이징 리스트 (L2 반복) + private String reqtDttm; // 요청일시 + private String crdtInfoIcluFlg; // 신용정보포함여부(Y,N) + private String crdtInfoDataChgTypCd; // 업무내역별 식별코드 부여 + private String crdtInfoIdfInEngAbbrNm; // 신용정보식별영문약어명 + private String crdtInfoIdfnSysCd; // 신용정보식별시스템코드 + private String scrButnNm; // 화면버튼명 + private String msgCnt; // 메시지 개수 + private List msgList; // 메시지 리스트 (L2 반복) + private String respDttm; // 응답일시 + private String svcRunNm; // 거래별로 유일한 ServiceExecutionID + private String stdate; // 기준일자 + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class OlPageDto { + private String pageSrno; // 페이지 인덱스값 (L3) + private String pageInqCnt; // 한페이지에 조회될 건수 (L3) + private String nxtButnNm; // 다음버튼ID (L3) + private String nxtButnEnbFlg; // 다음버튼 활성여부 (L3) + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class OlMsgDto { + private String msgNo; // 서버 측에서 세팅한 정상/에러 메시지코드 (L3) + private String msgTypCd; // 메시지유형코드 (L3) + private String msgNm; // 메시지코드의 내용 (L3) + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/ShinhanCommonHeaderDto.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/ShinhanCommonHeaderDto.java new file mode 100644 index 000000000..43e107577 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/ShinhanCommonHeaderDto.java @@ -0,0 +1,72 @@ +package io.shinhanlife.dap.lib.integration.mci.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @package io.shinhanlife.dap.lib.integration.mci.dto + * @className ShinhanCommonHeaderDto + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ShinhanCommonHeaderDto { + + private String tgrmLencn; // 전문길이 + private String glbId; // 글로벌ID (전사공통키) + private String pgrsSriaNo; // 진행일련번호 + private String tgrmVrsnInfoValu; // 전문버전정보값 + private String tgrmEncrYn; // 전문암호화여부 + private String gpcpCd; // 그룹사코드 + private String appliDutjCd; // 어플리케이션업무코드 + private String appliDtptDutjCd; // 어플리케이션상세업무코드 + private String frbuCd; // 대외기관코드 + private String cmouDutjCd; // 대외업무코드 + private String cmouCssfCd; // 대외종별코드 + private String cmouTraCd; // 대외거래코드 + private String rcvSvcId; // 수신서비스ID + private String rsltRcvSvcId; // 결과수신서비스ID + private String tgrmCreaChnnTypeCd; // 전문생성채널유형코드 + + private String lnggDvsnCd; // 언어구분코드 + private String simulTraYn; // 시뮬레이션거래여부 + private String itrIfId; // 인터페이스ID + private String reqRspnScCd; // 요청응답구분코드 + private String tnsmTypeCd; // 전송유형코드 + private String envrTypeCd; // 환경유형코드 + private String inqrTraTypeCd; // 조회거래유형코드 + private String reqTgrmTnsmDtptDt; // 요청전문전송상세일시 + private String strYmd; // 기준일자 + private String scrnId; // 화면ID + private String scrnBtnId; // 화면버튼ID + + private String userIpAddr; // 사용자IP주소 + private String drtmCd; // 부서코드 + private String userId; // 사용자ID + private String indvCtinRoleCd; // 개인신용정보역할코드 + private String acntOgnzNo; // 경리조직번호 + private String rspnTgrmTnsmDtptDt; // 응답전문전송상세일시 + private String tgrmDalRsltCd; // 전문처리결과코드 + private String ognzAsrtCd; // 조직분류코드 + private String ognzLeveCd; // 조직레벨코드 + private String psmrAsrtCd; // 인사조직분류코드 + private String sbsnRulpAsrtCd; // 영업규정분류코드 + private String bsduCd; // 영업지국코드 + private String bsquCd; // 영업자격코드 + private String linkPrafDutyCd; // 연계인사직책코드 + private String indvInfoLogWritYn; // 개인정보로그작성여부 + private String prepImhdNm; // 예비항목명 +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/ShinhanMessageDto.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/ShinhanMessageDto.java new file mode 100644 index 000000000..5c02fd310 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/ShinhanMessageDto.java @@ -0,0 +1,51 @@ +package io.shinhanlife.dap.lib.integration.mci.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @package io.shinhanlife.dap.lib.integration.mci.dto + * @className ShinhanMessageDto + * @description AX HUB 시스템 처리 클래스 - MCI 전문 메시지부 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ShinhanMessageDto { + + private MsgHddvValu msgHddvValu; // 메시지헤더부값 + private MsgDtdvValu msgDtdvValu; // 메시지데이터부값 + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class MsgHddvValu { + private String msgTnsmTypeCd; // 메시지전송유형코드 + private Integer msdvLencn; // 메시지부길이 + private Integer msgRpttCc; // 메시지반복건수 + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class MsgDtdvValu { + private String msgCd; // 메시지코드 + private String msgPrnAttrCd; // 메시지출력속성코드 + private String msgCt; // 메시지내용 + private String anxMsgCt; // 부가메시지내용 + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/ShinhanTelegramWrapper.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/ShinhanTelegramWrapper.java new file mode 100644 index 000000000..47133822a --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/ShinhanTelegramWrapper.java @@ -0,0 +1,38 @@ +package io.shinhanlife.dap.lib.integration.mci.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; + +/** + * @package io.shinhanlife.dap.lib.integration.mci.dto + * @className ShinhanTelegramWrapper + * @description AX HUB 시스템 처리 클래스 - MCI 전문 전체 래퍼 (공통헤더부 + 메시지부 + 데이터부) + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ShinhanTelegramWrapper { + + // 1. 공통 헤더부 + private ShinhanCommonHeaderDto tgrmCmnnhddValu; + + // 2. 메시지부 + private ShinhanMessageDto tgrmMsdvValu; + + // 3. 데이터부 (비즈니스마다 다름, JsonUnwrapped로 평탄화하거나 객체 자체로 유지 가능. 여기서는 객체 유지) + private T tgrmDtdvValu; +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/SlCommonHeaderDto.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/SlCommonHeaderDto.java new file mode 100644 index 000000000..73d3514c7 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/dto/SlCommonHeaderDto.java @@ -0,0 +1,84 @@ +package io.shinhanlife.dap.lib.integration.mci.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @package io.shinhanlife.dap.lib.integration.mci.dto + * @className SlCommonHeaderDto + * @description AX HUB 시스템 처리 클래스 - SL(신한라이프) 표준 헤더 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SlCommonHeaderDto { + private String length; // 전문길이 + private SlGlobalId globalId; // 글로벌ID + private String headerVer; // 전문헤더버전 + private String encodeFlag; // 전문암호화여부 + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class SlGlobalId { + private String writeDate; // 전문작성일 (8) + private String sysCd; // 생성시스템명 (8) + private String typeCd; // 구분코드 (2) + private String detailCd; // 세부업무코드 (4) + private String seqNo; // 채번번호 (8) + private String step; // 진행상황 (2) + } + + private String groupCoCd; // 그룹사코드 + private String instCd; // 기관코드 + private String applCd; // 업무코드 + private String kindCd; // 종별코드 + private String txCd; // 거래코드 + private String pfmAppName; // 어플리케이션 명 + private String pfmSvcName; // 서비스 명 + private String pfmFnName; // 오퍼레이션 명 + private String systemCd; // 생성시스템구분 + private String trFlag; // 요청응답구분 + private String syncFlag; // 동기구분 + private String envrFlag; // 환경구분 + private String crudFlag; // 조회거래구분 + private String sendTime; // 전문전송일시 + private String screenId; // 화면ID + private String clntIp; // Client IP + private String orgCd; // 부서(지점)코드 + private String userId; // 사용자 사번(아이디) + private String indvCrdtInfo; // 개인신용정보역할코드 + private String acntOgnzNo; // 경리조직번호 + private String ttiFlag; // TimeOut사용 + private String ttiStartTm; // 최초시작시간 + private String ttiKeepTm; // 유지시간초수 + private String outMsgTm; // 응답전문작성일시 + private String resType; // 처리결과 + private String resCode; // 응답코드 + private String resBascMsg; // 응답기본내역 + private String msgType; // 메시지 유형 + private String rcvSvcCd; // 수신 서비스 Code + private String rsltRcvSvcCd; // 결과수신 서비스 Code + private String realSvcCd; // Real 서비스 Code + private String ognzAsrtCd; // 조직분류코드 + private String ognzLeveCd; // 조직레벨구분코드 + private String psmrAsrtCd; // 인사조직분류코드 + private String sbsnRulpAsrtCd; // 영업규정분류코드 + private String bsduCd; // 영업지국코드 + private String bsquCd; // 영업자격코드 + private String linkPrafDutyCd; // 직책코드 + private String temp; // 예비 필드 +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/enums/IndvCtinRoleTyp.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/enums/IndvCtinRoleTyp.java new file mode 100644 index 000000000..5ed178870 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/integration/mci/enums/IndvCtinRoleTyp.java @@ -0,0 +1,27 @@ +package io.shinhanlife.dap.lib.integration.mci.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor + +/** + * @package io.shinhanlife.dap.lib.integration.mci.enums + * @className IndvCtinRoleTyp + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +public enum IndvCtinRoleTyp { + CD_Z99("Z99"); + + private final String code; +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/config/CacheConfig.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/config/CacheConfig.java new file mode 100644 index 000000000..9a313bf70 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/config/CacheConfig.java @@ -0,0 +1,32 @@ +package io.shinhanlife.dap.lib.mcp.config; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @package io.shinhanlife.dap.lib.mcp.config + * @className CacheConfig + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Configuration +@EnableCaching +public class CacheConfig { + + // 스프링이 캐시를 관리할 기본 저장소를 빈(Bean)으로 등록합니다. + @Bean + public CacheManager cacheManager() { + return new ConcurrentMapCacheManager("eimsData"); // 아까 설정한 캐시 이름 등록 + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/config/JacksonConfig.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/config/JacksonConfig.java new file mode 100644 index 000000000..1df9b21f1 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/config/JacksonConfig.java @@ -0,0 +1,41 @@ +package io.shinhanlife.dap.lib.mcp.config; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +/** + * @package io.shinhanlife.dap.lib.mcp.config + * @className JacksonConfig + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Configuration +public class JacksonConfig { + + // 1. JSON 변환기(ObjectMapper)를 스프링 Bean으로 등록 + @Bean + @Primary + public ObjectMapper jsonMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + return mapper; + } + + // 2. XML 변환기(XmlMapper)를 스프링 Bean으로 등록 + @Bean + public XmlMapper xmlMapper() { + return new XmlMapper(); + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/config/KafkaLocalConfig.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/config/KafkaLocalConfig.java new file mode 100644 index 000000000..f9bda4e0e --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/config/KafkaLocalConfig.java @@ -0,0 +1,58 @@ +package io.shinhanlife.dap.lib.mcp.config; + +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.core.ProducerFactory; + +import org.springframework.beans.factory.annotation.Value; +import java.util.HashMap; +import java.util.Map; + +/** + * @package io.shinhanlife.dap.lib.mcp.config + * @className KafkaLocalConfig + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Configuration +public class KafkaLocalConfig { + + @Value("${spring.kafka.bootstrap-servers:localhost:9092}") + private String bootstrapServers; + + // 1. 카프카 전송 공장(Factory) 세팅 + @Bean + public ProducerFactory producerFactory() { + Map configProps = new HashMap<>(); + // 가짜 로컬 주소 혹은 환경변수 세팅 + configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + // 데이터를 카프카로 보낼 때 문자열(String) 형태로 변환하겠다는 규칙 + configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + + // 3초 만에 빠른 실패 처리 (로컬 무한 대기 방지) + configProps.put(ProducerConfig.MAX_BLOCK_MS_CONFIG, 3000); + // 재접속 주기를 10초로 설정 (콘솔 로그 도배 방지) + configProps.put(ProducerConfig.RECONNECT_BACKOFF_MAX_MS_CONFIG, 10000); + + return new DefaultKafkaProducerFactory<>(configProps); + } + + // 2. EaiEimsSender가 애타게 찾던 KafkaTemplate을 스프링 Bean으로 등록! + @Bean + public KafkaTemplate kafkaTemplate() { + return new KafkaTemplate<>(producerFactory()); + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/config/SwaggerConfig.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/config/SwaggerConfig.java new file mode 100644 index 000000000..5c2e22c96 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/config/SwaggerConfig.java @@ -0,0 +1,47 @@ +package io.shinhanlife.dap.lib.mcp.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @package io.shinhanlife.dap.lib.mcp.config + * @className SwaggerConfig + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("Shinhan MCP Gateway API 명세서") + .version("v1.0") + .description("AI Agent와 신한라이프 내부망(EIMS/EAI)을 연결하는 Adapter Gateway API 문서입니다.")) + .addServersItem(new io.swagger.v3.oas.models.servers.Server().url("http://localhost:8080").description("Adapter Pod (8080)")) + .addServersItem(new io.swagger.v3.oas.models.servers.Server().url("http://localhost:8081").description("Gateway Pod (8081)")) + // 전역적으로 X-API-KEY 보안 설정을 Swagger UI에 추가합니다. + .addSecurityItem(new SecurityRequirement().addList("X-API-KEY")) + .components(new Components() + .addSecuritySchemes("X-API-KEY", + new SecurityScheme() + .name("X-API-KEY") + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .description("헤더에 API Key를 입력해주세요. "))); + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/config/WebConfig.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/config/WebConfig.java new file mode 100644 index 000000000..9e3cfc8b4 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/config/WebConfig.java @@ -0,0 +1,54 @@ +package io.shinhanlife.dap.lib.mcp.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.config.annotation.CorsRegistry; + +import io.shinhanlife.dap.lib.mcp.security.ApiKeyInterceptor; + +/** + * @package io.shinhanlife.dap.lib.mcp.config + * @className WebConfig + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Configuration +@RequiredArgsConstructor +public class WebConfig implements WebMvcConfigurer { + + // 1. 우리가 만든 인터셉터를 주입받습니다. + private final ApiKeyInterceptor apiKeyInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + // 2. 인터셉터 등록 및 검사할 URL 패턴 지정 + registry.addInterceptor(apiKeyInterceptor) + .addPathPatterns("/rpc/**", "/mcp/api/v1/**") // /rpc/, /mcp/api/v1/ 로 시작하는 모든 API는 API Key 검사 수행! + .excludePathPatterns( + "/test/**", "/health", "/error", "/mcp/api/v1/admin/**", + "/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**", "/webjars/**", // Swagger UI 경로는 인증 제외 + "/mcp/api/v1/tools/docs/markdown", "/favicon.ico", "/mcp/api/v1/tools/list" + ); + } + + @Override + public void addCorsMappings(CorsRegistry registry) { + // Swagger UI(8080)에서 Gateway(8081)로 API 호출 시 발생하는 CORS 에러 해결 + registry.addMapping("/**") + .allowedOriginPatterns("*") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .exposedHeaders("Mcp-Session-Id") + .allowCredentials(true); + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/exception/GlobalExceptionHandler.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/exception/GlobalExceptionHandler.java new file mode 100644 index 000000000..f5700193f --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/exception/GlobalExceptionHandler.java @@ -0,0 +1,60 @@ +package io.shinhanlife.dap.lib.mcp.exception; + + +/** + * @package io.shinhanlife.dap.lib.mcp.exception + * @className GlobalExceptionHandler + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +import io.shinhanlife.dap.lib.adapter.dto.ErrorDetail; +import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.resource.NoResourceFoundException; + +@Slf4j +@RestControllerAdvice // 이 어노테이션이 전역 적용의 핵심입니다! +public class GlobalExceptionHandler { + + @ExceptionHandler(NoResourceFoundException.class) + public ResponseEntity handleNoResourceFound(NoResourceFoundException e) { + log.warn(" [Gateway Not Found] 요청하신 리소스를 찾을 수 없습니다: {}", e.getResourcePath()); + return ResponseEntity.notFound().build(); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgument(IllegalArgumentException e) { + log.warn(" [Gateway Bad Request] 잘못된 요청: {}", e.getMessage()); + return buildErrorResponse(-32602, "Invalid params: " + e.getMessage()); + } + + @ExceptionHandler(RuntimeException.class) + public ResponseEntity handleRuntime(RuntimeException e) { + log.error(" [Gateway Internal Error] 시스템 장애: {}", e.getMessage(), e); + return buildErrorResponse(-32603, "Internal error: " + e.getMessage()); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleAllException(Exception e) { + log.error(" [Gateway Fatal Error] 치명적 오류 발생", e); + return buildErrorResponse(-32000, "Server error: 시스템 관리자에게 문의하세요."); + } + + private ResponseEntity buildErrorResponse(int code, String message) { + JsonRpcResponse response = new JsonRpcResponse(); + response.setError(new ErrorDetail(code, message)); + + return ResponseEntity.ok(response); + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/filter/MdcLoggingFilter.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/filter/MdcLoggingFilter.java new file mode 100644 index 000000000..69a76ff18 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/filter/MdcLoggingFilter.java @@ -0,0 +1,56 @@ +package io.shinhanlife.dap.lib.mcp.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.MDC; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.UUID; + +/** + * @package io.shinhanlife.dap.lib.mcp.filter + * @className MdcLoggingFilter + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Component +public class MdcLoggingFilter extends OncePerRequestFilter { + + private static final String TRACE_ID_HEADER = "X-Trace-Id"; + private static final String MDC_KEY = "traceId"; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + // 클라이언트가 보낸 Trace ID가 있으면 쓰고, 없으면 새로 생성 + String traceId = request.getHeader(TRACE_ID_HEADER); + if (traceId == null || traceId.isEmpty()) { + // 간결하게 8자리 UUID만 사용 + traceId = UUID.randomUUID().toString().substring(0, 8); + } + + // 로깅 컨텍스트에 고유 ID 저장 + MDC.put(MDC_KEY, traceId); + + try { + // 이 요청이 처리되는 동안 찍히는 모든 log.info, log.error에 traceId가 자동으로 붙습니다. + filterChain.doFilter(request, response); + } finally { + // 메모리 누수 방지를 위해 요청이 끝나면 반드시 비워줍니다. + MDC.clear(); + } + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/security/ApiKeyInterceptor.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/security/ApiKeyInterceptor.java new file mode 100644 index 000000000..89fb3db58 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/security/ApiKeyInterceptor.java @@ -0,0 +1,80 @@ +package io.shinhanlife.dap.lib.mcp.security; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.util.Map; + +/** + * @package io.shinhanlife.dap.lib.mcp.security + * @className ApiKeyInterceptor + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +@Component +@RequiredArgsConstructor +public class ApiKeyInterceptor implements HandlerInterceptor { + + // 1. 다중 테넌트 API Key 목록이 담긴 프로퍼티 객체를 주입받습니다. + private final SecurityProperties securityProperties; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + + if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { + return true; + } + + String apiKey = request.getHeader("X-API-KEY"); + Map validApiKeys = securityProperties.getApiKeys(); + + // 2. 만약 프로퍼티에 API Key가 하나도 설정되어 있지 않다면 (개발/로컬 환경 등) 인증 없이 통과시킵니다. + if (validApiKeys == null || validApiKeys.isEmpty()) { + MDC.put("tenantId", "anonymous"); + request.setAttribute("tenantId", "anonymous"); + log.debug(" [보안 패스] 등록된 API Key 없음 - 익명 사용자(anonymous)로 통과"); + return true; + } + + // 3. 헤더로 들어온 API Key가 우리가 발급해준 목록(Map)에 존재하는지 확인합니다. + if (apiKey == null || !validApiKeys.containsKey(apiKey)) { + log.warn(" [보안 차단] 유효하지 않은 API Key 접근 시도 - IP: {}", request.getRemoteAddr()); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid API Key"); + return false; // 컨트롤러로 넘어가지 않음 + } + + // 4. 유효하다면 해당 키에 맵핑된 Tenant ID(식별자)를 가져옵니다. (ex. mcp-client-1) + String tenantId = validApiKeys.get(apiKey); + + // 4. 추출한 Tenant ID를 현재 스레드의 로깅 컨텍스트(MDC)에 저장합니다. + // 이렇게 하면 이 요청이 끝날 때까지 찍히는 모든 로그에 어떤 테넌트가 호출했는지 자동으로 기록됩니다. + MDC.put("tenantId", tenantId); + + // 5. 필요시 컨트롤러 로직에서 사용할 수 있도록 Request 속성에도 담아줍니다. + request.setAttribute("tenantId", tenantId); + + log.debug(" [보안 통과] API Key 인증 성공 - 접속 테넌트: {}", tenantId); + + return true; // 인증 통과! 컨트롤러로 진행 + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + // 6. 메모리 누수를 방지하기 위해 요청 처리가 완전히 끝나면 MDC에서 테넌트 정보를 지워줍니다. + MDC.remove("tenantId"); + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/security/SecurityProperties.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/security/SecurityProperties.java new file mode 100644 index 000000000..381164e61 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/mcp/security/SecurityProperties.java @@ -0,0 +1,40 @@ +package io.shinhanlife.dap.lib.mcp.security; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * [다중 테넌트 설정 매핑 클래스] + * application-local.properties 파일에 정의된 mcp.security.api-keys.* 설정들을 + * Map 자료구조로 자동 바인딩(주입) 받기 위한 설정 클래스입니다. + */ +/** + * @package io.shinhanlife.dap.lib.mcp.security + * @className SecurityProperties + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Data +@Component +@ConfigurationProperties(prefix = "mcp.security") +public class SecurityProperties { + // API Key를 Key로, Tenant ID를 Value로 가지는 맵 + private Map apiKeys = new HashMap<>(); + + // Tenant ID를 Key로, 허용된 도메인 그룹 목록을 Value로 가지는 맵 (ex. mcp-client-1 -> [CUSTOMER, COMMON]) + // 만약 "ALL" 이 포함되어 있다면 모든 도메인에 접근 허용 + private Map> tenantDomains = new HashMap<>(); +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/converter/ZtUsacConverter.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/converter/ZtUsacConverter.java new file mode 100644 index 000000000..f70d1d398 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/converter/ZtUsacConverter.java @@ -0,0 +1,29 @@ +package io.shinhanlife.dap.lib.session.converter; + +import io.shinhanlife.dap.lib.session.dto.SessionDto; +import io.shinhanlife.dap.lib.session.dto.ZtUsacOutDto; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +/** + * @package io.shinhanlife.dap.lib.session.converter + * @className ZtUsacConverter + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Mapper(componentModel = "spring") +public abstract class ZtUsacConverter { + + @Mapping(target = "loginDtm", ignore = true) + @Mapping(target = "isManager", ignore = true) + public abstract SessionDto toSessionDto(ZtUsacOutDto dto); + +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/domain/model/ZtUsacModel.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/domain/model/ZtUsacModel.java new file mode 100644 index 000000000..7c16b1c43 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/domain/model/ZtUsacModel.java @@ -0,0 +1,51 @@ +package io.shinhanlife.dap.lib.session.domain.model; + +import io.shinhanlife.glow.db.dto.AuditInfo; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * @package io.shinhanlife.dap.lib.session.domain.model + * @className ZtUsacModel + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class ZtUsacModel extends AuditInfo { + + /* 인사번호 */ + private String prafNo; + /* 인사명 */ + private String prafNm; + /* 조직번호 */ + private String ognzNo; + /* 이메일주소 */ + private String addre; + /* 인사직무코드 */ + private String prafOfduCd; + /* 인사직무명 */ + private String prafOfduNm; + /* 인사직급코드 */ + private String prafOfleCd; + /* 인사직급명 */ + private String prafOfleNm; + /* 인사직책코드 */ + private String prafDutyCd; + /* 인사직책명 */ + private String prafDutyNm; + +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/domain/repository/ZtUsacRepository.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/domain/repository/ZtUsacRepository.java new file mode 100644 index 000000000..c300bbb4e --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/domain/repository/ZtUsacRepository.java @@ -0,0 +1,33 @@ +package io.shinhanlife.dap.lib.session.domain.repository; + +import io.shinhanlife.glow.GlowMybatisMapper; +import io.shinhanlife.dap.lib.session.dto.ZtUsacInDto; +import io.shinhanlife.dap.lib.session.dto.ZtUsacOutDto; + + +/** + * @package io.shinhanlife.dap.lib.session.domain.repository + * @className ZtUsacRepository + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@GlowMybatisMapper +public interface ZtUsacRepository { + + /** + * 사용자 조회 (단건) + * + * @param dto 사번 + * @return 인사정보 + */ + ZtUsacOutDto selectZtUsac(ZtUsacInDto dto); + +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/domain/usecase/ZtUsacUseCase.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/domain/usecase/ZtUsacUseCase.java new file mode 100644 index 000000000..235a75d18 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/domain/usecase/ZtUsacUseCase.java @@ -0,0 +1,29 @@ +package io.shinhanlife.dap.lib.session.domain.usecase; + +import io.shinhanlife.dap.lib.session.dto.ZtUsacInDto; +import io.shinhanlife.dap.lib.session.dto.ZtUsacOutDto; + +/** + * @package io.shinhanlife.dap.lib.session.domain.service + * @className ZtUsacUseCase + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +public interface ZtUsacUseCase { + + /** + * 사용자 조회 (단건) + * + * @param dto 사번 + * @return 인사정보 + */ + ZtUsacOutDto selectZtUsac(ZtUsacInDto dto); +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/domain/usecase/impl/ZtUsacUseCaseImpl.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/domain/usecase/impl/ZtUsacUseCaseImpl.java new file mode 100644 index 000000000..5486fb8b7 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/domain/usecase/impl/ZtUsacUseCaseImpl.java @@ -0,0 +1,42 @@ +package io.shinhanlife.dap.lib.session.domain.usecase.impl; + +import io.shinhanlife.dap.lib.session.domain.repository.ZtUsacRepository; +import io.shinhanlife.dap.lib.session.domain.usecase.ZtUsacUseCase; +import io.shinhanlife.dap.lib.session.dto.ZtUsacInDto; +import io.shinhanlife.dap.lib.session.dto.ZtUsacOutDto; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * @package io.shinhanlife.dap.lib.session.domain.usecase.impl + * @className ZtUsacUseCaseImpl + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Service +@RequiredArgsConstructor +public class ZtUsacUseCaseImpl implements ZtUsacUseCase { + + private final ZtUsacRepository ztUsacRepository; + + /** + * 사용자 조회 (단건) + * + * @param dto 사번 + * @return 인사정보 + */ + @Override + public ZtUsacOutDto selectZtUsac(ZtUsacInDto dto) { + ZtUsacOutDto result = ztUsacRepository.selectZtUsac(dto); + result.initLists(); + return result; + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/dto/SessionDto.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/dto/SessionDto.java new file mode 100644 index 000000000..f65211746 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/dto/SessionDto.java @@ -0,0 +1,90 @@ +package io.shinhanlife.dap.lib.session.dto; + +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +/** + * @package io.shinhanlife.dap.lib.session.dto + * @className SessionDto + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SessionDto { + + /* 인사번호 */ + private String prafNo; + /* 인사명 */ + private String prafNm; + /* 조직번호 */ + private String ognzNo; + /* 조직번호 */ + private String ognzNm; + /* 이메일주소 */ + private String addre; + /* 인사직무코드 */ + private String prafOfduCd; + /* 인사직무명 */ + private String prafOfduNm; + /* 인사직급코드 */ + private String prafOfleCd; + /* 인사직급명 */ + private String prafOfleNm; + /* 인사직책코드 */ + private String prafDutyCd; + /* 인사직책명 */ + private String prafDutyNm; + + private List roleNoList; + private List roleNmList; + private List tgtrPrafNoList; + private List tgtrOgnzNoList; + + // 추가된 LICO 연동 공통 헤더 필수 필드들 + private String strYmd; + private String brafNo; + private String psmrAsrtCd; + private String sbsnRulpAsrtCd; + private String bsduCd; + private String bsquCd; + private String ognzAsrtCd; + private String ognzLeveCd; + private String prgrId; + + + // 유틸성 + private String loginDtm; // 로그인일시 + private String isManager; // 관리자여부 + + public void setLoginDtm() { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"); + this.loginDtm = LocalDateTime.now().format(formatter); + } + + public void setIsManager(String isManager) { + // TODO 역할 필터링 후 관리자 여부 체크 + this.isManager = "Y"; + } + + + +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/dto/ZtUsacInDto.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/dto/ZtUsacInDto.java new file mode 100644 index 000000000..954b97157 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/dto/ZtUsacInDto.java @@ -0,0 +1,37 @@ +package io.shinhanlife.dap.lib.session.dto; + +import lombok.NoArgsConstructor; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +/** + * @package io.shinhanlife.dap.lib.session.dto + * @className ZtUsacInDto + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ZtUsacInDto { + /* 인사번호 */ + private String prafNo; + + /* 사용여부 */ + @Builder.Default + private String puseYn = "Y"; + +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/dto/ZtUsacOutDto.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/dto/ZtUsacOutDto.java new file mode 100644 index 000000000..b52e3b76f --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/dto/ZtUsacOutDto.java @@ -0,0 +1,85 @@ +package io.shinhanlife.dap.lib.session.dto; + +import lombok.Setter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * @package io.shinhanlife.dap.lib.session.dto + * @className ZtUsacOutDto + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Setter +public class ZtUsacOutDto { + + /* 인사번호 */ + private String prafNo; + /* 인사명 */ + private String prafNm; + /* 조직번호 */ + private String ognzNo; + /* 조직명 */ + private String ognzNm; + /* 이메일주소 */ +// @GlowSecureField(type = DataSecureType.DECRYPT_DB, direction = SafeDBType.COMM) TODO 방화벽 뚫리면 확인 + private String addre; + /* 인사직무코드 */ + private String prafOfduCd; + /* 인사직무명 */ + private String prafOfduNm; + /* 인사직급코드 */ + private String prafOfleCd; + /* 인사직급명 */ + private String prafOfleNm; + /* 인사직책코드 */ + private String prafDutyCd; + /* 인사직책명 */ + private String prafDutyNm; + /* 사용여부 */ + private String puseYn; + + private String roleNoStrList; + private String roleNmStrList; + private String tgtrPrafNoStrList; + private String tgtrOgnzNoStrList; + + private List roleNoList; + private List roleNmList; + private List tgtrPrafNoList; + private List tgtrOgnzNoList; + + public void initLists() { + this.roleNoList = convertStrToList(roleNoStrList); + this.roleNmList = convertStrToList(roleNmStrList); + this.tgtrPrafNoList = convertStrToList(tgtrPrafNoStrList); + this.tgtrOgnzNoList = convertStrToList(tgtrOgnzNoStrList); + } + + private List convertStrToList(String str) { + if (str == null || str.isEmpty()) { + return new ArrayList<>(); + } + return Arrays.asList(str.split(",")); + } + +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/presentation/SsoRestController.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/presentation/SsoRestController.java new file mode 100644 index 000000000..b98c76bf4 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/presentation/SsoRestController.java @@ -0,0 +1,91 @@ +package io.shinhanlife.dap.lib.session.presentation; + +import io.micrometer.common.util.StringUtils; +import io.shinhanlife.glow.BaseResponse; +import io.shinhanlife.glow.BizException; +import io.shinhanlife.glow.GlowControllerId; +import io.shinhanlife.glow.ResponseUtil; +import io.shinhanlife.dap.lib.session.converter.ZtUsacConverter; +import io.shinhanlife.dap.lib.session.domain.usecase.ZtUsacUseCase; +import io.shinhanlife.dap.lib.session.dto.SessionDto; +import io.shinhanlife.dap.lib.session.dto.ZtUsacInDto; +import io.shinhanlife.dap.lib.session.dto.ZtUsacOutDto; +import io.shinhanlife.dap.lib.session.presentation.io.SsoResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.javassist.NotFoundException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Objects; + +/** + * @package io.shinhanlife.dap.lib.session.presentation + * @className SsoRestController + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@RestController +@RequiredArgsConstructor +@Slf4j +@RequestMapping("/sso") +public class SsoRestController { + + private static final String NLS_LOGIN_URL = ""; + private final ZtUsacUseCase ztUsacService; + private final ZtUsacConverter ztUsacConverter; + + /** + * sso 연동 전 임시 로그인 + * + * @param request + * @param response + * @param session + * @param + * @return + */ + @GlowControllerId("tempLogin") + @PostMapping("/tempLogin") + public ResponseEntity> tempLogin(HttpServletRequest request, HttpServletResponse response, + HttpSession session, @RequestBody SessionDto requestDto) { + try { + if (StringUtils.isEmpty(requestDto.getPrafNo())) { + throw new NotFoundException("SSO >> not found sso id"); + } + + // DB 유저 가져오기 + ZtUsacOutDto ztUsacOutDto = ztUsacService.selectZtUsac(ZtUsacInDto.builder().prafNo(requestDto.getPrafNo()).puseYn("Y") + .build()); + if (Objects.isNull(ztUsacOutDto) || StringUtils.isEmpty(ztUsacOutDto.getPrafNo())) { + throw new BizException("SSO >> not found UserInfo >> retCode:"); + } + + SessionDto sessionDto = ztUsacConverter.toSessionDto(ztUsacOutDto); + sessionDto.setLoginDtm(); // 로그인 시점 세팅 + session.setAttribute("userInfo", sessionDto); + return ResponseUtil.ok(SsoResponse.builder().retCode("0").userInfo(sessionDto).build()); + + } catch (Exception e) { + log.error("로그인 실패", e); + session.invalidate(); + } + + return ResponseUtil.ok(SsoResponse.builder().redirectUrl(NLS_LOGIN_URL).build()); + + } + +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/presentation/io/SsoResponse.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/presentation/io/SsoResponse.java new file mode 100644 index 000000000..10594ebd4 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/session/presentation/io/SsoResponse.java @@ -0,0 +1,35 @@ +package io.shinhanlife.dap.lib.session.presentation.io; + +import io.shinhanlife.dap.lib.session.dto.SessionDto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * @package io.shinhanlife.dap.lib.session.presentation.io + * @className SsoResponse + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SsoResponse { + + private String retCode; + private SessionDto userInfo; + private String redirectUrl; + +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java new file mode 100644 index 000000000..d95dae6e3 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java @@ -0,0 +1,205 @@ +package io.shinhanlife.dap.lib.util; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import io.shinhanlife.dap.lib.annotation.McpParameter; +import io.shinhanlife.dap.lib.annotation.McpValidation; +import io.shinhanlife.dap.lib.annotation.McpAnyOf; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + + +/** + * @package io.shinhanlife.dap.lib.util + * @className JsonSchemaGenerator + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +public class JsonSchemaGenerator { + + /** + * Java DTO 클래스를 분석하여 MCP 규격의 완전한 JSON Schema를 생성합니다. + */ + public static Map generateSchema(Class clazz) { + return generateSchema(clazz, new HashSet<>()); + } + + private static Map generateSchema(Class clazz, Set> visiting) { + Map schema = new HashMap<>(); + schema.put("type", "object"); + schema.put("additionalProperties", false); + if (!visiting.add(clazz)) { + return schema; + } + + Map properties = new HashMap<>(); + List requiredList = new ArrayList<>(); + + for (Field field : clazz.getDeclaredFields()) { + Map fieldSchema = createFieldSchema(field, visiting); + + // 1. 타입 매핑 + + // 2. 어노테이션 기반 설명 추출 + McpParameter paramAnnotation = field.getAnnotation(McpParameter.class); + JsonPropertyDescription descAnnotation = field.getAnnotation(JsonPropertyDescription.class); + if (paramAnnotation != null && !paramAnnotation.description().isEmpty()) { + fieldSchema.put("description", paramAnnotation.description()); + } else if (descAnnotation != null && !descAnnotation.value().isEmpty()) { + fieldSchema.put("description", descAnnotation.value()); + } else { + fieldSchema.put("description", field.getName()); // 기본값 + } + + // 3. 필수 여부 판단 + JsonProperty jsonProp = field.getAnnotation(JsonProperty.class); + if ((jsonProp != null && jsonProp.required()) || (paramAnnotation != null && paramAnnotation.required())) { + requiredList.add(field.getName()); + } + + McpValidation validation = field.getAnnotation(McpValidation.class); + if (validation != null && validation.required() && !requiredList.contains(field.getName())) { + requiredList.add(field.getName()); + } + if (validation != null && !validation.pattern().isEmpty()) { + fieldSchema.put("pattern", validation.pattern()); + } + if (validation != null && validation.minimum() != Long.MIN_VALUE) { + fieldSchema.put("minimum", validation.minimum()); + } + if (validation != null && validation.maximum() != Long.MAX_VALUE) { + fieldSchema.put("maximum", validation.maximum()); + } + if (validation != null && validation.minLength() >= 0) { + fieldSchema.put("minLength", validation.minLength()); + } + if (validation != null && validation.maxLength() >= 0) { + fieldSchema.put("maxLength", validation.maxLength()); + } + if (validation != null && validation.allowedValues().length > 0) { + fieldSchema.put("enum", List.of(validation.allowedValues())); + } + if (validation != null && !validation.format().isEmpty()) { + fieldSchema.put("format", validation.format()); + } + if (validation != null && !validation.defaultValue().isEmpty()) { + fieldSchema.put("default", coerceDefaultValue(validation.defaultValue(), field.getType())); + } + if (validation != null && validation.examples().length > 0) { + fieldSchema.put("examples", List.of(validation.examples())); + } + if (validation != null && validation.nullable()) { + Map nonNullSchema = new HashMap<>(fieldSchema); + fieldSchema = new HashMap<>(); + fieldSchema.put("anyOf", List.of( + nonNullSchema, + Map.of("type", "null") + )); + } + + properties.put(field.getName(), fieldSchema); + } + + schema.put("properties", properties); + if (!requiredList.isEmpty()) { + schema.put("required", requiredList); + } + + McpAnyOf anyOfAnnotation = clazz.getAnnotation(McpAnyOf.class); + if (anyOfAnnotation != null && anyOfAnnotation.value().length > 0) { + List> anyOfList = new ArrayList<>(); + for (String fieldName : anyOfAnnotation.value()) { + anyOfList.add(Map.of("required", List.of(fieldName))); + + } + schema.put("anyOf", anyOfList); + } + + visiting.remove(clazz); + return schema; + } + + + private static Object coerceDefaultValue(String value, Class fieldType) { + try { + if (fieldType == Integer.class || fieldType == int.class + || fieldType == Long.class || fieldType == long.class + || fieldType == Short.class || fieldType == short.class + || fieldType == Byte.class || fieldType == byte.class) { + return Long.valueOf(value); + } + if (fieldType == Double.class || fieldType == double.class + || fieldType == Float.class || fieldType == float.class) { + return Double.valueOf(value); + } + if (fieldType == Boolean.class || fieldType == boolean.class) { + return Boolean.valueOf(value); + } + return value; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid MCP default value: " + value, e); + } + } + private static Map createFieldSchema(Field field, Set> visiting) { + Class fieldType = field.getType(); + if (isSimpleType(fieldType)) { + return new HashMap<>(Map.of("type", mapJavaTypeToJsonType(fieldType))); + } + if (List.class.isAssignableFrom(fieldType)) { + Map fieldSchema = new HashMap<>(); + fieldSchema.put("type", "array"); + fieldSchema.put("items", generateItemsSchema(field, visiting)); + return fieldSchema; + } + return generateSchema(fieldType, visiting); + } + + private static Map generateItemsSchema(Field field, Set> visiting) { + Type genericType = field.getGenericType(); + if (genericType instanceof ParameterizedType parameterizedType) { + Type itemType = parameterizedType.getActualTypeArguments()[0]; + if (itemType instanceof Class itemClass) { + if (isSimpleType(itemClass)) { + return new HashMap<>(Map.of("type", mapJavaTypeToJsonType(itemClass))); + } + return generateSchema(itemClass, visiting); + } + } + return new HashMap<>(Map.of("type", "object")); + } + + private static boolean isSimpleType(Class clazz) { + return clazz == String.class + || clazz == Integer.class || clazz == int.class + || clazz == Long.class || clazz == long.class + || clazz == Double.class || clazz == double.class + || clazz == Float.class || clazz == float.class + || clazz == Boolean.class || clazz == boolean.class; + } + + private static String mapJavaTypeToJsonType(Class clazz) { + if (clazz == String.class) return "string"; + if (clazz == Integer.class || clazz == int.class) return "integer"; + if (clazz == Long.class || clazz == long.class) return "integer"; + if (clazz == Double.class || clazz == double.class) return "number"; + if (clazz == Float.class || clazz == float.class) return "number"; + if (clazz == Boolean.class || clazz == boolean.class) return "boolean"; + if (List.class.isAssignableFrom(clazz)) return "array"; + return "object"; + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/PodScaffolder.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/PodScaffolder.java new file mode 100644 index 000000000..48d7b988a --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/PodScaffolder.java @@ -0,0 +1,309 @@ +package io.shinhanlife.dap.lib.util; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Scanner; + +public class PodScaffolder { + + public static void main(String[] args) throws IOException { + Scanner scanner = new Scanner(System.in); + + System.out.println("========================================="); + System.out.println(" MCP Tool Pod Scaffolder (Java CLI) "); + System.out.println("=========================================\n"); + + String rawModuleName = getOrAsk(args, 0, scanner, "1. 생성할 모듈(Pod) 이름 (예: payment 또는 dap-tool-payment): "); + String moduleName = rawModuleName.startsWith("dap-tool-") ? rawModuleName : "dap-tool-" + rawModuleName; + String portStr = getOrAsk(args, 1, scanner, "2. 사용할 포트 번호 (예: 8085): "); + String shortName = moduleName.replace("dap-tool-", "").replace("-", ""); + + String defaultAuthor = System.getProperty("user.name"); + String defaultDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd")); + + String author = getOrAsk(args, 2, scanner, "3. 작성자 (엔터 입력 시 '" + defaultAuthor + "'): "); + if (author.trim().isEmpty()) author = defaultAuthor; + String createDate = getOrAsk(args, 3, scanner, "4. 작성일 (엔터 입력 시 '" + defaultDate + "'): "); + if (createDate.trim().isEmpty()) createDate = defaultDate; + + String result = scaffoldPod(moduleName, portStr, shortName, author, createDate); + System.out.println(result); + } + + private static String getOrAsk(String[] args, int index, Scanner scanner, String prompt) { + if (args.length > index) { + return args[index]; + } + System.out.print(prompt); + return scanner.nextLine().trim(); + } + + public static String scaffoldPod(String moduleName, String portStr, String shortName, String author, String createDate) throws IOException { + String envSourceDir = System.getenv("AXHUB_SOURCE_DIR"); + Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get("."); + + Path modulePath = rootDir.resolve(Paths.get(moduleName)); + if (Files.exists(modulePath)) { + return "[오류] 이미 존재하는 모듈입니다: " + moduleName; + } + + StringBuilder log = new StringBuilder(); + log.append("[1/6] 모듈 디렉터리 생성 중...\n"); + Files.createDirectories(modulePath); + + log.append("[2/6] build.gradle 생성 중...\n"); + String buildGradle = """ + plugins { + id 'org.springframework.boot' + } + dependencies { + implementation project(':dap-tool-core') + } + dependencies { + compileOnly 'org.projectlombok:lombok:1.18.32' + annotationProcessor 'org.projectlombok:lombok:1.18.32' + } + """; + Files.writeString(modulePath.resolve("build.gradle"), buildGradle); + + log.append("[3/6] Dockerfile 생성 중...\n"); + String dockerfile = """ + FROM eclipse-temurin:21-jdk-alpine + WORKDIR /app + COPY build/libs/%s-0.0.1-SNAPSHOT.jar app.jar + ENTRYPOINT ["java", "-jar", "app.jar"] + """.formatted(moduleName); + Files.writeString(modulePath.resolve("Dockerfile"), dockerfile); + + log.append("[4/6] Application 클래스 및 설정 파일 생성 중...\n"); + Path srcPath = modulePath.resolve("src/main/java/io/shinhanlife/dap/mcc/" + shortName); + Files.createDirectories(srcPath); + + String appClass = """ + package io.shinhanlife.dap.mcc.%s; + + import org.springframework.boot.SpringApplication; + import org.springframework.boot.autoconfigure.SpringBootApplication; + import org.springframework.boot.context.properties.ConfigurationPropertiesScan; + import org.springframework.cache.annotation.EnableCaching; + + /** + * @package io.shinhanlife.dap.mcc.%s + * @className DapTool%sApplication + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+             * ---------- 개정이력 ----------
+             * 수정일      수정자    수정내용
+             * ---------- -------- ---------------------------
+             * %s  %s    최초생성
+             * 
+             * 
+ */ + @SpringBootApplication(scanBasePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.lib.adapter", "io.shinhanlife.dap.lib.mcp", "io.shinhanlife.dap.lib.config", "io.shinhanlife.dap.lib.integration"}) + @ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.lib.adapter", "io.shinhanlife.dap.lib.mcp", "io.shinhanlife.dap.lib.config", "io.shinhanlife.dap.lib.integration"}) + @EnableCaching + public class DapTool%sApplication { + public static void main(String[] args) { + SpringApplication.run(DapTool%sApplication.class, args); + } + } + """.formatted(shortName, shortName, capitalize(shortName), author, createDate, createDate, author, capitalize(shortName), capitalize(shortName)); + Files.writeString(srcPath.resolve("DapTool" + capitalize(shortName) + "Application.java"), appClass); + + Path resPath = modulePath.resolve("src/main/resources"); + Files.createDirectories(resPath); + String applicationYml = """ + server: + port: %s + spring: + application: + name: %s + profiles: + active: local + logging: + level: + org.apache.kafka: ERROR + mcp: + namespace: "" + security: + tenant-domains: + TESTER-DEV: ALL + """.formatted(portStr, moduleName); + Files.writeString(resPath.resolve("application.yml"), applicationYml); + + String applicationLocalYml = """ + # Local 환경 전용 설정 (H2 메모리 DB 등) + spring: + datasource: + url: jdbc:p6spy:h2:mem:testdb;DB_CLOSE_DELAY=-1; + driverClassName: com.p6spy.engine.spy.P6SpyDriver + username: sa + password: password + h2: + console: + enabled: true + + eims: + http: + url: http://localhost:${server.port}/api/gateway + tcp: + host: 127.0.0.1 + port: 8090 + timeout: 5000 + jsp: + form: + url: http://localhost:${server.port}/mock/jsp-form + json: + url: http://localhost:${server.port}/mock/jsp-json + mci: + url: http://localhost:${server.port}/api/mock/esb/api + mcistring: + url: http://localhost:${server.port}/api/mock/esb/string + + mcp: + security: + tenant-domains: + mcp-client-1: CUSTOMER,COMMON + mcp-client-2: ALL + + axhub: + gateway: + url: http://localhost:8081 + tool: + url: ${AXHUB_TOOL_URL:http://localhost:${server.port}} + """; + Files.writeString(resPath.resolve("application-local.yml"), applicationLocalYml); + + String applicationDevYml = """ + # OCI 클라우드 환경 전용 설정 + server: + port: ${PORT:%s} + + axhub: + gateway: + url: https://axhubmcp.devjun.net + tool: + url: http://144.24.70.100:%s + + eims: + http: + url: http://localhost:${server.port}/api/gateway + tcp: + host: 127.0.0.1 + port: 8090 + timeout: 5000 + jsp: + form: + url: http://localhost:${server.port}/mock/jsp-form + json: + url: http://localhost:${server.port}/mock/jsp-json + mci: + url: http://localhost:${server.port}/api/mock/esb/api + mcistring: + url: http://localhost:${server.port}/api/mock/esb/string + + shinhan: + integration: + envrTypeCd: D + eai: + url: http://10.176.32.181 + internalMci: + url: http://10.176.32.173 + bancaMci: + url: http://10.176.32.117 + externalMci: + url: http://10.176.32.176 + """.formatted(portStr, portStr); + Files.writeString(resPath.resolve("application-dev.yml"), applicationDevYml); + + String logbackXml = """ + + + + + + ${LOG_PATTERN} + + + + logs/%s.log + + logs/%s-%%d{yyyy-MM-dd}.log + 30 + + + ${LOG_PATTERN} + + + + + + + + + """.formatted(moduleName, moduleName); + Files.writeString(resPath.resolve("logback-spring.xml"), logbackXml); + + log.append("[5/6] settings.gradle 에 모듈 등록 중...\n"); + Path settingsPath = rootDir.resolve(Paths.get("settings.gradle")); + if (Files.exists(settingsPath)) { + String settings = Files.readString(settingsPath); + if (!settings.contains("include '" + moduleName + "'")) { + Files.writeString(settingsPath, System.lineSeparator() + "include '" + moduleName + "'" + System.lineSeparator(), StandardOpenOption.APPEND); + } + } + + log.append("[6/6] docker-compose.yml 에 서비스 추가 중...\n"); + Path dockerComposePath = rootDir.resolve(Paths.get("docker-compose.yml")); + if (Files.exists(dockerComposePath)) { + String compose = Files.readString(dockerComposePath); + String serviceName = moduleName.replace("dap-", ""); // e.g. tool-payment + if (!compose.contains(" " + serviceName + ":")) { + String newService = """ + %s: + build: + context: . + dockerfile: %s/Dockerfile + ports: + - "%s:%s" + depends_on: + - redis + environment: + - TZ=Asia/Seoul + - SPRING_REDIS_HOST=redis + - SPRING_REDIS_PORT=6379 + - SPRING_DATA_REDIS_PORT=6379 + - AXHUB_GATEWAY_URL=http://gateway:8081 + - AXHUB_TOOL_URL=http://%s:%s + - GLOW_COMMUNICATION_MCI_HOST=http://mci-mock + - GLOW_COMMUNICATION_MCI_PORT=8080 + - GLOW_COMMUNICATION_EXTMCI_HOST=http://mci-mock + - GLOW_COMMUNICATION_EXTMCI_PORT=8080 + - GLOW_COMMUNICATION_EAI_HOST=http://mci-mock + - GLOW_COMMUNICATION_EAI_PORT=8080 + """.formatted(serviceName, moduleName, portStr, portStr, serviceName, portStr); + Files.writeString(dockerComposePath, System.lineSeparator() + newService, StandardOpenOption.APPEND); + } + } + + log.append("\n=========================================\n"); + log.append(" Pod Scaffolding Complete! \n"); + log.append("=========================================\n"); + log.append("1. [새로운 모듈] ").append(moduleName).append(" 폴더가 생성되었습니다.\n"); + log.append("2. [ToolScaffolder]를 사용해 이 모듈 안에 툴을 추가하세요.\n"); + log.append("3. 실행 전 Gradle 동기화(Sync)를 한 번 진행해 주세요.\n"); + return log.toString(); + } + + private static String capitalize(String str) { + if (str == null || str.isEmpty()) return str; + return str.substring(0, 1).toUpperCase() + str.substring(1); + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/SessionUtil.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/SessionUtil.java new file mode 100644 index 000000000..e155b9811 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/SessionUtil.java @@ -0,0 +1,45 @@ +package io.shinhanlife.dap.lib.util; + +import io.shinhanlife.dap.lib.session.dto.SessionDto; +import jakarta.servlet.http.HttpSession; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +/** + * @package io.shinhanlife.dap.lib.util + * @className SessionUtil + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +public class SessionUtil { + + private static final String SESSION_KEY = "userInfo"; + + private SessionUtil() {} + + public static SessionDto getSession() { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes == null) return null; + HttpSession session = attributes.getRequest().getSession(false); + if (session == null) return null; + return (SessionDto) session.getAttribute(SESSION_KEY); + } + + public static String getPrafNo() { + SessionDto session = getSession(); + return session != null ? session.getPrafNo() : null; + } + + public static String getOgnzNo() { + SessionDto session = getSession(); + return session != null ? session.getOgnzNo() : null; + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java new file mode 100644 index 000000000..e2506564b --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java @@ -0,0 +1,714 @@ +package io.shinhanlife.dap.lib.util; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Locale; +import java.util.Scanner; + +/** + * MCP Tool 코드를 자동 생성(Scaffolding)하는 유틸리티 클래스 + * + * [실행 방법] + * 방법 1. IDE(IntelliJ 등)에서 직접 실행 (대화형 모드 추천 ⭐) + * - 이 클래스(ToolScaffolder.java)를 열고 main 메서드를 직접 실행(Run)합니다. + * - 콘솔 창에 뜨는 질문에 차례대로 값을 입력하기만 하면 파일이 생성됩니다. + * + * 방법 2. 커맨드라인(터미널)에서 실행 (명령어 기반) + * - 컴파일: javac -encoding UTF-8 dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/util/ToolScaffolder.java + * - 실행: java -cp dap-tool-core/src/main/java io.shinhanlife.dap.lib.util.ToolScaffolder [이름] [ID] "[설명]" "[그룹]" "[통신방식]" "[모듈명]" + */ +/** + * @package io.shinhanlife.dap.lib.util + * @className ToolScaffolder + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +public class ToolScaffolder { + + private static final String BASE_PACKAGE = "io.shinhanlife.dap.mcc"; + private static final String BASE_PACKAGE_PATH = "src/main/java/io/shinhanlife/dap/mcc"; + + public static void main(String[] args) throws IOException { + Scanner scanner = new Scanner(System.in); + + System.out.println("========================================="); + System.out.println(" MCP Tool Scaffolder (Java CLI) "); + System.out.println("=========================================\n"); + + String baseName = getOrAsk(args, 0, scanner, "1. 생성할 Tool의 기본 이름 (예: ExchangeRate) [영문 PascalCase]: "); + String interfaceId = getOrAsk(args, 1, scanner, "2. 레거시 API 인터페이스 ID (예: EXCH_001): "); + String description = getOrAsk(args, 2, scanner, "3. Tool 기능 설명 (예: 환율 조회): "); + String group = getOrAsk(args, 3, scanner, "4. Tool 소속 그룹 (예: SAMPLE, NOTIFICATION, CLAIM, POLICY, HR, CONTRACT, CUSTOMER 등): "); + if (group.isEmpty()) group = "COMMON"; + String routingType = getOrAsk(args, 4, scanner, "5. 통신 프로토콜 (예: HTTP, TCP, MCI, EAI): "); + if (routingType.trim().isEmpty()) { + routingType = "HTTP"; + } + String moduleName = getOrAsk(args, 5, scanner, "6. 코드를 생성할 모듈 (기본: dap-tool-oth): "); + if (moduleName.trim().isEmpty()) { + moduleName = "dap-tool-oth"; + } + + String defaultAuthor = System.getProperty("user.name"); + String defaultDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd")); + + String author = getOrAsk(args, 6, scanner, "7. 작성자 (엔터 입력 시 '" + defaultAuthor + "'): "); + if (author.trim().isEmpty()) author = defaultAuthor; + String createDate = getOrAsk(args, 7, scanner, "8. 작성일 (엔터 입력 시 '" + defaultDate + "'): "); + if (createDate.trim().isEmpty()) createDate = defaultDate; + + String result = scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, true, null); + System.out.println(result); + } + + private static String getOrAsk(String[] args, int index, Scanner scanner, String prompt) { + if (args.length > index) { + return args[index]; + } + System.out.print(prompt); + return scanner.nextLine().trim(); + } + + public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode) throws IOException { + baseName = toPascalCase(baseName); + String envSourceDir = System.getenv("AXHUB_SOURCE_DIR"); + Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get("."); + + Path usecaseDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "usecase")); + Path usecaseImplDir = usecaseDir.resolve("impl"); + Path dtoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "dto")); + + Path legacyDtoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "legacy")); + Path converterDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "converter")); + + String bizPackage = BASE_PACKAGE + ".biz." + group.toLowerCase(); + boolean isMci = "MCI".equalsIgnoreCase(routingType); + String mciGroupPath = "infra/itrf/mci/" + group.toLowerCase(); + String clientPkgSuffix = ""; + String clientPrefixCap = ""; + Path mciClientDir = null; + + if (isMci && clientSystemCode != null && clientSystemCode.length() == 4) { + String clientPrefix = clientSystemCode.toLowerCase(); + clientPkgSuffix = clientPrefix.substring(0, 3) + "." + clientPrefix.substring(3, 4); + clientPrefixCap = toPascalCase(clientSystemCode); + mciGroupPath = "infra/itrf/mci/" + clientPrefix.substring(0, 3) + "/" + clientPrefix.substring(3, 4); + mciClientDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, mciGroupPath)); + } + + Path mciIoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, mciGroupPath, "io")); + + Files.createDirectories(usecaseDir); + Files.createDirectories(usecaseImplDir); + Files.createDirectories(dtoDir); + if (isMci) { + Files.createDirectories(mciIoDir); + if (mciClientDir != null) { + Files.createDirectories(mciClientDir); + } + } else { + Files.createDirectories(legacyDtoDir); + } + Files.createDirectories(converterDir); + + StringBuilder log = new StringBuilder(); + + // Generate Request DTO + String reqContent = """ + package %s.dto; + + import com.fasterxml.jackson.annotation.JsonInclude; + import io.shinhanlife.dap.lib.annotation.McpParameter; + import io.shinhanlife.dap.lib.annotation.McpValidation; + import lombok.Data; + + /** + * @package %s.dto + * @className %sRequest + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+             * ---------- 개정이력 ----------
+             * 수정일      수정자    수정내용
+             * ---------- -------- ---------------------------
+             * %s  %s    최초생성
+             * 
+             * 
+ */ + @Data + @JsonInclude(JsonInclude.Include.NON_NULL) + public class %sRequest { + @McpParameter(description = "수신자 전화번호", required = true) + @McpValidation(pattern = "^01(?:0|1|[6-9])-(?:\\\\d{3}|\\\\d{4})-\\\\d{4}$", examples = {"010-1234-5678"}) + private String phoneNumber; + + @McpParameter(description = "전송할 메시지 내용", required = true) + @McpValidation(defaultValue = "안녕하세요.") + private String message; + } + """.formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName); + Files.writeString(dtoDir.resolve(baseName + "Request.java"), reqContent); + + // Generate Response DTO + String resContent = """ + package %s.dto; + + import com.fasterxml.jackson.annotation.JsonInclude; + import io.shinhanlife.dap.lib.annotation.McpOutputSchema; + import io.shinhanlife.dap.lib.annotation.McpValidation; + import lombok.Data; + + /** + * @package %s.dto + * @className %sResponse + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+             * ---------- 개정이력 ----------
+             * 수정일      수정자    수정내용
+             * ---------- -------- ---------------------------
+             * %s  %s    최초생성
+             * 
+             * 
+ */ + @Data + @McpOutputSchema + @JsonInclude(JsonInclude.Include.NON_NULL) + public class %sResponse { + @McpValidation(required = true) + private String resultCode; + + @McpValidation(maxLength = 200, nullable = true) + private String resultMessage; + + // TODO: Add response fields here. Do not include PII in the Tool response. + } + """.formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName); + Files.writeString(dtoDir.resolve(baseName + "Response.java"), resContent); + + String toolName = toToolName(moduleName, group, baseName); + + String serviceInterfaceContent = """ + package %s.usecase; + + import io.shinhanlife.dap.lib.annotation.McpFunction; + import io.shinhanlife.dap.lib.annotation.McpTool; + import %s.dto.%sRequest; + + @McpTool( + routingType = "%s", + categoryKey = "%s" + ) + public interface %sUseCase { + @McpFunction( + displayName = "%s 툴", + name = "%s", + description = "%s", + prompt = "%s", + mappingId = "%s", + register = %s, + requiresApproval = false, + openWorldHint = true, + version = "1.0.0", + timeoutMillis = 300000L, + enabled = true + ) + Object execute(%sRequest req); + } + """.formatted( + bizPackage, + bizPackage, baseName, + routingType, group.toLowerCase(), + baseName, + baseName, toolName, description, description + " ?줘.", interfaceId, register, + baseName + ); + + Files.writeString(usecaseDir.resolve(baseName + "UseCase.java"), serviceInterfaceContent); + + String serviceImplContent; + + if (isMci) { + serviceImplContent = """ + package %s.usecase.impl; + + import %s.dto.%sRequest; + import %s.dto.%sResponse; + import %s.usecase.%sUseCase; + import io.shinhanlife.dap.lib.integration.mci.component.AxhubMciComponent; + import io.shinhanlife.glow.communication.dto.Transfer; + import org.springframework.stereotype.Service; + import lombok.RequiredArgsConstructor; + import lombok.extern.slf4j.Slf4j; + import java.util.Map; + import %s.converter.%sConverter; + import %s.%s.io.%s_I; + %s + + /** + * @package %s.usecase.impl + * @className %sUseCaseImpl + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Slf4j + @Service + @RequiredArgsConstructor + public class %sUseCaseImpl implements %sUseCase { + + %s + private final %sConverter converter; + + @Override + public Object execute(%sRequest req) { + log.info("[MCI Tool] {} 요청 수신.", "%s"); + try { + // MapStruct를 이용한 자동 매핑 (AI DTO -> MCI DTO) + %s_I mciReq = converter.toLegacyRequest(req); + + Transfer resTransfer = mci.callTo( + "%s", + null, + mciReq, + Object.class + ); + return resTransfer.getBody() != null ? resTransfer.getBody() : Map.of("status", "SUCCESS"); + } catch (Exception e) { + log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e); + return Map.of("status", "ERROR", "message", e.getMessage() != null ? e.getMessage() : "Unknown error"); + } + } + } + """.formatted( + bizPackage, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, + BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, + (clientPrefixCap.isEmpty() ? "" : "import " + BASE_PACKAGE + "." + mciGroupPath.replace("/", ".") + ".Mci" + clientPrefixCap + "Client;\n"), + bizPackage, + baseName, + author, + createDate, + createDate, author, + baseName, + baseName, + (clientPrefixCap.isEmpty() ? "private final AxhubMciComponent mci;" : "private final Mci" + clientPrefixCap + "Client mci;"), + baseName, + baseName, + toolName, + interfaceId, + interfaceId + ); + } else { + serviceImplContent = """ + package %s.usecase.impl; + + import %s.dto.%sRequest; + import %s.dto.%sResponse; + import %s.usecase.%sUseCase; + import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase; + import %s.converter.%sConverter; + import org.springframework.stereotype.Service; + import lombok.RequiredArgsConstructor; + import lombok.extern.slf4j.Slf4j; + + /** + * @package %s.usecase.impl + * @className %sUseCaseImpl + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Slf4j + @Service + @RequiredArgsConstructor + public class %sUseCaseImpl extends AbstractMcpToolUseCase implements %sUseCase { + + private final %sConverter converter; + + @Override + public Object execute(%sRequest req) { + // %sLegacyRequest legacyRequest = converter.toLegacyRequest(req); + return executeLegacy("%s", "%s", req); // Or pass legacyRequest + } + } + """.formatted( + bizPackage, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, + author, + createDate, + createDate, author, + baseName, baseName, + baseName, + baseName, + baseName, + routingType, interfaceId + ); + } + + Files.writeString(usecaseImplDir.resolve(baseName + "UseCaseImpl.java"), serviceImplContent); + + if (isMci) { + String mciReqContent = """ + package %s.%s.io; + + import lombok.Data; + + /** + * @package %s.%s.io + * @className %s_I + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Data + public class %s_I { + /** + * EAI 시스템이 요구하는 수신자 번호 파라미터명 + */ + private String phone; + + /** + * EAI 시스템이 요구하는 메시지 내용 파라미터명 + */ + private String content; + } + """.formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId); + Files.writeString(mciIoDir.resolve(interfaceId + "_I.java"), mciReqContent); + + String mciResContent = """ + package %s.%s.io; + + import lombok.Data; + + /** + * @package %s.%s.io + * @className %s_O + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Data + public class %s_O { + // TODO: Add response fields here + } + """.formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId); + Files.writeString(mciIoDir.resolve(interfaceId + "_O.java"), mciResContent); + + String converterContent = """ + package %s.converter; + + import %s.dto.%sRequest; + import %s.dto.%sResponse; + import %s.%s.io.%s_I; + import %s.%s.io.%s_O; + import org.mapstruct.Mapper; + import org.mapstruct.Mapping; + import org.mapstruct.factory.Mappers; + + /** + * @package %s.converter + * @className %sConverter + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Mapper(componentModel = "spring") + public interface %sConverter { + + @Mapping(source = "phoneNumber", target = "phone") + @Mapping(source = "message", target = "content") + %s_I toLegacyRequest(%sRequest req); + + @Mapping(source = "phone", target = "phoneNumber") + @Mapping(source = "content", target = "message") + %sRequest toRequest(%s_I mciReq); + + // %sResponse toResponse(%s_O mciRes); + } + """.formatted( + bizPackage, + bizPackage, baseName, + bizPackage, baseName, + BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, + BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, + bizPackage, baseName, author, createDate, createDate, author, + baseName, interfaceId, baseName, + baseName, interfaceId, + baseName, interfaceId + ); + Files.writeString(converterDir.resolve(baseName + "Converter.java"), converterContent); + + log.append("\n=========================================\n"); + log.append(" Scaffolding Complete! (Routing: " + routingType + ")\n"); + log.append("=========================================\n"); + log.append("[Usecase Interface] ").append(usecaseDir.resolve(baseName + "UseCase.java")).append("\n"); + log.append("[Usecase Impl] ").append(usecaseImplDir.resolve(baseName + "UseCaseImpl.java")).append("\n"); + log.append("[Request DTO] ").append(dtoDir.resolve(baseName + "Request.java")).append("\n"); + log.append("[Response DTO] ").append(dtoDir.resolve(baseName + "Response.java")).append("\n"); + log.append("[MCI Request IO] ").append(mciIoDir.resolve(interfaceId + "_I.java")).append("\n"); + log.append("[MCI Response IO] ").append(mciIoDir.resolve(interfaceId + "_O.java")).append("\n"); + log.append("[MCI Converter] ").append(converterDir.resolve(baseName + "Converter.java")).append("\n"); + + if (!clientPrefixCap.isEmpty()) { + String mciClientContent = """ + package %s.%s; + + import org.springframework.stereotype.Component; + import lombok.RequiredArgsConstructor; + import io.shinhanlife.dap.lib.integration.mci.component.AxhubMciComponent; + import io.shinhanlife.glow.communication.dto.Transfer; + + /** + * @package %s.%s + * @className Mci%sClient + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                     * ---------- 개정이력 ----------
+                     * 수정일      수정자    수정내용
+                     * ---------- -------- ---------------------------
+                     * %s  %s    최초생성
+                     * 
+                     * 
+ */ + @Component + @RequiredArgsConstructor + public class Mci%sClient { + private final AxhubMciComponent mci; + + public Transfer callTo(String interfaceId, String dummy, Object mciReq, Class resType) throws Exception { + return mci.callTo(interfaceId, dummy, mciReq, resType); + } + } + """.formatted( + BASE_PACKAGE, mciGroupPath.replace("/", "."), + BASE_PACKAGE, mciGroupPath.replace("/", "."), clientPrefixCap, author, createDate, createDate, author, clientPrefixCap + ); + Files.writeString(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java"), mciClientContent); + log.append("[MCI Client] ").append(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java")).append("\n"); + } + + } else { + String legacyReqContent = """ + package %s.legacy; + + import lombok.Data; + + /** + * @package %s.legacy + * @className %sLegacyRequest + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Data + public class %sLegacyRequest { + /** + * EAI 시스템이 요구하는 수신자 번호 파라미터명 + */ + private String phone; + + /** + * EAI 시스템이 요구하는 메시지 내용 파라미터명 + */ + private String content; + } + """.formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName); + Files.writeString(legacyDtoDir.resolve(baseName + "LegacyRequest.java"), legacyReqContent); + + String legacyResContent = """ + package %s.legacy; + + import lombok.Data; + + /** + * @package %s.legacy + * @className %sLegacyResponse + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Data + public class %sLegacyResponse { + // TODO: Add legacy response fields here + } + """.formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName); + Files.writeString(legacyDtoDir.resolve(baseName + "LegacyResponse.java"), legacyResContent); + + String converterContent = """ + package %s.converter; + + import %s.dto.%sRequest; + import %s.dto.%sResponse; + import %s.legacy.%sLegacyRequest; + import %s.legacy.%sLegacyResponse; + import org.mapstruct.Mapper; + import org.mapstruct.Mapping; + import org.mapstruct.factory.Mappers; + + /** + * @package %s.converter + * @className %sConverter + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Mapper(componentModel = "spring") + public interface %sConverter { + + @Mapping(source = "phoneNumber", target = "phone") + @Mapping(source = "message", target = "content") + %sLegacyRequest toLegacyRequest(%sRequest req); + + @Mapping(source = "phone", target = "phoneNumber") + @Mapping(source = "content", target = "message") + %sRequest toRequest(%sLegacyRequest legacyRequest); + + // %sResponse toResponse(%sLegacyResponse legacyResponse); + } + """.formatted( + bizPackage, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, author, createDate, createDate, author, + baseName, baseName, baseName, baseName, baseName, baseName, baseName + ); + Files.writeString(converterDir.resolve(baseName + "Converter.java"), converterContent); + + log.append("\n=========================================\n"); + log.append(" Scaffolding Complete! (Routing: " + routingType + ")\n"); + log.append("=========================================\n"); + log.append("[Usecase Interface] ").append(usecaseDir.resolve(baseName + "UseCase.java")).append("\n"); + log.append("[Usecase Impl] ").append(usecaseImplDir.resolve(baseName + "UseCaseImpl.java")).append("\n"); + log.append("[Request DTO] ").append(dtoDir.resolve(baseName + "Request.java")).append("\n"); + log.append("[Response DTO] ").append(dtoDir.resolve(baseName + "Response.java")).append("\n"); + log.append("[Legacy Request DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyRequest.java")).append("\n"); + log.append("[Legacy Response DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyResponse.java")).append("\n"); + log.append("[Legacy Converter] ").append(converterDir.resolve(baseName + "Converter.java")).append("\n"); + } + log.append("\n Tip: ").append(interfaceId).append(" 목업 데이터를 mock-responses.json에 추가하세요.\n"); + + return log.toString(); + } + + private static String toToolName(String moduleName, String group, String baseName) { + String pod = moduleName.startsWith("dap-tool-") + ? moduleName.substring("dap-tool-".length()) + : "oth"; + String normalizedName = baseName.replaceAll("([a-z0-9])([A-Z])", "$1 $2") + .toLowerCase(Locale.ROOT) + .replaceAll("[^a-z0-9]+", " ") + .trim(); + String[] words = normalizedName.split("\\s+"); + String service = words[0]; + String action = words.length == 1 ? "execute" : words[words.length - 1]; + return "%s.%s.%s.%s".formatted( + pod.toLowerCase(Locale.ROOT), + group.toLowerCase(Locale.ROOT), + service, + action); + } + + private static String toPascalCase(String str) { + if (str == null || str.isEmpty()) { + return str; + } + StringBuilder result = new StringBuilder(); + boolean capitalizeNext = true; + for (char c : str.toCharArray()) { + if (c == '_' || c == '-' || c == ' ') { + capitalizeNext = true; + } else if (capitalizeNext) { + result.append(Character.toUpperCase(c)); + capitalizeNext = false; + } else { + result.append(c); + } + } + if (result.length() > 0) { + result.setCharAt(0, Character.toUpperCase(result.charAt(0))); + } + return result.toString(); + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java new file mode 100644 index 000000000..5ea54dfb1 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java @@ -0,0 +1,78 @@ +package io.shinhanlife.dap.lib.util; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.shinhanlife.dap.lib.annotation.McpFunction; +import io.shinhanlife.dap.lib.annotation.McpOutputSchema;import java.io.InputStream; +import java.util.Map; +import org.springframework.core.io.ClassPathResource; + +/** Resolves an MCP Tool input schema from resource, inline value, or DTO metadata. */ +public class ToolSchemaResolver { + + private final ObjectMapper objectMapper; + + public ToolSchemaResolver(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + public Map resolve(McpFunction function, Class requestType) { + if (function != null && !function.inputSchemaResource().isBlank()) { + return loadResource(function.inputSchemaResource()); + } + if (function != null && !function.inputSchema().isBlank() + && !"{}".equals(function.inputSchema().trim())) { + return parse(function.inputSchema(), "McpFunction.inputSchema"); + } + return JsonSchemaGenerator.generateSchema(requestType); + } + + /** + * Resolves an explicitly declared response schema. + * Response schemas are opt-in so existing tools keep their current response behavior. + */ + public Map resolveOutput(McpFunction function, Class responseType) { + if (function != null && !function.outputSchemaResource().isBlank()) { + return loadResource(function.outputSchemaResource()); + } + if (function != null && !function.outputSchema().isBlank() + && !"{}".equals(function.outputSchema().trim())) { + return parse(function.outputSchema(), "McpFunction.outputSchema"); + } + if (responseType != null && responseType.isAnnotationPresent(McpOutputSchema.class)) { + return JsonSchemaGenerator.generateSchema(responseType); + } + return Map.of(); + } + + /** + * Retained for callers that use only explicit output schemas. + */ + public Map resolveOutput(McpFunction function) { + return resolveOutput(function, null); + } + + private Map loadResource(String location) { + String path = location.startsWith("classpath:") + ? location.substring("classpath:".length()) + : location; + ClassPathResource resource = new ClassPathResource(path); + if (!resource.exists()) { + throw new IllegalStateException("MCP schema resource not found: " + location); + } + + try (InputStream inputStream = resource.getInputStream()) { + return objectMapper.readValue(inputStream, new TypeReference<>() { }); + } catch (Exception e) { + throw new IllegalStateException("Failed to load MCP schema resource: " + location, e); + } + } + + private Map parse(String schema, String source) { + try { + return objectMapper.readValue(schema, new TypeReference<>() { }); + } catch (Exception e) { + throw new IllegalStateException("Failed to parse MCP input schema from " + source, e); + } + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolSourceUpdater.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolSourceUpdater.java new file mode 100644 index 000000000..506ca7d5f --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolSourceUpdater.java @@ -0,0 +1,123 @@ +package io.shinhanlife.dap.lib.util; + + +/** + * @package io.shinhanlife.dap.lib.util + * @className ToolSourceUpdater + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ToolSourceUpdater { + + public static void updateToolSource(String toolName, String domainGroup, String description, boolean register, Boolean requiresApproval) throws Exception { + // 1. Find all *UseCase.java files in dap-tool-* directories + String envSourceDir = System.getenv("AXHUB_SOURCE_DIR"); + Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get("."); + + List javaFiles; + try (Stream paths = Files.walk(rootDir)) { + javaFiles = paths + .filter(Files::isRegularFile) + .filter(p -> p.toString().endsWith("UseCase.java")) + .filter(p -> p.toString().contains("dap-tool-") || p.toString().contains("axhub-tool-")) + .collect(Collectors.toList()); + } + + Path targetFile = null; + String content = null; + + // 2. Find the specific file for the tool + String functionName = toolName; + if (toolName.contains("_")) { + functionName = toolName.substring(toolName.indexOf("_") + 1); + } + + Pattern namePattern = Pattern.compile("@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\"", Pattern.DOTALL); + Pattern namePattern2 = Pattern.compile("@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(functionName) + "\"", Pattern.DOTALL); + + for (Path path : javaFiles) { + String text = Files.readString(path); + if (namePattern.matcher(text).find()) { + targetFile = path; + content = text; + break; + } else if (namePattern2.matcher(text).find()) { + targetFile = path; + content = text; + toolName = functionName; // Use baseName for subsequent replacements + break; + } + } + + if (targetFile == null) { + throw new Exception("소스 코드를 찾을 수 없습니다: " + toolName); + } + + // 3. Update @McpTool group + if (domainGroup != null && !domainGroup.trim().isEmpty()) { + Pattern groupPattern = Pattern.compile("(@McpTool\\s*\\([^)]*group\\s*=\\s*\")([^\"]+)(\")", Pattern.DOTALL); + Matcher groupMatcher = groupPattern.matcher(content); + if (groupMatcher.find()) { + content = groupMatcher.replaceFirst("$1" + domainGroup + "$3"); + } + } + + // 4. Update @McpFunction description + if (description != null) { + Pattern funcPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\"[^)]*description\\s*=\\s*\")([^\"]+)(\")", Pattern.DOTALL); + Matcher funcMatcher = funcPattern.matcher(content); + if (funcMatcher.find()) { + content = funcMatcher.replaceFirst("$1" + description.replace("\\", "\\\\").replace("$", "\\\\$") + "$3"); + } + } + + // 5. Update register flag + Pattern regPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\"[^)]*register\\s*=\\s*)(true|false)([^a-zA-Z0-9])", Pattern.DOTALL); + Matcher regMatcher = regPattern.matcher(content); + if (regMatcher.find()) { + content = regMatcher.replaceFirst("$1" + register + "$3"); + } else { + Pattern addRegPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\")", Pattern.DOTALL); + Matcher addRegMatcher = addRegPattern.matcher(content); + if (addRegMatcher.find()) { + content = addRegMatcher.replaceFirst("$1, register = " + register); + } + } + + // 5.5 Update requiresApproval flag + if (requiresApproval != null) { + Pattern appPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\"[^)]*requiresApproval\\s*=\\s*)(true|false)([^a-zA-Z0-9])", Pattern.DOTALL); + Matcher appMatcher = appPattern.matcher(content); + if (appMatcher.find()) { + content = appMatcher.replaceFirst("$1" + requiresApproval + "$3"); + } else { + Pattern addAppPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\")", Pattern.DOTALL); + Matcher addAppMatcher = addAppPattern.matcher(content); + if (addAppMatcher.find()) { + content = addAppMatcher.replaceFirst("$1, requiresApproval = " + requiresApproval); + } + } + } + + // 6. Write back to file + Files.writeString(targetFile, content); + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidationRunner.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidationRunner.java new file mode 100644 index 000000000..414843e38 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidationRunner.java @@ -0,0 +1,21 @@ +package io.shinhanlife.dap.lib.validation; + +import java.nio.file.Path; + +/** Gradle entry point for validating unique MCP Tool names before packaging. */ +public final class McpToolNameValidationRunner { + + private McpToolNameValidationRunner() { + } + + public static void main(String[] args) { + if (args.length != 1) { + throw new IllegalArgumentException("Usage: McpToolNameValidationRunner "); + } + validate(Path.of(args[0])); + } + + static void validate(Path projectRoot) { + McpToolNameValidator.assertUnique(projectRoot); + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidator.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidator.java new file mode 100644 index 000000000..e452fe2a4 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidator.java @@ -0,0 +1,173 @@ +package io.shinhanlife.dap.lib.validation; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @package io.shinhanlife.dap.lib.validation + * @className McpToolNameValidator + * @description Validates unique MCP function names across tool modules + * @author 0986406 + * @create 2026.07.27 + *
+ * ---------- revision history ----------
+ * date       author    description
+ * ---------- --------- ---------------------------
+ * 2026.07.27 0986406    initial creation
+ * 
+ */ +public final class McpToolNameValidator { + + private static final Pattern TOOL_NAME_PATTERN = Pattern.compile("\\bname\\s*=\\s*\\\"([^\\\"]+)\\\""); + private static final Pattern TOOL_NAME_CONVENTION = Pattern.compile("^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$"); + + private McpToolNameValidator() { + } + + public static void assertUnique(Path projectRoot) { + Map> declarationsByName = new LinkedHashMap<>(); + + try (var modules = Files.list(projectRoot)) { + modules.filter(Files::isDirectory) + .filter(path -> path.getFileName().toString().startsWith("dap-tool-")) + .filter(path -> !path.getFileName().toString().equals("dap-tool-core")) + .sorted() + .forEach(module -> collectDeclarations(module, declarationsByName)); + } catch (IOException exception) { + throw new UncheckedIOException("Failed to scan MCP tool modules", exception); + } + + List>> invalidNames = declarationsByName.entrySet().stream() + .filter(entry -> !TOOL_NAME_CONVENTION.matcher(entry.getKey()).matches()) + .sorted(Map.Entry.comparingByKey()) + .toList(); + + if (!invalidNames.isEmpty()) { + throw new IllegalStateException(buildInvalidNameMessage(invalidNames)); + } + List>> duplicates = declarationsByName.entrySet().stream() + .filter(entry -> entry.getValue().size() > 1) + .sorted(Map.Entry.comparingByKey()) + .toList(); + + if (!duplicates.isEmpty()) { + throw new IllegalStateException(buildDuplicateMessage(duplicates)); + } + } + + private static void collectDeclarations(Path module, Map> declarationsByName) { + Path sourceDirectory = module.resolve("src/main/java"); + if (!Files.isDirectory(sourceDirectory)) { + return; + } + + try (var sources = Files.walk(sourceDirectory)) { + sources.filter(path -> path.toString().endsWith(".java")) + .sorted() + .forEach(source -> collectDeclarations(module.getFileName().toString(), source, declarationsByName)); + } catch (IOException exception) { + throw new UncheckedIOException("Failed to scan module " + module.getFileName(), exception); + } + } + + private static void collectDeclarations(String moduleName, Path source, + Map> declarationsByName) { + String content; + try { + content = Files.readString(source); + } catch (IOException exception) { + throw new UncheckedIOException("Failed to read " + source, exception); + } + + int annotationOffset = content.indexOf("@McpFunction"); + while (annotationOffset >= 0) { + int openingParenthesis = content.indexOf('(', annotationOffset); + int closingParenthesis = findAnnotationEnd(content, openingParenthesis); + if (openingParenthesis < 0 || closingParenthesis < 0) { + break; + } + + Matcher matcher = TOOL_NAME_PATTERN.matcher(content.substring(openingParenthesis + 1, closingParenthesis)); + if (matcher.find()) { + String toolName = matcher.group(1); + int line = 1 + (int) content.substring(0, annotationOffset).chars().filter(character -> character == '\n').count(); + declarationsByName.computeIfAbsent(toolName, ignored -> new ArrayList<>()) + .add(new ToolDeclaration(moduleName, source, line)); + } + annotationOffset = content.indexOf("@McpFunction", closingParenthesis + 1); + } + } + + private static int findAnnotationEnd(String content, int openingParenthesis) { + if (openingParenthesis < 0) { + return -1; + } + + int depth = 0; + boolean inString = false; + boolean escaped = false; + for (int index = openingParenthesis; index < content.length(); index++) { + char character = content.charAt(index); + if (inString) { + if (escaped) { + escaped = false; + } else if (character == '\\') { + escaped = true; + } else if (character == '\"') { + inString = false; + } + continue; + } + if (character == '\"') { + inString = true; + } else if (character == '(') { + depth++; + } else if (character == ')' && --depth == 0) { + return index; + } + } + return -1; + } + + private static String buildInvalidNameMessage(List>> invalidNames) { + StringBuilder message = new StringBuilder("Invalid MCP tool name(s): expected pod.domain.service.action using lowercase letters, digits, or hyphens."); + for (Map.Entry> invalidName : invalidNames) { + message.append("\n\n").append(invalidName.getKey()); + invalidName.getValue().stream() + .sorted(Comparator.comparing(ToolDeclaration::moduleName).thenComparing(declaration -> declaration.source().toString())) + .forEach(declaration -> message.append("\n- ") + .append(declaration.moduleName()) + .append(": ") + .append(declaration.source()) + .append(':').append(declaration.line())); + } + return message.toString(); + } + + private static String buildDuplicateMessage(List>> duplicates) { + StringBuilder message = new StringBuilder("Duplicate MCP tool name(s):"); + for (Map.Entry> duplicate : duplicates) { + message.append("\n\n").append(duplicate.getKey()); + duplicate.getValue().stream() + .sorted(Comparator.comparing(ToolDeclaration::moduleName).thenComparing(declaration -> declaration.source().toString())) + .forEach(declaration -> message.append("\n- ") + .append(declaration.moduleName()) + .append(": ") + .append(declaration.source()) + .append(':').append(declaration.line())); + } + return message.toString(); + } + + private record ToolDeclaration(String moduleName, Path source, int line) { + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/dto/ToolMetadata.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/dto/ToolMetadata.java new file mode 100644 index 000000000..9ab099034 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/dto/ToolMetadata.java @@ -0,0 +1,76 @@ +package io.shinhanlife.dap.mcc.dto; + +import lombok.Getter; +import lombok.Setter; +import lombok.Builder; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.util.Map; + +/** + * @package io.shinhanlife.dap.mcc.dto + * @className ToolMetadata + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ToolMetadata { + // 1. Tool 기본 정보 + private String uid; // UUID 형식의 고유 식별자 + private String semver; // 버전 (예: 1.0.0) + private Long timeoutMillis; + private Boolean enabled; + private String displayName; // 사람이 읽는 라벨 (1-128자) + private String name; // MCP 서브툴 명칭 (64자 이하, 예: CustomerSearchTool) + private String description; // 툴의 목적 및 설명 (LLM 프롬프트에 활용 가능) + + // 2. 파라미터 스키마 (JSON Schema 형태의 Map) + private Map parametersSchema; + + // 2-0. 프론트엔드 UI용 함수별 프롬프트 매핑 (추가됨) + private Map actionPrompts; + + // 2-1. 도메인 부서 그룹명 (category_key, 슬러그 형식) + private String categoryKey; + private String endpoint; + private String podUrl; + private String integrationType; + private String mciServiceId; + + + @Builder.Default + private Boolean visible = true; + + @Builder.Default + private Boolean isRegistered = true; + + @Builder.Default + private Boolean requiresApproval = false; + + @Builder.Default + private Boolean readOnlyHint = false; + + @Builder.Default + private Boolean destructiveHint = false; + + @Builder.Default + private Boolean idempotentHint = false; + + @Builder.Default + private Boolean openWorldHint = false; + + +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/manifest/ToolManifestAnnotations.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/manifest/ToolManifestAnnotations.java new file mode 100644 index 000000000..35c966541 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/manifest/ToolManifestAnnotations.java @@ -0,0 +1,10 @@ +package io.shinhanlife.dap.mcc.manifest; + +/** Behaviour hints exposed by the Tool Service manifest. */ +public record ToolManifestAnnotations( + String title, + boolean readOnlyHint, + boolean destructiveHint, + boolean idempotentHint, + boolean openWorldHint) { +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/manifest/ToolManifestItem.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/manifest/ToolManifestItem.java new file mode 100644 index 000000000..9f9b8e092 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/manifest/ToolManifestItem.java @@ -0,0 +1,15 @@ +package io.shinhanlife.dap.mcc.manifest; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; + +/** One MCP Tool declaration published by a Tool Service. */ +public record ToolManifestItem( + String name, + String endpoint, + String title, + String description, + Map inputSchema, + ToolManifestAnnotations annotations, + @JsonProperty("_meta") ToolManifestMeta meta) { +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/manifest/ToolManifestMeta.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/manifest/ToolManifestMeta.java new file mode 100644 index 000000000..e99ac6d8f --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/manifest/ToolManifestMeta.java @@ -0,0 +1,5 @@ +package io.shinhanlife.dap.mcc.manifest; + +/** Operational metadata exposed by the Tool Service manifest. */ +public record ToolManifestMeta(String version, long timeoutMillis, boolean enabled) { +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/manifest/ToolManifestResponse.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/manifest/ToolManifestResponse.java new file mode 100644 index 000000000..891aaf651 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/manifest/ToolManifestResponse.java @@ -0,0 +1,7 @@ +package io.shinhanlife.dap.mcc.manifest; + +import java.util.List; + +/** Top-level response for GET /tool-manifest. */ +public record ToolManifestResponse(String bundleId, String revision, List tools) { +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/manifest/ToolManifestService.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/manifest/ToolManifestService.java new file mode 100644 index 000000000..75df12dfc --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/manifest/ToolManifestService.java @@ -0,0 +1,131 @@ +package io.shinhanlife.dap.mcc.manifest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.shinhanlife.dap.lib.config.McpProperties; +import io.shinhanlife.dap.mcc.dto.ToolMetadata; +import io.shinhanlife.dap.mcc.usecase.ToolRegistryHeartbeatSender; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** Builds the Tool Service owned manifest consumed by the MCP server. */ +@Service +public class ToolManifestService { + + private static final long DEFAULT_TIMEOUT_MILLIS = 300000L; + private static final AtomicLong LAST_ISSUED_REVISION = new AtomicLong(); + private final Supplier> toolSupplier; + private final ObjectMapper objectMapper; + private final McpProperties properties; + private String lastFingerprint; + private String lastRevision; + + @Autowired + public ToolManifestService(ToolRegistryHeartbeatSender heartbeatSender, ObjectMapper objectMapper, + McpProperties properties) { + this(heartbeatSender::getAllScannedTools, objectMapper, properties); + } + + ToolManifestService(Supplier> toolSupplier, ObjectMapper objectMapper, + McpProperties properties) { + this.toolSupplier = toolSupplier; + this.objectMapper = objectMapper; + this.properties = properties; + } + + public ToolManifestResponse currentManifest() { + String bundleId = properties.getManifest() == null ? null : properties.getManifest().getBundleId(); + if (bundleId == null || bundleId.isBlank()) { + throw new IllegalStateException("mcp.manifest.bundle-id must be configured"); + } + + List tools = toolSupplier.get().stream() + .map(this::toManifestItem) + .sorted(Comparator.comparing(ToolManifestItem::name)) + .toList(); + validate(tools); + return new ToolManifestResponse(bundleId, revision(bundleId, tools), tools); + } + + private ToolManifestItem toManifestItem(ToolMetadata tool) { + String title = tool.getDisplayName() == null || tool.getDisplayName().isBlank() + ? tool.getName() : tool.getDisplayName(); + Map schema = tool.getParametersSchema() == null ? emptySchema() : tool.getParametersSchema(); + return new ToolManifestItem( + tool.getName(), endpoint(tool), title, tool.getDescription(), schema, + new ToolManifestAnnotations(title, isTrue(tool.getReadOnlyHint()), isTrue(tool.getDestructiveHint()), + isTrue(tool.getIdempotentHint()), isTrue(tool.getOpenWorldHint())), + new ToolManifestMeta(defaultString(tool.getSemver(), "1.0.0"), + tool.getTimeoutMillis() == null ? DEFAULT_TIMEOUT_MILLIS : tool.getTimeoutMillis(), + tool.getEnabled() == null || tool.getEnabled())); + } + + private void validate(List tools) { + String namePrefix = properties.getManifest() == null ? null : properties.getManifest().getNamePrefix(); + Set names = new LinkedHashSet<>(); + for (ToolManifestItem tool : tools) { + if (tool.name() == null || tool.name().isBlank()) { + throw new IllegalStateException("Tool manifest contains a blank tool name"); + } + if (!names.add(tool.name())) { + throw new IllegalStateException("Tool manifest contains duplicate tool name: " + tool.name()); + } + if (namePrefix != null && !namePrefix.isBlank() && !tool.name().startsWith(namePrefix)) { + throw new IllegalStateException("Tool name does not match mcp.manifest.name-prefix: " + tool.name()); + } + if (!"object".equals(tool.inputSchema().get("type"))) { + throw new IllegalStateException("Tool inputSchema root type must be object: " + tool.name()); + } + } + } + + private synchronized String revision(String bundleId, List tools) { + String fingerprint = fingerprint(bundleId, tools); + if (!fingerprint.equals(lastFingerprint)) { + long localMinimum = lastRevision == null ? Long.MIN_VALUE : Long.parseLong(lastRevision) + 1; + long nextTimestamp = LAST_ISSUED_REVISION.updateAndGet(previous -> + Math.max(Math.max(System.currentTimeMillis(), localMinimum), previous + 1)); + lastFingerprint = fingerprint; + lastRevision = Long.toString(nextTimestamp); + } + return lastRevision; + } + + private String fingerprint(String bundleId, List tools) { + try { + return objectMapper.writeValueAsString(Map.of("bundleId", bundleId, "tools", tools)); + } catch (Exception exception) { + throw new IllegalStateException("Failed to build Tool manifest revision source", exception); + } + } + + private String endpoint(ToolMetadata tool) { + if (tool.getEndpoint() != null && !tool.getEndpoint().isBlank()) { + return tool.getEndpoint(); + } + if (tool.getPodUrl() == null || tool.getPodUrl().isBlank()) { + return "/mcp/" + tool.getName(); + } + return tool.getPodUrl().replaceAll("/+$", "") + "/mcp/" + tool.getName(); + } + private Map emptySchema() { + return Map.of("type", "object", "properties", Map.of(), "additionalProperties", false); + } + + private boolean isTrue(Boolean value) { + return Boolean.TRUE.equals(value); + } + + private String defaultString(String value, String fallback) { + return value == null || value.isBlank() ? fallback : value; + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/mcp/McpRequestHeaderContext.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/mcp/McpRequestHeaderContext.java new file mode 100644 index 000000000..e3cb547fe --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/mcp/McpRequestHeaderContext.java @@ -0,0 +1,21 @@ +package io.shinhanlife.dap.mcc.mcp; + +/** Holds optional MCP headers for the lifetime of one HTTP request thread. */ +public final class McpRequestHeaderContext { + private static final ThreadLocal CURRENT_HEADERS = new ThreadLocal<>(); + + private McpRequestHeaderContext() { + } + + public static McpRequestHeaders current() { + return CURRENT_HEADERS.get(); + } + + static void set(McpRequestHeaders headers) { + CURRENT_HEADERS.set(headers); + } + + static void clear() { + CURRENT_HEADERS.remove(); + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/mcp/McpRequestHeaderFilter.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/mcp/McpRequestHeaderFilter.java new file mode 100644 index 000000000..273629fba --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/mcp/McpRequestHeaderFilter.java @@ -0,0 +1,34 @@ +package io.shinhanlife.dap.mcc.mcp; + +import java.io.IOException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +/** Captures optional correlation and employee headers for an MCP HTTP call. */ +@Component +public class McpRequestHeaderFilter extends OncePerRequestFilter { + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + return !request.getRequestURI().endsWith("/mcp"); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + McpRequestHeaderContext.set(new McpRequestHeaders( + request.getHeader("X-Request-Id"), + request.getHeader("trace-id"), + request.getHeader("request-id"), + request.getHeader("employee-id"))); + try { + filterChain.doFilter(request, response); + } finally { + McpRequestHeaderContext.clear(); + } + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/mcp/McpRequestHeaders.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/mcp/McpRequestHeaders.java new file mode 100644 index 000000000..7ce4fed89 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/mcp/McpRequestHeaders.java @@ -0,0 +1,9 @@ +package io.shinhanlife.dap.mcc.mcp; + +/** Optional request headers propagated from an MCP HTTP request to a Tool invocation. */ +public record McpRequestHeaders( + String headerRequestId, + String traceId, + String requestId, + String encryptedEmployeeId) { +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/mcp/ToolMcpServerConfiguration.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/mcp/ToolMcpServerConfiguration.java new file mode 100644 index 000000000..b07916ab7 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/mcp/ToolMcpServerConfiguration.java @@ -0,0 +1,26 @@ +package io.shinhanlife.dap.mcc.mcp; + +import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +/** Exposes every Tool Pod through the MCP Streamable HTTP transport. */ +@Configuration +public class ToolMcpServerConfiguration { + + @Bean + @Primary + public HttpServletStreamableServerTransportProvider toolMcpTransportProvider() { + return HttpServletStreamableServerTransportProvider.builder() + .mcpEndpoint("/mcp") + .build(); + } + + @Bean + public ServletRegistrationBean toolMcpServlet( + HttpServletStreamableServerTransportProvider transportProvider) { + return new ServletRegistrationBean<>(transportProvider, "/mcp"); + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/mcp/ToolPodMcpToolSynchronizer.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/mcp/ToolPodMcpToolSynchronizer.java new file mode 100644 index 000000000..6bbadbc0a --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/mcp/ToolPodMcpToolSynchronizer.java @@ -0,0 +1,81 @@ +package io.shinhanlife.dap.mcc.mcp; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.server.McpServerFeatures; +import io.modelcontextprotocol.server.McpSyncServer; +import io.modelcontextprotocol.spec.McpSchema; +import io.shinhanlife.dap.mcc.dto.ToolMetadata; +import io.shinhanlife.dap.mcc.presentation.BusinessToolController; +import io.shinhanlife.dap.mcc.usecase.ToolRegistryHeartbeatSender; +import java.util.LinkedHashMap; +import java.util.Map; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +/** Registers the Tool Pod's existing annotated tools with its MCP SDK server. */ +@Component +public class ToolPodMcpToolSynchronizer { + private final McpSyncServer mcpServer; + private final ToolRegistryHeartbeatSender heartbeatSender; + private final BusinessToolController businessToolController; + private final ObjectMapper objectMapper; + + public ToolPodMcpToolSynchronizer(McpSyncServer mcpServer, ToolRegistryHeartbeatSender heartbeatSender, + BusinessToolController businessToolController, ObjectMapper objectMapper) { + this.mcpServer = mcpServer; + this.heartbeatSender = heartbeatSender; + this.businessToolController = businessToolController; + this.objectMapper = objectMapper; + } + + @EventListener(ApplicationReadyEvent.class) + public void registerLocalTools() { + heartbeatSender.getAllScannedTools().stream() + .filter(tool -> Boolean.TRUE.equals(tool.getVisible())) + .forEach(tool -> mcpServer.addTool(specification(tool))); + } + + private McpServerFeatures.SyncToolSpecification specification(ToolMetadata tool) { + McpSchema.Tool mcpTool = McpSchema.Tool.builder() + .name(tool.getName()) + .description(tool.getDescription() == null || tool.getDescription().isBlank() ? tool.getName() + " Tool" : tool.getDescription()) + .inputSchema(tool.getParametersSchema() == null ? emptySchema() : tool.getParametersSchema()) + .annotations(McpSchema.ToolAnnotations.builder() + .readOnlyHint(Boolean.TRUE.equals(tool.getReadOnlyHint())) + .destructiveHint(Boolean.TRUE.equals(tool.getDestructiveHint())) + .idempotentHint(Boolean.TRUE.equals(tool.getIdempotentHint())) + .openWorldHint(Boolean.TRUE.equals(tool.getOpenWorldHint())).build()) + .build(); + return McpServerFeatures.SyncToolSpecification.builder().tool(mcpTool) + .callHandler((context, request) -> invoke(tool.getName(), McpRequestHeaderContext.current(), request.arguments())).build(); + } + + private McpSchema.CallToolResult invoke(String toolName, McpRequestHeaders requestHeaders, + Map arguments) { + ResponseEntity response = businessToolController.executeDynamicTool( + toolName, + requestHeaders == null ? null : requestHeaders.headerRequestId(), + requestHeaders == null ? null : requestHeaders.traceId(), + requestHeaders == null ? null : requestHeaders.requestId(), + requestHeaders == null ? null : requestHeaders.encryptedEmployeeId(), + arguments); + boolean failed = !response.getStatusCode().is2xxSuccessful(); + Object body = response.getBody(); + try { + return McpSchema.CallToolResult.builder().addTextContent(objectMapper.writeValueAsString(body)) + .structuredContent(body).isError(failed).build(); + } catch (Exception error) { + return McpSchema.CallToolResult.builder().addTextContent(String.valueOf(body)).isError(failed).build(); + } + } + + private Map emptySchema() { + Map schema = new LinkedHashMap<>(); + schema.put("type", "object"); + schema.put("properties", Map.of()); + schema.put("additionalProperties", false); + return schema; + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java new file mode 100644 index 000000000..ec840ede2 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java @@ -0,0 +1,243 @@ +package io.shinhanlife.dap.mcc.presentation; + + +/** + * @package io.shinhanlife.dap.mcc.presentation + * @className BusinessToolController + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.Error; +import io.shinhanlife.dap.lib.annotation.McpFunction; +import io.shinhanlife.dap.lib.annotation.McpTool; +import io.shinhanlife.dap.lib.config.McpProperties; +import io.shinhanlife.dap.mcc.dto.ToolMetadata; +import io.shinhanlife.dap.mcc.usecase.ToolRegistryHeartbeatSender; +import io.shinhanlife.dap.lib.util.ToolSchemaResolver; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.aop.support.AopUtils; +import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.http.ResponseEntity; +import org.springframework.util.ClassUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +@RequestMapping("/") +@RequiredArgsConstructor +public class BusinessToolController { + + private final ApplicationContext applicationContext; + private final ObjectMapper objectMapper; + private final McpProperties mcpProperties; + private final ToolRegistryHeartbeatSender toolRegistryHeartbeatSender; + private final ToolArgumentSchemaValidator toolArgumentSchemaValidator; + + private final ToolSchemaResolver toolSchemaResolver; + // 내부 조회용 로컬 Tool 목록 엔드포인트 + @GetMapping("/mcp/api/v1/tools/local") + public List getLocalTools() { + return toolRegistryHeartbeatSender.getAllScannedTools(); + } + + // 순수 REST 기반 동적 라우팅 엔드포인트 + @PostMapping("/mcp/{name}") + public ResponseEntity executeDynamicTool( + @PathVariable("name") String functionName, + @RequestHeader(value = "X-Request-Id", required = false) String headerRequestId, + @RequestHeader(value = "trace-id", required = false) String traceId, + @RequestHeader(value = "request-id", required = false) String requestId, + @RequestHeader(value = "employee-id", required = false) String encryptedEmployeeId, + @RequestBody(required = false) Map arguments) { + + String finalRequestId = headerRequestId; + + log.info(" [Tool] IN - trace-id: {}, request-id: {}", traceId, requestId); + log.info(" [Tool] 동적 툴 실행 요청 수신 (함수명): {}", functionName); + if (arguments != null) { + try { + log.info(" [Tool] 호출 파라미터: {}", objectMapper.writeValueAsString(arguments)); + } catch (Exception e) { + log.info(" [Tool] 호출 파라미터: {}", arguments); + } + } + Object targetBean = null; + Method targetMethod = null; + McpFunction targetFunctionAnnotation = null; + + // McpTool 어노테이션 기반 조회가 프록시 문제로 누락될 수 있으므로, 전체 빈을 순회하며 @McpFunction을 찾습니다. + Map allBeans = applicationContext.getBeansOfType(Object.class); + outerLoop: + for (Object bean : allBeans.values()) { + Class targetClass = AopUtils.getTargetClass(bean); + for (Method targetMethodOfClass : targetClass.getDeclaredMethods()) { + McpFunction mcpFunc = AnnotationUtils.findAnnotation(targetMethodOfClass, McpFunction.class); + if (mcpFunc != null) { + String baseName = mcpFunc.name(); + String expectedName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty() + ? mcpProperties.getNamespace() + "_" + baseName + : baseName; + + if (expectedName.equals(functionName) || baseName.equals(functionName)) { + targetBean = bean; + try { + targetMethod = bean.getClass().getMethod(targetMethodOfClass.getName(), targetMethodOfClass.getParameterTypes()); + } catch (NoSuchMethodException e) { + targetMethod = targetMethodOfClass; + } + targetFunctionAnnotation = mcpFunc; + break outerLoop; + } + } + } + } + + if (targetBean == null || targetMethod == null) { + List availableFunctions = new ArrayList<>(); + for (Object bean : allBeans.values()) { + Class targetCls = AopUtils.getTargetClass(bean); + for (Method m : targetCls.getDeclaredMethods()) { + McpFunction func = AnnotationUtils.findAnnotation(m, McpFunction.class); + if (func != null) { + String baseName = func.name(); + String expName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty() + ? mcpProperties.getNamespace() + "_" + baseName : baseName; + availableFunctions.add(expName + " (in " + targetCls.getSimpleName() + ")"); + } + } + } + log.error("[Tool] 실행할 함수(Method)를 찾을 수 없습니다: {}. 현재 스캔된 툴 메서드 목록: {}", functionName, availableFunctions); + Map errorDetails = new HashMap<>(); + errorDetails.put("status", "404"); + Map errorBody = new HashMap<>(); + errorBody.put("code", "TOOL_NOT_FOUND"); + errorBody.put("message", "실행할 함수를 찾을 수 없습니다: " + functionName); + errorBody.put("details", errorDetails); + if (finalRequestId != null) errorBody.put("request_id", finalRequestId); + + return ResponseEntity.status(404).body(errorBody); + } + // (기존 차단 로직 제거됨) + + // 2. 파라미터 유효성 검증 (JSON Schema) + if (targetMethod.getParameterCount() > 0) { + Class paramType = targetMethod.getParameterTypes()[0]; + if (!Map.class.isAssignableFrom(paramType)) { + try { + Map inputSchema = toolSchemaResolver.resolve(targetFunctionAnnotation, paramType); + List errors = toolArgumentSchemaValidator.validate(inputSchema, arguments); + if (!errors.isEmpty()) { + log.error("[Tool] 파라미터 유효성 검증 실패: {}", errors); + List errorMessages = new ArrayList<>(); + for (Error validationError : errors) { + errorMessages.add(validationError.getMessage()); + } + Map errorDetails = new HashMap<>(); + errorDetails.put("status", "422"); + Map errorBody = new HashMap<>(); + errorBody.put("code", "INVALID_PARAM"); + errorBody.put("message", "파라미터 유효성 검증 실패"); + errorBody.put("details", errorDetails); + if (finalRequestId != null) errorBody.put("request_id", finalRequestId); + + return ResponseEntity.status(422).body(errorBody); + } + } catch (Exception e) { + log.error("[Tool] 스키마 검증 중 오류 발생: {}", e.getMessage()); + } + } + } + + log.info("[Tool] 리플렉션 직접 실행 -> Method: {}", targetMethod.getName()); + + try { + // 3. DTO 파라미터 자동 매핑 (Map -> DTO) + Object invokeArgument = arguments; + if (targetMethod.getParameterCount() > 0 && arguments != null) { + Class paramType = targetMethod.getParameterTypes()[0]; + if (!Map.class.isAssignableFrom(paramType)) { + invokeArgument = objectMapper.convertValue(arguments, paramType); + log.info("[Tool] DTO 자동 매핑 성공: {}", paramType.getSimpleName()); + } + } + + long startTime = System.currentTimeMillis(); + Object methodResult = null; + if (targetMethod.getParameterCount() == 0) { + methodResult = targetMethod.invoke(targetBean); + } else { + methodResult = targetMethod.invoke(targetBean, invokeArgument); + } + + Map outputSchema = toolSchemaResolver.resolveOutput(targetFunctionAnnotation, targetMethod.getReturnType()); + if (!outputSchema.isEmpty()) { + List outputErrors = toolArgumentSchemaValidator.validateValue(outputSchema, methodResult); + if (!outputErrors.isEmpty()) { + log.error("[Tool] Output schema validation failed. tool={}, errors={}", + functionName, outputErrors); + Map errorBody = new HashMap<>(); + errorBody.put("code", "INVALID_TOOL_RESPONSE"); + errorBody.put("message", "Tool response does not match its output schema"); + if (finalRequestId != null) { + errorBody.put("request_id", finalRequestId); + } + return ResponseEntity.internalServerError().body(errorBody); + } + } + + long elapsed = System.currentTimeMillis() - startTime; + + // 5. 결과 반환 (순수 REST 응답) + try { + log.info("[Tool -> MCP Gateway] Output Schema Result: {}", objectMapper.writeValueAsString(methodResult)); + } catch (Exception e) { + log.info("[Tool -> MCP Gateway] Output Schema Result: {}", methodResult); + } + + log.info(" [Tool] OUT - trace-id: {}, request-id: {}", traceId, requestId); + + ResponseEntity.BodyBuilder responseBuilder = ResponseEntity.ok(); + if (traceId != null) responseBuilder.header("trace-id", traceId); + if (requestId != null) responseBuilder.header("request-id", requestId); + + return responseBuilder.body(methodResult); + + } catch (Exception e) { + log.error("[Tool] 리플렉션 실행 중 예외 발생: {}", e.getMessage()); + Map errorDetails = new HashMap<>(); + errorDetails.put("status", "500"); + Map errorBody = new HashMap<>(); + errorBody.put("code", "TOOL_ERROR"); + errorBody.put("message", "Tool execution failed"); + errorDetails.clear(); // Hide details for upstream errors + errorBody.put("details", errorDetails); + if (finalRequestId != null) errorBody.put("request_id", finalRequestId); + + return ResponseEntity.status(502).body(errorBody); + } + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolArgumentSchemaValidator.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolArgumentSchemaValidator.java new file mode 100644 index 000000000..59c68ffec --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolArgumentSchemaValidator.java @@ -0,0 +1,32 @@ +package io.shinhanlife.dap.mcc.presentation; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.Error; +import com.networknt.schema.InputFormat; +import com.networknt.schema.Schema; +import com.networknt.schema.SchemaRegistry; +import com.networknt.schema.SpecificationVersion; +import java.util.List; +import java.util.Map; +import org.springframework.stereotype.Component; + +/** Validates tool arguments with the NetworkNT version selected by the MCP SDK. */ +@Component +public class ToolArgumentSchemaValidator { + + private final ObjectMapper objectMapper; + + public ToolArgumentSchemaValidator(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + public List validate(Map schemaDefinition, Map arguments) throws Exception { + return validateValue(schemaDefinition, arguments); + } + + public List validateValue(Map schemaDefinition, Object value) throws Exception { + SchemaRegistry schemaRegistry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7); + Schema schema = schemaRegistry.getSchema(objectMapper.writeValueAsString(schemaDefinition)); + return schema.validate(objectMapper.writeValueAsString(value), InputFormat.JSON); + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolManifestController.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolManifestController.java new file mode 100644 index 000000000..6ccdf9045 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolManifestController.java @@ -0,0 +1,32 @@ +package io.shinhanlife.dap.mcc.presentation; + +import io.shinhanlife.dap.mcc.manifest.ToolManifestResponse; +import io.shinhanlife.dap.mcc.manifest.ToolManifestService; +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.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +/** Read-only Tool Service manifest endpoint for MCP background discovery. */ +@RestController +public class ToolManifestController { + + private final ToolManifestService toolManifestService; + + public ToolManifestController(ToolManifestService toolManifestService) { + this.toolManifestService = toolManifestService; + } + + @GetMapping(value = "/tool-manifest", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getManifest( + @RequestHeader(value = HttpHeaders.IF_NONE_MATCH, required = false) String ifNoneMatch) { + ToolManifestResponse manifest = toolManifestService.currentManifest(); + String eTag = '"' + manifest.revision() + '"'; + if (eTag.equals(ifNoneMatch)) { + return ResponseEntity.status(304).eTag(eTag).build(); + } + return ResponseEntity.ok().eTag(eTag).body(manifest); + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/usecase/AbstractMcpToolUseCase.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/usecase/AbstractMcpToolUseCase.java new file mode 100644 index 000000000..a8729653b --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/usecase/AbstractMcpToolUseCase.java @@ -0,0 +1,95 @@ +package io.shinhanlife.dap.mcc.usecase; + +import io.shinhanlife.dap.lib.adapter.connector.LegacyEimsConnector; +import io.shinhanlife.dap.lib.adapter.util.PiiMaskingUtils; +import org.springframework.beans.factory.annotation.Autowired; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.core.type.TypeReference; + +import java.util.HashMap; +import java.util.Map; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +/** + * @package io.shinhanlife.dap.mcc.service + * @className AbstractMcpToolUseCase + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +public abstract class AbstractMcpToolUseCase { + + @Autowired + protected LegacyEimsConnector legacyEimsConnector; + + @Autowired + protected ObjectMapper objectMapper; + + protected Map executeLegacy(String routingType, String interfaceId, Object inputData) { + return executeLegacy(routingType, interfaceId, inputData, null); + } + + /** + * 레거시 시스템을 호출하고 공통 처리(PII 마스킹 등)를 수행합니다. (스펙 지정 가능) + */ + protected Map executeLegacy(String routingType, String interfaceId, Object inputData, List> spec) { + Map inputMap; + if (inputData == null) { + inputMap = new HashMap<>(); + } else if (inputData instanceof Map) { + @SuppressWarnings("unchecked") + Map map = (Map) inputData; + inputMap = map; + } else { + inputMap = objectMapper.convertValue(inputData, new TypeReference>() {}); + } + + try { + log.info("\n======================================================="); + log.info(" [Tool -> Legacy] 레거시 시스템 통신 시작"); + log.info(" - Routing Type: {}", routingType); + log.info(" - Interface ID: {}", interfaceId); + log.info(" - 호출 파라미터 (Request): \n{}", objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(inputMap)); + log.info("=======================================================\n"); + + // 1. Adapter 공통 모듈 직접 호출 + String executionResult = legacyEimsConnector.executeByTool(routingType, interfaceId, inputMap, spec); + + log.info("\n======================================================="); + log.info(" [Legacy -> Tool] 레거시 시스템 통신 완료"); + log.info(" - 응답 파라미터 (Response, 마스킹 전): \n{}", executionResult); + log.info("=======================================================\n"); + + // 2. PII 마스킹 처리 + String maskedResult = PiiMaskingUtils.mask(executionResult); + + if (executionResult != null && (executionResult.contains("\"status\":\"CIRCUIT_OPEN\"") || executionResult.contains("\"status\":\"TOO_MANY_REQUESTS\""))) { + try { + return objectMapper.readValue(executionResult, new TypeReference>() {}); + } catch (Exception e) { + log.error("Fallback JSON 파싱 에러: {}", e.getMessage()); + } + } + + Map result = new HashMap<>(); + result.put("status", "SUCCESS"); + result.put("legacy_response", maskedResult); + return result; + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("status", "ERROR"); + error.put("message", e.getMessage()); + return error; + } + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/usecase/ToolRegistryHeartbeatSender.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/usecase/ToolRegistryHeartbeatSender.java new file mode 100644 index 000000000..1591ce3ac --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/usecase/ToolRegistryHeartbeatSender.java @@ -0,0 +1,189 @@ +package io.shinhanlife.dap.mcc.usecase; + + +/** + * @package io.shinhanlife.dap.mcc.service + * @className ToolRegistryHeartbeatSender + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +import com.fasterxml.jackson.databind.ObjectMapper; +import io.shinhanlife.dap.lib.annotation.McpFunction; +import io.shinhanlife.dap.lib.annotation.McpTool; +import io.shinhanlife.dap.lib.config.McpProperties; +import io.shinhanlife.dap.mcc.dto.ToolMetadata; +import io.shinhanlife.dap.lib.util.ToolSchemaResolver; +import jakarta.annotation.PostConstruct; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.util.ClassUtils; +import org.springframework.web.client.RestClient; + +@Slf4j +@Component +@Configuration +@EnableScheduling +@RequiredArgsConstructor +public class ToolRegistryHeartbeatSender { + + private final ApplicationContext applicationContext; + private final ObjectMapper objectMapper; + private final McpProperties mcpProperties; + private final RestClient restClient = RestClient.create(); + private final ToolSchemaResolver toolSchemaResolver; + + @Value("${axhub.gateway.url:http://localhost:8081}") + private String gatewayUrl; + + @Value("${axhub.tool.url:http://localhost:8080}") + private String podUrl; + + private List registeredTools = new ArrayList<>(); + + @Getter + private List allScannedTools = new ArrayList<>(); + + @PostConstruct + public void init() { + log.info(" [HeartbeatSender] 초기화 시작. Gateway URL: {}, Pod URL: {}", gatewayUrl, podUrl); + scanAndBuildMetadata(); + } + + private void scanAndBuildMetadata() { + Map allBeans = applicationContext.getBeansOfType(Object.class); + for (Object bean : allBeans.values()) { + Class targetClass = AopUtils.getTargetClass(bean); + + // 클래스 또는 프록시(인터페이스)에서 @McpTool 스캔 + McpTool toolAnnotation = AnnotationUtils.findAnnotation(targetClass, McpTool.class); + if (toolAnnotation == null) { + toolAnnotation = AnnotationUtils.findAnnotation(bean.getClass(), McpTool.class); + } + + for (Method method : targetClass.getDeclaredMethods()) { + // 메서드, 수퍼클래스, 인터페이스를 모두 뒤져서 @McpFunction 스캔 + McpFunction functionAnnotation = AnnotationUtils.findAnnotation(method, McpFunction.class); + + if (functionAnnotation != null && toolAnnotation != null) { + String baseName = functionAnnotation.displayName(); + String rawSubToolName = functionAnnotation.name(); + String subToolName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty() + ? mcpProperties.getNamespace() + "_" + rawSubToolName + : rawSubToolName; + + boolean isRegister = functionAnnotation.register(); + if (!isRegister) { + log.info(" [HeartbeatSender] '{}' 툴은 어노테이션 설정에 의해 외부 등록(Redis) 대상에서 제외되었습니다. (최종 이름: {})", baseName, subToolName); + } + + ToolMetadata meta = new ToolMetadata(); + meta.setUid(UUID.nameUUIDFromBytes(subToolName.getBytes()).toString()); + meta.setDisplayName(baseName); + meta.setName(subToolName); + meta.setSemver(functionAnnotation.version()); + meta.setTimeoutMillis(functionAnnotation.timeoutMillis()); + meta.setEnabled(functionAnnotation.enabled()); + meta.setDescription(functionAnnotation.description()); + meta.setCategoryKey(toolAnnotation.categoryKey()); + meta.setIntegrationType(toolAnnotation.routingType()); + meta.setMciServiceId(functionAnnotation.mappingId()); + meta.setPodUrl(podUrl); + meta.setEndpoint(podUrl.replaceAll("/+$", "") + "/mcp/" + subToolName); + + boolean isVisible = functionAnnotation.visible(); + meta.setVisible(isVisible); + meta.setIsRegistered(isRegister); + meta.setRequiresApproval(functionAnnotation.requiresApproval()); + meta.setReadOnlyHint(functionAnnotation.readOnlyHint()); + meta.setDestructiveHint(functionAnnotation.destructiveHint()); + meta.setIdempotentHint(functionAnnotation.idempotentHint()); + meta.setOpenWorldHint(functionAnnotation.openWorldHint()); + + Map prompts = new HashMap<>(); + String promptText = functionAnnotation.prompt(); + prompts.put(subToolName, promptText); + meta.setActionPrompts(prompts); + + if (method.getParameterCount() > 0) { + try { + Class paramType = method.getParameterTypes()[0]; + Map finalSchema = toolSchemaResolver.resolve(functionAnnotation, paramType); + meta.setParametersSchema(finalSchema); + } catch (Exception e) { + log.error("Failed to generate schema for {}", subToolName, e); + } + } + + if (isRegister) { + registeredTools.add(meta); + } + allScannedTools.add(meta); + log.info(" [HeartbeatSender] 도구 메타데이터 생성: {} (isRegistered: {})", meta.getUid(), isRegister); + } + } + } + } + + @Scheduled(fixedRate = 30000) + public void sendHeartbeats() { + if (registeredTools.isEmpty()) return; + + for (ToolMetadata tool : registeredTools) { + try { + ResponseEntity response = restClient.post() + .uri(gatewayUrl + "/mcp/api/v1/registry/heartbeat") + .header("Content-Type", "application/json") + + .body(tool.getUid()) + .retrieve() + .toEntity(String.class); + + if (response.getStatusCode().is2xxSuccessful()) { + log.info(" [HeartbeatSender] 하트비트 전송 성공: {}", tool.getUid()); + } + } catch (Exception e) { + log.warn(" [HeartbeatSender] 하트비트 전송 실패 ({}): {}. 재등록을 시도합니다.", tool.getUid(), e.getMessage()); + registerTool(tool); + } + } + } + + private void registerTool(ToolMetadata tool) { + try { + restClient.post() + .uri(gatewayUrl + "/mcp/api/v1/registry/register") + .header("Content-Type", "application/json") + + .body(tool) + .retrieve() + .toBodilessEntity(); + log.info(" [HeartbeatSender] 툴 재등록 성공: {}", tool.getUid()); + } catch (Exception ex) { + log.error(" [HeartbeatSender] 툴 등록 실패: {}", ex.getMessage()); + } + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcg/dto/ToolMetadata.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcg/dto/ToolMetadata.java new file mode 100644 index 000000000..9c54fdd0a --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcg/dto/ToolMetadata.java @@ -0,0 +1,87 @@ +package io.shinhanlife.dap.mcg.dto; + +import lombok.Getter; +import lombok.Setter; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * Tool(Agent)의 명세 및 라우팅 정보를 담고 있는 메타데이터 클래스 + * Redis 레지스트리에 저장되며, Planner와 Router 간의 통신 객체(Plan)로 사용됩니다. + */ +/** + * @package io.shinhanlife.dap.mcg.dto + * @className ToolMetadata + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class ToolMetadata { + + // 1. Tool 기본 정보 + private String uid; // UUID 형식의 고유 식별자 + private String semver; // 버전 (예: 1.0.0) + private String displayName; // 사람이 읽는 라벨 (1-128자) + private String name; // MCP 서브툴 명칭 (64자 이하, 예: CustomerSearchTool) + private String description; // 툴의 목적 및 설명 (LLM 프롬프트에 활용 가능) + + // 2. 파라미터 스키마 (JSON Schema 형태의 Map) + private Map parametersSchema; + + // 2-0. 프론트엔드 UI용 함수별 프롬프트 매핑 (추가됨) + private Map actionPrompts; + + // 2-1. 도메인 부서 그룹명 (category_key, 슬러그 형식) + private String categoryKey; + + // 2-2. 툴 처리 엔드포인트 URI 경로 (예: /api/tool/customer-info) + private String endpoint; + + // 2-3. Pod 실행 URL (독립적인 Microservice 라우팅용, 예: http://localhost:8082) + private String podUrl; + + // 2-4. 가시성 여부 + @Builder.Default + private Boolean visible = true; + + // 2-5. Redis 등록 여부 (UI 표출용) + @Builder.Default + private Boolean isRegistered = true; + + // 2-6. HITL 승인 필요 여부 + @Builder.Default + private Boolean requiresApproval = false; + + + + // 3. 연동 아키텍처 구분 (DIRECT / MCI_EAI) + private String integrationType; // 연동 타입: "DIRECT" 또는 "MCI_EAI" + + // 4. 레거시(MCI/EAI) 연동 시 필수 정보 (integrationType이 "MCI_EAI"일 때 사용) + private String mciServiceId; // MCI/EAI 호출을 위한 서비스 ID (예: CRM_001, LICO_992) + + // 5. 인프라 상태 정보 (DIRECT 연동 시 사용) + private Long lastHeartbeat; // Redis TTL 갱신용 마지막 하트비트 타임스탬프 + + // 6. 동적 서킷 브레이커 & 속도 제어 설정 (Registry 기반) + private Integer failureRateThreshold; // 서킷 브레이커 동작 기준 실패율 (%) + private Integer slidingWindowSize; // 서킷 브레이커 에러율 계산 표본 요청 수 + private Integer rateLimitForPeriod; // 속도 제어: 1초당 허용 최대 요청 수 +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/glow/BaseException.java b/dap-tool-core/src/main/java/io/shinhanlife/glow/BaseException.java new file mode 100644 index 000000000..a2d8cffc7 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/glow/BaseException.java @@ -0,0 +1,43 @@ +package io.shinhanlife.glow; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +//@Schema(description = "응답 에러 객체. 성공 케이스일 경우 null. 실제 에러가 발생할 경우에만 예외명, 예외 메시지 필드 세팅 예정.") +/** + * @package io.shinhanlife.glow + * @className BaseException + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +public class BaseException { + +// @Schema(description = "Error 코드 Meta 참조 운영. (예) 20001, 50001 등", shinhanlife = "20001") + String code; + +// @Schema(description = "메시지") + String message; + +// @Schema(description = "예외명.", shinhanlife = "NullPointerException") + @Builder.Default + String exceptionName = ""; + +// @Schema(description = "예외상세", shinhanlife = "StackTrace") + @Builder.Default + String exceptionDetail = ""; + +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/glow/BaseResponse.java b/dap-tool-core/src/main/java/io/shinhanlife/glow/BaseResponse.java new file mode 100644 index 000000000..7ddcfca7c --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/glow/BaseResponse.java @@ -0,0 +1,39 @@ +package io.shinhanlife.glow; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +/** + * @package io.shinhanlife.glow + * @className BaseResponse + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@ToString +@Getter +@Builder +@AllArgsConstructor +public class BaseResponse { + +// @Schema(description = "응답 코드 HTTP STATUS 오류 - 실제 Header 는 200 으로 내려감", shinhanlife = "200") + private int code; + +// @Schema(description = "응답 메시지. 응답 코드와 매핑된 메시지.", shinhanlife = "데이터 생성 성공") + private String message; + +// @Schema(description = "응답 본문 데이터. 실제 비즈니스 데이터.") + private T data; + + private BaseException error; + +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/glow/BizException.java b/dap-tool-core/src/main/java/io/shinhanlife/glow/BizException.java new file mode 100644 index 000000000..835c800de --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/glow/BizException.java @@ -0,0 +1,20 @@ +package io.shinhanlife.glow; + +/** + * @package io.shinhanlife.glow + * @className BizException + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +public class BizException extends RuntimeException { + public BizException(String s) { + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowAppServiceId.java b/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowAppServiceId.java new file mode 100644 index 000000000..fe4ec3acb --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowAppServiceId.java @@ -0,0 +1,28 @@ +package io.shinhanlife.glow; + + +/** + * @package io.shinhanlife.glow + * @className GlowAppServiceId + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +import java.lang.annotation.*; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface GlowAppServiceId { + + String value(); + + String description() default ""; +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowControllerId.java b/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowControllerId.java new file mode 100644 index 000000000..d9e806c71 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowControllerId.java @@ -0,0 +1,20 @@ +package io.shinhanlife.glow; + + +/** + * @package io.shinhanlife.glow + * @className GlowControllerId + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +public @interface GlowControllerId { + String value(); +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowIndexPaging.java b/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowIndexPaging.java new file mode 100644 index 000000000..c03265fff --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowIndexPaging.java @@ -0,0 +1,24 @@ +package io.shinhanlife.glow; + + +/** + * @package io.shinhanlife.glow + * @className GlowIndexPaging + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +import java.lang.annotation.*; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface GlowIndexPaging { +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowLogTarget.java b/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowLogTarget.java new file mode 100644 index 000000000..a748a0670 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowLogTarget.java @@ -0,0 +1,29 @@ +package io.shinhanlife.glow; + +import java.lang.annotation.*; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface GlowLogTarget { + + Target[] value() default {}; + +/** + * @package io.shinhanlife.glow + * @className Target + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ + enum Target { + FILE, CONSOLE + } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowLogger.java b/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowLogger.java new file mode 100644 index 000000000..0cf9e84ef --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowLogger.java @@ -0,0 +1,33 @@ +package io.shinhanlife.glow; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +/** + * @package io.shinhanlife.glow + * @className GlowLogger + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Component +@Scope("prototype") +public class GlowLogger { + + private Logger log = LoggerFactory.getLogger(GlowLogger.class); + + public void debug(String message, Object... args) { log.debug(message, args); } + public void info(String message, Object... args) { log.info(message, args); } + public void warn(String message, Object... args) { log.warn(message, args); } + public void error(String message, Object... args) { log.error(message, args); } + public void error(String message, Throwable t) { log.error(message, t); } +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowMciFieldInfo.java b/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowMciFieldInfo.java new file mode 100644 index 000000000..bce1efef9 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowMciFieldInfo.java @@ -0,0 +1,44 @@ +package io.shinhanlife.glow; + + +/** + * @package io.shinhanlife.glow + * @className GlowMciFieldInfo + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +import java.lang.annotation.*; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface GlowMciFieldInfo { + + /** + * 필드 순서 + */ + int order(); + + /** + * 필드 길이 (List인 경우 전체 길이로 활용될 수 있음) + */ + int length(); + + /** + * 필드 설명 + */ + String description() default ""; + + /** + * List 매핑 시 대상 DTO 클래스 + */ + Class target() default void.class; +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowMybatisMapper.java b/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowMybatisMapper.java new file mode 100644 index 000000000..9c6c09f7f --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowMybatisMapper.java @@ -0,0 +1,33 @@ +package io.shinhanlife.glow; + + +/** + * @package io.shinhanlife.glow + * @className GlowMybatisMapper + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +import org.apache.ibatis.annotations.Mapper; +import org.springframework.stereotype.Component; + +import java.lang.annotation.*; + +/** + * MyBatis Mapper 인터페이스를 나타내는 어노테이션 + * MyBatis Mapper와 동일한 기능을 제공합니다. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Mapper +@Component +public @interface GlowMybatisMapper { +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowServiceGroupId.java b/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowServiceGroupId.java new file mode 100644 index 000000000..f6c381661 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowServiceGroupId.java @@ -0,0 +1,22 @@ +package io.shinhanlife.glow; + + +/** + * @package io.shinhanlife.glow + * @className GlowServiceGroupId + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +public @interface GlowServiceGroupId { + String value(); + + String description(); +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowTrgmField.java b/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowTrgmField.java new file mode 100644 index 000000000..6e44dfaea --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/glow/GlowTrgmField.java @@ -0,0 +1,41 @@ +package io.shinhanlife.glow; + + +/** + * @package io.shinhanlife.glow + * @className GlowTrgmField + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +import java.lang.annotation.*; + +@Deprecated(forRemoval = false) +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface GlowTrgmField { + + /** + * 필드 순서 + */ + int order(); + + /** + * 필드 길이 + */ + int length(); + + /** + * 필드 설명 + */ + String description() default ""; + +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/glow/PageInfo.java b/dap-tool-core/src/main/java/io/shinhanlife/glow/PageInfo.java new file mode 100644 index 000000000..985d76a4d --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/glow/PageInfo.java @@ -0,0 +1,83 @@ +package io.shinhanlife.glow; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.shinhanlife.glow.communication.annotation.GlowTrgmField; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.apache.ibatis.session.RowBounds; + +import java.io.Serial; +import java.io.Serializable; + +/** + * @package io.shinhanlife.glow + * @className PageInfo + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@Setter +@JsonIgnoreProperties(ignoreUnknown = true) +@EqualsAndHashCode(callSuper = true) +public class PageInfo extends RowBounds implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 페이지번호 ( 입력값 ) + */ + @GlowTrgmField(order = 1, length = 5, description = "페이지번호") + private int pageNo; + + /** + * 페이지 데이터 건수 ( 열 건수, 입력값 ) + */ + @GlowTrgmField(order = 2, length = 5, description = "페이지데이터건수") + private int pageDataCc; + + /** + * 총페이지 수 ( 리턴값 ) + */ + @GlowTrgmField(order = 3, length = 10, description = "총페이지수") + private int totaPageCn; + + /** + * 총 페이지 데이터 건수 ( 리턴값 ) + */ + @GlowTrgmField(order = 4, length = 10, description = "총페이지데이터건수") + private int totaPageDataCc; + + public PageInfo(int pageNo, int pageDataCc) { + super(((pageNo <= 0 ? 1 : pageNo) - 1) * pageDataCc, pageDataCc); + this.pageNo = pageNo; + this.pageDataCc = pageDataCc; + } + + public PageInfo() { + + } + + @JsonIgnore + @Override + public int getOffset() { + return super.getOffset(); + } + + @JsonIgnore + @Override + public int getLimit() { + return super.getLimit(); + } + +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/glow/ResponseCode.java b/dap-tool-core/src/main/java/io/shinhanlife/glow/ResponseCode.java new file mode 100644 index 000000000..84d60e955 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/glow/ResponseCode.java @@ -0,0 +1,45 @@ +package io.shinhanlife.glow; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +/** + * @package io.shinhanlife.glow + * @className ResponseCode + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@RequiredArgsConstructor +public enum ResponseCode { + + /* ===================== 성공 ===================== */ + SUCCESS(HttpStatus.OK, "정상 처리되었습니다."), + CREATED(HttpStatus.CREATED, "데이터 생성 성공"), + + /* ===================== 클라이언트 오류 (4xx) ===================== */ + BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), + INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "유효하지 않은 파라미터입니다."), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "인증이 필요합니다."), + FORBIDDEN(HttpStatus.FORBIDDEN, "접근 권한이 없습니다."), + NOT_FOUND(HttpStatus.NOT_FOUND, "요청한 리소스를 찾을 수 없습니다."), + METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "허용되지 않은 HTTP 메서드입니다."), + CONFLICT(HttpStatus.CONFLICT, "이미 존재하는 데이터입니다."), + + /* ===================== 서버 오류 (5xx) ===================== */ + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류가 발생했습니다."), + SERVICE_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "서비스를 사용할 수 없습니다."); + + private final HttpStatus status; + private final String message; + +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/glow/ResponseUtil.java b/dap-tool-core/src/main/java/io/shinhanlife/glow/ResponseUtil.java new file mode 100644 index 000000000..cda4738ee --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/glow/ResponseUtil.java @@ -0,0 +1,125 @@ +package io.shinhanlife.glow; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +/** + * @package io.shinhanlife.glow + * @className ResponseUtil + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +public final class ResponseUtil { + + private ResponseUtil() { + + } + + public static ResponseEntity> ok() { + return ResponseEntity.ok( + BaseResponse.builder() + .code(ResponseCode.SUCCESS.getStatus().value()) + .message(ResponseCode.SUCCESS.getMessage()) + .build() + ); + } + + public static ResponseEntity> ok(T data) { + return ResponseEntity.ok( + BaseResponse.builder() + .code(ResponseCode.SUCCESS.getStatus().value()) + .message(ResponseCode.SUCCESS.getMessage()) + .data(data) + .build() + ); + } + + public static ResponseEntity> ok(T data, ResponseCode responseCode) { + return ResponseEntity.ok( + BaseResponse.builder() + .code(responseCode.getStatus().value()) + .message(responseCode.getMessage()) + .data(data) + .build() + ); + } + + public static ResponseEntity> error( + HttpStatus status, String code, String message + ) { + return ResponseEntity.status(status) + .body( + BaseResponse.builder() + .code(status.value()) + .error( + BaseException.builder() + .code(code) + .message(message) + .build() + ) + .build() + ); + } + + public static ResponseEntity> error( + HttpStatus status, String code, String message, String exceptionName, String stackTrace + ) { + return ResponseEntity.status(status) + .body( + BaseResponse.builder() + .code(status.value()) + .error( + BaseException.builder() + .code(code) + .message(message) + .exceptionName(exceptionName) + .exceptionDetail(stackTrace) + .build() + ) + .build() + ); + } + + public static ResponseEntity> okError( + HttpStatus status, String code, String message + ) { + return ResponseEntity.ok( + BaseResponse.builder() + .code(status.value()) + .error( + BaseException.builder() + .code(code) + .message(message) + .build() + ) + .build() + ); + } + + public static ResponseEntity> okError( + HttpStatus status, String code, String message, String exceptionName, String stackTrace + ) { + return ResponseEntity.ok( + BaseResponse.builder() + .code(status.value()) + .error( + BaseException.builder() + .code(code) + .message(message) + .exceptionName(exceptionName) + .exceptionDetail(stackTrace) + .build() + ) + .build() + ); + } + +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/glow/communication/annotation/GlowTrgmField.java b/dap-tool-core/src/main/java/io/shinhanlife/glow/communication/annotation/GlowTrgmField.java new file mode 100644 index 000000000..9463acf2b --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/glow/communication/annotation/GlowTrgmField.java @@ -0,0 +1,26 @@ +package io.shinhanlife.glow.communication.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Declares fixed-width Glow transaction-message metadata for a DTO field. */ +@Target({ElementType.LOCAL_VARIABLE, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface GlowTrgmField { + + int order(); + + int length() default 0; + + int decimal() default 0; + + String description() default ""; + + String target() default ""; + + String type() default ""; +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/glow/communication/dto/CommonHeader.java b/dap-tool-core/src/main/java/io/shinhanlife/glow/communication/dto/CommonHeader.java new file mode 100644 index 000000000..7c991d52f --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/glow/communication/dto/CommonHeader.java @@ -0,0 +1,8 @@ +package io.shinhanlife.glow.communication.dto; +import lombok.Data; +@Data +public class CommonHeader { + private String itrfId; + private String rcvSvcId; + private String tgrmDalRsltCd; +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/glow/communication/dto/HeaderDefaults.java b/dap-tool-core/src/main/java/io/shinhanlife/glow/communication/dto/HeaderDefaults.java new file mode 100644 index 000000000..7df3c0a7f --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/glow/communication/dto/HeaderDefaults.java @@ -0,0 +1,6 @@ +package io.shinhanlife.glow.communication.dto; + +public enum HeaderDefaults { + ITRF_ID, RCV_SVC_ID, STR_YMD, ACNT_OGNZ_NO, PSMR_ASRT_CD, SBSN_RULP_ASRT_CD, BSDU_CD, BSQU_CD, + INDV_CTIN_ROLE_CD, SCRN_ID, OGNZ_ASRT_CD, OGNZ_LEVE_CD, TGRM_CREA_CHNN_TYPE_CD, ENVR_TYPE_CD +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/glow/communication/dto/Transfer.java b/dap-tool-core/src/main/java/io/shinhanlife/glow/communication/dto/Transfer.java new file mode 100644 index 000000000..31383d775 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/glow/communication/dto/Transfer.java @@ -0,0 +1,20 @@ +package io.shinhanlife.glow.communication.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * TODO: 실제 Glow Framework 의존성(JAR)이 추가되면 이 Mock 클래스를 삭제하세요. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Transfer { + private Object header; + private T body; + private Class resBodyClass; + private Object message; +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/glow/communication/exception/ItrfException.java b/dap-tool-core/src/main/java/io/shinhanlife/glow/communication/exception/ItrfException.java new file mode 100644 index 000000000..464cc52e3 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/glow/communication/exception/ItrfException.java @@ -0,0 +1,10 @@ +package io.shinhanlife.glow.communication.exception; + +public class ItrfException extends Exception { + public ItrfException(String msg) { + super(msg); + } + public ItrfException(String msgCd, String msgPrnAttrCd, String msgCt, String anxMsgCt) { + super(msgCd + ": " + msgCt); + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/glow/communication/module/eai/component/GlowEaiComponent.java b/dap-tool-core/src/main/java/io/shinhanlife/glow/communication/module/eai/component/GlowEaiComponent.java new file mode 100644 index 000000000..99bc00be7 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/glow/communication/module/eai/component/GlowEaiComponent.java @@ -0,0 +1,26 @@ +package io.shinhanlife.glow.communication.module.eai.component; + +import io.shinhanlife.glow.communication.dto.Transfer; + +import org.springframework.stereotype.Component; + +/** + * TODO: 실제 Glow Framework 의존성(JAR)이 추가되면 이 Mock 클래스를 삭제하세요. + */ +@Component +public class GlowEaiComponent { + public Transfer sync(Transfer request) { + // Mock 구현 + Transfer response = new Transfer<>(); + response.setHeader(request.getHeader()); + response.setResBodyClass((Class) request.getResBodyClass()); + return response; + } + + public Transfer call(Transfer request, Class resBody) { + Transfer response = new Transfer<>(); + response.setHeader(request.getHeader()); + response.setResBodyClass(resBody); + return response; + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/glow/communication/module/mci/component/GlowMciComponent.java b/dap-tool-core/src/main/java/io/shinhanlife/glow/communication/module/mci/component/GlowMciComponent.java new file mode 100644 index 000000000..ecbe4a963 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/glow/communication/module/mci/component/GlowMciComponent.java @@ -0,0 +1,18 @@ +package io.shinhanlife.glow.communication.module.mci.component; + +import io.shinhanlife.glow.communication.dto.Transfer; +import org.springframework.stereotype.Component; + +/** + * TODO: 실제 Glow Framework 의존성(JAR)이 추가되면 이 Mock 클래스를 삭제하세요. + */ +@Component +public class GlowMciComponent { + + public Transfer sync(Transfer request) { + // 실제 Glow HTTP 통신 (GlowHttpHeaderUtil, HttpClient 등) 수행 시뮬레이션 + return (Transfer) Transfer.builder() + .body(new Object()) + .build(); + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/glow/communication/util/CommonHeaderFactory.java b/dap-tool-core/src/main/java/io/shinhanlife/glow/communication/util/CommonHeaderFactory.java new file mode 100644 index 000000000..fbb154cd6 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/glow/communication/util/CommonHeaderFactory.java @@ -0,0 +1,18 @@ +package io.shinhanlife.glow.communication.util; +import io.shinhanlife.glow.communication.dto.CommonHeader; +import io.shinhanlife.glow.communication.dto.HeaderDefaults; +import java.util.Map; +public class CommonHeaderFactory { + public static CommonHeader createRequestHeader(Map commonHeaderMap) { + CommonHeader header = new CommonHeader(); + header.setItrfId(commonHeaderMap.get(HeaderDefaults.ITRF_ID)); + header.setRcvSvcId(commonHeaderMap.get(HeaderDefaults.RCV_SVC_ID)); + return header; + } + public static CommonHeader createRequestHeader(String itrfId, String rcvSvcId) { + CommonHeader header = new CommonHeader(); + header.setItrfId(itrfId); + header.setRcvSvcId(rcvSvcId); + return header; + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/glow/db/dto/AuditInfo.java b/dap-tool-core/src/main/java/io/shinhanlife/glow/db/dto/AuditInfo.java new file mode 100644 index 000000000..b9717f8d0 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/glow/db/dto/AuditInfo.java @@ -0,0 +1,40 @@ +package io.shinhanlife.glow.db.dto; + +import lombok.Getter; +import lombok.Setter; +import lombok.Builder; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.util.Date; + +/** + * @package io.shinhanlife.glow.db.dto + * @className AuditInfo + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class AuditInfo { + private Date systRgiDt; // 시스템등록일시 + private String systRgiPrafNo; // 시스템등록인사번호 + private String systRgiOgnzNo; // 시스템등록조직번호 + private String systRgiSystCd; // 시스템등록시스템코드 + private String systRgiPrgrId; // 시스템등록프로그램ID + private Date systChgDt; // 시스템변경일시 + private String systChgPrafNo; // 시스템변경인사번호 + private String systChgOgnzNo; // 시스템변경조직번호 + private String systChgSystCd; // 시스템변경시스템코드 + private String systChgPrgrId; // 시스템변경프로그램ID +} \ No newline at end of file diff --git a/dap-tool-core/src/main/java/io/shinhanlife/glow/util/GlowMciParser.java b/dap-tool-core/src/main/java/io/shinhanlife/glow/util/GlowMciParser.java new file mode 100644 index 000000000..5216ae1e8 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/glow/util/GlowMciParser.java @@ -0,0 +1,105 @@ +package io.shinhanlife.glow.util; + +import io.shinhanlife.glow.GlowMciFieldInfo; +import lombok.extern.slf4j.Slf4j; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * @package io.shinhanlife.glow.util + * @className GlowMciParser + * @description MCI 고정 길이 전문 파싱 유틸리티 (GlowMciFieldInfo 기반) + * @author 0986406 + * @create 2026.09.01 + */ +@Slf4j +public class GlowMciParser { + + public static T parse(String mciString, Class clazz) { + if (mciString == null || mciString.isEmpty()) { + return null; + } + + try { + T instance = clazz.getDeclaredConstructor().newInstance(); + + List fields = new ArrayList<>(); + for (Field field : clazz.getDeclaredFields()) { + if (field.isAnnotationPresent(GlowMciFieldInfo.class)) { + fields.add(field); + } + } + + fields.sort(Comparator.comparingInt(f -> f.getAnnotation(GlowMciFieldInfo.class).order())); + + int currentIndex = 0; + for (Field field : fields) { + GlowMciFieldInfo annotation = field.getAnnotation(GlowMciFieldInfo.class); + int length = annotation.length(); + + if (currentIndex >= mciString.length()) { + break; + } + + int endIndex = Math.min(currentIndex + length, mciString.length()); + String value = mciString.substring(currentIndex, endIndex); + + field.setAccessible(true); + setFieldValue(instance, field, value, annotation); + + currentIndex += length; + } + + return instance; + } catch (Exception e) { + log.error("GlowMciFieldInfo 파싱 중 오류 발생: {}", e.getMessage(), e); + throw new RuntimeException("MCI 전문 파싱 오류", e); + } + } + + private static void setFieldValue(Object instance, Field field, String value, GlowMciFieldInfo annotation) throws IllegalAccessException { + Class fieldType = field.getType(); + String trimmedValue = value.trim(); + + if (fieldType == String.class) { + field.set(instance, trimmedValue); + } else if (fieldType == int.class || fieldType == Integer.class) { + field.set(instance, trimmedValue.isEmpty() ? 0 : Integer.parseInt(trimmedValue)); + } else if (fieldType == long.class || fieldType == Long.class) { + field.set(instance, trimmedValue.isEmpty() ? 0L : Long.parseLong(trimmedValue)); + } else if (fieldType == boolean.class || fieldType == Boolean.class) { + field.set(instance, Boolean.parseBoolean(trimmedValue)); + } else if (fieldType == double.class || fieldType == Double.class) { + field.set(instance, trimmedValue.isEmpty() ? 0.0 : Double.parseDouble(trimmedValue)); + } else if (List.class.isAssignableFrom(fieldType)) { + Class targetClass = annotation.target(); + if (targetClass != void.class) { + List list = new ArrayList<>(); + int itemLength = calculateTotalLength(targetClass); + if (itemLength > 0) { + for (int i = 0; i < value.length(); i += itemLength) { + int end = Math.min(i + itemLength, value.length()); + String itemStr = value.substring(i, end); + if (itemStr.trim().isEmpty()) continue; + list.add(parse(itemStr, targetClass)); + } + } + field.set(instance, list); + } + } else { + field.set(instance, trimmedValue); + } + } + + private static int calculateTotalLength(Class clazz) { + int total = 0; + for (Field field : clazz.getDeclaredFields()) { + if (field.isAnnotationPresent(GlowMciFieldInfo.class)) { + total += field.getAnnotation(GlowMciFieldInfo.class).length(); + } + } + return total; + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/glow/util/GlowTrgmParser.java b/dap-tool-core/src/main/java/io/shinhanlife/glow/util/GlowTrgmParser.java new file mode 100644 index 000000000..1b89ede9a --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/glow/util/GlowTrgmParser.java @@ -0,0 +1,92 @@ +package io.shinhanlife.glow.util; + +import io.shinhanlife.glow.communication.annotation.GlowTrgmField; +import lombok.extern.slf4j.Slf4j; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * @package io.shinhanlife.glow.util + * @className GlowTrgmParser + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +public class GlowTrgmParser { + + /** + * 고정 길이 문자열 전문을 @GlowTrgmField 어노테이션 정보에 따라 DTO 객체로 파싱합니다. + * @param trgmString 원본 전문 문자열 + * @param clazz 매핑할 DTO 클래스 + * @return 파싱된 DTO 인스턴스 + */ + public static T parse(String trgmString, Class clazz) { + if (trgmString == null || trgmString.isEmpty()) { + return null; + } + + try { + T instance = clazz.getDeclaredConstructor().newInstance(); + + List fields = new ArrayList<>(); + for (Field field : clazz.getDeclaredFields()) { + if (field.isAnnotationPresent(GlowTrgmField.class)) { + fields.add(field); + } + } + + fields.sort(Comparator.comparingInt(f -> f.getAnnotation(GlowTrgmField.class).order())); + + int currentIndex = 0; + for (Field field : fields) { + GlowTrgmField annotation = field.getAnnotation(GlowTrgmField.class); + int length = annotation.length(); + + if (currentIndex >= trgmString.length()) { + break; + } + + int endIndex = Math.min(currentIndex + length, trgmString.length()); + String value = trgmString.substring(currentIndex, endIndex).trim(); + + field.setAccessible(true); + setFieldValue(instance, field, value); + + currentIndex += length; + } + + return instance; + } catch (Exception e) { + log.error("GlowTrgmField 파싱 중 오류 발생: {}", e.getMessage(), e); + throw new RuntimeException("전문 파싱 오류", e); + } + } + + private static void setFieldValue(Object instance, Field field, String value) throws IllegalAccessException { + Class fieldType = field.getType(); + + if (fieldType == String.class) { + field.set(instance, value); + } else if (fieldType == int.class || fieldType == Integer.class) { + field.set(instance, value.isEmpty() ? 0 : Integer.parseInt(value)); + } else if (fieldType == long.class || fieldType == Long.class) { + field.set(instance, value.isEmpty() ? 0L : Long.parseLong(value)); + } else if (fieldType == boolean.class || fieldType == Boolean.class) { + field.set(instance, Boolean.parseBoolean(value)); + } else if (fieldType == double.class || fieldType == Double.class) { + field.set(instance, value.isEmpty() ? 0.0 : Double.parseDouble(value)); + } else { + field.set(instance, value); + } + } +} diff --git a/dap-tool-core/src/main/resources/glow/application-glow-dev.yml b/dap-tool-core/src/main/resources/glow/application-glow-dev.yml new file mode 100644 index 000000000..693b2abf5 --- /dev/null +++ b/dap-tool-core/src/main/resources/glow/application-glow-dev.yml @@ -0,0 +1,5 @@ +# Glow 개발 환경 확장 설정 +spring: + config: + activate: + on-profile: dev \ No newline at end of file diff --git a/dap-tool-core/src/main/resources/glow/application-glow-local.yml b/dap-tool-core/src/main/resources/glow/application-glow-local.yml new file mode 100644 index 000000000..5db7f9336 --- /dev/null +++ b/dap-tool-core/src/main/resources/glow/application-glow-local.yml @@ -0,0 +1,5 @@ +# Glow 로컬 환경 확장 설정 +spring: + config: + activate: + on-profile: local \ No newline at end of file diff --git a/dap-tool-core/src/main/resources/glow/application-glow-prod.yml b/dap-tool-core/src/main/resources/glow/application-glow-prod.yml new file mode 100644 index 000000000..68ddaf64e --- /dev/null +++ b/dap-tool-core/src/main/resources/glow/application-glow-prod.yml @@ -0,0 +1,5 @@ +# Glow 운영 환경 확장 설정 +spring: + config: + activate: + on-profile: prod \ No newline at end of file diff --git a/dap-tool-core/src/main/resources/glow/application-glow-test.yml b/dap-tool-core/src/main/resources/glow/application-glow-test.yml new file mode 100644 index 000000000..c0e70f0fc --- /dev/null +++ b/dap-tool-core/src/main/resources/glow/application-glow-test.yml @@ -0,0 +1,5 @@ +# Glow 테스트 환경 확장 설정 +spring: + config: + activate: + on-profile: test \ No newline at end of file diff --git a/dap-tool-core/src/main/resources/glow/application-glow.yml b/dap-tool-core/src/main/resources/glow/application-glow.yml new file mode 100644 index 000000000..508f46c3f --- /dev/null +++ b/dap-tool-core/src/main/resources/glow/application-glow.yml @@ -0,0 +1,54 @@ +# ============================================================================== +# [AXHub 공통 환경 설정 파일] (Glow Framework 연동용) +# 이 파일은 axhub-tool-core에 위치하며, 각 툴 파드들이 상속받아 사용합니다. +# ============================================================================== + +# 1. 로깅(Logging) 공통 설정 +logging: + level: + root: INFO + io.shinhanlife.axhub: DEBUG + io.shinhanlife.glow: INFO + org.springframework.web: INFO + pattern: + console: "[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%thread] %logger{36} - %msg%n" + +# 2. Redis 공통 설정 (Gateway 연동 및 캐싱) +spring: + data: + redis: + host: 127.0.0.1 # TODO: 실제 Redis 망 IP로 변경 + port: 6379 # TODO: 실제 Redis 포트로 변경 + password: "" # TODO: 실제 비밀번호 입력 + +# 3. 신한라이프 Glow Framework 통신 (MCI / EAI) 설정 +glow: + communication: #Communication 설정 + common: + env-type: D # 대내표준 헤더의 환경 타입정보 (D: 개발, T: 테스트, P: 운영) + http: # HTTP 비표준 통신 설정 + connection-timeout: 5 # 연결 타임아웃 시간 (초 단위) + read-timeout: 5 # 읽기 타임아웃 시간 (초 단위) + mci: # 개발계 대내 MCI 통신 설정 + host: https://dev-ichmci.shinhanlife.co.kr + port: 26160 + uri: /ntl_mci/dap_rcv + receive-uri: /itrf/mciReceive + connection-timeout: 300 + read-timeout: 300 + encoding: UTF-8 + extmci: # 대외MCI 통신 설정 + host: http://host.docker.internal # 대외MCI 호스트 주소 + port: 8080 # 대외MCI 포트 번호 + uri: /extmci # 대외MCI URI 정보 + json-uri: /extmciJson # 대외MCI JSON방식 URI 정보 + receive-uri: /app/extMciReceive # 대외MCI 수신 URI + connection-timeout: 5 # 연결 타임아웃 시간 (초 단위) + read-timeout: 30 # 읽기 타임아웃 시간 (초 단위) + encoding: EUC-KR # 대외MCI 인코딩 정보 + eai: # EAI 통신 설정 + host: tcp://host.docker.internal # EAI 호스트 주소 (TODO: 실제 주소로 변경) + port: 9999 # EAI 포트 번호 + websocket: + endpoint: "/ws-glow" + allowed-origins: "*" diff --git a/dap-tool-core/src/main/resources/mock-responses.json b/dap-tool-core/src/main/resources/mock-responses.json new file mode 100644 index 000000000..7d6abfed8 --- /dev/null +++ b/dap-tool-core/src/main/resources/mock-responses.json @@ -0,0 +1,106 @@ +{ + "BILL_001": { + "status": "SUCCESS", + "message": "청구 심사 상태 조회가 완료되었습니다.", + "data": { + "billingStatus": "PROCESSING", + "expectedCompletionDate": "2026-07-05" + } + }, + "BILL_002": { + "status": "SUCCESS", + "message": "청구 처리가 완료되었습니다.", + "data": { + "processId": "PRC-998811", + "result": "APPROVED" + } + }, + "BOND_001": { + "status": "SUCCESS", + "message": "디지털 증권 발행 가능 한도 조회가 완료되었습니다.", + "data": { + "availableLimit": 500000000, + "currency": "KRW" + } + }, + "BOND_002": { + "status": "SUCCESS", + "message": "디지털 증권 발행이 성공적으로 완료되었습니다.", + "data": { + "bondId": "BND-2026-07-04-1234", + "issuedAmount": 10000000 + } + }, + "HR_VAC_01": { + "status": "SUCCESS", + "message": "연차 휴가 등록이 완료되었습니다.", + "data": { + "vacationId": "VAC-8877", + "status": "APPROVED" + } + }, + "HR_VAC_02": { + "status": "SUCCESS", + "message": "잔여 연차 일수 조회가 완료되었습니다.", + "data": { + "totalDays": 15, + "usedDays": 3, + "remainingDays": 12 + } + }, + "COM_SMS_01": { + "status": "SUCCESS", + "message": "SMS 발송이 성공적으로 완료되었습니다.", + "data": { + "messageId": "SMS-11223344", + "sentTime": "2026-07-04 10:00:00" + } + }, + "COM_EML_01": { + "status": "SUCCESS", + "message": "이메일 발송이 성공적으로 완료되었습니다.", + "data": { + "emailId": "EML-998877", + "sentTime": "2026-07-04 10:00:00" + } + }, + "CNTR_001": { + "status": "SUCCESS", + "message": "계약 상태 조회가 완료되었습니다.", + "data": { + "contractStatus": "ACTIVE", + "startDate": "2024-01-01" + } + }, + "CNTR_002": { + "status": "SUCCESS", + "message": "계약 상세 내역 조회가 완료되었습니다.", + "data": { + "productName": "신한 라이프 스마트 연금보험", + "monthlyPremium": 500000 + } + }, + "CRM_001": { + "status": "SUCCESS", + "message": "고객 등급 조회가 완료되었습니다.", + "data": { + "customerGrade": "VIP", + "loyaltyPoints": 15000 + } + }, + "CRM_002": { + "status": "SUCCESS", + "message": "고객 상세 정보 조회가 완료되었습니다.", + "data": { + "address": "서울특별시 중구 세종대로 9", + "phoneNumber": "010-XXXX-XXXX" + } + }, + "PAY_001": { + "status": "SUCCESS", + "message": "결제가 성공적으로 승인되었습니다.", + "data": { + "processStatus": "COMPLETED" + } + } +} diff --git a/dap-tool-core/src/main/resources/static/tool-test-console.html b/dap-tool-core/src/main/resources/static/tool-test-console.html new file mode 100644 index 000000000..951eda99c --- /dev/null +++ b/dap-tool-core/src/main/resources/static/tool-test-console.html @@ -0,0 +1,260 @@ + + + + + + AX HUB Tool Test Console + + + + + +
+
+ +
+ Manifest loading + v0.0.1 +
+
+
+
+

Schema 기반 Tool 테스트

Tool을 선택하고 요청 JSON을 확인한 뒤 실행하세요. 검증한 요청은 브라우저에 저장되며, 저장된 케이스 전체를 한 번에 다시 실행할 수 있습니다.

+
+ +
+

2. 요청 JSON

필수값과 형식은 Tool의 inputSchema 기준입니다. MCI·외부 연동 Tool은 업무에 맞는 테스트 데이터를 입력한 후 저장하세요.
+

3. 실행 결과

대기-trace-id: -request-id: -
Tool을 선택하고 실행하세요.
+
+
+ +
+ + + \ No newline at end of file diff --git a/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/common/adapter/sender/ShinhanMciSenderTest.java b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/common/adapter/sender/ShinhanMciSenderTest.java new file mode 100644 index 000000000..011b57150 --- /dev/null +++ b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/common/adapter/sender/ShinhanMciSenderTest.java @@ -0,0 +1,60 @@ +package io.shinhanlife.dap.lib.adapter.sender; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.shinhanlife.dap.lib.integration.mci.dto.MciRequestWrapper; +import io.shinhanlife.dap.lib.integration.mci.dto.ShinhanCommonHeaderDto; +import lombok.Data; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @package io.shinhanlife.dap.lib.adapter.sender + * @className ShinhanMciSenderTest + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +class ShinhanMciSenderTest { + + @Data + static class SampleBody { + private String msgCd; + private String anxMsgCt; + } + + @Test + void testJsonUnwrapped() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + + ShinhanCommonHeaderDto header = new ShinhanCommonHeaderDto(); + header.setGlbId("20211115150135679009808815NCS17007887"); + header.setPgrsSriaNo("002"); + header.setItrIfId("NCSBACO00001"); + + SampleBody body = new SampleBody(); + body.setMsgCd("12345"); + body.setAnxMsgCt("Test Message"); + + MciRequestWrapper wrapper = new MciRequestWrapper<>(); + wrapper.setTgrmCmnnhddValu(header); + wrapper.setBody(body); + + String json = mapper.writeValueAsString(wrapper); + + System.out.println(json); + + // 검증: body 필드가 json root 레벨에 평탄화되어 있는지 확인 + assertThat(json).contains("\"tgrmCmnnhddValu\":{"); + assertThat(json).contains("\"msgCd\":\"12345\""); + assertThat(json).contains("\"anxMsgCt\":\"Test Message\""); + assertThat(json).doesNotContain("\"body\":"); + } +} diff --git a/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/config/ToolSchemaConfigurationTest.java b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/config/ToolSchemaConfigurationTest.java new file mode 100644 index 000000000..efe770230 --- /dev/null +++ b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/config/ToolSchemaConfigurationTest.java @@ -0,0 +1,16 @@ +package io.shinhanlife.dap.lib.config; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +class ToolSchemaConfigurationTest { + + @Test + void registersToolSchemaResolver() { + ToolSchemaConfiguration configuration = new ToolSchemaConfiguration(); + + assertNotNull(configuration.toolSchemaResolver(new ObjectMapper())); + } +} diff --git a/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/JsonSchemaGeneratorTest.java b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/JsonSchemaGeneratorTest.java new file mode 100644 index 000000000..aa6529dae --- /dev/null +++ b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/JsonSchemaGeneratorTest.java @@ -0,0 +1,126 @@ +package io.shinhanlife.dap.lib.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.Error; +import com.networknt.schema.InputFormat; +import com.networknt.schema.Schema; +import com.networknt.schema.SchemaRegistry; +import com.networknt.schema.SpecificationVersion; +import io.shinhanlife.dap.lib.annotation.McpParameter; +import io.shinhanlife.dap.lib.annotation.McpValidation; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class JsonSchemaGeneratorTest { + + @Test + void includesMcpParameterConstraintsInGeneratedSchema() { + Map schema = JsonSchemaGenerator.generateSchema(ValidatedRequest.class); + Map> properties = properties(schema); + + assertTrue(((List) schema.get("required")).contains("phoneNumber")); + assertTrue(((List) schema.get("required")).contains("approvalStatus")); + assertEquals("^01[0-9]{8,9}$", properties.get("phoneNumber").get("pattern")); + assertEquals(1L, properties.get("amount").get("minimum")); + assertEquals(List.of("APPROVE", "REJECT"), properties.get("approvalStatus").get("enum")); + } + + @Test + void includesExtendedMcpValidationConstraintsInGeneratedSchema() { + Map schema = JsonSchemaGenerator.generateSchema(ValidatedRequest.class); + + assertEquals(50L, property(schema, "pageSize").get("maximum")); + assertEquals(20L, property(schema, "pageSize").get("default")); + assertEquals(1, property(schema, "reference").get("minLength")); + assertEquals(30, property(schema, "reference").get("maxLength")); + } + + @Test + void includesNestedDtoConstraintsInGeneratedSchema() { + Map schema = JsonSchemaGenerator.generateSchema(NestedRequest.class); + Map childSchema = property(schema, "child"); + + assertEquals("object", childSchema.get("type")); + assertTrue(required(childSchema).contains("businessDate")); + assertEquals("^\\d{8}$", property(childSchema, "businessDate").get("pattern")); + } + + @Test + void includesNestedDtoSchemaForListItems() { + Map schema = JsonSchemaGenerator.generateSchema(ListRequest.class); + Map itemSchema = map(property(schema, "items").get("items")); + + assertEquals("object", itemSchema.get("type")); + assertTrue(required(itemSchema).contains("businessDate")); + } + + @Test + void validatorRejectsInvalidNestedValue() throws Exception { + ObjectMapper objectMapper = new ObjectMapper(); + Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7) + .getSchema(objectMapper.writeValueAsString(JsonSchemaGenerator.generateSchema(NestedRequest.class))); + List errors = schema.validate(objectMapper.writeValueAsString(Map.of( + "child", Map.of("businessDate", "2026-07-28"))), InputFormat.JSON); + + assertFalse(errors.isEmpty()); + } + + @SuppressWarnings("unchecked") + private Map> properties(Map schema) { + return (Map>) schema.get("properties"); + } + + private Map property(Map schema, String name) { + return properties(schema).get(name); + } + + @SuppressWarnings("unchecked") + private Map map(Object value) { + return (Map) value; + } + + @SuppressWarnings("unchecked") + private List required(Map schema) { + return (List) schema.get("required"); + } + + private static class ValidatedRequest { + @McpParameter(description = "recipient phone number", required = true) + @McpValidation(pattern = "^01[0-9]{8,9}$") + private String phoneNumber; + + @McpParameter(description = "issue amount", required = true) + @McpValidation(minimum = 1) + private Long amount; + + @McpParameter(description = "approval result") + @McpValidation(required = true, allowedValues = {"APPROVE", "REJECT"}) + private String approvalStatus; + + @McpParameter(description = "page size") + @McpValidation(minimum = 1, maximum = 50, defaultValue = "20") + private Integer pageSize; + + @McpParameter(description = "reference") + @McpValidation(minLength = 1, maxLength = 30) + private String reference; + } + + private static class NestedRequest { + private NestedChild child; + } + + private static class ListRequest { + private List items; + } + + private static class NestedChild { + @McpValidation(required = true, pattern = "^\\d{8}$") + private String businessDate; + } +} diff --git a/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java new file mode 100644 index 000000000..f80f5011d --- /dev/null +++ b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java @@ -0,0 +1,28 @@ +package io.shinhanlife.dap.lib.util; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; + +class ToolScaffolderTest { + + @Test + void generatesManifestReadyToolAndOutputSchemaDto() throws Exception { + String moduleName = "build/scaffold-manifest-test"; + + ToolScaffolder.scaffold("claim search", "CLM0001", "청구 조회", "cmm", "HTTP", moduleName, + "tester", "2026.08.04", true, null); + + Path root = Path.of(moduleName, "src/main/java/io/shinhanlife/dap/mcc/biz/cmm"); + String useCase = Files.readString(root.resolve("usecase/ClaimSearchUseCase.java")); + String response = Files.readString(root.resolve("dto/ClaimSearchResponse.java")); + + assertTrue(useCase.contains("name = \"oth.cmm.claim.search\"")); + assertTrue(useCase.contains("version = \"1.0.0\"")); + assertTrue(useCase.contains("timeoutMillis = 300000L")); + assertTrue(response.contains("@McpOutputSchema")); + assertTrue(response.contains("@McpValidation")); + } +} diff --git a/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java new file mode 100644 index 000000000..b6d02d1bf --- /dev/null +++ b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java @@ -0,0 +1,119 @@ +package io.shinhanlife.dap.lib.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.shinhanlife.dap.lib.annotation.McpFunction; +import io.shinhanlife.dap.lib.annotation.McpOutputSchema; +import io.shinhanlife.dap.lib.annotation.McpValidation; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class ToolSchemaResolverTest { + + private final ToolSchemaResolver resolver = new ToolSchemaResolver(new ObjectMapper()); + + @Test + void usesInlineSchemaBeforeAutomaticDtoSchema() throws Exception { + Method method = InlineSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class); + + Map schema = resolver.resolve(method.getAnnotation(McpFunction.class), AutomaticRequest.class); + + assertEquals(false, schema.get("additionalProperties")); + assertTrue(!properties(schema).containsKey("differentField")); + } + + @Test + void generatesSchemaFromRequestDtoWhenNoExplicitSchemaIsConfigured() throws Exception { + Method method = AutomaticSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class); + + Map schema = resolver.resolve(method.getAnnotation(McpFunction.class), AutomaticRequest.class); + + assertTrue(properties(schema).containsKey("differentField")); + } + + @Test + void resolvesExplicitOutputSchema() throws Exception { + Method method = OutputSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class); + Map schema = resolver.resolveOutput(method.getAnnotation(McpFunction.class)); + assertEquals(false, schema.get("additionalProperties")); + assertTrue(properties(schema).containsKey("resultCode")); + } + + @Test + void generatesOutputSchemaFromMarkedResponseDto() throws Exception { + Method method = AutomaticOutputSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class); + + Map schema = resolver.resolveOutput( + method.getAnnotation(McpFunction.class), SimpleResponse.class); + + assertEquals(List.of("resultCode"), schema.get("required")); + assertEquals(List.of("SUCCESS", "FAILURE"), property(schema, "resultCode").get("enum")); + } + + @Test + void doesNotEnableOutputValidationWhenOutputSchemaIsNotDeclared() throws Exception { + Method method = AutomaticSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class); + Map schema = resolver.resolveOutput( + method.getAnnotation(McpFunction.class), AutomaticRequest.class); + assertTrue(schema.isEmpty()); + } + @SuppressWarnings("unchecked") + private Map properties(Map schema) { + return (Map) schema.get("properties"); + } + + @SuppressWarnings("unchecked") + private Map property(Map schema, String name) { + return (Map) properties(schema).get(name); + } + + static class InlineSchemaTool { + @McpFunction( + displayName = "inline", + name = "test.schema.inline", + description = "inline schema", + inputSchema = "{\"type\":\"object\",\"properties\":{\"keyword\":{\"type\":\"string\"}},\"additionalProperties\":false}") + void search(AutomaticRequest request) { + } + } + + static class AutomaticSchemaTool { + @McpFunction(displayName = "automatic", name = "test.schema.automatic", description = "automatic schema") + void search(AutomaticRequest request) { + } + } + + static class AutomaticOutputSchemaTool { + @McpFunction(displayName = "automatic-output", name = "test.schema.automatic-output", description = "automatic output") + SimpleResponse search(AutomaticRequest request) { + return null; + } + } + + @McpOutputSchema + static class SimpleResponse { + @McpValidation(required = true, allowedValues = {"SUCCESS", "FAILURE"}) + private String resultCode; + + @McpValidation(maxLength = 200) + private String message; + } + + static class OutputSchemaTool { + @McpFunction( + displayName = "output", + name = "test.schema.output", + description = "output schema", + outputSchema = "{\"type\":\"object\",\"properties\":{\"resultCode\":{\"type\":\"string\"}},\"required\":[\"resultCode\"],\"additionalProperties\":false}") + void search(AutomaticRequest request) { + } + } + + static class AutomaticRequest { + private String differentField; + } +} diff --git a/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java new file mode 100644 index 000000000..b83ec4d68 --- /dev/null +++ b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java @@ -0,0 +1,110 @@ +package io.shinhanlife.dap.lib.validation; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * @package io.shinhanlife.dap.lib.validation + * @className McpToolNameValidatorTest + * @description MCP tool name uniqueness validation test + * @author 0986406 + * @create 2026.07.27 + *
+ * ---------- revision history ----------
+ * date       author    description
+ * ---------- --------- ---------------------------
+ * 2026.07.27 0986406    initial creation
+ * 
+ */ +class McpToolNameValidatorTest { + + @TempDir + Path temporaryRoot; + + @Test + void rejectsDuplicateMcpFunctionNamesAcrossToolModules() throws IOException { + writeToolSource("dap-tool-first", "FirstTool.java", "first", "oth.sms.notification.send"); + writeToolSource("dap-tool-second", "SecondTool.java", "second", "oth.sms.notification.send"); + + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> McpToolNameValidator.assertUnique(temporaryRoot)); + + assertTrue(exception.getMessage().contains("oth.sms.notification.send")); + assertTrue(exception.getMessage().contains("dap-tool-first")); + assertTrue(exception.getMessage().contains("dap-tool-second")); + } + + @Test + void validationRunnerRejectsDuplicateMcpFunctionNamesBeforePackaging() throws IOException { + writeToolSource("dap-tool-first", "FirstTool.java", "first", "oth.sms.notification.send"); + writeToolSource("dap-tool-second", "SecondTool.java", "second", "oth.sms.notification.send"); + + assertThrows(IllegalStateException.class, + () -> McpToolNameValidationRunner.validate(temporaryRoot)); + } + + @Test + void rejectsToolNameOutsidePodDomainServiceActionConvention() throws IOException { + writeToolSource("dap-tool-first", "FirstTool.java", "first", "bond_issue"); + + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> McpToolNameValidator.assertUnique(temporaryRoot)); + + assertTrue(exception.getMessage().contains("Invalid MCP tool name(s)")); + assertTrue(exception.getMessage().contains("bond_issue")); + } + @Test + void acceptsCurrentProjectToolNames() { + assertDoesNotThrow(() -> McpToolNameValidator.assertUnique(findProjectRoot())); + } + + @Test + void usesDomainSpecificNamesForCustomerBillingAndBondTools() throws IOException { + Path root = findProjectRoot(); + + assertToolName(root, "dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CustomerInfoUseCase.java", + "oth.cmm.customer.detail", "detail"); + assertToolName(root, "dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BillingProcessUseCase.java", + "oth.cmm.billing.process", "process"); + assertToolName(root, "dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BondIssueUseCase.java", + "oth.cmm.bond.issue", "issue"); + } + + private void assertToolName(Path root, String relativePath, String expectedName, String legacyName) throws IOException { + String source = Files.readString(root.resolve(relativePath)); + assertTrue(source.contains("name = \"" + expectedName + "\"")); + assertFalse(source.contains("name = \"" + legacyName + "\"")); + } + + private void writeToolSource(String moduleName, String fileName, String className, String toolName) throws IOException { + Path source = temporaryRoot.resolve(moduleName).resolve("src/main/java/example").resolve(fileName); + Files.createDirectories(source.getParent()); + Files.writeString(source, """ + package example; + + class %s { + @McpFunction(displayName = "%s", name = "%s", description = "test") + void execute() { } + } + """.formatted(className, className, toolName)); + } + + private Path findProjectRoot() { + Path current = Path.of("").toAbsolutePath(); + while (current != null && !Files.exists(current.resolve("settings.gradle"))) { + current = current.getParent(); + } + if (current == null) { + throw new IllegalStateException("Could not locate Gradle project root"); + } + return current; + } +} diff --git a/dap-tool-core/src/test/java/io/shinhanlife/dap/mcc/manifest/ToolManifestServiceTest.java b/dap-tool-core/src/test/java/io/shinhanlife/dap/mcc/manifest/ToolManifestServiceTest.java new file mode 100644 index 000000000..044ac69cb --- /dev/null +++ b/dap-tool-core/src/test/java/io/shinhanlife/dap/mcc/manifest/ToolManifestServiceTest.java @@ -0,0 +1,100 @@ +package io.shinhanlife.dap.mcc.manifest; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.shinhanlife.dap.lib.config.McpProperties; +import io.shinhanlife.dap.mcc.dto.ToolMetadata; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class ToolManifestServiceTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void buildsStandardManifestAndDerivesRevisionFromToolDefinition() { + McpProperties properties = manifestProperties("insurance-processing", "processing."); + ToolManifestService service = new ToolManifestService( + () -> List.of(tool("processing.contract.inquiry", "1.2.0", 3000)), objectMapper, properties); + + ToolManifestResponse manifest = service.currentManifest(); + + assertEquals("insurance-processing", manifest.bundleId()); + assertTrue(manifest.revision().matches("\\d+")); + assertEquals(1, manifest.tools().size()); + ToolManifestItem item = manifest.tools().getFirst(); + assertEquals("processing.contract.inquiry", item.name()); + assertEquals("http://tool-processing.ax-hub.svc.cluster.local:8080/mcp/processing.contract.inquiry", + item.endpoint()); + assertEquals("계약 조회", item.title()); + assertEquals("object", item.inputSchema().get("type")); + assertTrue(item.annotations().readOnlyHint()); + assertEquals("1.2.0", item.meta().version()); + assertEquals(3000, item.meta().timeoutMillis()); + } + + @Test + void changesRevisionWhenToolDefinitionChanges() { + McpProperties properties = manifestProperties("insurance-processing", "processing."); + ToolManifestService before = new ToolManifestService( + () -> List.of(tool("processing.contract.inquiry", "1.2.0", 3000)), objectMapper, properties); + ToolManifestService after = new ToolManifestService( + () -> List.of(tool("processing.contract.inquiry", "1.2.0", 5000)), objectMapper, properties); + + assertTrue(!before.currentManifest().revision().equals(after.currentManifest().revision())); + } + + @Test + void rejectsEntireManifestWhenToolNameDoesNotMatchConfiguredPrefix() { + McpProperties properties = manifestProperties("insurance-processing", "processing."); + ToolManifestService service = new ToolManifestService( + () -> List.of(tool("notification.sms.send", "1.0.0", 3000)), objectMapper, properties); + + IllegalStateException error = assertThrows(IllegalStateException.class, service::currentManifest); + + assertTrue(error.getMessage().contains("name-prefix")); + } + + @Test + void rejectsEntireManifestWhenToolNamesAreDuplicated() { + McpProperties properties = manifestProperties("insurance-processing", "processing."); + ToolManifestService service = new ToolManifestService( + () -> List.of(tool("processing.contract.inquiry", "1.0.0", 3000), + tool("processing.contract.inquiry", "1.0.1", 3000)), objectMapper, properties); + + IllegalStateException error = assertThrows(IllegalStateException.class, service::currentManifest); + + assertTrue(error.getMessage().contains("duplicate")); + } + + private McpProperties manifestProperties(String bundleId, String namePrefix) { + McpProperties properties = new McpProperties(); + McpProperties.Manifest manifest = new McpProperties.Manifest(); + manifest.setBundleId(bundleId); + manifest.setNamePrefix(namePrefix); + properties.setManifest(manifest); + return properties; + } + + private ToolMetadata tool(String name, String version, long timeoutMillis) { + return ToolMetadata.builder() + .name(name) + .podUrl("http://tool-processing.ax-hub.svc.cluster.local:8080") + .displayName("계약 조회") + .description("계약번호로 계약 정보를 조회합니다.") + .parametersSchema(Map.of("type", "object", "properties", Map.of("contractNo", Map.of("type", "string")), + "required", List.of("contractNo"), "additionalProperties", false)) + .semver(version) + .timeoutMillis(timeoutMillis) + .enabled(true) + .readOnlyHint(true) + .destructiveHint(false) + .idempotentHint(true) + .openWorldHint(false) + .build(); + } +} diff --git a/dap-tool-core/src/test/java/io/shinhanlife/dap/mcc/mcp/McpRequestHeaderFilterTest.java b/dap-tool-core/src/test/java/io/shinhanlife/dap/mcc/mcp/McpRequestHeaderFilterTest.java new file mode 100644 index 000000000..7d8af77fb --- /dev/null +++ b/dap-tool-core/src/test/java/io/shinhanlife/dap/mcc/mcp/McpRequestHeaderFilterTest.java @@ -0,0 +1,62 @@ +package io.shinhanlife.dap.mcc.mcp; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.server.McpSyncServer; +import io.shinhanlife.dap.mcc.presentation.BusinessToolController; +import io.shinhanlife.dap.mcc.usecase.ToolRegistryHeartbeatSender; +import java.lang.reflect.Method; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +class McpRequestHeaderFilterTest { + + @Test + void capturesOptionalMcpHeadersOnlyForTheCurrentRequest() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/mcp"); + request.addHeader("X-Request-Id", "gateway-request-id"); + request.addHeader("trace-id", "trace-001"); + request.addHeader("request-id", "tool-request-001"); + request.addHeader("employee-id", "encrypted-employee-id"); + + new McpRequestHeaderFilter().doFilter(request, new MockHttpServletResponse(), (req, res) -> { + McpRequestHeaders headers = McpRequestHeaderContext.current(); + assertEquals("gateway-request-id", headers.headerRequestId()); + assertEquals("trace-001", headers.traceId()); + assertEquals("tool-request-001", headers.requestId()); + assertEquals("encrypted-employee-id", headers.encryptedEmployeeId()); + }); + + assertNull(McpRequestHeaderContext.current()); + } + + @Test + void forwardsCapturedHeadersToBusinessToolExecution() throws Exception { + BusinessToolController controller = mock(BusinessToolController.class); + doReturn(ResponseEntity.ok(Map.of("result", "ok"))) + .when(controller).executeDynamicTool(eq("sampleTool"), eq("gateway-request-id"), eq("trace-001"), + eq("tool-request-001"), eq("encrypted-employee-id"), eq(Map.of("key", "value"))); + ToolPodMcpToolSynchronizer synchronizer = new ToolPodMcpToolSynchronizer( + mock(McpSyncServer.class), mock(ToolRegistryHeartbeatSender.class), controller, new ObjectMapper()); + + Method invoke = ToolPodMcpToolSynchronizer.class.getDeclaredMethod( + "invoke", String.class, McpRequestHeaders.class, Map.class); + invoke.setAccessible(true); + invoke.invoke(synchronizer, "sampleTool", + new McpRequestHeaders("gateway-request-id", "trace-001", "tool-request-001", "encrypted-employee-id"), + Map.of("key", "value")); + + verify(controller).executeDynamicTool("sampleTool", "gateway-request-id", "trace-001", + "tool-request-001", "encrypted-employee-id", Map.of("key", "value")); + } +} \ No newline at end of file diff --git a/dap-tool-core/src/test/java/io/shinhanlife/dap/mcc/mcp/ToolMcpServerConfigurationTest.java b/dap-tool-core/src/test/java/io/shinhanlife/dap/mcc/mcp/ToolMcpServerConfigurationTest.java new file mode 100644 index 000000000..c0fb8101e --- /dev/null +++ b/dap-tool-core/src/test/java/io/shinhanlife/dap/mcc/mcp/ToolMcpServerConfigurationTest.java @@ -0,0 +1,22 @@ +package io.shinhanlife.dap.mcc.mcp; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider; +import org.junit.jupiter.api.Test; +import org.springframework.boot.web.servlet.ServletRegistrationBean; + +class ToolMcpServerConfigurationTest { + + private final ToolMcpServerConfiguration configuration = new ToolMcpServerConfiguration(); + + @Test + void exposesOnlyExactMcpEndpointSoLegacyMcpApiPathsRemainAvailable() { + HttpServletStreamableServerTransportProvider transport = configuration.toolMcpTransportProvider(); + ServletRegistrationBean registration = configuration.toolMcpServlet(transport); + + assertEquals("/mcp", registration.getUrlMappings().iterator().next()); + assertFalse(registration.getUrlMappings().contains("/mcp/*")); + } +} diff --git a/dap-tool-core/src/test/java/io/shinhanlife/dap/mcc/presentation/BusinessToolControllerHeaderContractTest.java b/dap-tool-core/src/test/java/io/shinhanlife/dap/mcc/presentation/BusinessToolControllerHeaderContractTest.java new file mode 100644 index 000000000..165b6aebc --- /dev/null +++ b/dap-tool-core/src/test/java/io/shinhanlife/dap/mcc/presentation/BusinessToolControllerHeaderContractTest.java @@ -0,0 +1,32 @@ +package io.shinhanlife.dap.mcc.presentation; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.springframework.web.bind.annotation.RequestHeader; + +class BusinessToolControllerHeaderContractTest { + + @Test + void encryptedEmployeeIdHeaderIsOptional() throws Exception { + Method method = BusinessToolController.class.getDeclaredMethod( + "executeDynamicTool", + String.class, + String.class, + String.class, + String.class, + String.class, + Map.class); + + Parameter employeeIdParameter = method.getParameters()[4]; + RequestHeader requestHeader = employeeIdParameter.getAnnotation(RequestHeader.class); + + assertEquals("employee-id", requestHeader.value()); + assertFalse(requestHeader.required()); + } +} \ No newline at end of file diff --git a/dap-tool-core/src/test/java/io/shinhanlife/dap/mcc/presentation/ToolArgumentSchemaValidatorTest.java b/dap-tool-core/src/test/java/io/shinhanlife/dap/mcc/presentation/ToolArgumentSchemaValidatorTest.java new file mode 100644 index 000000000..e10dcc7d9 --- /dev/null +++ b/dap-tool-core/src/test/java/io/shinhanlife/dap/mcc/presentation/ToolArgumentSchemaValidatorTest.java @@ -0,0 +1,25 @@ +package io.shinhanlife.dap.mcc.presentation; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class ToolArgumentSchemaValidatorTest { + + private final ToolArgumentSchemaValidator validator = new ToolArgumentSchemaValidator(new ObjectMapper()); + + @Test + void validatesDraft7SchemaWithTheRuntimeNetworkntVersion() throws Exception { + Map schema = Map.of( + "type", "object", + "properties", Map.of("name", Map.of("type", "string")), + "required", List.of("name")); + + assertTrue(validator.validate(schema, Map.of("name", "Hong")).isEmpty()); + assertFalse(validator.validate(schema, Map.of()).isEmpty()); + } +} diff --git a/dap-tool-core/src/test/java/io/shinhanlife/dap/mcc/presentation/ToolTestConsoleResourceTest.java b/dap-tool-core/src/test/java/io/shinhanlife/dap/mcc/presentation/ToolTestConsoleResourceTest.java new file mode 100644 index 000000000..6c21272c1 --- /dev/null +++ b/dap-tool-core/src/test/java/io/shinhanlife/dap/mcc/presentation/ToolTestConsoleResourceTest.java @@ -0,0 +1,24 @@ +package io.shinhanlife.dap.mcc.presentation; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; + +class ToolTestConsoleResourceTest { + + @Test + void publishesManifestDrivenToolTestConsole() throws Exception { + try (InputStream resource = getClass().getResourceAsStream("/static/tool-test-console.html")) { + assertNotNull(resource); + String html = new String(resource.readAllBytes(), StandardCharsets.UTF_8); + + assertTrue(html.contains("/tool-manifest")); + assertTrue(html.contains("Run saved cases")); + assertTrue(html.contains("localStorage")); + assertTrue(html.contains("request-id")); + } + } +} \ No newline at end of file diff --git a/dap-tool-core/src/test/java/io/shinhanlife/glow/communication/annotation/GlowTrgmFieldContractTest.java b/dap-tool-core/src/test/java/io/shinhanlife/glow/communication/annotation/GlowTrgmFieldContractTest.java new file mode 100644 index 000000000..3740a24ae --- /dev/null +++ b/dap-tool-core/src/test/java/io/shinhanlife/glow/communication/annotation/GlowTrgmFieldContractTest.java @@ -0,0 +1,27 @@ +package io.shinhanlife.glow.communication.annotation; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.reflect.Field; +import org.junit.jupiter.api.Test; + +class GlowTrgmFieldContractTest { + + private static class StandardTrgmDto { + @GlowTrgmField(order = 1, length = 8, decimal = 2, description = "금액", target = "body", type = "gs") + private String amount; + } + + @Test + void exposesTheStandardGlowTrgmFieldMetadata() throws Exception { + Field field = StandardTrgmDto.class.getDeclaredField("amount"); + GlowTrgmField metadata = field.getAnnotation(GlowTrgmField.class); + + assertEquals(1, metadata.order()); + assertEquals(8, metadata.length()); + assertEquals(2, metadata.decimal()); + assertEquals("금액", metadata.description()); + assertEquals("body", metadata.target()); + assertEquals("gs", metadata.type()); + } +} diff --git a/dap-tool-core/src/test/java/io/shinhanlife/glow/util/GlowMciParserTest.java b/dap-tool-core/src/test/java/io/shinhanlife/glow/util/GlowMciParserTest.java new file mode 100644 index 000000000..283a129a9 --- /dev/null +++ b/dap-tool-core/src/test/java/io/shinhanlife/glow/util/GlowMciParserTest.java @@ -0,0 +1,65 @@ +package io.shinhanlife.glow.util; + + +/** + * @package io.shinhanlife.glow.util + * @className GlowMciParserTest + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +import io.shinhanlife.glow.GlowMciFieldInfo; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class GlowMciParserTest { + + public static class DummyMciDto { + @GlowMciFieldInfo(order = 1, length = 10) + private String customerId; + + @GlowMciFieldInfo(order = 2, length = 15) + private String name; + + @GlowMciFieldInfo(order = 3, length = 3) + private int age; + + public String getCustomerId() { return customerId; } + public String getName() { return name; } + public int getAge() { return age; } + } + + @Test + @DisplayName("고정 길이 MCI 문자열을 DTO로 파싱하는 테스트") + public void testParseFixedLengthString() { + // given: 고정 길이 텍스트 (총 28자리) + // ID(10) + Name(15) + Age(3) + String rawMciString = "CUST000001KIM SHINHAN 035"; + + // when + DummyMciDto result = GlowMciParser.parse(rawMciString, DummyMciDto.class); + + // then + System.out.println("=================================================="); + System.out.println(" [원본 MCI 전문] : [" + rawMciString + "]"); + System.out.println(" [파싱된 ID (10자리)] : [" + result.getCustomerId() + "]"); + System.out.println(" [파싱된 Name (15자리)] : [" + result.getName() + "]"); + System.out.println(" [파싱된 Age (3자리)] : [" + result.getAge() + "]"); + System.out.println("=================================================="); + + assertNotNull(result); + assertEquals("CUST000001", result.getCustomerId()); + assertEquals("KIM SHINHAN", result.getName()); + assertEquals(35, result.getAge()); + } +} diff --git a/dap-tool-oth/Dockerfile b/dap-tool-oth/Dockerfile new file mode 100644 index 000000000..9cf239348 --- /dev/null +++ b/dap-tool-oth/Dockerfile @@ -0,0 +1,8 @@ +FROM eclipse-temurin:21-jre-alpine +WORKDIR /app +RUN apk add --no-cache tzdata +ENV TZ=Asia/Seoul +COPY dap-tool-oth/build/libs/*-SNAPSHOT.jar app.jar +EXPOSE 8084 +ENTRYPOINT ["java", "-jar", "app.jar"] + diff --git a/dap-tool-oth/build.gradle b/dap-tool-oth/build.gradle new file mode 100644 index 000000000..554427fc9 --- /dev/null +++ b/dap-tool-oth/build.gradle @@ -0,0 +1,12 @@ +plugins { + id 'org.springframework.boot' +} + +dependencies { + implementation project(':dap-tool-core') +} + +dependencies { + compileOnly 'org.projectlombok:lombok:1.18.32' + annotationProcessor 'org.projectlombok:lombok:1.18.32' +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/converter/MetaCommonCodeConverter.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/converter/MetaCommonCodeConverter.java new file mode 100644 index 000000000..87f56ee91 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/converter/MetaCommonCodeConverter.java @@ -0,0 +1,31 @@ +package io.shinhanlife.dap.mcc.biz.cmm.converter; + +import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeRequest; +import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeResponse; +import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_I; +import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_O; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +/** + * @package io.shinhanlife.dap.mcc.biz.cmm.converter + * @className MetaCommonCodeConverter + * @description AX HUB 시스템 처리 클래스 + * @author 09863409 + * @create 2026.07.29 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.07.29  09863409    최초생성
+ * 
+ * 
+ */ +@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); +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/converter/MetaTableConverter.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/converter/MetaTableConverter.java new file mode 100644 index 000000000..c59e3524e --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/converter/MetaTableConverter.java @@ -0,0 +1,31 @@ +package io.shinhanlife.dap.mcc.biz.cmm.converter; + +import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaTableRequest; +import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaTableResponse; +import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_I; +import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_O; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +/** + * @package io.shinhanlife.dap.mcc.biz.cmm.converter + * @className MetaTableConverter + * @description AX HUB 시스템 처리 클래스 + * @author 09863409 + * @create 2026.07.29 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.07.29  09863409    최초생성
+ * 
+ * 
+ */ +@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); +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BalanceRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BalanceRequest.java new file mode 100644 index 000000000..1e8df0911 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BalanceRequest.java @@ -0,0 +1,27 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + + +/** + * @package io.shinhanlife.dap.mcc.dto + * @className BalanceRequest + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +import io.shinhanlife.dap.lib.annotation.McpParameter; +import io.shinhanlife.dap.lib.annotation.McpValidation; +import lombok.Data; + +@Data +public class BalanceRequest { + @McpParameter(description = "고객의 계좌번호 (- 제외) ", required = true) + @McpValidation(pattern = "\\S") + private String accountNumber; +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BalanceResponse.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BalanceResponse.java new file mode 100644 index 000000000..b09377226 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BalanceResponse.java @@ -0,0 +1,26 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + + +/** + * @package io.shinhanlife.dap.mcc.dto + * @className BalanceResponse + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +import lombok.Data; + +@Data +public class BalanceResponse { + private String status; + private String message; + private String accountNumber; + private long balance; +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BillingProcessRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BillingProcessRequest.java new file mode 100644 index 000000000..795537ae6 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BillingProcessRequest.java @@ -0,0 +1,40 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + +import lombok.Builder; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import io.shinhanlife.dap.lib.annotation.McpParameter; +import io.shinhanlife.dap.lib.annotation.McpValidation; +import lombok.Getter; +import lombok.Setter; + +/** + * @package io.shinhanlife.dap.mcc.dto + * @className BillingProcessRequest + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BillingProcessRequest { + + @McpParameter(description = "처리할 청구 접수 번호", required = true) + @McpValidation(pattern = "\\S") + private String billingId; + + @McpParameter(description = "심사 승인 여부 (예: APPROVE, REJECT)") + @McpValidation(required = true, allowedValues = {"APPROVE", "REJECT"}) + private String approvalStatus; +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BillingStatusRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BillingStatusRequest.java new file mode 100644 index 000000000..33c672162 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BillingStatusRequest.java @@ -0,0 +1,34 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + +import lombok.Builder; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import io.shinhanlife.dap.lib.annotation.McpParameter; +import lombok.Getter; +import lombok.Setter; + +/** + * @package io.shinhanlife.dap.mcc.dto + * @className BillingStatusRequest + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BillingStatusRequest { + + @McpParameter(description = "조회할 청구 접수 번호", required = true) + private String billingId; +} \ No newline at end of file diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BondCheckRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BondCheckRequest.java new file mode 100644 index 000000000..35abb2465 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BondCheckRequest.java @@ -0,0 +1,34 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + +import lombok.Builder; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import io.shinhanlife.dap.lib.annotation.McpParameter; +import lombok.Getter; +import lombok.Setter; + +/** + * @package io.shinhanlife.dap.mcc.dto + * @className BondCheckRequest + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BondCheckRequest { + + @McpParameter(description = "확인하고자 하는 디지털 증권 발행 금액", required = true) + private Long amount; +} \ No newline at end of file diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BondIssueRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BondIssueRequest.java new file mode 100644 index 000000000..aa6d8248e --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BondIssueRequest.java @@ -0,0 +1,40 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + +import lombok.Builder; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import io.shinhanlife.dap.lib.annotation.McpParameter; +import io.shinhanlife.dap.lib.annotation.McpValidation; +import lombok.Getter; +import lombok.Setter; + +/** + * @package io.shinhanlife.dap.mcc.dto + * @className BondIssueRequest + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BondIssueRequest { + + @McpParameter(description = "발행할 디지털 증권 금액", required = true) + @McpValidation(minimum = 1) + private Long amount; + + @McpParameter(description = "발행 대상 계좌 번호", required = true) + @McpValidation(pattern = "\\S") + private String targetAccount; +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequest.java new file mode 100644 index 000000000..2d94abd5a --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequest.java @@ -0,0 +1,57 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + +import io.shinhanlife.dap.lib.annotation.McpAnyOf; +import io.shinhanlife.dap.lib.annotation.McpParameter; +import io.shinhanlife.dap.lib.annotation.McpValidation; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * 보험금 청구 조회 Tool의 입력 DTO 샘플이다. + * 청구번호 또는 계약번호 중 하나를 반드시 입력받는다. + */ +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@McpAnyOf({"claimNo", "contractNo"}) +public class ClaimSearchRequest { + + @McpParameter(description = "청구번호. CLM 다음 숫자 13자리 형식이다.") + @McpValidation(pattern = "^CLM[0-9]{13}$", examples = {"CLM2026070100123"}) + private String claimNo; + + @McpParameter(description = "계약번호. 숫자 11자리 형식이다.") + @McpValidation(pattern = "^[0-9]{11}$", examples = {"10023456789"}) + private String contractNo; + + @McpParameter(description = "청구 상태 필터") + @McpValidation(allowedValues = { + "RECEIVED", "REVIEWING", "ADDITIONAL_DOC_REQUIRED", + "APPROVED", "PAID", "REJECTED", "WITHDRAWN" + }) + private String status; + + @McpParameter(description = "청구 유형 필터") + @McpValidation(allowedValues = { + "MEDICAL", "SURGERY", "HOSPITALIZATION", + "DIAGNOSIS", "DEATH", "DISABILITY" + }) + private String claimType; + + @McpParameter(description = "접수일 조회 시작일(YYYY-MM-DD)") + @McpValidation(format = "date", examples = {"2026-01-01"}) + private String fromDate; + + @McpParameter(description = "접수일 조회 종료일(YYYY-MM-DD)") + @McpValidation(format = "date", examples = {"2026-07-31"}) + private String toDate; + + @McpParameter(description = "반환할 최대 건수") + @McpValidation(minimum = 1, maximum = 50, defaultValue = "20") + private Integer size; +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchResponse.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchResponse.java new file mode 100644 index 000000000..abd2d2d8f --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchResponse.java @@ -0,0 +1,88 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + +import io.shinhanlife.dap.lib.annotation.McpOutputSchema; +import io.shinhanlife.dap.lib.annotation.McpParameter; +import io.shinhanlife.dap.lib.annotation.McpValidation; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * Claim search response sample. + * Complex response rules are defined by outputSchemaResource; annotations document + * the same simple field constraints for automatic schema generation examples. + */ +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@McpOutputSchema +public class ClaimSearchResponse { + + @McpParameter(description = "Execution result code.") + @McpValidation(required = true, allowedValues = {"SUCCESS", "FAILURE"}) + private String resultCode; + + @McpParameter(description = "User-readable label for resultCode.") + @McpValidation(required = true, maxLength = 100) + private String resultLabel; + + @McpParameter(description = "Current claim processing status code.") + @McpValidation(required = true, allowedValues = { + "RECEIVED", "REVIEWING", "ADDITIONAL_DOC_REQUIRED", + "APPROVED", "PAID", "REJECTED", "WITHDRAWN" + }) + private String status; + + @McpParameter(description = "User-readable label for status.") + @McpValidation(required = true, maxLength = 100) + private String statusLabel; + + @McpParameter(description = "Approved amount. Null before review; do not interpret null as zero.") + @McpValidation(minimum = 0, nullable = true) + private Long approvedAmount; + + @McpParameter(description = "Present only when status is REJECTED; otherwise null.") + @McpValidation(maxLength = 200, nullable = true) + private String rejectionReason; + + @McpParameter(description = "Claim summaries, ordered by received date descending.") + @McpValidation(required = true) + private List items; + + @McpParameter(description = "True when additional results exist beyond this response.") + @McpValidation(required = true) + private Boolean hasMore; + + @McpParameter(description = "Total number of matched claims.") + @McpValidation(required = true, minimum = 0) + private Integer totalCount; + + @Getter + @Setter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ClaimSummary { + + @McpParameter(description = "Claim processing status code.") + @McpValidation(required = true) + private String status; + + @McpParameter(description = "User-readable label for status.") + @McpValidation(required = true) + private String statusLabel; + + @McpParameter(description = "Received date in YYYY-MM-DD format.") + @McpValidation(required = true, format = "date") + private String receivedDate; + + @McpParameter(description = "Approved amount. Null before review; do not interpret null as zero.") + @McpValidation(minimum = 0, nullable = true) + private Long approvedAmount; + } +} \ No newline at end of file diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ContractDetailRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ContractDetailRequest.java new file mode 100644 index 000000000..2105f28a9 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ContractDetailRequest.java @@ -0,0 +1,37 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + +import lombok.Builder; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import io.shinhanlife.dap.lib.annotation.McpParameter; +import lombok.Getter; +import lombok.Setter; + +/** + * @package io.shinhanlife.dap.mcc.dto + * @className ContractDetailRequest + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ContractDetailRequest { + + @McpParameter(description = "고객명", required = true) + private String customerName; + + @McpParameter(description = "조회할 계약 번호", required = true) + private String contractId; +} \ No newline at end of file diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ContractStatusRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ContractStatusRequest.java new file mode 100644 index 000000000..44966664f --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ContractStatusRequest.java @@ -0,0 +1,37 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + +import lombok.Builder; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import io.shinhanlife.dap.lib.annotation.McpParameter; +import lombok.Getter; +import lombok.Setter; + +/** + * @package io.shinhanlife.dap.mcc.dto + * @className ContractStatusRequest + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ContractStatusRequest { + + @McpParameter(description = "고객명", required = true) + private String customerName; + + @McpParameter(description = "조회할 계약 번호") + private String contractId; +} \ No newline at end of file diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/CustomerDetailRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/CustomerDetailRequest.java new file mode 100644 index 000000000..e1161919c --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/CustomerDetailRequest.java @@ -0,0 +1,37 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + +import lombok.Builder; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import io.shinhanlife.dap.lib.annotation.McpParameter; +import lombok.Getter; +import lombok.Setter; + +/** + * @package io.shinhanlife.dap.mcc.dto + * @className CustomerDetailRequest + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CustomerDetailRequest { + + @McpParameter(description = "고객명", required = true) + private String customerName; + + @McpParameter(description = "고객 식별 번호 (CID)") + private String customerId; +} \ No newline at end of file diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/CustomerGradeRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/CustomerGradeRequest.java new file mode 100644 index 000000000..53387ab27 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/CustomerGradeRequest.java @@ -0,0 +1,37 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + +import lombok.Builder; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import io.shinhanlife.dap.lib.annotation.McpParameter; +import lombok.Getter; +import lombok.Setter; + +/** + * @package io.shinhanlife.dap.mcc.dto + * @className CustomerGradeRequest + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CustomerGradeRequest { + + @McpParameter(description = "고객 이름 (예: 김신한)", required = true) + private String customerName; + + @McpParameter(description = "고객 식별 번호 (CID)") + private String customerId; +} \ No newline at end of file diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/LeaveCountRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/LeaveCountRequest.java new file mode 100644 index 000000000..2fdb449c9 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/LeaveCountRequest.java @@ -0,0 +1,34 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + +import lombok.Builder; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import io.shinhanlife.dap.lib.annotation.McpParameter; +import lombok.Getter; +import lombok.Setter; + +/** + * @package io.shinhanlife.dap.mcc.dto + * @className LeaveCountRequest + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class LeaveCountRequest { + + @McpParameter(description = "연차 내역을 조회할 사원 번호", required = true) + private String employeeId; +} \ No newline at end of file diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MciSampleStringResponse.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MciSampleStringResponse.java new file mode 100644 index 000000000..441b10929 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MciSampleStringResponse.java @@ -0,0 +1,32 @@ +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 targetList; +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MciSampleTargetDto.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MciSampleTargetDto.java new file mode 100644 index 000000000..6c11d39c0 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MciSampleTargetDto.java @@ -0,0 +1,20 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + +import io.shinhanlife.glow.GlowMciFieldInfo; +import lombok.Data; + +/** + * @package io.shinhanlife.dap.mcc.dto + * @className MciSampleTargetDto + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + */ +@Data +public class MciSampleTargetDto { + @GlowMciFieldInfo(order = 1, length = 5, description = "항목 코드") + private String itemCode; + + @GlowMciFieldInfo(order = 2, length = 5, description = "항목 값") + private String itemValue; +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MetaCommonCodeRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MetaCommonCodeRequest.java new file mode 100644 index 000000000..a28a3afee --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MetaCommonCodeRequest.java @@ -0,0 +1,32 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.shinhanlife.dap.lib.annotation.McpParameter; +import lombok.Data; + +/** + * @package io.shinhanlife.dap.mcc.biz.cmm.dto + * @className MetaCommonCodeRequest + * @description AX HUB 시스템 처리 클래스 + * @author 09863409 + * @create 2026.07.29 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.07.29  09863409    최초생성
+ * 
+ * 
+ */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class MetaCommonCodeRequest { + @McpParameter(description = "통합코드 그룹 ID (예: GRP_SYS_01, GRP_COMM_CD)", required = false) + private String groupCode; + + @McpParameter(description = "코드명 검색 키워드 (예: 사용, 상태)", required = false) + private String codeName; + + @McpParameter(description = "사용여부 (예: Y, N)", required = false) + private String useYn; +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MetaCommonCodeResponse.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MetaCommonCodeResponse.java new file mode 100644 index 000000000..653a32a24 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MetaCommonCodeResponse.java @@ -0,0 +1,37 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +import java.util.List; + +/** + * @package io.shinhanlife.dap.mcc.biz.cmm.dto + * @className MetaCommonCodeResponse + * @description AX HUB 시스템 처리 클래스 + * @author 09863409 + * @create 2026.07.29 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.07.29  09863409    최초생성
+ * 
+ * 
+ */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class MetaCommonCodeResponse { + private List codeList; + + @Data + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class MetaCommonCodeItem { + private String groupCode; + private String code; + private String codeName; + private String codeDesc; + private Integer sortSeq; + private String useYn; + } +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MetaTableRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MetaTableRequest.java new file mode 100644 index 000000000..1712fdec4 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MetaTableRequest.java @@ -0,0 +1,32 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.shinhanlife.dap.lib.annotation.McpParameter; +import lombok.Data; + +/** + * @package io.shinhanlife.dap.mcc.biz.cmm.dto + * @className MetaTableRequest + * @description AX HUB 시스템 처리 클래스 + * @author 09863409 + * @create 2026.07.29 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.07.29  09863409    최초생성
+ * 
+ * 
+ */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class MetaTableRequest { + @McpParameter(description = "테이블 물리명 키워드 (예: TB_CUST_BAS, TB_CONT)", required = false) + private String tableName; + + @McpParameter(description = "테이블 논리명(한글) 키워드 (예: 고객기본, 계약)", required = false) + private String tableLogicalName; + + @McpParameter(description = "스키마/소유자명 (예: DAPADM, SHLOWN)", required = false) + private String owner; +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MetaTableResponse.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MetaTableResponse.java new file mode 100644 index 000000000..1b9eaf6bc --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MetaTableResponse.java @@ -0,0 +1,37 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +import java.util.List; + +/** + * @package io.shinhanlife.dap.mcc.biz.cmm.dto + * @className MetaTableResponse + * @description AX HUB 시스템 처리 클래스 + * @author 09863409 + * @create 2026.07.29 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.07.29  09863409    최초생성
+ * 
+ * 
+ */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class MetaTableResponse { + private List tableList; + + @Data + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class MetaTableItem { + private String owner; + private String tableName; + private String tableLogicalName; + private String tableDesc; + private Integer columnCount; + private Long rowCount; + } +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/SampleStringRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/SampleStringRequest.java new file mode 100644 index 000000000..ef77b77c2 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/SampleStringRequest.java @@ -0,0 +1,24 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +/** + * @package io.shinhanlife.dap.mcc.dto + * @className SampleStringRequest + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SampleStringRequest { + private String query; +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/SampleStringResponse.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/SampleStringResponse.java new file mode 100644 index 000000000..ab1810d73 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/SampleStringResponse.java @@ -0,0 +1,38 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.shinhanlife.glow.communication.annotation.GlowTrgmField; +import lombok.Data; + +/** + * @package io.shinhanlife.dap.mcc.dto + * @className SampleStringResponse + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SampleStringResponse { + @GlowTrgmField(order = 1, length = 10, description = "이름") + private String name; + + @GlowTrgmField(order = 2, length = 3, description = "나이") + private int age; + + @GlowTrgmField(order = 3, length = 8, description = "가입일자(YYYYMMDD)") + private String joinDate; + + @GlowTrgmField(order = 4, length = 2, description = "상태코드") + private String statusCode; + + @GlowTrgmField(order = 5, length = 10, description = "타겟") + private String target; +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/TemplateDownloadRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/TemplateDownloadRequest.java new file mode 100644 index 000000000..21ae5d343 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/TemplateDownloadRequest.java @@ -0,0 +1,35 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * @package io.shinhanlife.dap.mcc.dto + * @className TemplateDownloadRequest + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@Builder +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class TemplateDownloadRequest { + + /** + * 다운로드할 템플릿의 종류 ID (예: CUSTOMER_EXCEL, PRODUCT_PDF 등) + */ + private String templateId; +} \ No newline at end of file diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/VacationRegisterRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/VacationRegisterRequest.java new file mode 100644 index 000000000..bde48595e --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/VacationRegisterRequest.java @@ -0,0 +1,40 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + +import lombok.Builder; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import io.shinhanlife.dap.lib.annotation.McpParameter; +import io.shinhanlife.dap.lib.annotation.McpValidation; +import lombok.Getter; +import lombok.Setter; + +/** + * @package io.shinhanlife.dap.mcc.dto + * @className VacationRegisterRequest + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class VacationRegisterRequest { + + @McpParameter(description = "연차를 등록할 사원 번호", required = true) + @McpValidation(pattern = "\\S") + private String employeeId; + + @McpParameter(description = "휴가 일자 (YYYY-MM-DD 형식)", required = true) + @McpValidation(pattern = "^\\d{4}-\\d{2}-\\d{2}$") + private String date; +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BalanceUseCase.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BalanceUseCase.java new file mode 100644 index 000000000..1966f9df1 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BalanceUseCase.java @@ -0,0 +1,15 @@ +package io.shinhanlife.dap.mcc.biz.cmm.usecase; + +import io.shinhanlife.dap.lib.annotation.McpTool; +import io.shinhanlife.dap.lib.annotation.McpFunction; +import io.shinhanlife.dap.mcc.biz.cmm.dto.*; + +@McpTool(routingType = "MCI", categoryKey = "cmm") +public interface BalanceUseCase { + @McpFunction(register = false, displayName = "balance 툴", name = "oth.cmm.balance.inquiry", + description = "고객의 계좌 잔액을 조회합니다.", + prompt = "고객 계좌 잔액을 조회해줘.", + mappingId = "ACC_001" + ) + Object execute(BalanceRequest req); +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BillingProcessUseCase.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BillingProcessUseCase.java new file mode 100644 index 000000000..3071df0c9 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BillingProcessUseCase.java @@ -0,0 +1,16 @@ +package io.shinhanlife.dap.mcc.biz.cmm.usecase; + +import io.shinhanlife.dap.lib.annotation.McpTool; +import io.shinhanlife.dap.lib.annotation.McpFunction; +import io.shinhanlife.dap.mcc.biz.cmm.dto.*; + +@McpTool( + routingType = "MCI", + categoryKey = "cmm" +) +public interface BillingProcessUseCase { + Object getStatus(BillingStatusRequest req); + + @McpFunction(register = false, displayName = "process 툴", name = "oth.cmm.billing.process", description = "청구 처리", prompt = "현재 접수된 청구건에 대한 심사 처리를 진행해.", mappingId = "BILL_002") + Object processBilling(BillingProcessRequest data); +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BondIssueUseCase.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BondIssueUseCase.java new file mode 100644 index 000000000..1290b73fa --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BondIssueUseCase.java @@ -0,0 +1,16 @@ +package io.shinhanlife.dap.mcc.biz.cmm.usecase; + +import io.shinhanlife.dap.lib.annotation.McpTool; +import io.shinhanlife.dap.lib.annotation.McpFunction; +import io.shinhanlife.dap.mcc.biz.cmm.dto.*; + +@McpTool( + routingType = "EAI", + categoryKey = "cmm" +) +public interface BondIssueUseCase { + Object check(BondCheckRequest req); + + @McpFunction(displayName = "issue 툴", name = "oth.cmm.bond.issue", register = false, description = "증권 발행 테스트1", prompt = "디지털 증권 발행 프로세스를 실행해.", mappingId = "BOND_002") + Object issue(BondIssueRequest data); +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java new file mode 100644 index 000000000..8e0dea3f2 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java @@ -0,0 +1,26 @@ +package io.shinhanlife.dap.mcc.biz.cmm.usecase; + +import io.shinhanlife.dap.lib.annotation.McpFunction; +import io.shinhanlife.dap.lib.annotation.McpTool; +import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequest; +import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse; + +@McpTool( + routingType = "DIRECT", + categoryKey = "cmm" +) +public interface ClaimSearchSchemaSampleUseCase { + + @McpFunction( + register = false, + displayName = "Claim search JSON Schema sample", + name = "oth.cmm.claim.search", + description = "Claim search Tool sample using input and output JSON Schema resources.", + prompt = "Search an insurance claim by claim number or contract number.", + inputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-input-schema.json", + outputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-output-schema.json", + readOnlyHint = true, + idempotentHint = true + ) + ClaimSearchResponse search(ClaimSearchRequest request); +} \ No newline at end of file diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CommonUtilityUseCase.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CommonUtilityUseCase.java new file mode 100644 index 000000000..200534b56 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CommonUtilityUseCase.java @@ -0,0 +1,19 @@ +package io.shinhanlife.dap.mcc.biz.cmm.usecase; + +import io.shinhanlife.dap.lib.annotation.McpTool; +import io.shinhanlife.dap.lib.annotation.McpFunction; +import io.shinhanlife.dap.mcc.biz.cmm.dto.*; + +@McpTool( + routingType = "HTTP", + categoryKey = "cmm" +) +public interface CommonUtilityUseCase { + Object registerVacation(VacationRegisterRequest req); + + @McpFunction(register = false, displayName = "get_leave_count 툴", name = "oth.cmm.leave.count", description = "연차 갯수 조회", prompt = "현재 사용 가능한 남은 연차 일수를 알려줘.", mappingId = "HR_VAC_02") + Object getLeaveCount(LeaveCountRequest data); + + @McpFunction(register = false, displayName = "secret_tool 툴", name = "oth.cmm.secret.execute", description = "비공개 툴 테스트", prompt = "숨겨진 툴 강제 호출", mappingId = "SECRET_001", visible = false) + Object secretTool(LeaveCountRequest data); +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ContractInquiryUseCase.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ContractInquiryUseCase.java new file mode 100644 index 000000000..87dfcdd3c --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ContractInquiryUseCase.java @@ -0,0 +1,16 @@ +package io.shinhanlife.dap.mcc.biz.cmm.usecase; + +import io.shinhanlife.dap.lib.annotation.McpTool; +import io.shinhanlife.dap.lib.annotation.McpFunction; +import io.shinhanlife.dap.mcc.biz.cmm.dto.*; + +@McpTool( + routingType = "HTTP", + categoryKey = "cmm" +) +public interface ContractInquiryUseCase { + Object getStatus(ContractStatusRequest req); + + @McpFunction(register = false, displayName = "contract_detail 툴", name = "oth.cmm.contract.detail", description = "계약상세 조회", prompt = "김신한 고객의 계약 상세 내역을 알려줘.", mappingId = "CNTR_002") + Object getDetail(ContractDetailRequest data); +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CustomerInfoUseCase.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CustomerInfoUseCase.java new file mode 100644 index 000000000..faf82c0ad --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CustomerInfoUseCase.java @@ -0,0 +1,16 @@ +package io.shinhanlife.dap.mcc.biz.cmm.usecase; + +import io.shinhanlife.dap.lib.annotation.McpTool; +import io.shinhanlife.dap.lib.annotation.McpFunction; +import io.shinhanlife.dap.mcc.biz.cmm.dto.*; + +@McpTool( + routingType = "TCP", + categoryKey = "cmm" +) +public interface CustomerInfoUseCase { + Object getGrade(CustomerGradeRequest req); + + @McpFunction(register = false, displayName = "detail 툴", name = "oth.cmm.customer.detail", description = "고객상세 정보 조회", prompt = "이 고객의 상세 기본정보(주소, 연락처 등)를 알려줘.", mappingId = "CRM_002") + Object getDetail(CustomerDetailRequest data); +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaCommonCodeUseCase.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaCommonCodeUseCase.java new file mode 100644 index 000000000..dd5d7ddcb --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaCommonCodeUseCase.java @@ -0,0 +1,37 @@ +package io.shinhanlife.dap.mcc.biz.cmm.usecase; + +import io.shinhanlife.dap.lib.annotation.McpFunction; +import io.shinhanlife.dap.lib.annotation.McpTool; +import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeRequest; + +/** + * @package io.shinhanlife.dap.mcc.biz.cmm.usecase + * @className MetaCommonCodeUseCase + * @description AX HUB 시스템 처리 클래스 + * @author 09863409 + * @create 2026.07.29 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.07.29  09863409    최초생성
+ * 
+ * 
+ */ +@McpTool( + routingType = "MCI", + categoryKey = "cmm" +) +public interface MetaCommonCodeUseCase { + @McpFunction( + displayName = "메타 통합코드 조회 툴", + name = "oth.cmm.common-code.lookup", + description = "메타 통합코드 목록을 조회해줘", + prompt = "메타 통합코드 목록을 조회해줘", + mappingId = "CLCNNB00001", + register = false, + requiresApproval = false, + openWorldHint = true + ) + Object execute(MetaCommonCodeRequest req); +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaTableUseCase.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaTableUseCase.java new file mode 100644 index 000000000..0d7616c93 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaTableUseCase.java @@ -0,0 +1,37 @@ +package io.shinhanlife.dap.mcc.biz.cmm.usecase; + +import io.shinhanlife.dap.lib.annotation.McpFunction; +import io.shinhanlife.dap.lib.annotation.McpTool; +import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaTableRequest; + +/** + * @package io.shinhanlife.dap.mcc.biz.cmm.usecase + * @className MetaTableUseCase + * @description AX HUB 시스템 처리 클래스 + * @author 09863409 + * @create 2026.07.29 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.07.29  09863409    최초생성
+ * 
+ * 
+ */ +@McpTool( + routingType = "MCI", + categoryKey = "cmm" +) +public interface MetaTableUseCase { + @McpFunction( + displayName = "메타 테이블 조회 툴", + name = "oth.cmm.meta.table", + description = "메타 테이블 정보 목록을 조회해줘", + prompt = "메타 테이블 정보 목록을 조회해줘", + mappingId = "CLCNNB00001", + register = false, + requiresApproval = false, + openWorldHint = true + ) + Object execute(MetaTableRequest req); +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/TemplateUtilityUseCase.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/TemplateUtilityUseCase.java new file mode 100644 index 000000000..1612921c1 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/TemplateUtilityUseCase.java @@ -0,0 +1,21 @@ +package io.shinhanlife.dap.mcc.biz.cmm.usecase; + +import io.shinhanlife.dap.lib.annotation.McpTool; +import io.shinhanlife.dap.lib.annotation.McpFunction; +import io.shinhanlife.dap.mcc.biz.cmm.dto.*; + +import java.util.Map; + +@McpTool( + routingType = "HTTP", + categoryKey = "cmm" +) +public interface TemplateUtilityUseCase { + @McpFunction( + displayName = "템플릿 유틸리티", + name = "oth.cmm.template.url", + description = "요청한 템플릿(엑셀, 워드 등) 파일(양식)을 다운로드 받을 수 있는 시스템 URL을 반환합니다. AI는 이 URL을 사용자에게 마크다운 링크 형태로 제공해야 합니다.", + prompt = "요청하신 템플릿 양식 파일 다운로드 URL은 다음과 같습니다. 클릭하여 다운로드하세요:" + ) + Map getTemplateFileUrl(TemplateDownloadRequest req); +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/BalanceUseCaseImpl.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/BalanceUseCaseImpl.java new file mode 100644 index 000000000..0c49a90af --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/BalanceUseCaseImpl.java @@ -0,0 +1,48 @@ +package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl; + + +/** + * @package io.shinhanlife.dap.mcc.service + * @className BalanceService + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +import io.shinhanlife.dap.mcc.biz.cmm.usecase.BalanceUseCase; +import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase; +import io.shinhanlife.dap.mcc.biz.cmm.dto.BalanceRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Slf4j +@Service +public class BalanceUseCaseImpl extends AbstractMcpToolUseCase implements BalanceUseCase { + + + @Override + public Object execute(BalanceRequest req) { + log.info("[Balance] 계좌 잔액 조회 요청 수신. 계좌번호: {}", req.getAccountNumber()); + + // 레거시 연동 + Map result = executeLegacy("MCI", "ACC_001", req); + + // 가짜 데이터 응답 매핑 (AI 에이전트가 그럴듯하게 보여주기 위함) + if ("SUCCESS".equals(result.get("status"))) { + result.put("accountNumber", req.getAccountNumber()); + result.put("balance", 1520300); // 1,520,300원 (가상의 잔액) + result.put("currency", "KRW"); + result.put("message", "잔액 조회가 완료되었습니다."); + } + + return result; + } +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/BillingProcessUseCaseImpl.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/BillingProcessUseCaseImpl.java new file mode 100644 index 000000000..bf81db59e --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/BillingProcessUseCaseImpl.java @@ -0,0 +1,68 @@ +package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.shinhanlife.dap.mcc.biz.cmm.usecase.BillingProcessUseCase; +import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase; +import org.springframework.stereotype.Service; +import io.shinhanlife.dap.mcc.biz.cmm.dto.BillingProcessRequest; +import io.shinhanlife.dap.mcc.biz.cmm.dto.BillingStatusRequest; + +import java.util.HashMap; +import java.util.Map; + + +/** + * @package io.shinhanlife.dap.mcc.service + * @className BillingProcessService + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@lombok.extern.slf4j.Slf4j +@Service +public class BillingProcessUseCaseImpl extends AbstractMcpToolUseCase implements BillingProcessUseCase { + + + @Override + public Object getStatus(BillingStatusRequest data) { + return executeBillingLogic("BILL_001", data); + } + + @Override + public Object processBilling(BillingProcessRequest data) { + return executeBillingLogic("BILL_002", data); + } + + private Object executeBillingLogic(String mappingId, Object data) { + log.info(" [Billing] 청구 처리 전용 커스텀 전/후처리 로직 수행 시작"); + + Map payload; + if (data == null) { + payload = new HashMap<>(); + } else { + ObjectMapper mapper = new ObjectMapper(); + payload = mapper.convertValue(data, new TypeReference>() {}); + } + + // 커스텀 전처리 + payload.put("custom_injected_data", "Billing System Check OK"); + log.info(" [Billing] 커스텀 파라미터 주입 완료"); + + // 부모 클래스의 레거시 공통 연동 메서드 호출 (PII 마스킹 포함) + Map result = executeLegacy("MCI", mappingId, payload); + + // 커스텀 후처리 + if ("SUCCESS".equals(result.get("status"))) { + result.put("billing_custom_insight", "청구 특화 후처리 로직이 적용되었습니다."); + } + return result; + } +} \ No newline at end of file diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/BondIssueUseCaseImpl.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/BondIssueUseCaseImpl.java new file mode 100644 index 000000000..9851714bd --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/BondIssueUseCaseImpl.java @@ -0,0 +1,37 @@ +package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl; + +import io.shinhanlife.dap.mcc.biz.cmm.usecase.BondIssueUseCase; +import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase; +import org.springframework.stereotype.Service; +import io.shinhanlife.dap.mcc.biz.cmm.dto.BondCheckRequest; +import io.shinhanlife.dap.mcc.biz.cmm.dto.BondIssueRequest; + + +/** + * @package io.shinhanlife.dap.mcc.service + * @className BondIssueService + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Service +public class BondIssueUseCaseImpl extends AbstractMcpToolUseCase implements BondIssueUseCase { + + + @Override + public Object check(BondCheckRequest data) { + return executeLegacy("EAI", "BOND_001", data); + } + + @Override + public Object issue(BondIssueRequest data) { + return executeLegacy("EAI", "BOND_002", data); + } +} \ No newline at end of file diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchSchemaSampleUseCaseImpl.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchSchemaSampleUseCaseImpl.java new file mode 100644 index 000000000..1a817f598 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchSchemaSampleUseCaseImpl.java @@ -0,0 +1,36 @@ +package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl; + +import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequest; +import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse; +import io.shinhanlife.dap.mcc.biz.cmm.usecase.ClaimSearchSchemaSampleUseCase; +import java.util.List; +import org.springframework.stereotype.Service; + +/** + * Non-exposed sample Tool. It does not call MCI or EIMS. + */ +@Service +public class ClaimSearchSchemaSampleUseCaseImpl implements ClaimSearchSchemaSampleUseCase { + + @Override + public ClaimSearchResponse search(ClaimSearchRequest request) { + ClaimSearchResponse.ClaimSummary item = ClaimSearchResponse.ClaimSummary.builder() + .status("REVIEWING") + .statusLabel("Under review") + .receivedDate("2026-08-04") + .approvedAmount(null) + .build(); + + return ClaimSearchResponse.builder() + .resultCode("SUCCESS") + .resultLabel("Success") + .status("REVIEWING") + .statusLabel("Under review") + .approvedAmount(null) + .rejectionReason(null) + .items(List.of(item)) + .hasMore(false) + .totalCount(1) + .build(); + } +} \ No newline at end of file diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/CommonUtilityUseCaseImpl.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/CommonUtilityUseCaseImpl.java new file mode 100644 index 000000000..56a600a31 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/CommonUtilityUseCaseImpl.java @@ -0,0 +1,43 @@ +package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl; + +import io.shinhanlife.dap.mcc.biz.cmm.usecase.CommonUtilityUseCase; +import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase; +import org.springframework.stereotype.Service; +import io.shinhanlife.dap.mcc.biz.cmm.dto.LeaveCountRequest; +import io.shinhanlife.dap.mcc.biz.cmm.dto.VacationRegisterRequest; + + +/** + * @package io.shinhanlife.dap.mcc.service + * @className CommonUtilityService + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Service +public class CommonUtilityUseCaseImpl extends AbstractMcpToolUseCase implements CommonUtilityUseCase { + + + @Override + public Object registerVacation(VacationRegisterRequest data) { + return executeLegacy("HTTP", "HR_VAC_01", data); + } + + @Override + public Object getLeaveCount(LeaveCountRequest data) { + return executeLegacy("HTTP", "HR_VAC_02", data); + } + + @Override + public Object secretTool(LeaveCountRequest data) { + return executeLegacy("HTTP", "SECRET_001", data); + } + +} \ No newline at end of file diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ContractInquiryUseCaseImpl.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ContractInquiryUseCaseImpl.java new file mode 100644 index 000000000..403f1556c --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ContractInquiryUseCaseImpl.java @@ -0,0 +1,37 @@ +package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl; + +import io.shinhanlife.dap.mcc.biz.cmm.usecase.ContractInquiryUseCase; +import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase; +import org.springframework.stereotype.Service; +import io.shinhanlife.dap.mcc.biz.cmm.dto.ContractDetailRequest; +import io.shinhanlife.dap.mcc.biz.cmm.dto.ContractStatusRequest; + + +/** + * @package io.shinhanlife.dap.mcc.service + * @className ContractInquiryService + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Service +public class ContractInquiryUseCaseImpl extends AbstractMcpToolUseCase implements ContractInquiryUseCase { + + + @Override + public Object getStatus(ContractStatusRequest data) { + return executeLegacy("HTTP", "CNTR_001", data); + } + + @Override + public Object getDetail(ContractDetailRequest data) { + return executeLegacy("HTTP", "CNTR_002", data); + } +} \ No newline at end of file diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/CustomerInfoUseCaseImpl.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/CustomerInfoUseCaseImpl.java new file mode 100644 index 000000000..cb06f6238 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/CustomerInfoUseCaseImpl.java @@ -0,0 +1,37 @@ +package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl; + +import io.shinhanlife.dap.mcc.biz.cmm.usecase.CustomerInfoUseCase; +import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase; +import org.springframework.stereotype.Service; +import io.shinhanlife.dap.mcc.biz.cmm.dto.CustomerDetailRequest; +import io.shinhanlife.dap.mcc.biz.cmm.dto.CustomerGradeRequest; + + +/** + * @package io.shinhanlife.dap.mcc.service + * @className CustomerInfoService + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Service +public class CustomerInfoUseCaseImpl extends AbstractMcpToolUseCase implements CustomerInfoUseCase { + + + @Override + public Object getGrade(CustomerGradeRequest req) { + return executeLegacy("TCP", "CRM_001", req); + } + + @Override + public Object getDetail(CustomerDetailRequest data) { + return executeLegacy("TCP", "CRM_002", data); + } +} \ No newline at end of file diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/MetaCommonCodeUseCaseImpl.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/MetaCommonCodeUseCaseImpl.java new file mode 100644 index 000000000..92ddf781a --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/MetaCommonCodeUseCaseImpl.java @@ -0,0 +1,89 @@ +package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl; + +import io.shinhanlife.dap.mcc.biz.cmm.converter.MetaCommonCodeConverter; +import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeRequest; +import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeResponse; +import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeResponse.MetaCommonCodeItem; +import io.shinhanlife.dap.mcc.biz.cmm.usecase.MetaCommonCodeUseCase; +import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.MciCfpaClient; +import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_I; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * @package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl + * @className MetaCommonCodeUseCaseImpl + * @description AX HUB 시스템 처리 클래스 + * @author 09863409 + * @create 2026.07.29 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.07.29  09863409    최초생성
+ * 
+ * 
+ */ +@Slf4j +@Service +@RequiredArgsConstructor +public class MetaCommonCodeUseCaseImpl implements MetaCommonCodeUseCase { + + private final MciCfpaClient mci; + private final MetaCommonCodeConverter converter; + + @Override + public Object execute(MetaCommonCodeRequest req) { + log.info("[MCI Tool] {} 요청 수신. 파라미터: {}", "metaCommonCode", req); + try { + CLCNNB00001_I mciRequest = converter.toLegacyRequest(req); + + Object mciResponse = mci.callCfpa0001(mciRequest); + log.info("[MCI Tool] CLCNNB00001 MCI call completed. Returning response status: {}", + mciResponse != null); + + // 현업 테스트용으로 MCI 통신을 바이패스하고 하드코딩된 메타 통합코드 샘플 결과를 반환합니다. + MetaCommonCodeResponse res = new MetaCommonCodeResponse(); + List list = new ArrayList<>(); + + MetaCommonCodeItem item1 = new MetaCommonCodeItem(); + item1.setGroupCode(req.getGroupCode() != null ? req.getGroupCode() : "GRP_COMM_CD"); + item1.setCode("CD001"); + item1.setCodeName("진행중"); + item1.setCodeDesc("SR 요청 처리 진행 중 상태"); + item1.setSortSeq(1); + item1.setUseYn("Y"); + list.add(item1); + + MetaCommonCodeItem item2 = new MetaCommonCodeItem(); + item2.setGroupCode(req.getGroupCode() != null ? req.getGroupCode() : "GRP_COMM_CD"); + item2.setCode("CD002"); + item2.setCodeName("완료"); + item2.setCodeDesc("SR 요청 처리 완료 상태"); + item2.setSortSeq(2); + item2.setUseYn("Y"); + list.add(item2); + + MetaCommonCodeItem item3 = new MetaCommonCodeItem(); + item3.setGroupCode(req.getGroupCode() != null ? req.getGroupCode() : "GRP_COMM_CD"); + item3.setCode("CD003"); + item3.setCodeName("보류"); + item3.setCodeDesc("SR 요청 처리 일시 보류 상태"); + item3.setSortSeq(3); + item3.setUseYn("N"); + list.add(item3); + + res.setCodeList(list); + + return res; + } catch (Exception e) { + log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e); + return Map.of("status", "ERROR", "message", e.getMessage() != null ? e.getMessage() : "Unknown error"); + } + } +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/MetaTableUseCaseImpl.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/MetaTableUseCaseImpl.java new file mode 100644 index 000000000..ef981898e --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/MetaTableUseCaseImpl.java @@ -0,0 +1,89 @@ +package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl; + +import io.shinhanlife.dap.mcc.biz.cmm.converter.MetaTableConverter; +import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaTableRequest; +import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaTableResponse; +import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaTableResponse.MetaTableItem; +import io.shinhanlife.dap.mcc.biz.cmm.usecase.MetaTableUseCase; +import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.MciCfpaClient; +import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_I; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * @package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl + * @className MetaTableUseCaseImpl + * @description AX HUB 시스템 처리 클래스 + * @author 09863409 + * @create 2026.07.29 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.07.29  09863409    최초생성
+ * 
+ * 
+ */ +@Slf4j +@Service +@RequiredArgsConstructor +public class MetaTableUseCaseImpl implements MetaTableUseCase { + + private final MciCfpaClient mci; + private final MetaTableConverter converter; + + @Override + public Object execute(MetaTableRequest req) { + log.info("[MCI Tool] {} 요청 수신. 파라미터: {}", "metaTable", req); + try { + CLCNNB00001_I mciRequest = converter.toLegacyRequest(req); + + Object mciResponse = mci.callCfpa0001(mciRequest); + log.info("[MCI Tool] CLCNNB00001 MCI call completed. Returning response status: {}", + mciResponse != null); + + // 현업 테스트용으로 MCI 통신을 바이패스하고 하드코딩된 메타 테이블 샘플 결과를 반환합니다. + MetaTableResponse res = new MetaTableResponse(); + List list = new ArrayList<>(); + + MetaTableItem item1 = new MetaTableItem(); + item1.setOwner(req.getOwner() != null ? req.getOwner() : "DAPADM"); + item1.setTableName(req.getTableName() != null ? req.getTableName() : "TB_CUST_BAS"); + item1.setTableLogicalName("고객기본정보"); + item1.setTableDesc("고객 기본 프로필 및 인적사항 관리 테이블"); + item1.setColumnCount(35); + item1.setRowCount(1250000L); + list.add(item1); + + MetaTableItem item2 = new MetaTableItem(); + item2.setOwner(req.getOwner() != null ? req.getOwner() : "DAPADM"); + item2.setTableName("TB_CONT_MCD"); + item2.setTableLogicalName("계약주계약정보"); + item2.setTableDesc("보험 계약 주계약 상세 원장 테이블"); + item2.setColumnCount(58); + item2.setRowCount(3400000L); + list.add(item2); + + MetaTableItem item3 = new MetaTableItem(); + item3.setOwner(req.getOwner() != null ? req.getOwner() : "DAPADM"); + item3.setTableName("TB_CLAIM_DTL"); + item3.setTableLogicalName("청구접수상세"); + item3.setTableDesc("보험금 청구 접수 건별 내역 테이블"); + item3.setColumnCount(42); + item3.setRowCount(890000L); + list.add(item3); + + res.setTableList(list); + + return res; + } catch (Exception e) { + log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e); + return Map.of("status", "ERROR", "message", e.getMessage() != null ? e.getMessage() : "Unknown error"); + } + } +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/TemplateUtilityUseCaseImpl.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/TemplateUtilityUseCaseImpl.java new file mode 100644 index 000000000..42a9505b2 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/TemplateUtilityUseCaseImpl.java @@ -0,0 +1,58 @@ +package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl; + +import io.shinhanlife.dap.mcc.biz.cmm.usecase.TemplateUtilityUseCase; +import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase; +import io.shinhanlife.dap.mcc.biz.cmm.dto.TemplateDownloadRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Service + +/** + * @package io.shinhanlife.dap.mcc.service + * @className TemplateUtilityService + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +public class TemplateUtilityUseCaseImpl extends AbstractMcpToolUseCase implements TemplateUtilityUseCase { + + @Override + public Map getTemplateFileUrl(TemplateDownloadRequest data) { + try { + String templateId = (data != null && data.getTemplateId() != null) ? data.getTemplateId().toLowerCase() : "default"; + log.info("MCP 툴 호출됨: get_template_file_url, 요청 템플릿 ID: {}", templateId); + + String fileName = "sample_" + templateId + ".xlsx"; + String downloadUrl = "https://axhub-file-server.shinhanlife.io/downloads/" + fileName; + + Map result = new HashMap<>(); + result.put("status", "success"); + + Map contract = new HashMap<>(); + contract.put("fileName", fileName); + contract.put("downloadUrl", downloadUrl); + contract.put("message", "다운로드 링크가 성공적으로 생성되었습니다. AI는 이 링크를 마크다운 형식으로 사용자에게 전달해야 합니다."); + contract.put("status", "success"); + + result.put("contracts", Collections.singletonList(contract)); + + return result; + } catch (Exception e) { + log.error("getTemplateFileUrl 내부 예외 발생", e); + throw new RuntimeException("템플릿 URL 생성 실패", e); + } + } +} \ No newline at end of file diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/converter/Onnba3011Converter.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/converter/Onnba3011Converter.java new file mode 100644 index 000000000..97c6820cb --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/converter/Onnba3011Converter.java @@ -0,0 +1,29 @@ +package io.shinhanlife.dap.mcc.biz.oth.converter; + +import io.shinhanlife.dap.mcc.biz.oth.dto.Onnba3011Request; +import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_I; +import org.mapstruct.Mapper; + +/** + * @package io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.converter + * @className Onnba3011Converter + * @description Converts ONNBA tool input into the CLCNNB00001 MCI payload + * @author 0986406 + * @create 2026.07.27 + *
+ * ---------- revision history ----------
+ * date       author    description
+ * ---------- --------- ---------------------------
+ * 2026.07.27 0986406    initial creation
+ * 
+ */ +@Mapper(componentModel = "spring") +public interface Onnba3011Converter { + + CLCNNB00001_I toMciRequest(Onnba3011Request source); + + CLCNNB00001_I.UnfcPrbuIrcoAdduDto toUnfcPrbuIrcoAddu( + Onnba3011Request.UnfcPrbuIrcoAdduDto source); + + CLCNNB00001_I.SucoIspaBasDto toSucoIspaBas(Onnba3011Request.SucoIspaBasDto source); +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/dto/Onnba3011Request.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/dto/Onnba3011Request.java new file mode 100644 index 000000000..7eafeaae3 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/dto/Onnba3011Request.java @@ -0,0 +1,119 @@ +package io.shinhanlife.dap.mcc.biz.oth.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import lombok.Data; + +/** + * @package io.shinhanlife.dap.mcc.dto + * @className Onnba3011ReqDto + * @description 보종By가입설계한도계산조회 MCI 요청 전문 (ONNBA3011_I) + * @author 0986406 + * @create 2026.09.01 + */ +@Data +public class Onnba3011Request { + + @JsonPropertyDescription("통합기계약보험 (유형: gs, 길이: 72)") + @JsonProperty("unfcPrbuIrcoAddu") + private UnfcPrbuIrcoAdduDto unfcPrbuIrcoAddu; + + @JsonPropertyDescription("처리구분코드 (길이: 1)") + @JsonProperty("dalScCd") + private String dalScCd; + + @JsonPropertyDescription("고객청약관계 (길이: 2)") + @JsonProperty("cstSucoRltyCd") + private String cstSucoRltyCd; + + @JsonPropertyDescription("고객번호 (길이: 12)") + @JsonProperty("csNo") + private String csNo; + + @JsonPropertyDescription("주민등록번호 (길이: 50)") + @JsonProperty("rdreNo") + private String rdreNo; + + @JsonPropertyDescription("통합급부계산 (길이: 1)") + @JsonProperty("unfcPvsCalReqYn") + private String unfcPvsCalReqYn; + + @JsonPropertyDescription("한국신용정보 (길이: 1)") + @JsonProperty("kcisPymmTnnrRequest") + private String kcisPymmTnnrRequest; + + @JsonPropertyDescription("한도초과여부 (길이: 1)") + @JsonProperty("lmovYn") + private String lmovYn; + + @JsonPropertyDescription("일반경유승인 (길이: 1)") + @JsonProperty("genPsthApvTrgtYn") + private String genPsthApvTrgtYn; + + @JsonPropertyDescription("보험사한도초과 (길이: 1)") + @JsonProperty("ircoLmovEcpbTrgtYn") + private String ircoLmovEcpbTrgtYn; + + @JsonPropertyDescription("진단계산여부 (길이: 1)") + @JsonProperty("digCalYn") + private String digCalYn; + + @JsonPropertyDescription("기계약포함진단 (길이: 1)") + @JsonProperty("prbuIciDigCalYn") + private String prbuIciDigCalYn; + + @JsonPropertyDescription("청약심사기본Dto (유형: gs, 길이: 2532)") + @JsonProperty("sucoIspaBasDto") + private SucoIspaBasDto sucoIspaBasDto; + + // ----- Nested DTO Classes ----- + + @Data + public static class UnfcPrbuIrcoAdduDto { + // 실제 필요한 하위 필드들 추가 (사진 생략부분) + } + + @Data + public static class SucoIspaBasDto { + + @JsonPropertyDescription("계약처리유형 (길이: 2)") + @JsonProperty("ccnDalTypCd") + private String ccnDalTypCd; + + @JsonPropertyDescription("신계약입력경로 (길이: 2)") + @JsonProperty("nwcnptCursCd") + private String nwcnptCursCd; + + @JsonPropertyDescription("개인단체계약 (길이: 2)") + @JsonProperty("induAsctScCd") + private String induAsctScCd; + + @JsonPropertyDescription("모집조직번호 (길이: 7)") + @JsonProperty("cepeOgnzNo") + private String cepeOgnzNo; + + @JsonPropertyDescription("모집자사번번호 (길이: 8)") + @JsonProperty("cepePrafNo") + private String cepePrafNo; + + @JsonPropertyDescription("수금조직번호 (길이: 7)") + @JsonProperty("clmoOgnzNo") + private String clmoOgnzNo; + + @JsonPropertyDescription("수금자사번번호 (길이: 8)") + @JsonProperty("clmoPrafNo") + private String clmoPrafNo; + + @JsonPropertyDescription("청약일자 (길이: 20)") + @JsonProperty("sucoYmd") + private String sucoYmd; + + @JsonPropertyDescription("발행일자 (길이: 20)") + @JsonProperty("ispDt") + private String ispDt; + + @JsonPropertyDescription("계약일자 (길이: 20)") + @JsonProperty("contYmd") + private String contYmd; + } +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/Onnba3011UseCase.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/Onnba3011UseCase.java new file mode 100644 index 000000000..b214d073a --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/Onnba3011UseCase.java @@ -0,0 +1,10 @@ +package io.shinhanlife.dap.mcc.biz.oth.usecase; + +import io.shinhanlife.dap.lib.annotation.McpTool; +import io.shinhanlife.dap.lib.annotation.McpFunction; +import io.shinhanlife.dap.mcc.biz.oth.dto.*; + +@McpTool(routingType = "MCI", categoryKey = "oth") +public interface Onnba3011UseCase { + Object callOnnba3011(Onnba3011Request req); +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/impl/Onnba3011UseCaseImpl.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/impl/Onnba3011UseCaseImpl.java new file mode 100644 index 000000000..c96edf38a --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/impl/Onnba3011UseCaseImpl.java @@ -0,0 +1,60 @@ +package io.shinhanlife.dap.mcc.biz.oth.usecase.impl; + +import io.shinhanlife.dap.mcc.biz.oth.converter.Onnba3011Converter; +import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.MciCfpaClient; +import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_I; +import io.shinhanlife.dap.mcc.biz.oth.usecase.Onnba3011UseCase; +import io.shinhanlife.dap.mcc.biz.oth.dto.Onnba3011Request; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * @package io.shinhanlife.dap.mcc.service + * @className OnnbaMciToolService + * @description 보종By가입설계한도계산조회 MCI 연동 툴 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +@Service +@RequiredArgsConstructor +public class Onnba3011UseCaseImpl implements Onnba3011UseCase { + + private static final String INTERFACE_CODE_3011 = "CLCNNB00001"; + + // 공통 MCI Client 주입 + private final MciCfpaClient mciCfpaClient; + private final Onnba3011Converter onnba3011Converter; + /** + * AI Agent가 호출하게 될 메서드입니다. + * @McpFunction 어노테이션 하나로 AI 도구로 자동 노출 및 라우팅됩니다. + */ + + @Override + public Object callOnnba3011(Onnba3011Request req) { + log.info("[MCI Tool] 보종By가입설계한도계산조회 요청 수신."); + + try { + // MciCfpaClient를 통한 호출 + CLCNNB00001_I mciRequest = onnba3011Converter.toMciRequest(req); + Object response = mciCfpaClient.callCfpa0001(mciRequest); + + log.info("[MCI Tool] Glow 기반 MCI 연동 성공."); + + // 결과 반환 + return response != null ? response : "{\"status\":\"SUCCESS\", \"message\":\"GlowMciComponent 통신 완료\"}"; + + } catch (Exception e) { + log.error("[MCI Tool] MCI 연동 중 오류 발생: {}", e.getMessage(), e); + return "{\"status\":\"ERROR\", \"message\":\"MCI 통신 실패: " + e.getMessage() + "\"}"; + } + } +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/DailyQuoteRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/DailyQuoteRequest.java new file mode 100644 index 000000000..e6baaf4e6 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/DailyQuoteRequest.java @@ -0,0 +1,3 @@ +package io.shinhanlife.dap.mcc.biz.smp.dto; + +public record DailyQuoteRequest(String category) {} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/DailyQuoteResponse.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/DailyQuoteResponse.java new file mode 100644 index 000000000..bb09b7161 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/DailyQuoteResponse.java @@ -0,0 +1,3 @@ +package io.shinhanlife.dap.mcc.biz.smp.dto; + +public record DailyQuoteResponse(String quote, String author) {} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/ExchangeRateRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/ExchangeRateRequest.java new file mode 100644 index 000000000..f013becfa --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/ExchangeRateRequest.java @@ -0,0 +1,3 @@ +package io.shinhanlife.dap.mcc.biz.smp.dto; + +public record ExchangeRateRequest(String currencyCode) {} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/ExchangeRateResponse.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/ExchangeRateResponse.java new file mode 100644 index 000000000..4968d01a3 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/ExchangeRateResponse.java @@ -0,0 +1,3 @@ +package io.shinhanlife.dap.mcc.biz.smp.dto; + +public record ExchangeRateResponse(String baseCurrency, String targetCurrency, double rate) {} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/TeamMemberRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/TeamMemberRequest.java new file mode 100644 index 000000000..c81ee0d79 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/TeamMemberRequest.java @@ -0,0 +1,26 @@ +package io.shinhanlife.dap.mcc.biz.smp.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.shinhanlife.dap.lib.annotation.McpParameter; +import lombok.Data; + +/** + * @package io.shinhanlife.dap.mcc.biz.smp.dto + * @className TeamMemberRequest + * @description AX HUB 시스템 처리 클래스 + * @author jade + * @create 2026.07.29 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.07.29  jade    최초생성
+ *
+ * 
+ */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class TeamMemberRequest { + @McpParameter(description = "조회할 팀 이름 (예: AX, MCP, TOOL, 전체 등)", required = false) + private String teamName; +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/TeamMemberResponse.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/TeamMemberResponse.java new file mode 100644 index 000000000..f10b8ace3 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/TeamMemberResponse.java @@ -0,0 +1,24 @@ +package io.shinhanlife.dap.mcc.biz.smp.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +/** + * @package io.shinhanlife.dap.mcc.biz.smp.dto + * @className TeamMemberResponse + * @description AX HUB 시스템 처리 클래스 + * @author jade + * @create 2026.07.29 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.07.29  jade    최초생성
+ *
+ * 
+ */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class TeamMemberResponse { + private String result; +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/WeatherRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/WeatherRequest.java new file mode 100644 index 000000000..e6e97ff2e --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/WeatherRequest.java @@ -0,0 +1,25 @@ +package io.shinhanlife.dap.mcc.biz.smp.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; + +/** + * @package io.shinhanlife.dap.mcc.dto + * @className WeatherRequest + * @description 기상 조회 요청 클래스 + * @author 0986406 + * @create 2026.07.14 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.07.14  0986406    최초생성
+ * 
+ * 
+ */ +public record WeatherRequest( + @JsonProperty(required = true, value = "city") + @JsonPropertyDescription("날씨를 조회할 도시 이름 (예: 서울, 부산, 제주)") + String city +) { +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/WeatherResponse.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/WeatherResponse.java new file mode 100644 index 000000000..4e1536818 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/WeatherResponse.java @@ -0,0 +1,24 @@ +package io.shinhanlife.dap.mcc.biz.smp.dto; + +/** + * @package io.shinhanlife.dap.mcc.dto + * @className WeatherResponse + * @description 기상 조회 응답 클래스 + * @author 0986406 + * @create 2026.07.14 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.07.14  0986406    최초생성
+ * 
+ * 
+ */ +public record WeatherResponse( + String city, + double temperature, + double windSpeed, + String reportTime, + String summary +) { +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/DailyQuoteToolUseCase.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/DailyQuoteToolUseCase.java new file mode 100644 index 000000000..7c7a88e01 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/DailyQuoteToolUseCase.java @@ -0,0 +1,19 @@ +package io.shinhanlife.dap.mcc.biz.smp.usecase; + +import io.shinhanlife.dap.lib.annotation.McpTool; +import io.shinhanlife.dap.lib.annotation.McpFunction; +import io.shinhanlife.dap.mcc.biz.smp.dto.DailyQuoteRequest; +import io.shinhanlife.dap.mcc.biz.smp.dto.DailyQuoteResponse; + +@McpTool( + routingType = "DIRECT", + categoryKey = "smp" +) +public interface DailyQuoteToolUseCase { + @McpFunction(register = false, displayName = "랜덤 명언 툴", name = "oth.smp.quote.daily", + description = "무작위로 영감을 주는 명언을 하나 가져옵니다.", + prompt = "오늘의 명언 하나 알려줘, 동기부여 명언 등", + mappingId = "QUOTE_001" + ) + DailyQuoteResponse execute(DailyQuoteRequest req); +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/ExchangeRateToolUseCase.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/ExchangeRateToolUseCase.java new file mode 100644 index 000000000..2515d6111 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/ExchangeRateToolUseCase.java @@ -0,0 +1,21 @@ +package io.shinhanlife.dap.mcc.biz.smp.usecase; + +import io.shinhanlife.dap.lib.annotation.McpTool; +import io.shinhanlife.dap.lib.annotation.McpFunction; +import io.shinhanlife.dap.mcc.biz.smp.dto.ExchangeRateRequest; +import io.shinhanlife.dap.mcc.biz.smp.dto.ExchangeRateResponse; +import io.shinhanlife.dap.mcc.biz.smp.dto.ExchangeRateRequest; +import io.shinhanlife.dap.mcc.biz.smp.dto.ExchangeRateResponse; + +@McpTool( + routingType = "DIRECT", + categoryKey = "smp" +) +public interface ExchangeRateToolUseCase { + @McpFunction(register = false, displayName = "실시간 환율 조회 툴", name = "oth.smp.exchange-rate.inquiry", + description = "원하는 통화의 실시간 환율을 조회합니다. (예: USD, EUR, JPY)", + prompt = "현재 달러 환율 알려줘, 엔화 환율은?", + mappingId = "EXCHANGE_001" + ) + ExchangeRateResponse execute(ExchangeRateRequest req); +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/TeamMemberUseCase.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/TeamMemberUseCase.java new file mode 100644 index 000000000..3296f1251 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/TeamMemberUseCase.java @@ -0,0 +1,23 @@ +package io.shinhanlife.dap.mcc.biz.smp.usecase; + +import io.shinhanlife.dap.lib.annotation.McpFunction; +import io.shinhanlife.dap.lib.annotation.McpTool; +import io.shinhanlife.dap.mcc.biz.smp.dto.TeamMemberRequest; + +@McpTool( + routingType = "DIRECT", + categoryKey = "smp" +) +public interface TeamMemberUseCase { + @McpFunction( + displayName = "신한라이프 MCP, TOOL 파트 구성원 조회", + name = "oth.smp.team.list", + description = "신한라이프 MCP, TOOL 파트 구성원을 조회합니다.", + prompt = "신한라이프 MCP, TOOL 파트 구성원을 조회해 줘. (주의: 응답 시 ** 등 마크다운 기호를 절대 사용하지 말고 평문으로만 출력해 줘)", + mappingId = "DIRECT0001", + register = false, + requiresApproval = false, + openWorldHint = true + ) + Object execute(TeamMemberRequest req); +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/WeatherToolUseCase.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/WeatherToolUseCase.java new file mode 100644 index 000000000..cd929b31f --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/WeatherToolUseCase.java @@ -0,0 +1,18 @@ +package io.shinhanlife.dap.mcc.biz.smp.usecase; + +import io.shinhanlife.dap.lib.annotation.McpTool; +import io.shinhanlife.dap.lib.annotation.McpFunction; +import io.shinhanlife.dap.mcc.biz.smp.dto.*; + +@McpTool( + routingType = "DIRECT", + categoryKey = "smp" +) +public interface WeatherToolUseCase { + @McpFunction(register = false, displayName = "날씨 조회 툴", name = "oth.smp.weather.inquiry", + description = "특정 도시의 현재 날씨, 온도, 풍속 정보를 조회합니다.", + prompt = "서울 날씨 알려줘, 부산 기온 알려줘 등 실시간 기상 조회", + mappingId = "WEATHER_001" + ) + WeatherResponse execute(WeatherRequest req); +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/impl/DailyQuoteToolUseCaseImpl.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/impl/DailyQuoteToolUseCaseImpl.java new file mode 100644 index 000000000..36059326c --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/impl/DailyQuoteToolUseCaseImpl.java @@ -0,0 +1,46 @@ +package io.shinhanlife.dap.mcc.biz.smp.usecase.impl; + +import io.shinhanlife.dap.mcc.biz.smp.dto.DailyQuoteRequest; +import io.shinhanlife.dap.mcc.biz.smp.dto.DailyQuoteResponse; +import io.shinhanlife.dap.mcc.biz.smp.usecase.DailyQuoteToolUseCase; +import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Random; + +/** + * @package io.shinhanlife.dap.mcc.service + * @className DailyQuoteToolService + * @description 랜덤 명언 제공 툴 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +@Service +public class DailyQuoteToolUseCaseImpl extends AbstractMcpToolUseCase implements DailyQuoteToolUseCase { + + private final List quotes = List.of( + new DailyQuoteResponse("성공은 매일 반복한 작은 노력들의 합이다.", "로버트 콜리어"), + new DailyQuoteResponse("시작이 반이다.", "아리스토텔레스"), + new DailyQuoteResponse("포기하지 않는 한 실패는 없다.", "알베르트 아인슈타인"), + new DailyQuoteResponse("가장 큰 위험은 위험 없는 삶이다.", "스티븐 코비") + ); + + @Override + public DailyQuoteResponse execute(DailyQuoteRequest req) { + int index = new Random().nextInt(quotes.size()); + DailyQuoteResponse selected = quotes.get(index); + + log.info("[DailyQuoteTool] 명언 제공 완료: {}", selected.author()); + return selected; + } +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/impl/ExchangeRateToolUseCaseImpl.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/impl/ExchangeRateToolUseCaseImpl.java new file mode 100644 index 000000000..10853b02d --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/impl/ExchangeRateToolUseCaseImpl.java @@ -0,0 +1,50 @@ +package io.shinhanlife.dap.mcc.biz.smp.usecase.impl; + +import io.shinhanlife.dap.mcc.biz.smp.dto.ExchangeRateRequest; +import io.shinhanlife.dap.mcc.biz.smp.dto.ExchangeRateResponse; +import io.shinhanlife.dap.mcc.biz.smp.usecase.ExchangeRateToolUseCase; +import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; + +/** + * @package io.shinhanlife.dap.mcc.service + * @className ExchangeRateToolService + * @description 실시간 환율 조회 툴 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +@Service +public class ExchangeRateToolUseCaseImpl extends AbstractMcpToolUseCase implements ExchangeRateToolUseCase { + + private final RestClient restClient; + + public ExchangeRateToolUseCaseImpl() { + this.restClient = RestClient.create(); + } + + @Override + public ExchangeRateResponse execute(ExchangeRateRequest req) { + String targetCurrency = req.currencyCode() != null ? req.currencyCode().toUpperCase().trim() : "USD"; + + // 간단한 모의 데이터로 반환 (실제 구현 시 외부 연동) + double dummyRate = 1350.50; + if (targetCurrency.contains("JPY")) { + dummyRate = 905.20; + } else if (targetCurrency.contains("EUR")) { + dummyRate = 1450.30; + } + + log.info("[ExchangeRateTool] 환율 조회 완료: {} -> {}", targetCurrency, dummyRate); + return new ExchangeRateResponse("KRW", targetCurrency, dummyRate); + } +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/impl/TeamMemberUseCaseImpl.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/impl/TeamMemberUseCaseImpl.java new file mode 100644 index 000000000..19d1e25fd --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/impl/TeamMemberUseCaseImpl.java @@ -0,0 +1,54 @@ +package io.shinhanlife.dap.mcc.biz.smp.usecase.impl; + +import io.shinhanlife.dap.mcc.biz.smp.dto.TeamMemberRequest; +import io.shinhanlife.dap.mcc.biz.smp.dto.TeamMemberResponse; +import io.shinhanlife.dap.mcc.biz.smp.usecase.TeamMemberUseCase; +import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase; +import org.springframework.stereotype.Service; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * @package io.shinhanlife.dap.mcc.biz.smp.usecase.impl + * @className TeamMemberUseCaseImpl + * @description AX HUB 시스템 처리 클래스 + * @author jade + * @create 2026.07.29 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.07.29  jade    최초생성
+ *
+ * 
+ */ +@Slf4j +@Service +@RequiredArgsConstructor +public class TeamMemberUseCaseImpl extends AbstractMcpToolUseCase implements TeamMemberUseCase { + + @Override + public Object execute(TeamMemberRequest req) { + log.info("[A01] 신한라이프 MCP, TOOL 파트 구성원 조회 요청: {}", req); + + String filter = req != null && req.getTeamName() != null ? req.getTeamName().toUpperCase() : "전체"; + + String resultString = ""; + if (filter.contains("AX")) { + resultString += "ax 추진팀 박세진 프로\n"; + } else if (filter.contains("MCP") && !filter.contains("TOOL")) { + resultString += "MCP 팀은 고석민 수석 , 장효원 책임\n"; + } else if (filter.contains("TOOL") && !filter.contains("MCP")) { + resultString += "TOOL 팀은 김형식 수석 ,김영진 책임 , 김도겸 대리 , 이주희 선임 , 문주현 선임 , 이보람 대리 , 박수빈 대리\n"; + } else { + resultString += "ax 추진팀 박세진 프로\n" + + "MCP & TOOL 팀 담당자는 윤희준 이사\n" + + "MCP 팀은 고석민 수석 , 장효원 책임\n" + + "TOOL 팀은 김형식 수석 ,김영진 책임 , 김도겸 대리 , 이주희 선임 , 문주현 선임 , 이보람 대리 , 박수빈 대리"; + } + + TeamMemberResponse res = new TeamMemberResponse(); + res.setResult(resultString.trim()); + return res; + } +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/impl/WeatherToolUseCaseImpl.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/impl/WeatherToolUseCaseImpl.java new file mode 100644 index 000000000..b581199cf --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/impl/WeatherToolUseCaseImpl.java @@ -0,0 +1,105 @@ +package io.shinhanlife.dap.mcc.biz.smp.usecase.impl; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.shinhanlife.dap.mcc.biz.smp.usecase.WeatherToolUseCase; +import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase; +import io.shinhanlife.dap.mcc.biz.smp.dto.WeatherRequest; +import io.shinhanlife.dap.mcc.biz.smp.dto.WeatherResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * @package io.shinhanlife.dap.mcc.service + * @className WeatherToolService + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.07.14 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.07.14  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +@Service +public class WeatherToolUseCaseImpl extends AbstractMcpToolUseCase implements WeatherToolUseCase { + + private final RestClient restClient; + + public WeatherToolUseCaseImpl() { + this.restClient = RestClient.create(); + } + public WeatherResponse execute(WeatherRequest req) { + String city = req.city() != null ? req.city().trim() : "서울"; + + // 지역별 위경도 매핑 (간단한 예시) + double lat = 37.566; + double lon = 126.978; + + if (city.contains("부산")) { + lat = 35.179; + lon = 129.075; + } else if (city.contains("제주")) { + lat = 33.499; + lon = 126.531; + } else if (city.contains("인천")) { + lat = 37.456; + lon = 126.705; + } + + try { + String newRequestId = java.util.UUID.randomUUID().toString(); + String url = String.format("https://api.open-meteo.com/v1/forecast?latitude=%f&longitude=%f¤t_weather=true", lat, lon); + + log.info("[WeatherTool] OUTBOUND HTTP IN - request-id: {}", newRequestId); + log.info("[WeatherTool] 날씨 조회 요청 URL: {}", url); + + String responseStr = restClient.get() + .uri(url) + .header("request-id", newRequestId) + .retrieve() + .body(String.class); + + log.info("[WeatherTool] OUTBOUND HTTP OUT - request-id: {}", newRequestId); + + ObjectMapper mapper = new ObjectMapper(); + JsonNode response = mapper.readTree(responseStr); + + if (response != null && response.has("current_weather")) { + JsonNode current = response.get("current_weather"); + double temp = current.path("temperature").asDouble(); + double windSpeed = current.path("windspeed").asDouble(); + String time = current.path("time").asText(); + + int weatherCode = current.path("weathercode").asInt(); + String summary = parseWeatherCode(weatherCode); + + String formattedTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); + + return new WeatherResponse(city, temp, windSpeed, formattedTime, summary); + } + } catch (Exception e) { + log.error("[WeatherTool] 날씨 API 연동 실패: {}", e.getMessage()); + return new WeatherResponse(city, 0.0, 0.0, "", "날씨 정보를 불러오는데 실패했습니다."); + } + + return new WeatherResponse(city, 0.0, 0.0, "", "알 수 없는 응답입니다."); + } + + private String parseWeatherCode(int code) { + if (code == 0) return "맑음 (Clear)"; + if (code >= 1 && code <= 3) return "구름조금/흐림 (Cloudy)"; + if (code >= 45 && code <= 48) return "안개 (Fog)"; + if (code >= 51 && code <= 67) return "비/이슬비 (Rain)"; + if (code >= 71 && code <= 77) return "눈 (Snow)"; + if (code >= 95) return "뇌우/천둥번개 (Thunderstorm)"; + return "알 수 없음 (Unknown)"; + } +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/converter/SolReqDetailConverter.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/converter/SolReqDetailConverter.java new file mode 100644 index 000000000..6b3a70194 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/converter/SolReqDetailConverter.java @@ -0,0 +1,31 @@ +package io.shinhanlife.dap.mcc.biz.sol.converter; + +import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqDetailRequest; +import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqDetailResponse; +import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io.SOLG00000002_I; +import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io.SOLG00000002_O; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +/** + * @package io.shinhanlife.dap.mcc.biz.sol.converter + * @className SolReqDetailConverter + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ *
+ * 
+ */ +@Mapper(componentModel = "spring") +public interface SolReqDetailConverter { + + @Mapping(source = "srId", target = "srId") + SOLG00000002_I toLegacyRequest(SolReqDetailRequest req); + + SolReqDetailResponse toResponse(SOLG00000002_O mciRes); +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/converter/SolReqListConverter.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/converter/SolReqListConverter.java new file mode 100644 index 000000000..f71e31e28 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/converter/SolReqListConverter.java @@ -0,0 +1,33 @@ +package io.shinhanlife.dap.mcc.biz.sol.converter; + +import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqListRequest; +import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqListResponse; +import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io.SOLG00000001_I; +import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io.SOLG00000001_O; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * @package io.shinhanlife.dap.mcc.biz.sol.converter + * @className SolReqListConverter + * @description AX HUB 시스템 처리 클래스 + * @author jade + * @create 2026.07.29 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.07.29  jade    최초생성
+ *
+ * 
+ */ +@Mapper(componentModel = "spring") +public interface SolReqListConverter { + @Mapping(source = "status", target = "reqStatus", defaultValue = "진행중") + @Mapping(source = "period", target = "reqPeriod", defaultValue = "최근 3개월") + @Mapping(source = "target", target = "reqTarget", defaultValue = "나의 업무") + SOLG00000001_I toLegacyRequest(SolReqListRequest req); + + SolReqListResponse toResponse(SOLG00000001_O mciRes); +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/dto/SolReqDetailRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/dto/SolReqDetailRequest.java new file mode 100644 index 000000000..130454190 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/dto/SolReqDetailRequest.java @@ -0,0 +1,27 @@ +package io.shinhanlife.dap.mcc.biz.sol.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.shinhanlife.dap.lib.annotation.McpParameter; +import lombok.Data; + +/** + * @package io.shinhanlife.dap.mcc.biz.sol.dto + * @className SolReqDetailRequest + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ *
+ * 
+ */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SolReqDetailRequest { + + @McpParameter(description = "상세 조회할 SOL 의뢰서 ID", required = true) + private String srId; +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/dto/SolReqDetailResponse.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/dto/SolReqDetailResponse.java new file mode 100644 index 000000000..b1c1c2302 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/dto/SolReqDetailResponse.java @@ -0,0 +1,33 @@ +package io.shinhanlife.dap.mcc.biz.sol.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +/** + * @package io.shinhanlife.dap.mcc.biz.sol.dto + * @className SolReqDetailResponse + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ *
+ * 
+ */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SolReqDetailResponse { + + private String srId; + private String srName; + private String process; + private String devStage; + private String appName; + private String requester; + private String requestDate; + private String dueDate; + private String description; +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/dto/SolReqListRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/dto/SolReqListRequest.java new file mode 100644 index 000000000..c669fabd5 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/dto/SolReqListRequest.java @@ -0,0 +1,32 @@ +package io.shinhanlife.dap.mcc.biz.sol.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.shinhanlife.dap.lib.annotation.McpParameter; +import lombok.Data; + +/** + * @package io.shinhanlife.dap.mcc.biz.sol.dto + * @className SolReqListRequest + * @description AX HUB 시스템 처리 클래스 + * @author jade + * @create 2026.07.29 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.07.29  jade    최초생성
+ *
+ * 
+ */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SolReqListRequest { + @McpParameter(description = "진행상태 (예: 진행중, 완료 등)", required = false) + private String status; + + @McpParameter(description = "조회기간 (예: 1개월, 3개월 등)", required = false) + private String period; + + @McpParameter(description = "조회대상 (예: 나의 업무, 전체 등)", required = false) + private String target; +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/dto/SolReqListResponse.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/dto/SolReqListResponse.java new file mode 100644 index 000000000..842694bbb --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/dto/SolReqListResponse.java @@ -0,0 +1,37 @@ +package io.shinhanlife.dap.mcc.biz.sol.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +import java.util.List; + +/** + * @package io.shinhanlife.dap.mcc.biz.sol.dto + * @className SolReqListResponse + * @description AX HUB 시스템 처리 클래스 + * @author jade + * @create 2026.07.29 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.07.29  jade    최초생성
+ *
+ * 
+ */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SolReqListResponse { + private List reqList; + + @Data + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class SolReqListItem { + private String srId; + private String srName; + private String process; + private String devStage; + private String appName; + private String requester; + } +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqDetailUseCase.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqDetailUseCase.java new file mode 100644 index 000000000..7ec6ac381 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqDetailUseCase.java @@ -0,0 +1,39 @@ +package io.shinhanlife.dap.mcc.biz.sol.usecase; + +import io.shinhanlife.dap.lib.annotation.McpFunction; +import io.shinhanlife.dap.lib.annotation.McpTool; +import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqDetailRequest; + +/** + * @package io.shinhanlife.dap.mcc.biz.sol.usecase + * @className SolReqDetailUseCase + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ *
+ * 
+ */ +@McpTool( + routingType = "MCI", + categoryKey = "sol" +) +public interface SolReqDetailUseCase { + + @McpFunction( + displayName = "SolReqDetail 툴", + name = "oth.sol.request.detail", + description = "SOL 의뢰서 상세 조회", + prompt = "SOL 의뢰서 상세 조회해줘", + mappingId = "SOLG00000002", + register = false, + requiresApproval = false, + readOnlyHint = true, + openWorldHint = true + ) + Object execute(SolReqDetailRequest req); +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqListUseCase.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqListUseCase.java new file mode 100644 index 000000000..1a1e9caac --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqListUseCase.java @@ -0,0 +1,23 @@ +package io.shinhanlife.dap.mcc.biz.sol.usecase; + +import io.shinhanlife.dap.lib.annotation.McpFunction; +import io.shinhanlife.dap.lib.annotation.McpTool; +import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqListRequest; + +@McpTool( + routingType = "MCI", + categoryKey = "sol" +) +public interface SolReqListUseCase { + @McpFunction( + displayName = "SolReqList 툴", + name = "oth.sol.request.list", + description = "SOL 의뢰서 목록 조회해줘", + prompt = "SOL 의뢰서 목록 조회해줘", + mappingId = "SOLG00000001", + register = false, + requiresApproval = false, + openWorldHint = true + ) + Object execute(SolReqListRequest req); +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/impl/SolReqDetailUseCaseImpl.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/impl/SolReqDetailUseCaseImpl.java new file mode 100644 index 000000000..be197500d --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/impl/SolReqDetailUseCaseImpl.java @@ -0,0 +1,101 @@ +package io.shinhanlife.dap.mcc.biz.sol.usecase.impl; + +import io.shinhanlife.dap.mcc.biz.sol.converter.SolReqDetailConverter; +import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqDetailRequest; +import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqDetailResponse; +import io.shinhanlife.dap.mcc.biz.sol.usecase.SolReqDetailUseCase; +import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.MciNclgClient; +import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io.SOLG00000002_I; +import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io.SOLG00000002_O; +import io.shinhanlife.glow.communication.dto.Transfer; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +/** + * @package io.shinhanlife.dap.mcc.biz.sol.usecase.impl + * @className SolReqDetailUseCaseImpl + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ *
+ * 
+ */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SolReqDetailUseCaseImpl implements SolReqDetailUseCase { + + private final MciNclgClient mci; + private final SolReqDetailConverter converter; + + @Value("${sol.req-detail.mock-enabled:false}") + private boolean mockEnabled; + + @Override + public Object execute(SolReqDetailRequest req) { + log.info("[MCI Tool] {} 요청 수신. 파라미터: {}", "solReqDetail", req); + if (req == null || req.getSrId() == null || req.getSrId().isBlank()) { + return Map.of("status", "ERROR", "message", "srId는 필수입니다."); + } + + if (mockEnabled) { + return createLocalSampleResponse(req.getSrId()); + } + + try { + SOLG00000002_I mciRequest = converter.toLegacyRequest(req); + Transfer mciResponse = mci.callTo( + "SOLG00000002", "SOLG00000002", mciRequest, SOLG00000002_O.class); + + if (mciResponse == null || mciResponse.getBody() == null) { + return Map.of("status", "NOT_FOUND", "message", "의뢰서 상세 정보를 찾을 수 없습니다."); + } + return converter.toResponse(mciResponse.getBody()); + } catch (Exception e) { + log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e); + return Map.of( + "status", "ERROR", + "message", e.getMessage() != null ? e.getMessage() : "Unknown error"); + } + } + + private Object createLocalSampleResponse(String srId) { + SolReqDetailResponse response = new SolReqDetailResponse(); + if ("SR-2026-001".equalsIgnoreCase(srId)) { + response.setSrId("SR-2026-001"); + response.setSrName("AX HUB 메인 화면 UI 개편"); + response.setProcess("진행중"); + response.setDevStage("개발(단위테스트)"); + response.setAppName("AX HUB"); + response.setRequester("신한준"); + response.setRequestDate("2026-07-01"); + response.setDueDate("2026-08-31"); + response.setDescription("AX HUB 메인 화면의 사용성과 접근성을 개선하는 UI 개편 의뢰입니다."); + return response; + } + if ("SR-2026-002".equalsIgnoreCase(srId)) { + response.setSrId("SR-2026-002"); + response.setSrName("SOL 연동 모듈 추가 개발"); + response.setProcess("진행중"); + response.setDevStage("분석/설계"); + response.setAppName("MCP Gateway"); + response.setRequester("고석민"); + response.setRequestDate("2026-07-15"); + response.setDueDate("2026-09-30"); + response.setDescription("SOL 의뢰서 조회 기능을 MCP 도구로 제공하기 위한 연동 모듈 개발 의뢰입니다."); + return response; + } + return Map.of( + "status", "NOT_FOUND", + "message", "의뢰서를 찾을 수 없습니다.", + "srId", srId); + } +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/impl/SolReqListUseCaseImpl.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/impl/SolReqListUseCaseImpl.java new file mode 100644 index 000000000..35ea868e5 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/impl/SolReqListUseCaseImpl.java @@ -0,0 +1,80 @@ +package io.shinhanlife.dap.mcc.biz.sol.usecase.impl; + +import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqListRequest; +import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqListResponse; +import io.shinhanlife.dap.mcc.biz.sol.usecase.SolReqListUseCase; +import io.shinhanlife.glow.communication.dto.Transfer; +import org.springframework.stereotype.Service; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import java.util.Map; +import java.util.List; +import java.util.ArrayList; +import io.shinhanlife.dap.mcc.biz.sol.converter.SolReqListConverter; +import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io.SOLG00000001_I; +import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io.SOLG00000001_O; +import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.MciNclgClient; +import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqListResponse.SolReqListItem; + +/** + * @package io.shinhanlife.dap.mcc.biz.sol.usecase.impl + * @className SolReqListUseCaseImpl + * @description AX HUB 시스템 처리 클래스 + * @author jade + * @create 2026.07.29 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.07.29  jade    최초생성
+ *
+ * 
+ */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SolReqListUseCaseImpl implements SolReqListUseCase { + + private final MciNclgClient mci; + private final SolReqListConverter converter; + + @Override + public Object execute(SolReqListRequest req) { + log.info("[MCI Tool] {} 요청 수신. 파라미터: {}", "solReqList", req); + try { + SOLG00000001_I mciRequest = converter.toLegacyRequest(req); + Transfer mciResponse = mci.callTo( + "SOLG00000001", "SOLG00000001", mciRequest, SOLG00000001_O.class); + log.info("[MCI Tool] SOLG00000001 MCI call completed. Returning dummy response: {}", + mciResponse != null); + // 현업 테스트용으로 MCI 통신을 바이패스하고 하드코딩된 결과를 반환합니다. + SolReqListResponse res = new SolReqListResponse(); + List list = new ArrayList<>(); + + SolReqListItem item1 = new SolReqListItem(); + item1.setSrId("SR-2026-001"); + item1.setSrName("AX HUB 메인 화면 UI 개편"); + item1.setProcess("진행중"); + item1.setDevStage("개발(단위테스트)"); + item1.setAppName("AX HUB"); + item1.setRequester("윤희준"); + list.add(item1); + + SolReqListItem item2 = new SolReqListItem(); + item2.setSrId("SR-2026-002"); + item2.setSrName("툴 연동 모듈 추가 개발"); + item2.setProcess("진행중"); + item2.setDevStage("분석/설계"); + item2.setAppName("MCP Gateway"); + item2.setRequester("고석민"); + list.add(item2); + + res.setReqList(list); + + return res; + } catch (Exception e) { + log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e); + return Map.of("status", "ERROR", "message", e.getMessage() != null ? e.getMessage() : "Unknown error"); + } + } +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/cfp/a/MciCfpaClient.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/cfp/a/MciCfpaClient.java new file mode 100644 index 000000000..f952241d6 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/cfp/a/MciCfpaClient.java @@ -0,0 +1,72 @@ +package io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a; + +import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_I; +import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_O; +import io.shinhanlife.dap.lib.integration.mci.component.AxhubMciComponent; +import io.shinhanlife.glow.communication.dto.Transfer; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * @package io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io + * @className MciCfpaClient + * @description 보장분석결과조회 MCI 호출 클라이언트 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +@Component +@RequiredArgsConstructor +public class MciCfpaClient { + + private final AxhubMciComponent mci; + + // 인터페이스 코드 상수 + private static final String INTERFACE_CODE = "CLCNNB00001"; // CLCCFP00001 (Onnba용 코드 유지) + + // 정상 응답 코드 상수 + private static final String SUCCESS_CODE_COM = "COM00139"; + private static final String SUCCESS_CODE_CFP = "CFP00000"; + private static final String NO_DATA_CODE = "COM00150"; + + // 예외 코드 상수 + private static final String ERROR_CODE_PROCESS = "CLC00007"; + private static final String ERROR_CODE_MESSAGE = "CLC00043"; + + /** + * 보장분석결과조회 + * + * @param inDto 입력 DTO + * @return 출력 객체 + */ + public Object callCfpa0001(CLCNNB00001_I mciReq) throws Exception { + // 인터페이스 IO 객체 생성 및 매핑 + + // 인터페이스 호출 + Transfer resTransfer = mci.callTo(INTERFACE_CODE, mciReq, CLCNNB00001_O.class); + + // 응답 검증 + validateResponse(resTransfer); + + // 메시지 검증 + validateMessage(String.valueOf(resTransfer.getMessage())); + + return resTransfer.getBody(); + } + + private void validateResponse(Transfer resTransfer) { + log.info("응답 검증 로직 수행"); + } + + private void validateMessage(String message) { + log.info("메시지 검증 로직 수행: {}", message); + } +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/cfp/a/io/CLCNNB00001_I.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/cfp/a/io/CLCNNB00001_I.java new file mode 100644 index 000000000..7de87b33f --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/cfp/a/io/CLCNNB00001_I.java @@ -0,0 +1,91 @@ +package io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * @package io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io + * @className CLCNNB00001_I + * @description 보장분석결과조회 MCI 요청 전문 + * @author 0986406 + * @create 2026.09.01 + */ +@Data +public class CLCNNB00001_I { + + @JsonProperty("unfcPrbuIrcoAddu") + private UnfcPrbuIrcoAdduDto unfcPrbuIrcoAddu; + + @JsonProperty("dalScCd") + private String dalScCd; + + @JsonProperty("cstSucoRltyCd") + private String cstSucoRltyCd; + + @JsonProperty("csNo") + private String csNo; + + @JsonProperty("rdreNo") + private String rdreNo; + + @JsonProperty("unfcPvsCalReqYn") + private String unfcPvsCalReqYn; + + @JsonProperty("kcisPymmTnnrRequest") + private String kcisPymmTnnrRequest; + + @JsonProperty("lmovYn") + private String lmovYn; + + @JsonProperty("genPsthApvTrgtYn") + private String genPsthApvTrgtYn; + + @JsonProperty("ircoLmovEcpbTrgtYn") + private String ircoLmovEcpbTrgtYn; + + @JsonProperty("digCalYn") + private String digCalYn; + + @JsonProperty("prbuIciDigCalYn") + private String prbuIciDigCalYn; + + @JsonProperty("sucoIspaBasDto") + private SucoIspaBasDto sucoIspaBasDto; + + @Data + public static class UnfcPrbuIrcoAdduDto { + } + + @Data + public static class SucoIspaBasDto { + @JsonProperty("ccnDalTypCd") + private String ccnDalTypCd; + + @JsonProperty("nwcnptCursCd") + private String nwcnptCursCd; + + @JsonProperty("induAsctScCd") + private String induAsctScCd; + + @JsonProperty("cepeOgnzNo") + private String cepeOgnzNo; + + @JsonProperty("cepePrafNo") + private String cepePrafNo; + + @JsonProperty("clmoOgnzNo") + private String clmoOgnzNo; + + @JsonProperty("clmoPrafNo") + private String clmoPrafNo; + + @JsonProperty("sucoYmd") + private String sucoYmd; + + @JsonProperty("ispDt") + private String ispDt; + + @JsonProperty("contYmd") + private String contYmd; + } +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/cfp/a/io/CLCNNB00001_O.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/cfp/a/io/CLCNNB00001_O.java new file mode 100644 index 000000000..fcb0f3fc7 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/cfp/a/io/CLCNNB00001_O.java @@ -0,0 +1,16 @@ +package io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io; + +import lombok.Data; + +/** + * @package io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io + * @className CLCNNB00001_O + * @description 보장분석결과조회 MCI 응답 전문 + * @author 0986406 + * @create 2026.09.01 + */ +@Data +public class CLCNNB00001_O { + // 응답 전문 필드 정의 (필요에 따라 추가) + private Object resultData; +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncl/g/MciNclgClient.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncl/g/MciNclgClient.java new file mode 100644 index 000000000..6ca133b0a --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncl/g/MciNclgClient.java @@ -0,0 +1,30 @@ +package io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g; + +import org.springframework.stereotype.Component; +import lombok.RequiredArgsConstructor; +import io.shinhanlife.dap.lib.integration.mci.component.AxhubMciComponent; +import io.shinhanlife.glow.communication.dto.Transfer; + +/** + * @package io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g + * @className MciNclgClient + * @description AX HUB 시스템 처리 클래스 + * @author jade + * @create 2026.07.29 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.07.29  jade    최초생성
+ *
+ * 
+ */ +@Component +@RequiredArgsConstructor +public class MciNclgClient { + private final AxhubMciComponent mci; + + public Transfer callTo(String interfaceId, String dummy, Object mciReq, Class resType) throws Exception { + return mci.callTo(interfaceId, dummy, mciReq, resType); + } +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncl/g/io/SOLG00000001_I.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncl/g/io/SOLG00000001_I.java new file mode 100644 index 000000000..cc976137f --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncl/g/io/SOLG00000001_I.java @@ -0,0 +1,24 @@ +package io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io; + +import lombok.Data; + +/** + * @package io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io + * @className SOLG00000001_I + * @description AX HUB 시스템 처리 클래스 + * @author jade + * @create 2026.07.29 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.07.29  jade    최초생성
+ *
+ * 
+ */ +@Data +public class SOLG00000001_I { + private String reqStatus; + private String reqPeriod; + private String reqTarget; +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncl/g/io/SOLG00000001_O.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncl/g/io/SOLG00000001_O.java new file mode 100644 index 000000000..7c1e49599 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncl/g/io/SOLG00000001_O.java @@ -0,0 +1,34 @@ +package io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io; + +import lombok.Data; + +import java.util.List; + +/** + * @package io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io + * @className SOLG00000001_O + * @description AX HUB 시스템 처리 클래스 + * @author jade + * @create 2026.07.29 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.07.29  jade    최초생성
+ *
+ * 
+ */ +@Data +public class SOLG00000001_O { + private List reqList; + + @Data + public static class SOLG00000001_O_Item { + private String srId; + private String srName; + private String process; + private String devStage; + private String appName; + private String requester; + } +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncl/g/io/SOLG00000002_I.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncl/g/io/SOLG00000002_I.java new file mode 100644 index 000000000..3b8b2cb11 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncl/g/io/SOLG00000002_I.java @@ -0,0 +1,23 @@ +package io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io; + +import lombok.Data; + +/** + * @package io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io + * @className SOLG00000002_I + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ *
+ * 
+ */ +@Data +public class SOLG00000002_I { + + private String srId; +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncl/g/io/SOLG00000002_O.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncl/g/io/SOLG00000002_O.java new file mode 100644 index 000000000..1aa61472a --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncl/g/io/SOLG00000002_O.java @@ -0,0 +1,31 @@ +package io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io; + +import lombok.Data; + +/** + * @package io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io + * @className SOLG00000002_O + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ *
+ * 
+ */ +@Data +public class SOLG00000002_O { + + private String srId; + private String srName; + private String process; + private String devStage; + private String appName; + private String requester; + private String requestDate; + private String dueDate; + private String description; +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/oth/DapToolOthApplication.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/oth/DapToolOthApplication.java new file mode 100644 index 000000000..8ac735218 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/oth/DapToolOthApplication.java @@ -0,0 +1,30 @@ +package io.shinhanlife.dap.mcc.oth; + + +/** + * @package io.shinhanlife.dap.mcc.oth + * @className DapToolOthApplication + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.cache.annotation.EnableCaching; + +@SpringBootApplication(scanBasePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.lib.adapter", "io.shinhanlife.dap.lib.mcp", "io.shinhanlife.dap.lib.config", "io.shinhanlife.dap.lib.integration"}) +@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.lib.adapter", "io.shinhanlife.dap.lib.mcp", "io.shinhanlife.dap.lib.config", "io.shinhanlife.dap.lib.integration"}) +@EnableCaching +public class DapToolOthApplication { + public static void main(String[] args) { + SpringApplication.run(DapToolOthApplication.class, args); + } +} diff --git a/dap-tool-oth/src/main/resources/application-dev.yml b/dap-tool-oth/src/main/resources/application-dev.yml new file mode 100644 index 000000000..fa5ebf307 --- /dev/null +++ b/dap-tool-oth/src/main/resources/application-dev.yml @@ -0,0 +1,28 @@ +# OCI ?대씪?곕뱶 ?섍꼍 ?꾩슜 ?ㅼ젙 +server: + port: ${PORT:8084} + +axhub: + gateway: + url: https://axhubmcp.devjun.net + +eims: + http: + url: http://localhost:${server.port}/api/gateway + tcp: + host: 127.0.0.1 + port: 8090 + timeout: 5000 + jsp: + form: + url: http://localhost:${server.port}/mock/jsp-form + json: + url: http://localhost:${server.port}/mock/jsp-json + mci: + url: http://localhost:${server.port}/api/mock/esb/api + mcistring: + url: http://localhost:${server.port}/api/mock/esb/string + +spring: + config: + import: optional:classpath:glow/application-glow.yml diff --git a/dap-tool-oth/src/main/resources/application-local.yml b/dap-tool-oth/src/main/resources/application-local.yml new file mode 100644 index 000000000..a4e5f24c7 --- /dev/null +++ b/dap-tool-oth/src/main/resources/application-local.yml @@ -0,0 +1,43 @@ +# Local ?섍꼍 ?꾩슜 ?ㅼ젙 (H2 硫붾え由?DB ?? +spring: + datasource: + url: jdbc:p6spy:h2:mem:testdb;DB_CLOSE_DELAY=-1; + driverClassName: com.p6spy.engine.spy.P6SpyDriver + username: sa + password: password + h2: + console: + enabled: true + +eims: + http: + url: http://localhost:${server.port}/api/gateway + tcp: + host: 127.0.0.1 + port: 8090 + timeout: 5000 + jsp: + form: + url: http://localhost:${server.port}/mock/jsp-form + json: + url: http://localhost:${server.port}/mock/jsp-json + mci: + url: http://localhost:${server.port}/api/mock/esb/api + mcistring: + url: http://localhost:${server.port}/api/mock/esb/string + +mcp: + security: + tenant-domains: + mcp-client-1: CUSTOMER,COMMON + mcp-client-2: ALL + +axhub: + gateway: + url: http://localhost:8081 + tool: + url: ${AXHUB_TOOL_URL:http://localhost:${server.port}} + +sol: + req-detail: + mock-enabled: true diff --git a/dap-tool-oth/src/main/resources/application.yml b/dap-tool-oth/src/main/resources/application.yml new file mode 100644 index 000000000..d51b81264 --- /dev/null +++ b/dap-tool-oth/src/main/resources/application.yml @@ -0,0 +1,19 @@ +server: + port: 8084 +spring: + application: + name: dap-tool-oth + profiles: + active: local +logging: + level: + org.apache.kafka: ERROR +mcp: + namespace: "" + manifest: + bundle-id: tool-oth + # Set the AA-assigned prefix before MCP pull activation (for example: oth.). + name-prefix: "" + security: + tenant-domains: + TESTER-DEV: ALL \ No newline at end of file diff --git a/dap-tool-oth/src/main/resources/logback-spring.xml b/dap-tool-oth/src/main/resources/logback-spring.xml new file mode 100644 index 000000000..8b4cb3af8 --- /dev/null +++ b/dap-tool-oth/src/main/resources/logback-spring.xml @@ -0,0 +1,39 @@ + + + + + + + + + + ${LOG_PATTERN} + + + + + + + + + /swlog/dap-tool-oth/A01/${HOSTNAME}_A01.log + + + /swlog/dap-tool-oth/A01/${HOSTNAME}_A01_%d{yyyyMMdd}.log + + 30 + + + ${LOG_PATTERN} + + + + + + + + + + + + diff --git a/dap-tool-oth/src/main/resources/tool-schemas/cmm/claim-search-resource-input-schema.json b/dap-tool-oth/src/main/resources/tool-schemas/cmm/claim-search-resource-input-schema.json new file mode 100644 index 000000000..f2c2f6335 --- /dev/null +++ b/dap-tool-oth/src/main/resources/tool-schemas/cmm/claim-search-resource-input-schema.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "claimNo": { + "type": "string", + "description": "청구번호. CLM 다음 숫자 13자리 형식이다.", + "pattern": "^CLM[0-9]{13}$", + "examples": ["CLM2026070100123"] + }, + "contractNo": { + "type": "string", + "description": "계약번호. 숫자 11자리 형식이다.", + "pattern": "^[0-9]{11}$", + "examples": ["10023456789"] + }, + "status": { + "type": "string", + "enum": [ + "RECEIVED", "REVIEWING", "ADDITIONAL_DOC_REQUIRED", + "APPROVED", "PAID", "REJECTED", "WITHDRAWN" + ] + }, + "size": { + "type": "integer", + "minimum": 1, + "maximum": 50, + "default": 20 + } + }, + "required": [], + "additionalProperties": false, + "anyOf": [ + { "required": ["claimNo"] }, + { "required": ["contractNo"] } + ] +} diff --git a/dap-tool-oth/src/main/resources/tool-schemas/cmm/claim-search-resource-output-schema.json b/dap-tool-oth/src/main/resources/tool-schemas/cmm/claim-search-resource-output-schema.json new file mode 100644 index 000000000..f94c66137 --- /dev/null +++ b/dap-tool-oth/src/main/resources/tool-schemas/cmm/claim-search-resource-output-schema.json @@ -0,0 +1,81 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "description": "Claim search response. This schema intentionally excludes employee identifiers, customer names, contact details, account information, and other PII.", + "properties": { + "resultCode": { + "type": "string", + "enum": ["SUCCESS", "FAILURE"], + "description": "Machine-readable execution result code." + }, + "resultLabel": { + "type": "string", + "description": "User-readable label for resultCode." + }, + "status": { + "type": "string", + "enum": ["RECEIVED", "REVIEWING", "ADDITIONAL_DOC_REQUIRED", "APPROVED", "PAID", "REJECTED", "WITHDRAWN"], + "description": "Current claim processing status code." + }, + "statusLabel": { + "type": "string", + "description": "User-readable label for status." + }, + "approvedAmount": { + "anyOf": [ + { "type": "number", "minimum": 0 }, + { "type": "null" } + ], + "description": "Approved amount. It is null before review and must not be interpreted as zero." + }, + "rejectionReason": { + "type": ["string", "null"], + "maxLength": 200, + "description": "Has a value only when status is REJECTED. It is null for every other status." + }, + "items": { + "type": "array", + "description": "Claim summaries ordered by receivedDate descending. No personally identifiable information is included.", + "items": { + "type": "object", + "properties": { + "status": { "type": "string", "description": "Claim status code." }, + "statusLabel": { "type": "string", "description": "User-readable label for status." }, + "receivedDate": { "type": "string", "format": "date", "description": "Claim received date." }, + "approvedAmount": { + "anyOf": [ + { "type": "number", "minimum": 0 }, + { "type": "null" } + ], + "description": "Null before review; do not interpret as zero." + } + }, + "required": ["status", "statusLabel", "receivedDate"], + "additionalProperties": false + } + }, + "hasMore": { + "type": "boolean", + "description": "True when additional results exist beyond this response." + }, + "totalCount": { + "type": "integer", + "minimum": 0, + "description": "Total number of matched claims." + } + }, + "required": ["resultCode", "resultLabel", "status", "statusLabel", "items", "hasMore", "totalCount"], + "allOf": [ + { + "if": { + "properties": { "status": { "const": "REJECTED" } }, + "required": ["status"] + }, + "then": { + "properties": { "rejectionReason": { "type": "string", "minLength": 1 } }, + "required": ["rejectionReason"] + } + } + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java b/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java new file mode 100644 index 000000000..cead06211 --- /dev/null +++ b/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java @@ -0,0 +1,88 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.shinhanlife.dap.lib.annotation.McpFunction; +import io.shinhanlife.dap.lib.util.JsonSchemaGenerator; +import io.shinhanlife.dap.lib.util.ToolSchemaResolver; +import io.shinhanlife.dap.mcc.biz.cmm.usecase.impl.ClaimSearchSchemaSampleUseCaseImpl; +import io.shinhanlife.dap.mcc.presentation.ToolArgumentSchemaValidator;import io.shinhanlife.dap.mcc.biz.cmm.usecase.ClaimSearchSchemaSampleUseCase; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class ClaimSearchRequestSchemaTest { + + @Test + void generatesClaimOrContractSearchSchema() { + Map schema = JsonSchemaGenerator.generateSchema(ClaimSearchRequest.class); + Map> properties = properties(schema); + + assertEquals(false, schema.get("additionalProperties")); + assertEquals("^CLM[0-9]{13}$", properties.get("claimNo").get("pattern")); + assertEquals(50L, properties.get("size").get("maximum")); + assertEquals(20L, properties.get("size").get("default")); + assertEquals(List.of( + Map.of("required", List.of("claimNo")), + Map.of("required", List.of("contractNo"))), schema.get("anyOf")); + } + + @Test + void resolvesSchemaFromToolModuleResource() throws Exception { + Method method = ClaimSearchSchemaSampleUseCase.class + .getDeclaredMethod("search", ClaimSearchRequest.class); + McpFunction function = method.getAnnotation(McpFunction.class); + + Map schema = new ToolSchemaResolver(new ObjectMapper()) + .resolve(function, ClaimSearchRequest.class); + + assertEquals("https://json-schema.org/draft/2020-12/schema", schema.get("$schema")); + assertTrue(schema.containsKey("anyOf")); + assertEquals(50, property(schema, "size").get("maximum")); + } + + @Test + void resolvesOutputSchemaFromToolModuleResource() throws Exception { + Method method = ClaimSearchSchemaSampleUseCase.class + .getDeclaredMethod("search", ClaimSearchRequest.class); + McpFunction function = method.getAnnotation(McpFunction.class); + Map schema = new ToolSchemaResolver(new ObjectMapper()).resolveOutput(function); + assertEquals("https://json-schema.org/draft/2020-12/schema", schema.get("$schema")); + assertTrue(properties(schema).containsKey("resultCode")); + assertTrue(properties(schema).containsKey("statusLabel")); + assertTrue(properties(schema).containsKey("hasMore")); + assertTrue(schema.containsKey("allOf")); + } + + @Test + void sampleResponseConformsToOutputSchema() throws Exception { + Method method = ClaimSearchSchemaSampleUseCase.class + .getDeclaredMethod("search", ClaimSearchRequest.class); + Map schema = new ToolSchemaResolver(new ObjectMapper()) + .resolveOutput(method.getAnnotation(McpFunction.class)); + + ClaimSearchResponse response = new ClaimSearchSchemaSampleUseCaseImpl().search(new ClaimSearchRequest()); + ToolArgumentSchemaValidator validator = new ToolArgumentSchemaValidator(new ObjectMapper()); + + assertTrue(validator.validateValue(schema, response).isEmpty()); + } + @Test + void sampleResponseConformsToAutomaticallyGeneratedOutputSchema() throws Exception { + Map schema = JsonSchemaGenerator.generateSchema(ClaimSearchResponse.class); + ClaimSearchResponse response = new ClaimSearchSchemaSampleUseCaseImpl().search(new ClaimSearchRequest()); + ToolArgumentSchemaValidator validator = new ToolArgumentSchemaValidator(new ObjectMapper()); + + assertTrue(validator.validateValue(schema, response).isEmpty()); + } + @SuppressWarnings("unchecked") + private Map> properties(Map schema) { + return (Map>) schema.get("properties"); + } + + private Map property(Map schema, String name) { + return properties(schema).get(name); + } +} diff --git a/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/oth/usecase/impl/Onnba3011UseCaseImplTest.java b/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/oth/usecase/impl/Onnba3011UseCaseImplTest.java new file mode 100644 index 000000000..cc909c812 --- /dev/null +++ b/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/oth/usecase/impl/Onnba3011UseCaseImplTest.java @@ -0,0 +1,46 @@ +package io.shinhanlife.dap.mcc.biz.oth.usecase.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.shinhanlife.dap.mcc.biz.oth.converter.Onnba3011Converter; +import io.shinhanlife.dap.mcc.biz.oth.dto.Onnba3011Request; +import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.MciCfpaClient; +import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_I; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +/** + * @package io.shinhanlife.dap.mcc.biz.oth.usecase.impl + * @className OnnbaMciToolUseCaseImplTest + * @description ONNBA MCI tool use case test + * @author 0986406 + * @create 2026.07.27 + *
+ * ---------- revision history ----------
+ * date       author    description
+ * ---------- --------- ---------------------------
+ * 2026.07.27 0986406    initial creation
+ * 
+ */ +class Onnba3011UseCaseImplTest { + + @Test + void convertsToolRequestBeforeCallingMciClient() throws Exception { + MciCfpaClient mciCfpaClient = Mockito.mock(MciCfpaClient.class); + Onnba3011Converter converter = Mockito.mock(Onnba3011Converter.class); + Onnba3011UseCaseImpl useCase = new Onnba3011UseCaseImpl(mciCfpaClient, converter); + Onnba3011Request request = new Onnba3011Request(); + CLCNNB00001_I mciRequest = new CLCNNB00001_I(); + + when(converter.toMciRequest(request)).thenReturn(mciRequest); + when(mciCfpaClient.callCfpa0001(mciRequest)).thenReturn("success"); + + Object result = useCase.callOnnba3011(request); + + assertEquals("success", result); + verify(converter).toMciRequest(request); + verify(mciCfpaClient).callCfpa0001(mciRequest); + } +} diff --git a/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/sol/usecase/impl/SolReqDetailUseCaseImplTest.java b/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/sol/usecase/impl/SolReqDetailUseCaseImplTest.java new file mode 100644 index 000000000..e2b245f0d --- /dev/null +++ b/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/sol/usecase/impl/SolReqDetailUseCaseImplTest.java @@ -0,0 +1,45 @@ +package io.shinhanlife.dap.mcc.biz.sol.usecase.impl; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.shinhanlife.dap.mcc.biz.sol.converter.SolReqDetailConverter; +import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqDetailRequest; +import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqDetailResponse; +import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.MciNclgClient; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; + +/** + * @package io.shinhanlife.dap.mcc.biz.sol.usecase.impl + * @className SolReqDetailUseCaseImplTest + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ *
+ * 
+ */ +class SolReqDetailUseCaseImplTest { + + @Test + void returnsLocalSampleDetailBySrId() { + MciNclgClient mci = Mockito.mock(MciNclgClient.class); + SolReqDetailConverter converter = Mockito.mock(SolReqDetailConverter.class); + SolReqDetailUseCaseImpl useCase = new SolReqDetailUseCaseImpl(mci, converter); + ReflectionTestUtils.setField(useCase, "mockEnabled", true); + + SolReqDetailRequest request = new SolReqDetailRequest(); + request.setSrId("SR-2026-001"); + + SolReqDetailResponse response = (SolReqDetailResponse) useCase.execute(request); + + assertThat(response.getSrId()).isEqualTo("SR-2026-001"); + assertThat(response.getSrName()).isEqualTo("AX HUB 메인 화면 UI 개편"); + Mockito.verifyNoInteractions(mci); + } +} diff --git a/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/sol/usecase/impl/SolReqListUseCaseImplTest.java b/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/sol/usecase/impl/SolReqListUseCaseImplTest.java new file mode 100644 index 000000000..26ef60f74 --- /dev/null +++ b/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/sol/usecase/impl/SolReqListUseCaseImplTest.java @@ -0,0 +1,39 @@ +package io.shinhanlife.dap.mcc.biz.sol.usecase.impl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.shinhanlife.dap.mcc.biz.sol.converter.SolReqListConverter; +import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqListRequest; +import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqListResponse; +import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.MciNclgClient; +import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io.SOLG00000001_I; +import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io.SOLG00000001_O; +import io.shinhanlife.glow.communication.dto.Transfer; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class SolReqListUseCaseImplTest { + + @Test + void callsMciWithConvertedRequestAndReturnsDummyResponse() throws Exception { + MciNclgClient mci = Mockito.mock(MciNclgClient.class); + SolReqListConverter converter = Mockito.mock(SolReqListConverter.class); + SolReqListUseCaseImpl useCase = new SolReqListUseCaseImpl(mci, converter); + SolReqListRequest request = new SolReqListRequest(); + SOLG00000001_I mciRequest = new SOLG00000001_I(); + + when(converter.toLegacyRequest(request)).thenReturn(mciRequest); + when(mci.callTo(eq("SOLG00000001"), eq("SOLG00000001"), eq(mciRequest), eq(SOLG00000001_O.class))) + .thenReturn(new Transfer<>()); + + SolReqListResponse response = (SolReqListResponse) useCase.execute(request); + + verify(converter).toLegacyRequest(request); + verify(mci).callTo("SOLG00000001", "SOLG00000001", mciRequest, SOLG00000001_O.class); + assertThat(response.getReqList()).extracting(SolReqListResponse.SolReqListItem::getSrId) + .containsExactly("SR-2026-001", "SR-2026-002"); + } +} diff --git a/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/infra/itrf/mci/cfp/a/converter/Onnba3011MciRequestConverterTest.java b/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/infra/itrf/mci/cfp/a/converter/Onnba3011MciRequestConverterTest.java new file mode 100644 index 000000000..d7e3a564f --- /dev/null +++ b/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/infra/itrf/mci/cfp/a/converter/Onnba3011MciRequestConverterTest.java @@ -0,0 +1,55 @@ +package io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.converter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import io.shinhanlife.dap.mcc.biz.oth.converter.Onnba3011Converter; +import io.shinhanlife.dap.mcc.biz.oth.dto.Onnba3011Request; +import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_I; +import org.junit.jupiter.api.Test; +import org.mapstruct.factory.Mappers; + +/** + * @package io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.converter + * @className Onnba3011MciRequestConverterTest + * @description ONNBA 3011 request converter test + * @author 0986406 + * @create 2026.07.27 + *
+ * ---------- revision history ----------
+ * date       author    description
+ * ---------- --------- ---------------------------
+ * 2026.07.27 0986406    initial creation
+ * 
+ */ +class Onnba3011MciRequestConverterTest { + + private final Onnba3011Converter converter = Mappers.getMapper(Onnba3011Converter.class); + + @Test + void mapsRootAndNestedRequestFieldsToMciPayload() { + Onnba3011Request source = request(); + + CLCNNB00001_I result = converter.toMciRequest(source); + + assertEquals("A", result.getDalScCd()); + assertEquals("123456", result.getCsNo()); + assertNotNull(result.getUnfcPrbuIrcoAddu()); + assertNotNull(result.getSucoIspaBasDto()); + assertEquals("01", result.getSucoIspaBasDto().getCcnDalTypCd()); + assertEquals("20260727", result.getSucoIspaBasDto().getSucoYmd()); + } + + private Onnba3011Request request() { + Onnba3011Request request = new Onnba3011Request(); + request.setDalScCd("A"); + request.setCsNo("123456"); + request.setUnfcPrbuIrcoAddu(new Onnba3011Request.UnfcPrbuIrcoAdduDto()); + + Onnba3011Request.SucoIspaBasDto suco = new Onnba3011Request.SucoIspaBasDto(); + suco.setCcnDalTypCd("01"); + suco.setSucoYmd("20260727"); + request.setSucoIspaBasDto(suco); + return request; + } +} diff --git a/dap-tool-sms/Dockerfile b/dap-tool-sms/Dockerfile new file mode 100644 index 000000000..ec6c3cc02 --- /dev/null +++ b/dap-tool-sms/Dockerfile @@ -0,0 +1,8 @@ +FROM eclipse-temurin:21-jre-alpine +WORKDIR /app +RUN apk add --no-cache tzdata +ENV TZ=Asia/Seoul +COPY dap-tool-sms/build/libs/*-SNAPSHOT.jar app.jar +EXPOSE 8082 +ENTRYPOINT ["java", "-jar", "app.jar"] + diff --git a/dap-tool-sms/build.gradle b/dap-tool-sms/build.gradle new file mode 100644 index 000000000..554427fc9 --- /dev/null +++ b/dap-tool-sms/build.gradle @@ -0,0 +1,12 @@ +plugins { + id 'org.springframework.boot' +} + +dependencies { + implementation project(':dap-tool-core') +} + +dependencies { + compileOnly 'org.projectlombok:lombok:1.18.32' + annotationProcessor 'org.projectlombok:lombok:1.18.32' +} diff --git a/dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/converter/SmsLegacyConverter.java b/dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/converter/SmsLegacyConverter.java new file mode 100644 index 000000000..04479c379 --- /dev/null +++ b/dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/converter/SmsLegacyConverter.java @@ -0,0 +1,29 @@ +package io.shinhanlife.dap.mcc.biz.sms.converter; + + +/** + * @package io.shinhanlife.dap.mcc.sms.converter + * @className SmsLegacyConverter + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +import io.shinhanlife.dap.mcc.biz.sms.dto.SmsSendRequest; +import io.shinhanlife.dap.mcc.biz.sms.legacy.SmsLegacyRequest; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(componentModel = "spring") +public interface SmsLegacyConverter { + + @Mapping(source = "phoneNumber", target = "phone") + @Mapping(source = "message", target = "content") + SmsLegacyRequest toLegacyRequest(SmsSendRequest req); +} diff --git a/dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/dto/SmsSendRequest.java b/dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/dto/SmsSendRequest.java new file mode 100644 index 000000000..b7cea0df7 --- /dev/null +++ b/dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/dto/SmsSendRequest.java @@ -0,0 +1,42 @@ +package io.shinhanlife.dap.mcc.biz.sms.dto; + +import lombok.Builder; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import io.shinhanlife.dap.lib.annotation.McpParameter; +import io.shinhanlife.dap.lib.annotation.McpValidation; +import lombok.Getter; +import lombok.Setter; + +/** + * @package io.shinhanlife.dap.mcc.dto + * @className SmsSendRequest + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SmsSendRequest { + + @McpParameter(description = "수신자 전화번호", required = true) + @McpValidation(pattern = "^01[0-9]-?\\d{3,4}-?\\d{4}$") + private String phoneNumber; + + @McpParameter(description = "전송할 메시지 내용", required = true) + @McpValidation(pattern = "\\S") + private String message; + + +} diff --git a/dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/legacy/SmsLegacyRequest.java b/dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/legacy/SmsLegacyRequest.java new file mode 100644 index 000000000..b2d6f7075 --- /dev/null +++ b/dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/legacy/SmsLegacyRequest.java @@ -0,0 +1,33 @@ +package io.shinhanlife.dap.mcc.biz.sms.legacy; + + +/** + * @package io.shinhanlife.dap.mcc.sms.dto + * @className SmsLegacyRequest + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class SmsLegacyRequest { + /** + * EAI 시스템이 요구하는 수신자 번호 파라미터명 + */ + private String phone; + + /** + * EAI 시스템이 요구하는 메시지 내용 파라미터명 + */ + private String content; +} diff --git a/dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/usecase/SmsToolUseCase.java b/dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/usecase/SmsToolUseCase.java new file mode 100644 index 000000000..74f28f74e --- /dev/null +++ b/dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/usecase/SmsToolUseCase.java @@ -0,0 +1,10 @@ +package io.shinhanlife.dap.mcc.biz.sms.usecase; + +import io.shinhanlife.dap.lib.annotation.McpTool; +import io.shinhanlife.dap.lib.annotation.McpFunction; +import io.shinhanlife.dap.mcc.biz.sms.dto.*; + +@McpTool(routingType = "EAI", categoryKey = "notification") +public interface SmsToolUseCase { + Object sendSms(SmsSendRequest req); +} diff --git a/dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/usecase/impl/SmsToolUseCaseImpl.java b/dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/usecase/impl/SmsToolUseCaseImpl.java new file mode 100644 index 000000000..385856874 --- /dev/null +++ b/dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/usecase/impl/SmsToolUseCaseImpl.java @@ -0,0 +1,53 @@ +package io.shinhanlife.dap.mcc.biz.sms.usecase.impl; + +import io.shinhanlife.dap.mcc.biz.sms.usecase.SmsToolUseCase; +import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase; +import org.springframework.stereotype.Service; +import io.shinhanlife.dap.mcc.biz.sms.converter.SmsLegacyConverter; +import io.shinhanlife.dap.mcc.biz.sms.dto.SmsSendRequest; +import io.shinhanlife.dap.mcc.biz.sms.legacy.SmsLegacyRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +/** + * @package io.shinhanlife.dap.mcc.sms + * @className SmsToolService + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Slf4j +@RequiredArgsConstructor +@Service +public class SmsToolUseCaseImpl extends AbstractMcpToolUseCase implements SmsToolUseCase { + + private final SmsLegacyConverter converter; + + + @Override + public Object sendSms(SmsSendRequest req) { + log.info("[SMS] SMS 발송 요청 수신. 수신자: {}", req.getPhoneNumber()); + + // MapStruct를 이용한 자동 매핑 (AI DTO -> MCI DTO) + SmsLegacyRequest legacyRequest = converter.toLegacyRequest(req); + + // 레거시 시스템 연동 (EAI) - DTO 객체를 그대로 넘김 + Map result = executeLegacy("EAI", "SMS_SEND_001", legacyRequest); + + // 결과 가공 + if ("SUCCESS".equals(result.get("status"))) { + result.put("message", "SMS가 성공적으로 발송되었습니다."); + } + + return result; + } +} \ No newline at end of file diff --git a/dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/sms/DapToolSmsApplication.java b/dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/sms/DapToolSmsApplication.java new file mode 100644 index 000000000..f308e6b9d --- /dev/null +++ b/dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/sms/DapToolSmsApplication.java @@ -0,0 +1,30 @@ +package io.shinhanlife.dap.mcc.sms; + + +/** + * @package io.shinhanlife.dap.mcc.sms + * @className DapToolSmsApplication + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.cache.annotation.EnableCaching; + +@SpringBootApplication(scanBasePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.lib.adapter", "io.shinhanlife.dap.lib.mcp", "io.shinhanlife.dap.lib.config", "io.shinhanlife.dap.lib.integration"}) +@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.lib.adapter", "io.shinhanlife.dap.lib.mcp", "io.shinhanlife.dap.lib.config", "io.shinhanlife.dap.lib.integration"}) +@EnableCaching +public class DapToolSmsApplication { + public static void main(String[] args) { + SpringApplication.run(DapToolSmsApplication.class, args); + } +} diff --git a/dap-tool-sms/src/main/resources/application-dev.yml b/dap-tool-sms/src/main/resources/application-dev.yml new file mode 100644 index 000000000..4020bb263 --- /dev/null +++ b/dap-tool-sms/src/main/resources/application-dev.yml @@ -0,0 +1,28 @@ +# OCI ?대씪?곕뱶 ?섍꼍 ?꾩슜 ?ㅼ젙 +server: + port: ${PORT:8082} + +axhub: + gateway: + url: https://axhubmcp.devjun.net + +eims: + http: + url: http://localhost:${server.port}/api/gateway + tcp: + host: 127.0.0.1 + port: 8090 + timeout: 5000 + jsp: + form: + url: http://localhost:${server.port}/mock/jsp-form + json: + url: http://localhost:${server.port}/mock/jsp-json + mci: + url: http://localhost:${server.port}/api/mock/esb/api + mcistring: + url: http://localhost:${server.port}/api/mock/esb/string + +spring: + config: + import: optional:classpath:glow/application-glow.yml diff --git a/dap-tool-sms/src/main/resources/application-local.yml b/dap-tool-sms/src/main/resources/application-local.yml new file mode 100644 index 000000000..13593bf4a --- /dev/null +++ b/dap-tool-sms/src/main/resources/application-local.yml @@ -0,0 +1,39 @@ +# Local ?섍꼍 ?꾩슜 ?ㅼ젙 (H2 硫붾え由?DB ?? +spring: + datasource: + url: jdbc:p6spy:h2:mem:testdb;DB_CLOSE_DELAY=-1; + driverClassName: com.p6spy.engine.spy.P6SpyDriver + username: sa + password: password + h2: + console: + enabled: true + +eims: + http: + url: http://localhost:${server.port}/api/gateway + tcp: + host: 127.0.0.1 + port: 8090 + timeout: 5000 + jsp: + form: + url: http://localhost:${server.port}/mock/jsp-form + json: + url: http://localhost:${server.port}/mock/jsp-json + mci: + url: http://localhost:${server.port}/api/mock/esb/api + mcistring: + url: http://localhost:${server.port}/api/mock/esb/string + +mcp: + security: + tenant-domains: + mcp-client-1: CUSTOMER,COMMON + mcp-client-2: ALL + +axhub: + gateway: + url: http://localhost:8081 + tool: + url: ${AXHUB_TOOL_URL:http://localhost:${server.port}} \ No newline at end of file diff --git a/dap-tool-sms/src/main/resources/application.yml b/dap-tool-sms/src/main/resources/application.yml new file mode 100644 index 000000000..fac526a8f --- /dev/null +++ b/dap-tool-sms/src/main/resources/application.yml @@ -0,0 +1,19 @@ +server: + port: 8082 +spring: + application: + name: dap-tool-sms + profiles: + active: local +logging: + level: + org.apache.kafka: ERROR +mcp: + namespace: "" + manifest: + bundle-id: tool-sms + # Set the AA-assigned prefix before MCP pull activation (for example: sms.). + name-prefix: "" + security: + tenant-domains: + TESTER-DEV: ALL \ No newline at end of file diff --git a/dap-tool-sms/src/main/resources/logback-spring.xml b/dap-tool-sms/src/main/resources/logback-spring.xml new file mode 100644 index 000000000..0fac150b5 --- /dev/null +++ b/dap-tool-sms/src/main/resources/logback-spring.xml @@ -0,0 +1,39 @@ + + + + + + + + + + ${LOG_PATTERN} + + + + + + + + + /swlog/dap-tool-sms/A01/${HOSTNAME}_A01.log + + + /swlog/dap-tool-sms/A01/${HOSTNAME}_A01_%d{yyyyMMdd}.log + + 30 + + + ${LOG_PATTERN} + + + + + + + + + + + + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..214d7642b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,92 @@ +name: ax_hub_mcp_tool + +services: + redis: + image: redis:latest + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + + + mci-mock: + image: wiremock/wiremock:latest + ports: + - "8089:8080" + volumes: + - ./mci-mock:/home/wiremock + + tool-sms: + build: + context: . + dockerfile: dap-tool-sms/Dockerfile + ports: + - "8282:8082" + depends_on: + - redis + environment: + - TZ=Asia/Seoul + - SPRING_REDIS_HOST=redis + - SPRING_REDIS_PORT=6379 + - SPRING_DATA_REDIS_PORT=6379 + - AXHUB_GATEWAY_URL=http://gateway:8081 + - AXHUB_SOURCE_DIR=/src + - AXHUB_TOOL_URL=http://tool-sms:8082 + - GLOW_COMMUNICATION_MCI_HOMT=http://mci-mock + - GLOW_COMMUNICATION_MCI_PORT=8080 + - GLOW_COMMUNICATION_EXTMCI_HOMT=http://mci-mock + - GLOW_COMMUNICATION_EXTMCI_PORT=8080 + - GLOW_COMMUNICATION_EAI_HOMT=http://mci-mock + - GLOW_COMMUNICATION_EAI_PORT=8080 + - SPRING_PROFILES_ACTIVE=${ACTIVE_PROFILE:-local} + + tool-oth: + build: + context: . + dockerfile: dap-tool-oth/Dockerfile + ports: + - "8284:8084" + depends_on: + - redis + environment: + - TZ=Asia/Seoul + - SPRING_REDIS_HOST=redis + - SPRING_REDIS_PORT=6379 + - SPRING_DATA_REDIS_PORT=6379 + - AXHUB_GATEWAY_URL=http://gateway:8081 + - AXHUB_TOOL_URL=http://tool-oth:8084 + - GLOW_COMMUNICATION_MCI_HOMT=http://mci-mock + - GLOW_COMMUNICATION_MCI_PORT=8080 + - GLOW_COMMUNICATION_EXTMCI_HOMT=http://mci-mock + - GLOW_COMMUNICATION_EXTMCI_PORT=8080 + - GLOW_COMMUNICATION_EAI_HOMT=http://mci-mock + - GLOW_COMMUNICATION_EAI_PORT=8080 + - SPRING_PROFILES_ACTIVE=${ACTIVE_PROFILE:-local} + + dozzle: + image: amir20/dozzle:latest + ports: + - "8288:8080" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /home/ubuntu/app/ax_hub_mcp_tool:/app + restart: always + + gitea-runner: + image: gitea/act_runner:latest + environment: + - GITEA_INSTANCE_URL=https://git.devjun.net + - GITEA_RUNNER_REGISTRATION_TOKEN=2N7ciuFIp6Jm29ewtzfl9ruQtpwGgB4ngFcXC2lF + - GITEA_RUNNER_NAME=ociwp-runner + - GLOW_COMMUNICATION_EXTMCI_PORT=8080 + - GITEA_RUNNER_LABELS=ubuntu-latest,ubuntu-22.04,linux-arm64 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /home/ubuntu/app/ax_hub_mcp_tool:/app + - ./act_runner_data:/data + - ./bin/docker:/usr/bin/docker + - /usr/libexec/docker/cli-plugins/docker-compose:/usr/libexec/docker/cli-plugins/docker-compose + restart: always diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..1b33c55baabb587c669f562ae36f953de2481846 GIT binary patch literal 43764 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..8de1053a1 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS=-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mci-mock/mappings/extmci.json b/mci-mock/mappings/extmci.json new file mode 100644 index 000000000..c7998968e --- /dev/null +++ b/mci-mock/mappings/extmci.json @@ -0,0 +1,21 @@ +{ + "request": { + "method": "POST", + "urlPath": "/extmci" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json;charset=UTF-8" + }, + "jsonBody": { + "resCode": "0000", + "resMsg": "정상 처리되었습니다.", + "data": { + "status": "SUCCESS", + "mock": true, + "detail": "가짜 응답 서버(extmci)에서 내려주는 샘플 데이터입니다." + } + } + } +} diff --git a/mci-mock/mappings/mci-send.json b/mci-mock/mappings/mci-send.json new file mode 100644 index 000000000..620d41a7a --- /dev/null +++ b/mci-mock/mappings/mci-send.json @@ -0,0 +1,21 @@ +{ + "request": { + "method": "POST", + "urlPath": "/mciSend" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json;charset=UTF-8" + }, + "jsonBody": { + "resCode": "0000", + "resMsg": "정상 처리되었습니다.", + "data": { + "status": "SUCCESS", + "mock": true, + "detail": "가짜 응답 서버에서 내려주는 샘플 데이터입니다." + } + } + } +} diff --git a/mci_sample_test.http b/mci_sample_test.http new file mode 100644 index 000000000..fd34ef20e --- /dev/null +++ b/mci_sample_test.http @@ -0,0 +1,35 @@ +### 대내 MCI 샘플 툴(send_mci_sample) 테스트 +# 이 스크립트는 IntelliJ IDEA의 HTTP Client 플러그인을 위한 테스트 파일입니다. +# 1. 서버(Gateway 및 Tool 모듈)를 먼저 실행해 주세요. +# 2. 아래 초록색 플레이(▶) 버튼을 누르면 툴 함수가 호출됩니다! + +POST http://localhost:8081/mcp/api/v1/tools/call +Content-Type: application/json +Accept: application/json + +{ + "jsonrpc": "2.0", + "id": "test-1234", + "params": { + "name": "other_send_mci_sample", + "arguments": { + "tgrmCmnnhddValu": { + "envrTypeCd": "D", + "itrIfId": "IF_TEST_001" + }, + "tgrmMsdvValu": { + "msgHddvValu": { + "msgTnsmTypeCd": "0" + }, + "msgDtdvValu": { + "msgCd": "0000" + } + }, + "tgrmDtdvValu": { + "customerName": "홍길동", + "targetDate": "20260901", + "remarks": "MCI 연동 테스트" + } + } + } +} diff --git a/scratch_rewrite.py b/scratch_rewrite.py new file mode 100644 index 000000000..98a69d9f4 --- /dev/null +++ b/scratch_rewrite.py @@ -0,0 +1,66 @@ +import yaml +import copy + +with open('docker-compose.yml', 'r') as f: + compose = yaml.safe_load(f) + +services = compose['services'] +new_services = {} + +# Services to duplicate +app_services = ['gateway', 'tool-sms', 'tool-email', 'tool-oth', 'tool-payment'] + +for name, svc in services.items(): + if name in app_services: + # Create blue + blue_svc = copy.deepcopy(svc) + if 'ports' in blue_svc: + del blue_svc['ports'] # Nginx will handle ports + + # Update AXHUB_GATEWAY_URL and AXHUB_TOOL_URL to point to blue if they refer to the base name + if 'environment' in blue_svc: + env = blue_svc['environment'] + for i, e in enumerate(env): + if isinstance(e, str): + env[i] = e.replace('http://gateway:8081', 'http://gateway-blue:8081') + env[i] = env[i].replace(f'http://{name}:', f'http://{name}-blue:') + + new_services[f'{name}-blue'] = blue_svc + + # Create green + green_svc = copy.deepcopy(svc) + if 'ports' in green_svc: + del green_svc['ports'] + + if 'environment' in green_svc: + env = green_svc['environment'] + for i, e in enumerate(env): + if isinstance(e, str): + env[i] = e.replace('http://gateway-blue:8081', 'http://gateway-green:8081') # replace from previous + env[i] = e.replace('http://gateway:8081', 'http://gateway-green:8081') + env[i] = env[i].replace(f'http://{name}:', f'http://{name}-green:') + + new_services[f'{name}-green'] = green_svc + else: + new_services[name] = svc + +# Add nginx +new_services['nginx'] = { + 'image': 'nginx:latest', + 'ports': [ + '8281:8281', + '8282:8282', + '8283:8283', + '8284:8284', + '8285:8285' + ], + 'volumes': [ + './nginx/conf.d:/etc/nginx/conf.d' + ], + 'depends_on': ['redis'] +} + +compose['services'] = new_services + +with open('docker-compose.yml', 'w') as f: + yaml.dump(compose, f, sort_keys=False, default_flow_style=False) diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000..fce083d94 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,5 @@ +rootProject.name = 'dap-tool' + +include 'dap-tool-core' +include 'dap-tool-sms' +include 'dap-tool-oth' diff --git a/sql.txt b/sql.txt new file mode 100644 index 000000000..959f76b92 --- /dev/null +++ b/sql.txt @@ -0,0 +1,283 @@ +-------------------------------------------------------------------------------- +-- ?곸슜?쒖뒪?쒕챸 : ?댁쁺怨?ORACLE) +-- ?뚯씠釉?: ZT_?듯빀肄붾뱶 +-- ?뚯씠釉봊D : ZT_UNFC_CD +-------------------------------------------------------------------------------- +CREATE TABLE S_NNZ.ZT_UNFC_CD +( + UNFC_CD_ID VARCHAR2(50) NOT NULL, + UNFC_CD_NM VARCHAR2(100) NULL, + UNFC_CD_HAN_NM VARCHAR2(100) NULL, + UNFC_CD_ENG_NM VARCHAR2(100) NULL, + UNFC_CD_DS VARCHAR2(4000) NULL, + META_SYST_CD VARCHAR2(3) NOT NULL, + META_DUTJ_TYPE_CD VARCHAR2(5) NOT NULL, + PUSE_YN VARCHAR2(1) NOT NULL, + SPPO_UNFC_CD_ID VARCHAR2(50) NULL, + DATA_LOAD_DT DATE NULL, + SYST_RGI_DT DATE NOT NULL, + SYST_RGI_PRAF_NO VARCHAR2(8) NOT NULL, + SYST_RGI_OGNZ_NO VARCHAR2(7) NOT NULL, + SYST_RGI_SYST_CD VARCHAR2(3) NOT NULL, + SYST_RGI_PRGR_ID VARCHAR2(100) NOT NULL, + SYST_CHG_DT DATE NOT NULL, + SYST_CHG_PRAF_NO VARCHAR2(8) NOT NULL, + SYST_CHG_OGNZ_NO VARCHAR2(7) NOT NULL, + SYST_CHG_SYST_CD VARCHAR2(3) NOT NULL, + SYST_CHG_PRGR_ID VARCHAR2(100) NOT NULL +) +TABLESPACE TS_ZT_D01; + + + +COMMENT ON TABLE S_NNZ.ZT_UNFC_CD IS 'ZT_?듯빀肄붾뱶'; + +COMMENT ON COLUMN S_NNZ.ZT_UNFC_CD.UNFC_CD_ID IS '?듯빀肄붾뱶ID'; +COMMENT ON COLUMN S_NNZ.ZT_UNFC_CD.UNFC_CD_NM IS '?듯빀肄붾뱶紐?; +COMMENT ON COLUMN S_NNZ.ZT_UNFC_CD.UNFC_CD_HAN_NM IS '?듯빀肄붾뱶?쒓?紐?; +COMMENT ON COLUMN S_NNZ.ZT_UNFC_CD.UNFC_CD_ENG_NM IS '?듯빀肄붾뱶?곷Ц紐?; +COMMENT ON COLUMN S_NNZ.ZT_UNFC_CD.UNFC_CD_DS IS '?듯빀肄붾뱶?ㅻ챸'; +COMMENT ON COLUMN S_NNZ.ZT_UNFC_CD.META_SYST_CD IS '硫뷀??쒖뒪?쒖퐫??; +COMMENT ON COLUMN S_NNZ.ZT_UNFC_CD.META_DUTJ_TYPE_CD IS '硫뷀??낅Т?좏삎肄붾뱶'; +COMMENT ON COLUMN S_NNZ.ZT_UNFC_CD.PUSE_YN IS '?ъ슜?щ?'; +COMMENT ON COLUMN S_NNZ.ZT_UNFC_CD.SPPO_UNFC_CD_ID IS '?곸쐞?듯빀肄붾뱶ID'; +COMMENT ON COLUMN S_NNZ.ZT_UNFC_CD.DATA_LOAD_DT IS '?곗씠?곗쟻?ъ씪??; +COMMENT ON COLUMN S_NNZ.ZT_UNFC_CD.SYST_RGI_DT IS '?쒖뒪?쒕벑濡앹씪??; +COMMENT ON COLUMN S_NNZ.ZT_UNFC_CD.SYST_RGI_PRAF_NO IS '?쒖뒪?쒕벑濡앹씤?щ쾲??; +COMMENT ON COLUMN S_NNZ.ZT_UNFC_CD.SYST_RGI_OGNZ_NO IS '?쒖뒪?쒕벑濡앹“吏곷쾲??; +COMMENT ON COLUMN S_NNZ.ZT_UNFC_CD.SYST_RGI_SYST_CD IS '?쒖뒪?쒕벑濡앹떆?ㅽ뀥肄붾뱶'; +COMMENT ON COLUMN S_NNZ.ZT_UNFC_CD.SYST_RGI_PRGR_ID IS '?쒖뒪?쒕벑濡앺봽濡쒓렇?쭵D'; +COMMENT ON COLUMN S_NNZ.ZT_UNFC_CD.SYST_CHG_DT IS '?쒖뒪?쒕?寃쎌씪??; +COMMENT ON COLUMN S_NNZ.ZT_UNFC_CD.SYST_CHG_PRAF_NO IS '?쒖뒪?쒕?寃쎌씤?щ쾲??; +COMMENT ON COLUMN S_NNZ.ZT_UNFC_CD.SYST_CHG_OGNZ_NO IS '?쒖뒪?쒕?寃쎌“吏곷쾲??; +COMMENT ON COLUMN S_NNZ.ZT_UNFC_CD.SYST_CHG_SYST_CD IS '?쒖뒪?쒕?寃쎌떆?ㅽ뀥肄붾뱶'; +COMMENT ON COLUMN S_NNZ.ZT_UNFC_CD.SYST_CHG_PRGR_ID IS '?쒖뒪?쒕?寃쏀봽濡쒓렇?쭵D'; + + + +-- 媛쒕퀎沅뚰븳 Role +GRANT SELECT ON S_NNZ.ZT_UNFC_CD TO RL_A_EAI_NNZ_SEL; + +GRANT INSERT,UPDATE,DELETE ON S_NNZ.ZT_UNFC_CD TO RL_A_EAI_NNZ_IUD; + +-- Default Role +GRANT SELECT ON S_NNZ.ZT_UNFC_CD TO RL_NNZ_SEL; + +GRANT INSERT,UPDATE,DELETE ON S_NNZ.ZT_UNFC_CD TO RL_NNZ_IUD; + + + +CREATE INDEX S_NNZ.IX_ZT_UNFC_CD_01 ON S_NNZ.ZT_UNFC_CD +(UNFC_CD_NM) + TABLESPACE TS_ZT_I01; + +CREATE INDEX S_NNZ.IX_ZT_UNFC_CD_02 ON S_NNZ.ZT_UNFC_CD +(UNFC_CD_HAN_NM) + TABLESPACE TS_ZT_I01; + +CREATE INDEX S_NNZ.IX_ZT_UNFC_CD_03 ON S_NNZ.ZT_UNFC_CD +(UNFC_CD_ENG_NM) + TABLESPACE TS_ZT_I01; + + +CREATE UNIQUE INDEX S_NNZ.PK_ZT_UNFC_CD ON S_NNZ.ZT_UNFC_CD + (UNFC_CD_ID) + TABLESPACE TS_ZT_I01; + +ALTER TABLE S_NNZ.ZT_UNFC_CD ADD ( + CONSTRAINT PK_ZT_UNFC_CD + PRIMARY KEY (UNFC_CD_ID) +USING INDEX S_NNZ.PK_ZT_UNFC_CD); + +-------------------------------------------------------------------------------- +-- ?곸슜?쒖뒪?쒕챸 : 媛쒕컻怨?ORACLE) +-- ?뚯씠釉?: ZT_?듯빀肄붾뱶?곸꽭 +-- ?뚯씠釉봊D : ZT_UNFC_CD_DTPT +-------------------------------------------------------------------------------- +CREATE TABLE S_TFG.ZT_UNFC_CD_DTPT +( + UNFC_CD_ID VARCHAR2(50) NOT NULL, + CD_VLDT_VALU VARCHAR2(50) NOT NULL, + CD_VLDT_VALU_INQR_SEQ NUMBER(5) NULL, + CD_VLDT_VALU_NM VARCHAR2(400) NULL, + CD_VLDT_VALU_HNGL_ABR_NM VARCHAR2(200) NULL, + CD_VLDT_VALU_ENGC_ABR_NM VARCHAR2(200) NULL, + CD_VLDT_VALU_HAN_NM VARCHAR2(400) NULL, + CD_VLDT_VALU_ENG_NM VARCHAR2(400) NULL, + CD_VLDT_VALU_DS VARCHAR2(4000) NULL, + CD_ASRT_TYPE_BIT_CD VARCHAR2(20) NOT NULL, + SPPO_CD_VLDT_VALU VARCHAR2(50) NULL, + SPPO_UNFC_CD_ID VARCHAR2(50) NULL, + VLDT_STRT_YMD DATE NOT NULL, + VLDT_END_YMD DATE NOT NULL, + USER_DEF_VALU_1 VARCHAR2(100) NULL, + USER_DEF_VALU_2 VARCHAR2(100) NULL, + SYST_RGI_DT DATE NOT NULL, + SYST_RGI_PRAF_NO VARCHAR2(8) NOT NULL, + SYST_RGI_OGNZ_NO VARCHAR2(7) NOT NULL, + SYST_RGI_SYST_CD VARCHAR2(3) NOT NULL, + SYST_RGI_PRGR_ID VARCHAR2(100) NOT NULL, + SYST_CHG_DT DATE NOT NULL, + SYST_CHG_PRAF_NO VARCHAR2(8) NOT NULL, + SYST_CHG_OGNZ_NO VARCHAR2(7) NOT NULL, + SYST_CHG_SYST_CD VARCHAR2(3) NOT NULL, + SYST_CHG_PRGR_ID VARCHAR2(100) NOT NULL, + DATA_LOAD_DT DATE NOT NULL +) +TABLESPACE TS_TFG_D01; + + + +COMMENT ON TABLE S_TFG.ZT_UNFC_CD_DTPT IS 'ZT_?듯빀肄붾뱶?곸꽭'; + +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.UNFC_CD_ID IS '?듯빀肄붾뱶ID'; +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.CD_VLDT_VALU IS '肄붾뱶?좏슚媛?; +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.CD_VLDT_VALU_INQR_SEQ IS '肄붾뱶?좏슚媛믪“?뚯닚??; +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.CD_VLDT_VALU_NM IS '肄붾뱶?좏슚媛믩챸'; +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.CD_VLDT_VALU_HNGL_ABR_NM IS '肄붾뱶?좏슚媛믫븳湲€?쎌뼱紐?; +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.CD_VLDT_VALU_ENGC_ABR_NM IS '肄붾뱶?좏슚媛믪쁺臾몄빟?대챸'; +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.CD_VLDT_VALU_HAN_NM IS '肄붾뱶?좏슚媛믫븳湲€紐?; +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.CD_VLDT_VALU_ENG_NM IS '肄붾뱶?좏슚媛믪쁺臾몃챸'; +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.CD_VLDT_VALU_DS IS '肄붾뱶?좏슚媛믪꽕紐?; +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.CD_ASRT_TYPE_BIT_CD IS '肄붾뱶遺꾨쪟?좏삎鍮꾪듃肄붾뱶'; +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.SPPO_CD_VLDT_VALU IS '?곸쐞肄붾뱶?좏슚媛?; +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.SPPO_UNFC_CD_ID IS '?곸쐞?듯빀肄붾뱶ID'; +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.VLDT_STRT_YMD IS '?좏슚?쒖옉?쇱옄'; +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.VLDT_END_YMD IS '?좏슚醫낅즺?쇱옄'; +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.USER_DEF_VALU_1 IS '?ъ슜?먯젙?섍컪1'; +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.USER_DEF_VALU_2 IS '?ъ슜?먯젙?섍컪2'; +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.SYST_RGI_DT IS '?쒖뒪?쒕벑濡앹씪??; +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.SYST_RGI_PRAF_NO IS '?쒖뒪?쒕벑濡앹씤?щ쾲??; +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.SYST_RGI_OGNZ_NO IS '?쒖뒪?쒕벑濡앹“吏곷쾲??; +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.SYST_RGI_SYST_CD IS '?쒖뒪?쒕벑濡앹떆?ㅽ뀥肄붾뱶'; +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.SYST_RGI_PRGR_ID IS '?쒖뒪?쒕벑濡앺봽濡쒓렇?쭵D'; +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.SYST_CHG_DT IS '?쒖뒪?쒕?寃쎌씪??; +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.SYST_CHG_PRAF_NO IS '?쒖뒪?쒕?寃쎌씤?щ쾲??; +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.SYST_CHG_OGNZ_NO IS '?쒖뒪?쒕?寃쎌“吏곷쾲??; +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.SYST_CHG_SYST_CD IS '?쒖뒪?쒕?寃쎌떆?ㅽ뀥肄붾뱶'; +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.SYST_CHG_PRGR_ID IS '?쒖뒪?쒕?寃쏀봽濡쒓렇?쭵D'; +COMMENT ON COLUMN S_TFG.ZT_UNFC_CD_DTPT.DATA_LOAD_DT IS '?곗씠?곗쟻?ъ씪??; + + + +-- Default Role +GRANT INSERT,UPDATE,DELETE ON S_TFG.ZT_UNFC_CD_DTPT TO RL_TFG_IUD; + +GRANT SELECT ON S_TFG.ZT_UNFC_CD_DTPT TO RL_TFG_SEL; + + + +CREATE UNIQUE INDEX S_TFG.PK_ZT_UNFC_CD_DTPT ON S_TFG.ZT_UNFC_CD_DTPT + (UNFC_CD_ID,CD_VLDT_VALU) + TABLESPACE TS_TFG_I01; + +ALTER TABLE S_TFG.ZT_UNFC_CD_DTPT ADD ( + CONSTRAINT PK_ZT_UNFC_CD_DTPT + PRIMARY KEY (UNFC_CD_ID,CD_VLDT_VALU) +USING INDEX S_TFG.PK_ZT_UNFC_CD_DTPT); + +------------------------------------------------------------------------------------------- +-- ZT_MENU ?뚯씠釉??앹꽦 (硫붾돱 湲곕낯 ?뺣낫 ?€?? +CREATE TABLE ZT_MENU ( + MENU_ID INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- ?먮룞 利앷? ID (PK) + PARENT_MENU_ID INTEGER, -- 遺€紐?硫붾돱 ID (FK, NULL 媛€??for 猷⑦듃 硫붾돱) + NAME VARCHAR(100), -- 而댄룷?뚰듃 ?대쫫 (?듭뀛?? + PATH VARCHAR(255) NOT NULL, -- ?쇱슦??寃쎈줈 (?꾩닔) + TITLE VARCHAR(100) NOT NULL, -- 硫붾돱 ?대쫫 (meta.title, ?꾩닔) + ICON VARCHAR(50), -- 硫붾돱 ?꾩씠肄?(?듭뀛?? + SHOW_BADGE VARCHAR(1) DEFAULT 'N' CHECK (SHOW_BADGE IN ('Y', 'N')), -- 諛곗? ?쒖떆 ?щ? (Y/N, ?듭뀛?? + SHOW_TEXT_BADGE VARCHAR(50), -- ?띿뒪??諛곗? ?쒖떆 (?듭뀛?? + IS_HIDE VARCHAR(1) DEFAULT 'N' CHECK (IS_HIDE IN ('Y', 'N')), -- 硫붾돱 ?④? ?щ? (Y/N, ?듭뀛?? + IS_HIDE_TAB VARCHAR(1) DEFAULT 'N' CHECK (IS_HIDE_TAB IN ('Y', 'N')),-- ???④? ?щ? (Y/N, ?듭뀛?? + LINK VARCHAR(255), -- 留곹겕 (?듭뀛?? + IS_IFRAME VARCHAR(1) DEFAULT 'N' CHECK (IS_IFRAME IN ('Y', 'N')), -- iframe ?щ? (Y/N, ?듭뀛?? + KEEP_ALIVE VARCHAR(1) DEFAULT 'N' CHECK (KEEP_ALIVE IN ('Y', 'N')), -- 罹먯떆 ?щ? (Y/N, ?듭뀛?? + ORDER_SEQ INTEGER DEFAULT 0 NOT NULL, -- 硫붾돱 ?쒖떆 ?쒖꽌 (?뺣젹?? ?꾩닔) + IS_ACTIVE VARCHAR(1) DEFAULT 'Y' CHECK (IS_ACTIVE IN ('Y', 'N')), -- ?쒖꽦 ?щ? (Y/N, 湲곕낯 Y) + SYST_RGI_DT DATE, -- ?쒖뒪?쒕벑濡앹씪?? + SYST_RGI_PRAF_NO VARCHAR(50), -- ?쒖뒪?쒕벑濡앹씤?щ쾲?? + SYST_RGI_OGNZ_NO VARCHAR(50), -- ?쒖뒪?쒕벑濡앹“吏곷쾲?? + SYST_RGI_SYST_CD VARCHAR(50), -- ?쒖뒪?쒕벑濡앹떆?ㅽ뀥肄붾뱶 + SYST_RGI_PRGR_ID VARCHAR(50), -- ?쒖뒪?쒕벑濡앺봽濡쒓렇?쭵D + SYST_CHG_DT DATE, -- ?쒖뒪?쒕?寃쎌씪?? + SYST_CHG_PRAF_NO VARCHAR(50), -- ?쒖뒪?쒕?寃쎌씤?щ쾲?? + SYST_CHG_OGNZ_NO VARCHAR(50), -- ?쒖뒪?쒕?寃쎌“吏곷쾲?? + SYST_CHG_SYST_CD VARCHAR(50), -- ?쒖뒪?쒕?寃쎌떆?ㅽ뀥肄붾뱶 + SYST_CHG_PRGR_ID VARCHAR(50) -- ?쒖뒪?쒕?寃쏀봽濡쒓렇?쭵D +); + +-- ?쒖빟 議곌굔 異붽? +ALTER TABLE ZT_MENU ADD CONSTRAINT FK_ZT_MENU_PARENT FOREIGN KEY (PARENT_MENU_ID) REFERENCES ZT_MENU (MENU_ID) ON DELETE CASCADE; -- 遺€紐???젣 ???먯떇????젣 (怨꾩링 ??젣 吏€?? + +-- ?몃뜳???앹꽦 (議고쉶 理쒖쟻?? +CREATE INDEX IDX_ZT_MENU_PARENT ON ZT_MENU (PARENT_MENU_ID); +CREATE INDEX IDX_ZT_MENU_PATH ON ZT_MENU (PATH); + +-- 而щ읆 肄붾찘??異붽? +COMMENT ON COLUMN ZT_MENU.MENU_ID IS '硫붾돱 ID (PK)'; +COMMENT ON COLUMN ZT_MENU.PARENT_MENU_ID IS '遺€紐?硫붾돱 ID (FK)'; +COMMENT ON COLUMN ZT_MENU.NAME IS '而댄룷?뚰듃 ?대쫫'; +COMMENT ON COLUMN ZT_MENU.PATH IS '?쇱슦??寃쎈줈'; +COMMENT ON COLUMN ZT_MENU.TITLE IS '硫붾돱 ?대쫫'; +COMMENT ON COLUMN ZT_MENU.ICON IS '硫붾돱 ?꾩씠肄?; +COMMENT ON COLUMN ZT_MENU.SHOW_BADGE IS '諛곗? ?쒖떆 ?щ? (Y/N)'; +COMMENT ON COLUMN ZT_MENU.SHOW_TEXT_BADGE IS '?띿뒪??諛곗? ?쒖떆'; +COMMENT ON COLUMN ZT_MENU.IS_HIDE IS '硫붾돱 ?④? ?щ? (Y/N)'; +COMMENT ON COLUMN ZT_MENU.IS_HIDE_TAB IS '???④? ?щ? (Y/N)'; +COMMENT ON COLUMN ZT_MENU.LINK IS '留곹겕'; +COMMENT ON COLUMN ZT_MENU.IS_IFRAME IS 'iframe ?щ? (Y/N)'; +COMMENT ON COLUMN ZT_MENU.KEEP_ALIVE IS '罹먯떆 ?щ? (Y/N)'; +COMMENT ON COLUMN ZT_MENU.ORDER_SEQ IS '硫붾돱 ?쒖떆 ?쒖꽌'; +COMMENT ON COLUMN ZT_MENU.IS_ACTIVE IS '?쒖꽦 ?щ? (Y/N)'; +COMMENT ON COLUMN ZT_MENU.SYST_RGI_DT IS '?쒖뒪?쒕벑濡앹씪??; +COMMENT ON COLUMN ZT_MENU.SYST_RGI_PRAF_NO IS '?쒖뒪?쒕벑濡앹씤?щ쾲??; +COMMENT ON COLUMN ZT_MENU.SYST_RGI_OGNZ_NO IS '?쒖뒪?쒕벑濡앹“吏곷쾲??; +COMMENT ON COLUMN ZT_MENU.SYST_RGI_SYST_CD IS '?쒖뒪?쒕벑濡앹떆?ㅽ뀥肄붾뱶'; +COMMENT ON COLUMN ZT_MENU.SYST_RGI_PRGR_ID IS '?쒖뒪?쒕벑濡앺봽濡쒓렇?쭵D'; +COMMENT ON COLUMN ZT_MENU.SYST_CHG_DT IS '?쒖뒪?쒕?寃쎌씪??; +COMMENT ON COLUMN ZT_MENU.SYST_CHG_PRAF_NO IS '?쒖뒪?쒕?寃쎌씤?щ쾲??; +COMMENT ON COLUMN ZT_MENU.SYST_CHG_OGNZ_NO IS '?쒖뒪?쒕?寃쎌“吏곷쾲??; +COMMENT ON COLUMN ZT_MENU.SYST_CHG_SYST_CD IS '?쒖뒪?쒕?寃쎌떆?ㅽ뀥肄붾뱶'; +COMMENT ON COLUMN ZT_MENU.SYST_CHG_PRGR_ID IS '?쒖뒪?쒕?寃쏀봽濡쒓렇?쭵D'; + +-- ZT_MENU_AUTH ?뚯씠釉??앹꽦 (硫붾돱 沅뚰븳 紐⑸줉 ?€?? +CREATE TABLE ZT_MENU_AUTH ( + AUTH_ID INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- ?먮룞 利앷? ID (PK) + MENU_ID INTEGER NOT NULL, -- 硫붾돱 ID (FK) + TITLE VARCHAR(100) NOT NULL, -- 沅뚰븳 ?쒕ぉ (e.g., ''異붽?'', ''?몄쭛'') + AUTH_MARK VARCHAR(100) NOT NULL, -- 沅뚰븳 留덊겕 (e.g., ''add'', ''edit'') + ORDER_SEQ INTEGER DEFAULT 0 NOT NULL, -- 沅뚰븳 ?쒖떆 ?쒖꽌 (?뺣젹?? + SYST_RGI_DT DATE, -- ?쒖뒪?쒕벑濡앹씪?? + SYST_RGI_PRAF_NO VARCHAR(50), -- ?쒖뒪?쒕벑濡앹씤?щ쾲?? + SYST_RGI_OGNZ_NO VARCHAR(50), -- ?쒖뒪?쒕벑濡앹“吏곷쾲?? + SYST_RGI_SYST_CD VARCHAR(50), -- ?쒖뒪?쒕벑濡앹떆?ㅽ뀥肄붾뱶 + SYST_RGI_PRGR_ID VARCHAR(50), -- ?쒖뒪?쒕벑濡앺봽濡쒓렇?쭵D + SYST_CHG_DT DATE, -- ?쒖뒪?쒕?寃쎌씪?? + SYST_CHG_PRAF_NO VARCHAR(50), -- ?쒖뒪?쒕?寃쎌씤?щ쾲?? + SYST_CHG_OGNZ_NO VARCHAR(50), -- ?쒖뒪?쒕?寃쎌“吏곷쾲?? + SYST_CHG_SYST_CD VARCHAR(50), -- ?쒖뒪?쒕?寃쎌떆?ㅽ뀥肄붾뱶 + SYST_CHG_PRGR_ID VARCHAR(50) -- ?쒖뒪?쒕?寃쏀봽濡쒓렇?쭵D +); + +-- ?쒖빟 議곌굔 異붽? +ALTER TABLE ZT_MENU_AUTH ADD CONSTRAINT FK_ZT_MENU_AUTH_MENU FOREIGN KEY (MENU_ID) REFERENCES ZT_MENU (MENU_ID) ON DELETE CASCADE; -- 硫붾돱 ??젣 ??沅뚰븳????젣 + +-- ?몃뜳???앹꽦 +CREATE INDEX IDX_ZT_MENU_AUTH_MENU ON ZT_MENU_AUTH (MENU_ID); + +-- 而щ읆 肄붾찘??異붽? +COMMENT ON COLUMN ZT_MENU_AUTH.AUTH_ID IS '沅뚰븳 ID (PK)'; +COMMENT ON COLUMN ZT_MENU_AUTH.MENU_ID IS '硫붾돱 ID (FK)'; +COMMENT ON COLUMN ZT_MENU_AUTH.TITLE IS '沅뚰븳 ?쒕ぉ'; +COMMENT ON COLUMN ZT_MENU_AUTH.AUTH_MARK IS '沅뚰븳 留덊겕'; +COMMENT ON COLUMN ZT_MENU_AUTH.ORDER_SEQ IS '沅뚰븳 ?쒖떆 ?쒖꽌'; +COMMENT ON COLUMN ZT_MENU_AUTH.SYST_RGI_DT IS '?쒖뒪?쒕벑濡앹씪??; +COMMENT ON COLUMN ZT_MENU_AUTH.SYST_RGI_PRAF_NO IS '?쒖뒪?쒕벑濡앹씤?щ쾲??; +COMMENT ON COLUMN ZT_MENU_AUTH.SYST_RGI_OGNZ_NO IS '?쒖뒪?쒕벑濡앹“吏곷쾲??; +COMMENT ON COLUMN ZT_MENU_AUTH.SYST_RGI_SYST_CD IS '?쒖뒪?쒕벑濡앹떆?ㅽ뀥肄붾뱶'; +COMMENT ON COLUMN ZT_MENU_AUTH.SYST_RGI_PRGR_ID IS '?쒖뒪?쒕벑濡앺봽濡쒓렇?쭵D'; +COMMENT ON COLUMN ZT_MENU_AUTH.SYST_CHG_DT IS '?쒖뒪?쒕?寃쎌씪??; +COMMENT ON COLUMN ZT_MENU_AUTH.SYST_CHG_PRAF_NO IS '?쒖뒪?쒕?寃쎌씤?щ쾲??; +COMMENT ON COLUMN ZT_MENU_AUTH.SYST_CHG_OGNZ_NO IS '?쒖뒪?쒕?寃쎌“吏곷쾲??; +COMMENT ON COLUMN ZT_MENU_AUTH.SYST_CHG_SYST_CD IS '?쒖뒪?쒕?寃쎌떆?ㅽ뀥肄붾뱶'; +COMMENT ON COLUMN ZT_MENU_AUTH.SYST_CHG_PRGR_ID IS '?쒖뒪?쒕?寃쏀봽濡쒓렇?쭵D'; diff --git a/sse_out.txt b/sse_out.txt new file mode 100644 index 000000000..085415956 --- /dev/null +++ b/sse_out.txt @@ -0,0 +1,2 @@ +event:endpoint +data:/mcp/message/common?sessionId=b21c1837-e1ef-43d6-890d-5a57ca7428e7