From 9ea8540979859a02ffed221d91cf7787ce38d9aa Mon Sep 17 00:00:00 2001 From: Macbook Date: Thu, 23 Jul 2026 12:46:52 +0900 Subject: [PATCH] Deploy: GitHub submodule sync 2026-07-23 12:46:52 --- .gitmodules | 16 + chatbotApi-chatbot2.0-agent | 1 + chatbotApi-chatbot2.0-agent/.gitignore | 148 - .../RAG_API_IMPLEMENTATION.md | 283 -- chatbotApi-chatbot2.0-agent/README.md | 102 - chatbotApi-chatbot2.0-agent/build.gradle | 48 - chatbotApi-chatbot2.0-agent/env.example | 15 - chatbotApi-chatbot2.0-agent/gradle.properties | 7 - .../gradle/wrapper/gradle-wrapper.jar | Bin 63375 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 - chatbotApi-chatbot2.0-agent/gradlew | 248 -- chatbotApi-chatbot2.0-agent/gradlew.bat | 92 - chatbotApi-chatbot2.0-agent/lombok.config | 2 - chatbotApi-chatbot2.0-agent/settings.gradle | 1 - .../ex/chatbotapi/ChatbotapiApplication.java | 12 - .../ex/chatbotapi/config/AgentProperties.java | 22 - .../chatbotapi/config/KakaoSkillConfig.java | 22 - .../ex/chatbotapi/config/MongoDbSettings.java | 29 - .../ex/chatbotapi/config/OracleDbConfig.java | 69 - .../com/ex/chatbotapi/config/RagConfig.java | 86 - .../ex/chatbotapi/config/RestfulConfig.java | 44 - .../domain/agent/api/AgentApiController.java | 70 - .../domain/agent/api/AgentToolController.java | 72 - .../agent/api/DomainOnlyApiController.java | 68 - .../domain/agent/api/LlmAgentController.java | 26 - .../agent/client/KakaoSkillToolClient.java | 65 - .../domain/agent/dto/AgentQueryRequest.java | 47 - .../domain/agent/dto/AgentQueryResponse.java | 47 - .../dto/CarNoParamNormalizationResult.java | 21 - .../domain/agent/dto/DomainOnlyRequest.java | 31 - .../domain/agent/dto/DomainOnlyResponse.java | 36 - .../domain/agent/dto/IcNameCandidate.java | 15 - .../agent/dto/IcParamNormalizationResult.java | 29 - .../agent/dto/IntentValidationResult.java | 24 - .../domain/agent/dto/LlmAgentRequest.java | 13 - .../domain/agent/dto/LlmAgentResponse.java | 33 - .../domain/agent/dto/PendingIntentState.java | 26 - .../dto/RestAreaParamNormalizationResult.java | 24 - .../domain/agent/dto/ToolExecuteRequest.java | 21 - .../domain/agent/dto/ToolExecuteResponse.java | 32 - .../agent/dto/ToolParamPipelineResult.java | 58 - .../formatter/LlmDomainDataFormatter.java | 20 - .../domain/agent/handler/IntentHandler.java | 29 - .../domain/agent/intent/FetchOwner.java | 7 - .../domain/agent/intent/IntentDefinition.java | 53 - .../intent/IntentDefinitionRegistry.java | 199 -- .../service/AgentOrchestratorService.java | 131 - .../agent/service/CarNoParamNormalizer.java | 75 - .../agent/service/DomainOnlyService.java | 77 - .../agent/service/IcParamNormalizer.java | 149 - .../IntentAnalysisParamNormalizer.java | 51 - .../agent/service/IntentParamValidator.java | 90 - .../agent/service/LlmAgentRouterService.java | 473 --- .../agent/service/PendingIntentService.java | 56 - .../service/RestAreaParamNormalizer.java | 52 - .../agent/service/ToolDefinitionService.java | 86 - .../agent/service/ToolExecutionService.java | 147 - .../agent/service/ToolParamPipeline.java | 68 - .../domain/fare/api/FareApiController.java | 58 - .../formatter/FareRefundLlmFormatter.java | 40 - .../formatter/FareSearchLlmFormatter.java | 105 - .../formatter/FareUnpaidLlmFormatter.java | 64 - .../fare/formatter/IcTelLlmFormatter.java | 49 - .../fare/handler/FareRefundHandler.java | 25 - .../fare/handler/FareSearchHandler.java | 25 - .../fare/handler/FareUnpaidHandler.java | 25 - .../domain/fare/handler/IcTelHandler.java | 25 - .../fare/repository/FareRepository.java | 171 - .../domain/fare/service/FareAmountHelper.java | 163 - .../domain/fare/service/FareService.java | 322 -- .../domain/rag/api/RagApiController.java | 41 - .../domain/rag/client/RagServerClient.java | 76 - .../domain/rag/dto/IntentAnalysisRequest.java | 23 - .../rag/dto/IntentAnalysisResponse.java | 25 - .../domain/rag/dto/RagQueryRequest.java | 27 - .../domain/rag/dto/RagServerRequest.java | 32 - .../domain/rag/dto/RagServerResponse.java | 34 - .../domain/rag/service/RagService.java | 117 - .../restarea/api/RestAreaApiController.java | 31 - .../formatter/BrandShopLlmFormatter.java | 65 - .../formatter/FoodListLlmFormatter.java | 59 - .../formatter/GasstationLlmFormatter.java | 74 - .../formatter/RestAreaPriceLlmFormatter.java | 80 - .../handler/RestAreaPriceHandler.java | 25 - .../repository/RestAreaRepository.java | 45 - .../restarea/service/RestAreaService.java | 78 - .../domain/troad/api/TroadApiController.java | 41 - .../TroadBreakingNewsLlmFormatter.java | 95 - .../TroadCongestionLlmFormatter.java | 125 - .../TroadSearchRouteLlmFormatter.java | 149 - .../handler/TroadBreakingNewsHandler.java | 25 - .../troad/handler/TroadCongestionHandler.java | 25 - .../handler/TroadSearchRouteHandler.java | 32 - .../domain/troad/service/TroadService.java | 255 -- .../src/main/resources/application.yml | 72 - .../src/main/resources/icTel.json | 2240 ------------- .../src/main/resources/intent-definitions.yml | 245 -- .../ChatbotapiApplicationTests.java | 13 - .../service/IntentParamValidatorTest.java | 51 - .../service/ToolDefinitionServiceTest.java | 70 - .../service/ToolExecutionServiceTest.java | 136 - .../fare/service/FareAmountHelperTest.java | 48 - .../domain/rag/dto/RagServerResponseTest.java | 35 - chatbotApp-chatbot2.0-agent | 1 + chatbotApp-chatbot2.0-agent/README.md | 109 - chatbotApp-chatbot2.0-agent/build.sh | 49 - .../docker-compose.yml | 29 - .../embed/chatbot-banner.js | 85 - .../nginx/default.conf | 2 - .../nginx/default.conf.template | 104 - .../nginx/docker-entrypoint.sh | 11 - .../sample/css/minwon-sample.css | 97 - .../sample/css/partcptn-sample.css | 306 -- chatbotApp-chatbot2.0-agent/sample/index.html | 244 -- .../sample/minwon/index.html | 52 - chatbotApp-chatbot2.0-agent/start.sh | 56 - chatbotApp-chatbot2.0-agent/web/.gitignore | 4 - chatbotApp-chatbot2.0-agent/web/next-env.d.ts | 5 - .../web/next.config.mjs | 29 - .../web/package-lock.json | 1993 ----------- chatbotApp-chatbot2.0-agent/web/package.json | 25 - .../web/public/images/giltong.jpg | Bin 51769 -> 0 bytes .../web/scripts/export-public.mjs | 48 - .../web/src/app/globals.css | 1095 ------ .../web/src/app/layout.tsx | 56 - .../web/src/app/page.tsx | 13 - .../web/src/components/ActionButtonList.tsx | 43 - .../web/src/components/ChatApp.tsx | 65 - .../web/src/components/ChatCloseButton.tsx | 29 - .../web/src/components/Composer.tsx | 137 - .../web/src/components/ExBrandTitle.tsx | 38 - .../web/src/components/FaqPanel.tsx | 50 - .../web/src/components/FareResultCard.tsx | 94 - .../web/src/components/ListCardStack.tsx | 104 - .../web/src/components/MarkdownBody.tsx | 118 - .../web/src/components/MessageBubble.tsx | 55 - .../web/src/components/MessageList.tsx | 28 - .../web/src/components/QuickReplyChips.tsx | 34 - .../src/components/RelatedQuestionsBar.tsx | 43 - .../web/src/components/ai/AiChatApp.tsx | 89 - .../web/src/components/ai/AiComposer.tsx | 127 - .../web/src/components/ai/AiMessageBubble.tsx | 53 - .../web/src/components/ai/AiMessageList.tsx | 82 - .../web/src/components/ai/AiVoiceOverlay.tsx | 104 - .../src/components/banner/BannerChatApp.tsx | 419 --- .../components/banner/BannerMessageBubble.tsx | 76 - .../components/voicebot/VoicebotChatApp.tsx | 174 - .../web/src/hooks/useBrowserTts.ts | 53 - .../web/src/hooks/useChatSession.ts | 237 -- .../web/src/hooks/useChatStream.ts | 130 - .../web/src/hooks/useEmbedShell.ts | 14 - .../web/src/hooks/useSpeechRecognition.ts | 247 -- .../web/src/hooks/useVoiceConversation.ts | 527 --- .../web/src/lib/apiCapabilities.ts | 54 - .../web/src/lib/brand.ts | 3 - .../web/src/lib/config.ts | 36 - .../web/src/lib/embedShell.ts | 36 - .../web/src/lib/faqItems.ts | 21 - .../web/src/lib/fareResultCard.ts | 211 -- .../web/src/lib/format.ts | 11 - .../web/src/lib/listCards.ts | 216 -- .../web/src/lib/microphone.ts | 52 - .../web/src/lib/partcptnIntents.ts | 24 - .../web/src/lib/relatedQuestions.ts | 16 - .../web/src/lib/session.ts | 85 - .../web/src/lib/types.ts | 116 - .../web/src/lib/uiMode.ts | 45 - .../web/src/lib/voiceText.ts | 11 - .../web/src/styles/ai-mode.css | 974 ------ .../web/src/styles/banner-mode.css | 443 --- .../web/src/styles/ex-brand.css | 59 - .../web/src/styles/fare-result-card.css | 200 -- .../web/src/styles/voicebot-mode.css | 364 -- .../web/src/types/speech.d.ts | 86 - chatbotApp-chatbot2.0-agent/web/tsconfig.json | 21 - exAiChatBot-chatbot2.0-agent | 1 + exAiChatBot-chatbot2.0-agent/.gitignore | 76 - .../DOCKER_COMMANDS.md | 711 ---- exAiChatBot-chatbot2.0-agent/PROCESS_FLOW.md | 607 ---- exAiChatBot-chatbot2.0-agent/README.md | 79 - .../VECTORDB_ADMIN_API.md | 308 -- exAiChatBot-chatbot2.0-agent/clean-more.sh | 34 - .../create-offline-package-slim.sh | 428 --- .../docker-compose-slim.yml | 231 -- .../docker/base-slim.Dockerfile | 31 - .../docker/base.Dockerfile | 32 - .../docker/batch-slim.Dockerfile | 32 - .../docker/batch.Dockerfile | 6 - .../docker/service-slim.Dockerfile | 38 - .../docker/service.Dockerfile | 7 - exAiChatBot-chatbot2.0-agent/env.example | 59 - exAiChatBot-chatbot2.0-agent/env.template | 242 -- exAiChatBot-chatbot2.0-agent/force-reindex.sh | 55 - .../llmServer/docker 실행 명령어 | 78 - .../llmServer/reranker_server.py | 80 - .../old_docs/FLOW_DIAGRAM.md | 443 --- .../old_docs/OFFLINE_QUICKSTART.md | 210 -- .../old_docs/QUICKSTART.md | 200 -- .../old_docs/RAG_PIPELINE.md | 362 -- .../old_docs/README.md | 416 --- .../old_docs/TROUBLESHOOTING.md | 200 -- exAiChatBot-chatbot2.0-agent/reset-repo.sh | 40 - .../run-with-excel-col34.sh | 96 - .../run-with-excel.sh | 161 - .../scripts/admin_service.py | 582 ---- .../scripts/agent/__init__.py | 0 .../scripts/agent/agent_service.py | 907 ----- .../scripts/agent/chatbot_tool_client.py | 55 - .../scripts/agent/pending_store.py | 117 - .../scripts/agent/tool_executor.py | 324 -- .../scripts/api_clients.py | 455 --- .../scripts/build_index_qa.py | 80 - .../scripts/chat_history.py | 283 -- .../scripts/excel_to_jsonl.py | 168 - .../scripts/handlers/__init__.py | 33 - .../scripts/handlers/config.py | 92 - .../scripts/handlers/emotion_detector.py | 169 - .../scripts/handlers/emotion_handler.py | 171 - .../scripts/handlers/greeting_handler.py | 132 - .../scripts/handlers/intent_detector.py | 115 - .../scripts/handlers/llm_handler.py | 132 - .../scripts/handlers/prompt_builder.py | 399 --- .../scripts/handlers/query_rewriter.py | 245 -- .../scripts/handlers/response_handler.py | 202 -- .../scripts/handlers/search_handler.py | 165 - .../scripts/handlers/suggestion_handler.py | 193 -- .../scripts/ingest_qa.py | 194 -- .../scripts/preprocess_qa.py | 77 - .../scripts/run_service_qa.py | 832 ----- .../scripts/sparse_encoder.py | 115 - .../scripts/tests/test_agent_service.py | 1296 -------- .../scripts/tests/test_pending_store.py | 40 - .../scripts/vector_store.py | 1131 ------- git_fetch_all.sh | 164 + kakaoChatbotSkill-chatbot2.0-agent | 1 + kakaoChatbotSkill-chatbot2.0-agent/.gitignore | 37 - .../CALLBACK_구현_정리.md | 359 -- kakaoChatbotSkill-chatbot2.0-agent/README.md | 115 - .../build.gradle | 43 - .../env.example | 15 - .../gradle.properties | 1 - .../gradle/wrapper/gradle-wrapper.jar | Bin 60756 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - kakaoChatbotSkill-chatbot2.0-agent/gradlew | 241 -- .../gradlew.bat | 91 - kakaoChatbotSkill-chatbot2.0-agent/nginx.conf | 344 -- .../settings.gradle | 1 - .../ex/chatbot/kakao/KakaoApplication.java | 12 - .../kakao/api/InternalToolController.java | 65 - .../kakao/api/dto/DataPortalToolRequest.java | 13 - .../chatbot/kakao/config/AgentProperties.java | 33 - .../kakao/config/InternalToolProperties.java | 14 - .../chatbot/kakao/config/RestfulConfig.java | 53 - .../kakao/controller/SkillController.java | 67 - .../kakao/response/build/KakaoIntent.java | 12 - .../kakao/response/build/KakaoIntentApp.java | 2939 ----------------- .../kakao/response/build/ResultBuilder.java | 61 - .../kakao/response/build/ValidCheck.java | 16 - .../kakao/response/element/SkillResponse.java | 132 - .../response/element/View/BasicCardView.java | 29 - .../response/element/View/CanCarousel.java | 5 - .../response/element/View/CarouselView.java | 16 - .../element/View/CommerceCardView.java | 15 - .../response/element/View/ListCardView.java | 16 - .../element/View/SimpleImageView.java | 15 - .../response/element/View/SimpleTextView.java | 15 - .../response/element/template/BasicCard.java | 40 - .../response/element/template/Carousel.java | 27 - .../element/template/CommerceCard.java | 40 - .../response/element/template/ListCard.java | 34 - .../element/template/SimpleImage.java | 25 - .../response/element/template/SimpleText.java | 23 - .../response/element/template/Template.java | 6 - .../kakao/response/element/type/Block.java | 9 - .../response/element/type/ContextControl.java | 12 - .../response/element/type/ContextValue.java | 14 - .../response/element/type/DetailParam.java | 8 - .../kakao/response/element/type/Item.java | 17 - .../response/element/type/QuickReply.java | 34 - .../response/element/type/SkillTemplate.java | 78 - .../kakao/response/element/type/User.java | 17 - .../element/type/buttons/BlockButton.java | 15 - .../response/element/type/buttons/Button.java | 8 - .../element/type/buttons/MessageButton.java | 15 - .../element/type/buttons/OsLinkButton.java | 26 - .../element/type/buttons/PhoneButton.java | 22 - .../element/type/buttons/ShareButton.java | 12 - .../element/type/buttons/WebLinkButton.java | 22 - .../element/type/subtype/CarouselHeader.java | 12 - .../response/element/type/subtype/Link.java | 18 - .../element/type/subtype/Profile.java | 11 - .../element/type/subtype/Properties.java | 14 - .../response/element/type/subtype/Social.java | 15 - .../element/type/subtype/Thumbnail.java | 17 - .../kakao/service/DataPortalService.java | 285 -- .../InternalDataPortalToolService.java | 123 - .../chatbot/kakao/service/RagAgentClient.java | 61 - .../ex/chatbot/kakao/skillRequest/Action.java | 14 - .../ex/chatbot/kakao/skillRequest/Bot.java | 10 - .../kakao/skillRequest/CallbackRequest.java | 64 - .../ex/chatbot/kakao/skillRequest/Intent.java | 9 - .../kakao/skillRequest/KakaoRequest.java | 12 - .../kakao/skillRequest/UserRequest.java | 16 - .../kakao/web/SkillResponseWebExtractor.java | 413 --- .../kakao/web/WebChatAgentService.java | 265 -- .../chatbot/kakao/web/WebChatController.java | 81 - .../kakao/web/WebChatResponseMapper.java | 610 ---- .../ex/chatbot/kakao/web/WebChatService.java | 66 - .../kakao/web/dto/WebActionButtonDto.java | 18 - .../chatbot/kakao/web/dto/WebAskRequest.java | 9 - .../chatbot/kakao/web/dto/WebAskResponse.java | 26 - .../chatbot/kakao/web/dto/WebListCardDto.java | 16 - .../kakao/web/dto/WebListCardItemDto.java | 13 - .../kakao/web/dto/WebQuickReplyDto.java | 11 - .../src/main/resources/application.yml | 45 - .../src/main/resources/icTel.json | 2240 ------------- .../src/main/resources/logback-spring.xml | 40 - .../chatbot/kakao/KakaoApplicationTests.java | 10 - .../kakao/config/AgentPropertiesTest.java | 21 - .../build/KakaoIntentAppFaqLinkTest.java | 50 - .../web/SkillResponseWebExtractorTest.java | 80 - .../kakao/web/WebChatControllerTest.java | 71 - local-dev/README.md | 19 +- .../docker-compose.rag.local.yml | 23 +- local-dev/env.exdev-ai.template | 13 +- local-dev/start-all.sh | 82 +- local-dev/stop-all.sh | 2 +- local-dev/sync-qdrant-from-exdev.sh | 7 +- scripts/sync-upstream-repos.sh | 3 + server-dev/README.md | 67 +- server-dev/bootstrap-qdrant-from-legacy.sh | 54 +- server-dev/caddy/Caddyfile.agent.exdev | 81 +- server-dev/deploy.sh | 22 +- .../docker-compose.rag.server.yml | 37 +- server-dev/env.exdev-server.template | 2 +- server-dev/hooks/post-receive | 6 +- server-dev/push-to-gitea.sh | 118 + server-dev/start-all-server.sh | 184 +- server-dev/wait-remote-deploy.sh | 43 + 339 files changed, 811 insertions(+), 44137 deletions(-) create mode 100644 .gitmodules create mode 160000 chatbotApi-chatbot2.0-agent delete mode 100644 chatbotApi-chatbot2.0-agent/.gitignore delete mode 100644 chatbotApi-chatbot2.0-agent/RAG_API_IMPLEMENTATION.md delete mode 100644 chatbotApi-chatbot2.0-agent/README.md delete mode 100644 chatbotApi-chatbot2.0-agent/build.gradle delete mode 100644 chatbotApi-chatbot2.0-agent/env.example delete mode 100644 chatbotApi-chatbot2.0-agent/gradle.properties delete mode 100644 chatbotApi-chatbot2.0-agent/gradle/wrapper/gradle-wrapper.jar delete mode 100644 chatbotApi-chatbot2.0-agent/gradle/wrapper/gradle-wrapper.properties delete mode 100755 chatbotApi-chatbot2.0-agent/gradlew delete mode 100644 chatbotApi-chatbot2.0-agent/gradlew.bat delete mode 100644 chatbotApi-chatbot2.0-agent/lombok.config delete mode 100644 chatbotApi-chatbot2.0-agent/settings.gradle delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/ChatbotapiApplication.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/config/AgentProperties.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/config/KakaoSkillConfig.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/config/MongoDbSettings.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/config/OracleDbConfig.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/config/RagConfig.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/config/RestfulConfig.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/api/AgentApiController.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/api/AgentToolController.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/api/DomainOnlyApiController.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/api/LlmAgentController.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/client/KakaoSkillToolClient.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/AgentQueryRequest.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/AgentQueryResponse.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/CarNoParamNormalizationResult.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/DomainOnlyRequest.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/DomainOnlyResponse.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/IcNameCandidate.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/IcParamNormalizationResult.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/IntentValidationResult.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/LlmAgentRequest.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/LlmAgentResponse.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/PendingIntentState.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/RestAreaParamNormalizationResult.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/ToolExecuteRequest.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/ToolExecuteResponse.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/ToolParamPipelineResult.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/formatter/LlmDomainDataFormatter.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/handler/IntentHandler.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/intent/FetchOwner.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/intent/IntentDefinition.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/intent/IntentDefinitionRegistry.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/AgentOrchestratorService.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/CarNoParamNormalizer.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/DomainOnlyService.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/IcParamNormalizer.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/IntentAnalysisParamNormalizer.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/IntentParamValidator.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/LlmAgentRouterService.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/PendingIntentService.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/RestAreaParamNormalizer.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/ToolDefinitionService.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/ToolExecutionService.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/ToolParamPipeline.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/api/FareApiController.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/formatter/FareRefundLlmFormatter.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/formatter/FareSearchLlmFormatter.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/formatter/FareUnpaidLlmFormatter.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/formatter/IcTelLlmFormatter.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/handler/FareRefundHandler.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/handler/FareSearchHandler.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/handler/FareUnpaidHandler.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/handler/IcTelHandler.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/repository/FareRepository.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/service/FareAmountHelper.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/service/FareService.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/api/RagApiController.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/client/RagServerClient.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/dto/IntentAnalysisRequest.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/dto/IntentAnalysisResponse.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/dto/RagQueryRequest.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/dto/RagServerRequest.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/dto/RagServerResponse.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/service/RagService.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/api/RestAreaApiController.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/formatter/BrandShopLlmFormatter.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/formatter/FoodListLlmFormatter.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/formatter/GasstationLlmFormatter.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/formatter/RestAreaPriceLlmFormatter.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/handler/RestAreaPriceHandler.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/repository/RestAreaRepository.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/service/RestAreaService.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/api/TroadApiController.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/formatter/TroadBreakingNewsLlmFormatter.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/formatter/TroadCongestionLlmFormatter.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/formatter/TroadSearchRouteLlmFormatter.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/handler/TroadBreakingNewsHandler.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/handler/TroadCongestionHandler.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/handler/TroadSearchRouteHandler.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/service/TroadService.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/resources/application.yml delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/resources/icTel.json delete mode 100644 chatbotApi-chatbot2.0-agent/src/main/resources/intent-definitions.yml delete mode 100644 chatbotApi-chatbot2.0-agent/src/test/java/com/ex/chatbotapi/ChatbotapiApplicationTests.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/test/java/com/ex/chatbotapi/domain/agent/service/IntentParamValidatorTest.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/test/java/com/ex/chatbotapi/domain/agent/service/ToolDefinitionServiceTest.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/test/java/com/ex/chatbotapi/domain/agent/service/ToolExecutionServiceTest.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/test/java/com/ex/chatbotapi/domain/fare/service/FareAmountHelperTest.java delete mode 100644 chatbotApi-chatbot2.0-agent/src/test/java/com/ex/chatbotapi/domain/rag/dto/RagServerResponseTest.java create mode 160000 chatbotApp-chatbot2.0-agent delete mode 100644 chatbotApp-chatbot2.0-agent/README.md delete mode 100755 chatbotApp-chatbot2.0-agent/build.sh delete mode 100644 chatbotApp-chatbot2.0-agent/docker-compose.yml delete mode 100644 chatbotApp-chatbot2.0-agent/embed/chatbot-banner.js delete mode 100644 chatbotApp-chatbot2.0-agent/nginx/default.conf delete mode 100644 chatbotApp-chatbot2.0-agent/nginx/default.conf.template delete mode 100755 chatbotApp-chatbot2.0-agent/nginx/docker-entrypoint.sh delete mode 100644 chatbotApp-chatbot2.0-agent/sample/css/minwon-sample.css delete mode 100644 chatbotApp-chatbot2.0-agent/sample/css/partcptn-sample.css delete mode 100644 chatbotApp-chatbot2.0-agent/sample/index.html delete mode 100644 chatbotApp-chatbot2.0-agent/sample/minwon/index.html delete mode 100755 chatbotApp-chatbot2.0-agent/start.sh delete mode 100644 chatbotApp-chatbot2.0-agent/web/.gitignore delete mode 100644 chatbotApp-chatbot2.0-agent/web/next-env.d.ts delete mode 100644 chatbotApp-chatbot2.0-agent/web/next.config.mjs delete mode 100644 chatbotApp-chatbot2.0-agent/web/package-lock.json delete mode 100644 chatbotApp-chatbot2.0-agent/web/package.json delete mode 100644 chatbotApp-chatbot2.0-agent/web/public/images/giltong.jpg delete mode 100644 chatbotApp-chatbot2.0-agent/web/scripts/export-public.mjs delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/app/globals.css delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/app/layout.tsx delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/app/page.tsx delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/components/ActionButtonList.tsx delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/components/ChatApp.tsx delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/components/ChatCloseButton.tsx delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/components/Composer.tsx delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/components/ExBrandTitle.tsx delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/components/FaqPanel.tsx delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/components/FareResultCard.tsx delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/components/ListCardStack.tsx delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/components/MarkdownBody.tsx delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/components/MessageBubble.tsx delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/components/MessageList.tsx delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/components/QuickReplyChips.tsx delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/components/RelatedQuestionsBar.tsx delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/components/ai/AiChatApp.tsx delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/components/ai/AiComposer.tsx delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/components/ai/AiMessageBubble.tsx delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/components/ai/AiMessageList.tsx delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/components/ai/AiVoiceOverlay.tsx delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/components/banner/BannerChatApp.tsx delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/components/banner/BannerMessageBubble.tsx delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/components/voicebot/VoicebotChatApp.tsx delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/hooks/useBrowserTts.ts delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/hooks/useChatSession.ts delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/hooks/useChatStream.ts delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/hooks/useEmbedShell.ts delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/hooks/useSpeechRecognition.ts delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/hooks/useVoiceConversation.ts delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/lib/apiCapabilities.ts delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/lib/brand.ts delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/lib/config.ts delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/lib/embedShell.ts delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/lib/faqItems.ts delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/lib/fareResultCard.ts delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/lib/format.ts delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/lib/listCards.ts delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/lib/microphone.ts delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/lib/partcptnIntents.ts delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/lib/relatedQuestions.ts delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/lib/session.ts delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/lib/types.ts delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/lib/uiMode.ts delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/lib/voiceText.ts delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/styles/ai-mode.css delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/styles/banner-mode.css delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/styles/ex-brand.css delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/styles/fare-result-card.css delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/styles/voicebot-mode.css delete mode 100644 chatbotApp-chatbot2.0-agent/web/src/types/speech.d.ts delete mode 100644 chatbotApp-chatbot2.0-agent/web/tsconfig.json create mode 160000 exAiChatBot-chatbot2.0-agent delete mode 100644 exAiChatBot-chatbot2.0-agent/.gitignore delete mode 100644 exAiChatBot-chatbot2.0-agent/DOCKER_COMMANDS.md delete mode 100644 exAiChatBot-chatbot2.0-agent/PROCESS_FLOW.md delete mode 100644 exAiChatBot-chatbot2.0-agent/README.md delete mode 100644 exAiChatBot-chatbot2.0-agent/VECTORDB_ADMIN_API.md delete mode 100755 exAiChatBot-chatbot2.0-agent/clean-more.sh delete mode 100755 exAiChatBot-chatbot2.0-agent/create-offline-package-slim.sh delete mode 100644 exAiChatBot-chatbot2.0-agent/docker-compose-slim.yml delete mode 100644 exAiChatBot-chatbot2.0-agent/docker/base-slim.Dockerfile delete mode 100644 exAiChatBot-chatbot2.0-agent/docker/base.Dockerfile delete mode 100644 exAiChatBot-chatbot2.0-agent/docker/batch-slim.Dockerfile delete mode 100644 exAiChatBot-chatbot2.0-agent/docker/batch.Dockerfile delete mode 100644 exAiChatBot-chatbot2.0-agent/docker/service-slim.Dockerfile delete mode 100644 exAiChatBot-chatbot2.0-agent/docker/service.Dockerfile delete mode 100644 exAiChatBot-chatbot2.0-agent/env.example delete mode 100644 exAiChatBot-chatbot2.0-agent/env.template delete mode 100755 exAiChatBot-chatbot2.0-agent/force-reindex.sh delete mode 100644 exAiChatBot-chatbot2.0-agent/llmServer/docker 실행 명령어 delete mode 100644 exAiChatBot-chatbot2.0-agent/llmServer/reranker_server.py delete mode 100644 exAiChatBot-chatbot2.0-agent/old_docs/FLOW_DIAGRAM.md delete mode 100644 exAiChatBot-chatbot2.0-agent/old_docs/OFFLINE_QUICKSTART.md delete mode 100644 exAiChatBot-chatbot2.0-agent/old_docs/QUICKSTART.md delete mode 100644 exAiChatBot-chatbot2.0-agent/old_docs/RAG_PIPELINE.md delete mode 100644 exAiChatBot-chatbot2.0-agent/old_docs/README.md delete mode 100644 exAiChatBot-chatbot2.0-agent/old_docs/TROUBLESHOOTING.md delete mode 100755 exAiChatBot-chatbot2.0-agent/reset-repo.sh delete mode 100755 exAiChatBot-chatbot2.0-agent/run-with-excel-col34.sh delete mode 100755 exAiChatBot-chatbot2.0-agent/run-with-excel.sh delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/admin_service.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/agent/__init__.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/agent/agent_service.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/agent/chatbot_tool_client.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/agent/pending_store.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/agent/tool_executor.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/api_clients.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/build_index_qa.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/chat_history.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/excel_to_jsonl.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/handlers/__init__.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/handlers/config.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/handlers/emotion_detector.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/handlers/emotion_handler.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/handlers/greeting_handler.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/handlers/intent_detector.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/handlers/llm_handler.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/handlers/prompt_builder.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/handlers/query_rewriter.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/handlers/response_handler.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/handlers/search_handler.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/handlers/suggestion_handler.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/ingest_qa.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/preprocess_qa.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/run_service_qa.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/sparse_encoder.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/tests/test_agent_service.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/tests/test_pending_store.py delete mode 100644 exAiChatBot-chatbot2.0-agent/scripts/vector_store.py create mode 100755 git_fetch_all.sh create mode 160000 kakaoChatbotSkill-chatbot2.0-agent delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/.gitignore delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/CALLBACK_구현_정리.md delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/README.md delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/build.gradle delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/env.example delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/gradle.properties delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/gradle/wrapper/gradle-wrapper.jar delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/gradle/wrapper/gradle-wrapper.properties delete mode 100755 kakaoChatbotSkill-chatbot2.0-agent/gradlew delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/gradlew.bat delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/nginx.conf delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/settings.gradle delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/KakaoApplication.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/api/InternalToolController.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/api/dto/DataPortalToolRequest.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/config/AgentProperties.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/config/InternalToolProperties.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/config/RestfulConfig.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/controller/SkillController.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/build/KakaoIntent.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/build/KakaoIntentApp.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/build/ResultBuilder.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/build/ValidCheck.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/SkillResponse.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/View/BasicCardView.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/View/CanCarousel.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/View/CarouselView.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/View/CommerceCardView.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/View/ListCardView.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/View/SimpleImageView.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/View/SimpleTextView.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/template/BasicCard.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/template/Carousel.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/template/CommerceCard.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/template/ListCard.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/template/SimpleImage.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/template/SimpleText.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/template/Template.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/type/Block.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/type/ContextControl.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/type/ContextValue.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/type/DetailParam.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/type/Item.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/type/QuickReply.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/type/SkillTemplate.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/type/User.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/type/buttons/BlockButton.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/type/buttons/Button.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/type/buttons/MessageButton.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/type/buttons/OsLinkButton.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/type/buttons/PhoneButton.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/type/buttons/ShareButton.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/type/buttons/WebLinkButton.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/type/subtype/CarouselHeader.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/type/subtype/Link.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/type/subtype/Profile.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/type/subtype/Properties.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/type/subtype/Social.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/response/element/type/subtype/Thumbnail.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/service/DataPortalService.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/service/InternalDataPortalToolService.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/service/RagAgentClient.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/skillRequest/Action.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/skillRequest/Bot.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/skillRequest/CallbackRequest.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/skillRequest/Intent.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/skillRequest/KakaoRequest.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/skillRequest/UserRequest.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/web/SkillResponseWebExtractor.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/web/WebChatAgentService.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/web/WebChatController.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/web/WebChatResponseMapper.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/web/WebChatService.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/web/dto/WebActionButtonDto.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/web/dto/WebAskRequest.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/web/dto/WebAskResponse.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/web/dto/WebListCardDto.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/web/dto/WebListCardItemDto.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/java/com/ex/chatbot/kakao/web/dto/WebQuickReplyDto.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/resources/application.yml delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/resources/icTel.json delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/main/resources/logback-spring.xml delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/test/java/com/ex/chatbot/kakao/KakaoApplicationTests.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/test/java/com/ex/chatbot/kakao/config/AgentPropertiesTest.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/test/java/com/ex/chatbot/kakao/response/build/KakaoIntentAppFaqLinkTest.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/test/java/com/ex/chatbot/kakao/web/SkillResponseWebExtractorTest.java delete mode 100644 kakaoChatbotSkill-chatbot2.0-agent/src/test/java/com/ex/chatbot/kakao/web/WebChatControllerTest.java rename exAiChatBot-chatbot2.0-agent/docker-compose.local.yml => local-dev/docker-compose.rag.local.yml (70%) create mode 100755 scripts/sync-upstream-repos.sh rename exAiChatBot-chatbot2.0-agent/docker-compose.server.yml => server-dev/docker-compose.rag.server.yml (52%) create mode 100755 server-dev/push-to-gitea.sh create mode 100755 server-dev/wait-remote-deploy.sh diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..3a9dff5 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,16 @@ +[submodule "chatbotApp-chatbot2.0-agent"] + path = chatbotApp-chatbot2.0-agent + url = https://github.com/KoreaExpressWayCorp/chatbotApp.git + branch = chatbot2.0-agent +[submodule "chatbotApi-chatbot2.0-agent"] + path = chatbotApi-chatbot2.0-agent + url = https://github.com/KoreaExpressWayCorp/chatbotApi.git + branch = chatbot2.0-agent +[submodule "exAiChatBot-chatbot2.0-agent"] + path = exAiChatBot-chatbot2.0-agent + url = https://github.com/KoreaExpressWayCorp/exAiChatBot.git + branch = chatbot2.0-agent +[submodule "kakaoChatbotSkill-chatbot2.0-agent"] + path = kakaoChatbotSkill-chatbot2.0-agent + url = https://github.com/KoreaExpressWayCorp/kakaoChatbotSkill.git + branch = chatbot2.0-agent diff --git a/chatbotApi-chatbot2.0-agent b/chatbotApi-chatbot2.0-agent new file mode 160000 index 0000000..647e047 --- /dev/null +++ b/chatbotApi-chatbot2.0-agent @@ -0,0 +1 @@ +Subproject commit 647e047bf09ac621e15a5015cf7d3cba2ec7f710 diff --git a/chatbotApi-chatbot2.0-agent/.gitignore b/chatbotApi-chatbot2.0-agent/.gitignore deleted file mode 100644 index 9fd0d94..0000000 --- a/chatbotApi-chatbot2.0-agent/.gitignore +++ /dev/null @@ -1,148 +0,0 @@ -# ==================================== -# Spring Boot & Gradle -# ==================================== -HELP.md -.gradle -build/ -!gradle/wrapper/gradle-wrapper.jar -!**/src/main/**/build/ -!**/src/test/**/build/ - -# ==================================== -# IDE - Eclipse -# ==================================== -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache -bin/ -!**/src/main/**/bin/ -!**/src/test/**/bin/ - -# ==================================== -# IDE - IntelliJ IDEA -# ==================================== -.idea -*.iws -*.iml -*.ipr -out/ -!**/src/main/**/out/ -!**/src/test/**/out/ - -# ==================================== -# IDE - NetBeans -# ==================================== -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ - -# ==================================== -# IDE - VS Code & Cursor -# ==================================== -.vscode/ -.cursor/ - -# ==================================== -# OS Files -# ==================================== -# Windows -Thumbs.db -ehthumbs.db -Desktop.ini -$RECYCLE.BIN/ - -# macOS -.DS_Store -.AppleDouble -.LSOverride -._* - -# Linux -*~ -.directory - -# ==================================== -# Logs -# ==================================== -*.log -logs/ -*.log.* -log/ - -# ==================================== -# Environment & Configuration -# ==================================== -# 로컬 개발 환경 설정 (민감정보 포함) -application-local.yml -application-dev.yml -application-prod.yml -*-secret.yml - -# 환경변수 파일 -.env -.env.local -.env.*.local - -# ==================================== -# Temporary Files -# ==================================== -*.tmp -*.temp -*.swp -*.swo -*~.nib -*.bak -*.class - -# ==================================== -# Gradle -# ==================================== -.gradle/ -gradle-app.setting -!gradle-wrapper.jar -.gradletasknamecache - -# ==================================== -# Package Files -# ==================================== -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar -!gradle/wrapper/gradle-wrapper.jar - -# ==================================== -# Application Specific -# ==================================== -# 업로드된 파일 -uploads/ -upload/ - -# 다운로드 파일 -downloads/ -download/ - -# 임시 데이터 -data/ -tmp/ - -# ==================================== -# Documentation (optional) -# ==================================== -# 프로젝트 문서는 유지하되, 임시 문서는 제외 -**/temp-*.md -**/draft-*.md - -# ==================================== -# Agent Tools (Cursor specific) -# ==================================== -.cursor/projects/*/agent-tools/ diff --git a/chatbotApi-chatbot2.0-agent/RAG_API_IMPLEMENTATION.md b/chatbotApi-chatbot2.0-agent/RAG_API_IMPLEMENTATION.md deleted file mode 100644 index d8e39e9..0000000 --- a/chatbotApi-chatbot2.0-agent/RAG_API_IMPLEMENTATION.md +++ /dev/null @@ -1,283 +0,0 @@ -# RAG API 구현 완료 문서 - -## ✅ 수정 완료된 파일 - -### 1️⃣ **RagConfig.java** - Docker 스펙에 맞게 수정 -```java -- host: localhost (Docker 컨테이너) -- port: 28012 (Docker 외부 포트) -- askEndpoint: /ask -- healthEndpoint: /health (추가) -``` - -### 2️⃣ **RagServerRequest.java** - API 스펙 적용 -```java -{ - "query": "사용자 질문" // Docker API 필드명에 맞춤 -} -``` - -### 3️⃣ **RagServerResponse.java** - 실제 응답 구조 -```java -{ - "answer": "LLM 답변", - "matched_questions": ["질문1", "질문2", "질문3"], - "scores": [0.95, 0.89, 0.82], - "num_references": 5 -} -``` - -### 4️⃣ **RagService.java** - 변환 로직 추가 -- question → query 자동 변환 -- 헬스체크 기능 추가 -- 상세 로깅 추가 - -### 5️⃣ **RagApiController.java** - 헬스체크 엔드포인트 추가 -- POST /api/v1/rag-only (질문 처리) -- GET /api/v1/rag-only/health (헬스체크) - -### 6️⃣ **application.yml** - Docker 설정 -```yaml -rag: - server: - host: localhost - port: 28012 - ask-endpoint: /ask - health-endpoint: /health -``` - ---- - -## 🔄 API 처리 흐름 - -``` -챗봇 웹 - ↓ POST /api/v1/rag-only - ↓ { - "question": "회원가입은 어떻게 하나요?", - "botId": "bot-001" - } - ↓ -[Spring Boot API - RagApiController] - ↓ -[RagService] - - 입력 검증 - - question → query 변환 - ↓ POST http://localhost:28012/ask - ↓ { - "query": "회원가입은 어떻게 하나요?" - } - ↓ -[Docker 컨테이너 - RAG 서버] - - LLM 처리 - - 벡터 검색 - - 답변 생성 - ↓ 응답 - ↓ { - "answer": "회원가입은 다음과 같이...", - "matched_questions": ["회원가입 방법", "가입 절차", "회원 등록"], - "scores": [0.95, 0.89, 0.82], - "num_references": 5 - } - ↓ -[RagService] - - 응답 가공 - ↓ -[RagApiController] - ↓ 최종 응답 - ↓ { - "status": true, - "statusMsg": "정상응답", - "answer": "회원가입은 다음과 같이...", - "matchedQuestions": ["회원가입 방법", "가입 절차", "회원 등록"], - "scores": [0.95, 0.89, 0.82], - "numReferences": 5 - } - ↓ -챗봇 웹 -``` - ---- - -## 🧪 테스트 방법 - -### 1. Docker 컨테이너 직접 테스트 - -```bash -# 질문 테스트 -curl -X POST http://localhost:28012/ask \ - -H "Content-Type: application/json" \ - -d '{"query":"회원가입은 어떻게 하나요?"}' - -# 헬스체크 -curl http://localhost:28012/health -``` - -**예상 응답:** -```json -{ - "answer": "회원가입은...", - "matched_questions": ["회원가입 방법", "가입 절차"], - "scores": [0.95, 0.89], - "num_references": 5 -} -``` - -### 2. Spring Boot API 테스트 - -```bash -# 질문 API -curl -X POST http://localhost:8086/api/v1/rag-only \ - -H "Content-Type: application/json" \ - -d '{ - "question": "회원가입은 어떻게 하나요?", - "botId": "bot-001" - }' - -# 헬스체크 API -curl http://localhost:8086/api/v1/rag-only/health -``` - -**예상 응답 (성공):** -```json -{ - "status": true, - "statusMsg": "정상응답", - "answer": "회원가입은 다음과 같이 진행됩니다...", - "matchedQuestions": ["회원가입 방법", "가입 절차", "회원 등록"], - "scores": [0.95, 0.89, 0.82], - "numReferences": 5 -} -``` - -**예상 응답 (실패):** -```json -{ - "status": false, - "statusMsg": "RAG 서버 통신 실패" -} -``` - -### 3. Postman으로 테스트 - -**질문 API:** -- Method: POST -- URL: `http://localhost:8086/api/v1/rag-only` -- Headers: `Content-Type: application/json` -- Body (raw JSON): -```json -{ - "question": "배송은 얼마나 걸리나요?", - "botId": "bot-001" -} -``` - -**헬스체크 API:** -- Method: GET -- URL: `http://localhost:8086/api/v1/rag-only/health` - ---- - -## 📋 환경별 설정 - -### 로컬 개발 (application-local.yml) -```yaml -rag: - server: - host: localhost - port: 28012 -``` - -### 개발 서버 (application-dev.yml) -```yaml -rag: - server: - host: 172.16.163.62 # 개발 서버 IP - port: 28012 -``` - -### 운영 서버 (application-prod.yml) -```yaml -rag: - server: - host: ${RAG_SERVER_HOST:localhost} - port: ${RAG_SERVER_PORT:28012} -``` - ---- - -## 🔍 로그 확인 - -### 질문 처리 시 -``` -INFO - RAG 질문 처리 시작 - 질문: 회원가입은 어떻게 하나요?, 봇ID: bot-001 -INFO - RAG 서버 요청 시작 - URL: http://localhost:28012/ask, 질문: 회원가입은 어떻게 하나요? -INFO - RAG 서버 응답 성공 - Status: 200 OK -INFO - RAG 질문 처리 완료 - 참조: 5개, 유사 질문: 3개 -``` - -### 헬스체크 시 -``` -INFO - RAG 서버 헬스체크 시작 -INFO - 헬스체크 URL: http://localhost:28012/health -INFO - RAG 서버 상태: ok, 벡터 개수: 1000 -``` - -### 에러 시 -``` -ERROR - RAG 서버 통신 실패 - Connection refused -``` - ---- - -## ⚠️ 주의사항 - -1. **Docker 컨테이너 실행 확인** - ```bash - docker ps | grep 28012 - ``` - -2. **포트 접근 가능 확인** - ```bash - telnet localhost 28012 - # 또는 - nc -zv localhost 28012 - ``` - -3. **방화벽 설정 확인** - - 28012 포트가 열려있는지 확인 - - 개발 서버에서는 172.16.163.62:28012 접근 가능한지 확인 - -4. **botId는 선택사항** - - 현재 Docker API에는 전달되지 않음 - - 로깅/추적 목적으로만 사용 - ---- - -## 📌 다음 단계 (선택사항) - -1. **응답 캐싱** - - 동일한 질문에 대한 캐시 처리 - - Redis 또는 로컬 캐시 활용 - -2. **재시도 로직** - - RAG 서버 통신 실패 시 재시도 - - Exponential Backoff 적용 - -3. **모니터링** - - 응답 시간 측정 - - 에러율 추적 - - Prometheus/Grafana 연동 - -4. **보안 강화** - - API 키 인증 - - Rate Limiting - ---- - -## 🎯 완료! - -모든 파일이 Docker RAG 서버 API 스펙에 맞춰 수정되었습니다. - -테스트 후 문제가 있으면 알려주세요! 😊 - diff --git a/chatbotApi-chatbot2.0-agent/README.md b/chatbotApi-chatbot2.0-agent/README.md deleted file mode 100644 index 539ac39..0000000 --- a/chatbotApi-chatbot2.0-agent/README.md +++ /dev/null @@ -1,102 +0,0 @@ -# chatbotApi 설명서 - -> 해당 문서는 md(Markdown) 방식으로 작성되었기 때문에 마크다운 뷰어 기능이있는 편집기(vscode 등)에서 보는것을 추천드립니다.(vscode 사용 시 문서 열고 Ctrl + Shift + V) - - -## 1. 개요 -챗봇에서 데이터 조회를 위한 api 서비스(backend, was). intent 정책(SSOT `intent-definitions.yml`)·검증·정규화와 도메인 데이터 조회(Oracle Handler / WEB 위임)를 담당합니다. - -### 1.1 시스템 구성 흐름 (WAS 역할) - -```mermaid -flowchart TD - RAG[exAiChatBot AgentService] --> TE[AgentToolController /api/v1/tools/*] - - POL[intent-definitions.yml + validator/normalizer] - TE --> POL - POL --> OWN{fetchOwner} - OWN -->|WAS| ORA[IntentHandler → Oracle/TROAD] - OWN -->|WEB| SKI[KakaoSkillToolClient → Skill /internal/tools/data-portal] - ORA --> FMT[LlmDomainDataFormatter] - SKI --> FMT - FMT --> OUT([domainData + uiType]) -``` - -| 진입 | 사용 모드 | 설명 | -|------|-----------|------| -| `GET /api/v1/tools/definitions` | Agent | YAML → OpenAI function schema (RAG tool 목록) | -| `POST /api/v1/tools/execute` | Agent | domain tool 실행 (`fetchOwner` 분기: WAS Oracle / WEB Skill 역호출) | - -- **정책 SSOT:** `src/main/resources/intent-definitions.yml` (`fetchOwner`/`uiType`/`requiredParams`/`clarification`). -- **핵심 원칙:** LLM은 intent·params만 결정, `fetchOwner`/`uiType`/`needsClarification`은 WAS+YAML+validator가 결정. -- 전체 구조: [`../SYSTEM_ARCHITECTURE.md`](../SYSTEM_ARCHITECTURE.md) · Agent tool API 상세: [`../docs/AGENT_ARCHITECTURE.md`](../docs/AGENT_ARCHITECTURE.md) · RAG API: [`RAG_API_IMPLEMENTATION.md`](RAG_API_IMPLEMENTATION.md) - - -## 2. 프로그램 관련 정보 -서비스 기동 서버 : 172.16.180.130(통합정산개발#1) -* Java version : 8(JDK 1.8) -* 서비스 기동 시점 : 상시 -* 연계DB : TROAD(교통정보, Oracle), chatbot(MongoDB) -* 사용 계정 : song -* 서비스 홈 디렉토리 : /home/song/bin/chatbotApi -* 서비스 파일 : chatbotApi-0.0.1-SNAPSHOT.jar -* 서비스 설정 파일(config) : application.yml, logback-spring.xml -* 서비스 로그 파일(logs) : chatbotApi.log -* 서비스 시작 명령어(서비스 홈 디렉토리에서 수행해야한다.) - -``` bash -./start.sh start ## 백그라운드(실운영) 기동 명령어, 평시에는 해당 명령어를 사용한다. -./start.sh starta ## 포그라운드 기동 명령어, 에러 탐색 및 모니터링시 활용 -``` - -## 3. 기타 주의 사항 -> 해당 서비스에는 교통정보 관련 API만 개발 되어있다. 향후에는 미납 조회 등 모든 서비스를 구현하면 좋을듯. (현재는 DMZ존에 위치한 챗봇 WEB서버(128.200.100.61)에 대부분의 조회 서비스가 운영되고있다. 별로 좋지않은 구조라 생각된다.) - -> [ASIS] WEB ↔ DB, [TOBE] WEB ↔ WAS ↔ DB) - -## ※ API 일별 사용 건수 조회 Mongo쿼리 -```javascript -db.api_use_log.aggregate([ - { - $group: { - _id: { - $substr: ["$request_time", 0, 10] // 'yyyy-MM-dd' 부분만 추출 - }, - count: { $sum: 1 } // 각 날짜별로 문서 수를 계산 - } - }, - { - $sort: { "_id": 1 } // 날짜별로 정렬 - } -]); -``` - -## ※ API apikey 및 일별 사용 건수 조회 Mongo쿼리 -```javascript -db.api_use_log.aggregate([ - { - $addFields: { - // 'yyyy-MM-dd' 형식으로 날짜 부분만 추출하여 새 필드에 저장 - "dateOnly": { - $substr: ["$request_time", 0, 10] // '2023-11-11' 부분만 추출 - } - } - }, - { - $group: { - // api_key와 추출된 날짜를 기준으로 그룹화 - _id: { - api_key: "$api_key", - date: "$dateOnly" - }, - count: { $sum: 1 } // 각 api_key와 날짜별로 문서 수를 계산 - } - }, - { - $sort: { - "_id.date": 1, // 날짜별로 정렬 - "_id.api_key": 1 // api_key별로 정렬 - } - } -]); -``` \ No newline at end of file diff --git a/chatbotApi-chatbot2.0-agent/build.gradle b/chatbotApi-chatbot2.0-agent/build.gradle deleted file mode 100644 index 4c2269e..0000000 --- a/chatbotApi-chatbot2.0-agent/build.gradle +++ /dev/null @@ -1,48 +0,0 @@ -plugins { - id 'java' - id 'org.springframework.boot' version '2.7.14' - id 'io.spring.dependency-management' version '1.0.15.RELEASE' -} - -group = 'com.ex.' -version = '0.0.1-SNAPSHOT' - -java { - // Java 17로 빌드하되, Java 8 바이트코드로 컴파일 (Java 8 환경에서 실행 가능) - toolchain { - languageVersion = JavaLanguageVersion.of(17) - } - sourceCompatibility = '1.8' - targetCompatibility = '1.8' -} - -configurations { - compileOnly { - extendsFrom annotationProcessor - } -} - -repositories { - mavenCentral() -} - -dependencies { - implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-jdbc' - implementation 'org.apache.httpcomponents:httpclient:4.5.13' - implementation 'com.google.code.gson:gson:2.8.9' - implementation 'commons-io:commons-io:2.8.0' - compileOnly 'org.projectlombok:lombok:1.18.30' - runtimeOnly 'com.oracle.database.jdbc:ojdbc8' - annotationProcessor 'org.projectlombok:lombok:1.18.30' - testImplementation 'org.springframework.boot:spring-boot-starter-test' -} - -tasks.named('compileJava') { - options.release = 8 -} - -tasks.named('test') { - useJUnitPlatform() -} diff --git a/chatbotApi-chatbot2.0-agent/env.example b/chatbotApi-chatbot2.0-agent/env.example deleted file mode 100644 index d5f0717..0000000 --- a/chatbotApi-chatbot2.0-agent/env.example +++ /dev/null @@ -1,15 +0,0 @@ -# chatbotApi (WAS 172.16.180.130) — 기동 전 export - -INTERNAL_TOOL_API_KEY= - -# kakaoChatbotSkill 웹 서버 — 실제 IP/호스트로 수정 (기본: 레거시 DMZ 128.200.100.61) -SKILL_INTERNAL_BASE_URL=http://128.200.100.61:8083 - -# RAG (동일 WAS 서버) -RAG_SERVER_HOST=127.0.0.1 -RAG_SERVER_PORT=28012 - -# MongoDB (동일 서버 로컬) -# MONGODB_URI=mongodb://exDev:ehfhrhdtk!!@127.0.0.1:27017/chatbot?authSource=chatbot - -TOOL_EXECUTE_TIMEOUT_MS=30000 diff --git a/chatbotApi-chatbot2.0-agent/gradle.properties b/chatbotApi-chatbot2.0-agent/gradle.properties deleted file mode 100644 index 6d66cc0..0000000 --- a/chatbotApi-chatbot2.0-agent/gradle.properties +++ /dev/null @@ -1,7 +0,0 @@ -# Gradle이 사용할 Java 버전 설정 -# Gradle 데몬은 Java 17 이하를 사용하도록 제한 -org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m - -# Java Toolchain 자동 다운로드 활성화 -org.gradle.java.installations.auto-download=true - diff --git a/chatbotApi-chatbot2.0-agent/gradle/wrapper/gradle-wrapper.jar b/chatbotApi-chatbot2.0-agent/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 033e24c4cdf41af1ab109bc7f253b2b887023340..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 63375 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≫L+crb^Bz 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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##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || 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=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# 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=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=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='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# 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/chatbotApi-chatbot2.0-agent/gradlew.bat b/chatbotApi-chatbot2.0-agent/gradlew.bat deleted file mode 100644 index 93e3f59..0000000 --- a/chatbotApi-chatbot2.0-agent/gradlew.bat +++ /dev/null @@ -1,92 +0,0 @@ -@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 - -@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="-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. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -: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/chatbotApi-chatbot2.0-agent/lombok.config b/chatbotApi-chatbot2.0-agent/lombok.config deleted file mode 100644 index 8ab6efb..0000000 --- a/chatbotApi-chatbot2.0-agent/lombok.config +++ /dev/null @@ -1,2 +0,0 @@ -config.stopBubbling = true -lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier diff --git a/chatbotApi-chatbot2.0-agent/settings.gradle b/chatbotApi-chatbot2.0-agent/settings.gradle deleted file mode 100644 index e099cda..0000000 --- a/chatbotApi-chatbot2.0-agent/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'chatbotApi' diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/ChatbotapiApplication.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/ChatbotapiApplication.java deleted file mode 100644 index 8e4e1eb..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/ChatbotapiApplication.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.ex.chatbotapi; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class ChatbotapiApplication { - public static void main(String[] args) { - SpringApplication.run(ChatbotapiApplication.class, args); - } - -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/config/AgentProperties.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/config/AgentProperties.java deleted file mode 100644 index d4853dc..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/config/AgentProperties.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.ex.chatbotapi.config; - -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -@Getter -@Setter -@Configuration -@ConfigurationProperties(prefix = "agent") -public class AgentProperties { - - private Tool tool = new Tool(); - - @Getter - @Setter - public static class Tool { - private String internalApiKey = ""; - private int executeTimeoutMs = 30_000; - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/config/KakaoSkillConfig.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/config/KakaoSkillConfig.java deleted file mode 100644 index e0e97c9..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/config/KakaoSkillConfig.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.ex.chatbotapi.config; - -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -@Getter -@Setter -@Configuration -@ConfigurationProperties(prefix = "kakao-skill") -public class KakaoSkillConfig { - private String baseUrl = "http://localhost:8083"; - private String dataPortalToolEndpoint = "/internal/tools/data-portal"; - private String internalApiKey = ""; - private int connectTimeout = 5000; - private int readTimeout = 30000; - - public String getDataPortalToolUrl() { - return baseUrl.replaceAll("/$", "") + dataPortalToolEndpoint; - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/config/MongoDbSettings.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/config/MongoDbSettings.java deleted file mode 100644 index 208fe55..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/config/MongoDbSettings.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.ex.chatbotapi.config; - -import com.mongodb.ConnectionString; -import com.mongodb.MongoClientSettings; -import com.mongodb.client.MongoClient; -import com.mongodb.client.MongoClients; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; - -@Configuration -@EnableMongoRepositories(basePackages = "com.ex.chatbotapi.domain") -@ConfigurationProperties(prefix = "spring.data.mongodb") -public class MongoDbSettings { - - @Value("${spring.data.mongodb.uri}") - private String URI; - - @Bean - public MongoClient mongoClient() { - ConnectionString connectionString = new ConnectionString(URI); - MongoClientSettings mongoClientSettings = MongoClientSettings.builder() - .applyConnectionString(connectionString) - .build(); - return MongoClients.create(mongoClientSettings); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/config/OracleDbConfig.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/config/OracleDbConfig.java deleted file mode 100644 index 8e2635d..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/config/OracleDbConfig.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.ex.chatbotapi.config; - -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; -import org.springframework.jdbc.core.JdbcTemplate; -import javax.sql.DataSource; - -@Configuration -public class OracleDbConfig { - @Bean - @ConfigurationProperties("spring.datasource.bpstl") - public DataSourceProperties bpstlDataSourceProperties() { - return new DataSourceProperties(); - } - @Bean - @ConfigurationProperties("spring.datasource.troad") - public DataSourceProperties troadDataSourceProperties() { - return new DataSourceProperties(); - } - @Bean - @ConfigurationProperties("spring.datasource.restarea") - public DataSourceProperties restAreaDataSourceProperties() { - return new DataSourceProperties(); - } - - @Bean - @Primary - @Qualifier("bpstlDataSource") - public DataSource bpstlDataSource(){ - return bpstlDataSourceProperties() - .initializeDataSourceBuilder() - .build(); - } - - @Bean - @Qualifier("troadDataSource") - public DataSource troadDataSource(){ - return troadDataSourceProperties() - .initializeDataSourceBuilder() - .build(); - } - - @Bean - @Qualifier("restAreaDataSource") - public DataSource restAreaDataSource(){ - return restAreaDataSourceProperties() - .initializeDataSourceBuilder() - .build(); - } - - @Bean(name = "bpstlTemplate") - public JdbcTemplate bpstlJdbcTemplate(@Qualifier("bpstlDataSource") DataSource dataSource) { - return new JdbcTemplate(dataSource); - } - - @Bean(name = "troadTemplate") - public JdbcTemplate troadJdbcTemplate(@Qualifier("troadDataSource") DataSource dataSource) { - return new JdbcTemplate(dataSource); - } - - @Bean(name = "restAreaTemplate") - public JdbcTemplate restAreaJdbcTemplate(@Qualifier("restAreaDataSource") DataSource dataSource) { - return new JdbcTemplate(dataSource); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/config/RagConfig.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/config/RagConfig.java deleted file mode 100644 index b984264..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/config/RagConfig.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.ex.chatbotapi.config; - -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -/** - * RAG 서버 관련 설정 (Docker 컨테이너) - */ -@Getter -@Setter -@Configuration -@ConfigurationProperties(prefix = "rag.server") -public class RagConfig { - /** - * RAG 서버 호스트 (Docker 컨테이너는 localhost) - */ - private String host = "localhost"; - - /** - * RAG 서버 포트 (Docker 외부 포트) - */ - private int port = 28012; - - /** - * RAG 서버 기본 URL - */ - private String baseUrl; - - /** - * 질의 엔드포인트 - */ - private String askEndpoint = "/ask"; - - /** - * 헬스체크 엔드포인트 - */ - private String healthEndpoint = "/health"; - - /** - * LLM 기반 intent 분석 엔드포인트 - */ - private String intentAnalyzeEndpoint = "/intent/analyze"; - - /** - * 연결 타임아웃 (ms) - */ - private int connectTimeout = 5000; - - /** - * 읽기 타임아웃 (ms) - */ - private int readTimeout = 30000; - - /** - * RAG 서버 질의 URL 반환 - */ - public String getFullUrl() { - if (baseUrl != null && !baseUrl.isEmpty()) { - return baseUrl + askEndpoint; - } - return String.format("http://%s:%d%s", host, port, askEndpoint); - } - - /** - * RAG 서버 헬스체크 URL 반환 - */ - public String getHealthUrl() { - if (baseUrl != null && !baseUrl.isEmpty()) { - return baseUrl + healthEndpoint; - } - return String.format("http://%s:%d%s", host, port, healthEndpoint); - } - - /** - * RAG 서버 intent 분석 URL 반환 - */ - public String getIntentAnalyzeUrl() { - if (baseUrl != null && !baseUrl.isEmpty()) { - return baseUrl + intentAnalyzeEndpoint; - } - return String.format("http://%s:%d%s", host, port, intentAnalyzeEndpoint); - } -} - diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/config/RestfulConfig.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/config/RestfulConfig.java deleted file mode 100644 index d3a23d4..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/config/RestfulConfig.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.ex.chatbotapi.config; - -import org.apache.http.client.HttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; -import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; -import org.springframework.web.client.RestTemplate; - -@Configuration -public class RestfulConfig { - @Value("${restTemplate.factory.readTimeout}") - private int READ_TIMEOUT; - - @Value("${restTemplate.factory.connectTimeout}") - private int CONNECT_TIMEOUT; - - @Value("${restTemplate.httpClient.maxConnTotal}") - private int MAX_CONN_TOTAL; - - @Value("${restTemplate.httpClient.maxConnPerRoute}") - private int MAX_CONN_PER_ROUTE; - - @Bean - @Primary - public RestTemplate restTemplate() { - - HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(); - factory.setReadTimeout(READ_TIMEOUT); - factory.setConnectTimeout(CONNECT_TIMEOUT); - - HttpClient httpClient = HttpClientBuilder.create() - .setMaxConnTotal(MAX_CONN_TOTAL) - .setMaxConnPerRoute(MAX_CONN_PER_ROUTE) - .build(); - - factory.setHttpClient(httpClient); - RestTemplate restTemplate = new RestTemplate(factory); - - return restTemplate; - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/api/AgentApiController.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/api/AgentApiController.java deleted file mode 100644 index d5bafa4..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/api/AgentApiController.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.ex.chatbotapi.domain.agent.api; - -import com.ex.chatbotapi.domain.agent.dto.AgentQueryRequest; -import com.ex.chatbotapi.domain.agent.dto.AgentQueryResponse; -import com.ex.chatbotapi.domain.agent.service.AgentOrchestratorService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.HashMap; -import java.util.Map; - -/** - * 에이전트 통합 API 컨트롤러 - * - * KakaoIntentApp 호출 규격: - * POST /api/v1/agent - * { - * "question": "판교에서 신갈까지 요금이 얼마야?", - * "intentType": "FARE_SEARCH", - * "params": { "fromIc": "판교", "toIc": "신갈" }, - * "botId": "user_id" - * } - */ -@Slf4j -@RestController -@RequestMapping("/api/v1/agent") -@RequiredArgsConstructor -public class AgentApiController { - - private final AgentOrchestratorService agentOrchestratorService; - - /** - * 통합 질의 엔드포인트 (KakaoIntentApp → chatbotApi 메인 진입점) - */ - @PostMapping - public ResponseEntity processQuery(@RequestBody AgentQueryRequest request) { - log.info("Agent API 호출 - question: {}, intentType: {}", - request.getQuestion(), request.getIntentType()); - return ResponseEntity.ok(agentOrchestratorService.processUserQuery(request)); - } - - /** - * 테스트용 GET 엔드포인트 - */ - @GetMapping - public ResponseEntity processQueryGet( - @RequestParam String question, - @RequestParam(required = false) String intentType, - @RequestParam(required = false) String carNo, - @RequestParam(required = false) String fromIc, - @RequestParam(required = false) String toIc, - @RequestParam(required = false) String icName, - @RequestParam(required = false) String restAreaName, - @RequestParam(required = false) String botId) { - - log.info("Agent API 호출 (GET) - question: {}, intentType: {}", question, intentType); - - Map params = new HashMap<>(); - if (carNo != null) params.put("carNo", carNo); - if (fromIc != null) params.put("fromIc", fromIc); - if (toIc != null) params.put("toIc", toIc); - if (icName != null) params.put("icName", icName); - if (restAreaName != null) params.put("restAreaName", restAreaName); - - AgentQueryRequest request = new AgentQueryRequest(question, intentType, params, botId, null); - return ResponseEntity.ok(agentOrchestratorService.processUserQuery(request)); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/api/AgentToolController.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/api/AgentToolController.java deleted file mode 100644 index 1b3e3de..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/api/AgentToolController.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.ex.chatbotapi.domain.agent.api; - -import com.ex.chatbotapi.config.AgentProperties; -import com.ex.chatbotapi.domain.agent.dto.ToolExecuteRequest; -import com.ex.chatbotapi.domain.agent.dto.ToolExecuteResponse; -import com.ex.chatbotapi.domain.agent.service.ToolDefinitionService; -import com.ex.chatbotapi.domain.agent.service.ToolExecutionService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.LinkedHashMap; -import java.util.Map; - -@Slf4j -@RestController -@RequestMapping("/api/v1/tools") -@RequiredArgsConstructor -public class AgentToolController { - private final ToolDefinitionService toolDefinitionService; - private final ToolExecutionService toolExecutionService; - private final AgentProperties agentProperties; - - @GetMapping("/definitions") - public ResponseEntity definitions( - @RequestHeader(value = "X-Internal-Tool-Key", required = false) String apiKey - ) { - ResponseEntity authError = authorize(apiKey); - if (authError != null) { - return authError; - } - - Map body = new LinkedHashMap<>(); - body.put("tools", toolDefinitionService.listToolDefinitions()); - return ResponseEntity.ok(body); - } - - @PostMapping("/execute") - public ResponseEntity execute( - @RequestHeader(value = "X-Internal-Tool-Key", required = false) String apiKey, - @RequestBody ToolExecuteRequest request - ) { - ResponseEntity authError = authorize(apiKey); - if (authError != null) { - return authError; - } - - log.info("Tool execute - tool: {}, botId: {}", request.getToolName(), request.getBotId()); - return ResponseEntity.ok(toolExecutionService.execute(request)); - } - - private ResponseEntity authorize(String apiKey) { - String expected = agentProperties.getTool().getInternalApiKey(); - if (expected == null || expected.trim().isEmpty()) { - return null; - } - if (expected.equals(apiKey)) { - return null; - } - Map errorBody = new LinkedHashMap<>(); - errorBody.put("status", false); - errorBody.put("statusMsg", "invalid or missing X-Internal-Tool-Key"); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorBody); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/api/DomainOnlyApiController.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/api/DomainOnlyApiController.java deleted file mode 100644 index 88bedf7..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/api/DomainOnlyApiController.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.ex.chatbotapi.domain.agent.api; - -import com.ex.chatbotapi.domain.agent.dto.DomainOnlyRequest; -import com.ex.chatbotapi.domain.agent.dto.DomainOnlyResponse; -import com.ex.chatbotapi.domain.agent.service.DomainOnlyService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.HashMap; -import java.util.Map; - -/** - * 도메인 데이터 전용 API 컨트롤러 (LLM/RAG 호출 없음) - * - * LLM 응답이 불필요한 인텐트(예: 전화번호 조회)에서 사용. - * DB 도메인 데이터만 조회하여 반환하므로 응답이 빠르고 비용이 없음. - * - * KakaoIntentApp 호출 규격: - * POST /api/v1/domain-only - * { - * "intentType": "IC_TEL", - * "params": { "icName": "서울" } - * } - */ -@Slf4j -@RestController -@RequestMapping("/api/v1/domain-only") -@RequiredArgsConstructor -public class DomainOnlyApiController { - - private final DomainOnlyService domainOnlyService; - - /** - * 도메인 데이터 전용 조회 엔드포인트 - */ - @PostMapping - public ResponseEntity fetchDomainData(@RequestBody DomainOnlyRequest request) { - log.info("DomainOnly API 호출 - intentType: {}", request.getIntentType()); - return ResponseEntity.ok(domainOnlyService.fetchDomainData(request)); - } - - /** - * 테스트용 GET 엔드포인트 - */ - @GetMapping - public ResponseEntity fetchDomainDataGet( - @RequestParam String intentType, - @RequestParam(required = false) String icName, - @RequestParam(required = false) String carNo, - @RequestParam(required = false) String fromIc, - @RequestParam(required = false) String toIc, - @RequestParam(required = false) String restAreaName) { - - log.info("DomainOnly API 호출 (GET) - intentType: {}", intentType); - - Map params = new HashMap<>(); - if (icName != null) params.put("icName", icName); - if (carNo != null) params.put("carNo", carNo); - if (fromIc != null) params.put("fromIc", fromIc); - if (toIc != null) params.put("toIc", toIc); - if (restAreaName != null) params.put("restAreaName", restAreaName); - - DomainOnlyRequest request = new DomainOnlyRequest(intentType, params); - return ResponseEntity.ok(domainOnlyService.fetchDomainData(request)); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/api/LlmAgentController.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/api/LlmAgentController.java deleted file mode 100644 index 71260ce..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/api/LlmAgentController.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.ex.chatbotapi.domain.agent.api; - -import com.ex.chatbotapi.domain.agent.dto.LlmAgentRequest; -import com.ex.chatbotapi.domain.agent.dto.LlmAgentResponse; -import com.ex.chatbotapi.domain.agent.service.LlmAgentRouterService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -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; - -@Slf4j -@RestController -@RequestMapping("/api/v1/llm-agent") -@RequiredArgsConstructor -public class LlmAgentController { - private final LlmAgentRouterService llmAgentRouterService; - - @PostMapping - public ResponseEntity process(@RequestBody LlmAgentRequest request) { - log.info("LLM Agent API 호출 - question: {}, botId: {}", request.getQuestion(), request.getBotId()); - return ResponseEntity.ok(llmAgentRouterService.process(request)); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/client/KakaoSkillToolClient.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/client/KakaoSkillToolClient.java deleted file mode 100644 index a24ab7a..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/client/KakaoSkillToolClient.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.ex.chatbotapi.domain.agent.client; - -import com.ex.chatbotapi.config.KakaoSkillConfig; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Component; -import org.springframework.web.client.RestClientException; -import org.springframework.web.client.RestTemplate; - -import java.util.HashMap; -import java.util.Map; - -@Slf4j -@Component -@RequiredArgsConstructor -public class KakaoSkillToolClient { - private final RestTemplate restTemplate; - private final KakaoSkillConfig kakaoSkillConfig; - - public Map fetchDataPortal( - String intentType, - Map params, - String userInput - ) { - String url = kakaoSkillConfig.getDataPortalToolUrl(); - Map body = new HashMap<>(); - body.put("intentType", intentType); - body.put("params", params); - body.put("userInput", userInput); - - try { - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - if (kakaoSkillConfig.getInternalApiKey() != null - && !kakaoSkillConfig.getInternalApiKey().trim().isEmpty()) { - headers.set("X-Internal-Tool-Key", kakaoSkillConfig.getInternalApiKey()); - } - HttpEntity> entity = new HttpEntity<>(body, headers); - ResponseEntity> response = restTemplate.exchange( - url, - HttpMethod.POST, - entity, - new ParameterizedTypeReference>() {} - ); - Map result = response.getBody(); - return result != null ? result : error("empty response"); - } catch (RestClientException e) { - log.error("kakao data portal tool failed - intentType: {}, error: {}", intentType, e.getMessage(), e); - return error(e.getMessage()); - } - } - - private Map error(String message) { - Map result = new HashMap<>(); - result.put("status", false); - result.put("statusMsg", message); - return result; - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/AgentQueryRequest.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/AgentQueryRequest.java deleted file mode 100644 index 33ca424..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/AgentQueryRequest.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.ex.chatbotapi.domain.agent.dto; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.Map; - -/** - * KakaoIntentApp → chatbotApi 통합 엔드포인트 요청 DTO - * - * KakaoIntentApp은 @KakaoIntent 라우팅에서 이미 의도를 알고 있으므로 - * intentType과 슬롯 파라미터를 직접 담아서 보냄 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class AgentQueryRequest { - - /** 사용자 원본 질문 */ - private String question; - - /** - * 의도 타입 (KakaoIntentApp의 @KakaoIntent 블록명 기반) - * 예: "FARE_SEARCH", "REST_AREA_FOOD_LIST", "IC_TEL" - * null 또는 등록된 핸들러 없음 → RAG 단독 처리 - */ - private String intentType; - - /** - * 슬롯 파라미터 (카카오 슬롯 필링 결과) - * 예: { "fromIc": "판교", "toIc": "신갈" } - * { "restAreaName": "화성" } - * { "carNo": "12가3456" } - */ - private Map params; - - /** 봇 ID (대화 이력 관리용) */ - private String botId; - - /** - * 클라이언트(skill)가 직접 수집한 도메인 데이터. - * 값이 있으면 chatbotApi의 IntentHandler 호출을 생략하고 formatter + RAG만 실행한다. - * (WAS에서 접근 불가한 외부 API를 Web 계층에서 호출하는 경우 사용) - */ - private Map domainData; -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/AgentQueryResponse.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/AgentQueryResponse.java deleted file mode 100644 index 10db133..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/AgentQueryResponse.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.ex.chatbotapi.domain.agent.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.Map; -import java.util.List; - -/** - * chatbotApi → KakaoIntentApp 통합 엔드포인트 응답 DTO - * - * KakaoIntentApp은 두 가지를 활용: - * - llmAnswer: SimpleText로 표시 (LLM 자연어 응답) - * - domainData: 기존 ListCard/BasicCard 포맷 구성에 활용 (구조화 DB 데이터) - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class AgentQueryResponse { - - /** 처리 성공 여부 */ - private boolean status; - - /** 처리된 의도 타입 */ - private String intentType; - - /** LLM이 생성한 자연어 응답 (SimpleText 표시용) */ - private String llmAnswer; - - /** - * DB 조회 구조화 데이터 (카드 포맷 구성용) - * intentType이 없거나 RAG 단독 처리 시 null - */ - private Map domainData; - - /** 공식 FAQ 상세 URL 목록 */ - private List faqUrls; - - /** RAG 참고자료 메타데이터 */ - private List> references; - - /** 상태 메시지 (에러 시 오류 내용) */ - private String statusMsg; -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/CarNoParamNormalizationResult.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/CarNoParamNormalizationResult.java deleted file mode 100644 index b95fd36..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/CarNoParamNormalizationResult.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.ex.chatbotapi.domain.agent.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.HashMap; -import java.util.Map; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class CarNoParamNormalizationResult { - private boolean valid; - private String clarificationQuestion; - - @Builder.Default - private Map normalizedParams = new HashMap<>(); -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/DomainOnlyRequest.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/DomainOnlyRequest.java deleted file mode 100644 index 92e8618..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/DomainOnlyRequest.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.ex.chatbotapi.domain.agent.dto; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.Map; - -/** - * KakaoIntentApp → chatbotApi 도메인 데이터 전용 엔드포인트 요청 DTO - * - * LLM/RAG 호출 없이 DB 도메인 데이터 조회만 수행. - * 예: getTelIcInfo (전화번호 조회) 등 LLM 응답이 불필요한 경우 사용. - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class DomainOnlyRequest { - - /** - * 의도 타입 (등록된 IntentHandler와 매핑) - * 예: "IC_TEL" - */ - private String intentType; - - /** - * 슬롯 파라미터 (카카오 슬롯 필링 결과) - * 예: { "icName": "서울" } - */ - private Map params; -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/DomainOnlyResponse.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/DomainOnlyResponse.java deleted file mode 100644 index 7bfdfc9..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/DomainOnlyResponse.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.ex.chatbotapi.domain.agent.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.Map; - -/** - * chatbotApi → KakaoIntentApp 도메인 데이터 전용 엔드포인트 응답 DTO - * - * llmAnswer 없이 domainData만 반환. - * KakaoIntentApp은 domainData를 직접 파싱해 카드 UI를 구성. - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class DomainOnlyResponse { - - /** 처리 성공 여부 */ - private boolean status; - - /** 처리된 의도 타입 */ - private String intentType; - - /** - * DB 조회 구조화 데이터 (카드 포맷 구성용) - * 핸들러 없거나 조회 실패 시 null - */ - private Map domainData; - - /** 상태 메시지 (에러 시 오류 내용) */ - private String statusMsg; -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/IcNameCandidate.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/IcNameCandidate.java deleted file mode 100644 index bc5ccf7..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/IcNameCandidate.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.ex.chatbotapi.domain.agent.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class IcNameCandidate { - private String icName; - private String icCode; -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/IcParamNormalizationResult.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/IcParamNormalizationResult.java deleted file mode 100644 index 667b3ad..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/IcParamNormalizationResult.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.ex.chatbotapi.domain.agent.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class IcParamNormalizationResult { - private boolean ambiguous; - private String ambiguousParam; - private String originalValue; - private String normalizedValue; - private String clarificationQuestion; - - @Builder.Default - private Map normalizedParams = new HashMap<>(); - - @Builder.Default - private List candidates = new ArrayList<>(); -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/IntentValidationResult.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/IntentValidationResult.java deleted file mode 100644 index adb96e4..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/IntentValidationResult.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.ex.chatbotapi.domain.agent.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class IntentValidationResult { - private boolean valid; - @Builder.Default - private Map cleanedParams = new HashMap<>(); - @Builder.Default - private List missingParams = new ArrayList<>(); - private String clarificationQuestion; -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/LlmAgentRequest.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/LlmAgentRequest.java deleted file mode 100644 index 27535cf..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/LlmAgentRequest.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.ex.chatbotapi.domain.agent.dto; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class LlmAgentRequest { - private String question; - private String botId; -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/LlmAgentResponse.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/LlmAgentResponse.java deleted file mode 100644 index 67022c1..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/LlmAgentResponse.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.ex.chatbotapi.domain.agent.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; -import java.util.Map; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class LlmAgentResponse { - private boolean status; - private String statusMsg; - private String routeType; - private String intentType; - private String fetchOwner; - private String uiType; - private String answer; - private String llmAnswer; - private Map domainData; - private List faqUrls; - private List> references; - private boolean needsClarification; - private String clarificationQuestion; - private String pendingIntentType; - private List missingParams; - private Map params; - private List icCandidates; -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/PendingIntentState.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/PendingIntentState.java deleted file mode 100644 index ee2400b..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/PendingIntentState.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.ex.chatbotapi.domain.agent.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class PendingIntentState { - private String botId; - private String intentType; - @Builder.Default - private Map collectedParams = new HashMap<>(); - @Builder.Default - private List missingParams = new ArrayList<>(); - private Instant createdAt; -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/RestAreaParamNormalizationResult.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/RestAreaParamNormalizationResult.java deleted file mode 100644 index 1626d3a..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/RestAreaParamNormalizationResult.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.ex.chatbotapi.domain.agent.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.HashMap; -import java.util.Map; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class RestAreaParamNormalizationResult { - private boolean ambiguous; - private String ambiguousParam; - private String originalValue; - private String normalizedValue; - private String clarificationQuestion; - - @Builder.Default - private Map normalizedParams = new HashMap<>(); -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/ToolExecuteRequest.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/ToolExecuteRequest.java deleted file mode 100644 index 341c7c7..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/ToolExecuteRequest.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.ex.chatbotapi.domain.agent.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.HashMap; -import java.util.Map; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class ToolExecuteRequest { - private String toolName; - @Builder.Default - private Map arguments = new HashMap<>(); - private String botId; - private String userInput; -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/ToolExecuteResponse.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/ToolExecuteResponse.java deleted file mode 100644 index 2daeca4..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/ToolExecuteResponse.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.ex.chatbotapi.domain.agent.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class ToolExecuteResponse { - private boolean status; - private String statusMsg; - private String intentType; - private String fetchOwner; - private String uiType; - @Builder.Default - private Map params = new HashMap<>(); - private Map domainData; - private boolean needsClarification; - private String clarificationQuestion; - @Builder.Default - private List missingParams = new ArrayList<>(); - @Builder.Default - private List icCandidates = new ArrayList<>(); -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/ToolParamPipelineResult.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/ToolParamPipelineResult.java deleted file mode 100644 index 9c5fb5c..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/dto/ToolParamPipelineResult.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.ex.chatbotapi.domain.agent.dto; - -import lombok.Builder; -import lombok.Data; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@Data -@Builder -public class ToolParamPipelineResult { - private boolean valid; - @Builder.Default - private Map params = new HashMap<>(); - private boolean needsClarification; - private String clarificationQuestion; - @Builder.Default - private List missingParams = new ArrayList<>(); - @Builder.Default - private List icCandidates = new ArrayList<>(); - - public static ToolParamPipelineResult success(Map params) { - return ToolParamPipelineResult.builder() - .valid(true) - .params(params) - .build(); - } - - public static ToolParamPipelineResult clarify( - Map params, - String question, - List missingParams - ) { - return ToolParamPipelineResult.builder() - .valid(false) - .params(params) - .needsClarification(true) - .clarificationQuestion(question) - .missingParams(missingParams != null ? missingParams : new ArrayList<>()) - .build(); - } - - public static ToolParamPipelineResult icAmbiguous( - Map params, - String question, - List candidates - ) { - return ToolParamPipelineResult.builder() - .valid(false) - .params(params) - .needsClarification(true) - .clarificationQuestion(question) - .icCandidates(candidates != null ? candidates : new ArrayList<>()) - .build(); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/formatter/LlmDomainDataFormatter.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/formatter/LlmDomainDataFormatter.java deleted file mode 100644 index ac565d6..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/formatter/LlmDomainDataFormatter.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.ex.chatbotapi.domain.agent.formatter; - -import java.util.Map; - -/** - * RAG/LLM 프롬프트용으로 domainData에 사람이 읽기 쉬운 요약(llmSummary)을 붙인다. - * KakaoIntentApp은 기존 키 구조(fareData 등)를 그대로 쓰므로, 이 인터페이스는 Map을 mutate 하되 - * 스킬이 쓰는 필드는 제거하지 않는다. - */ -public interface LlmDomainDataFormatter { - - String getIntentType(); - - /** - * domainData에 "llmSummary" 키(한국어 요약 문자열)를 추가한다. - * - * @param domainData {@link com.ex.chatbotapi.domain.agent.handler.IntentHandler} 조회 결과 (null이면 무시) - */ - void enrich(Map domainData); -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/handler/IntentHandler.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/handler/IntentHandler.java deleted file mode 100644 index 644ed20..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/handler/IntentHandler.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.ex.chatbotapi.domain.agent.handler; - -import java.util.Map; - -/** - * 의도(Intent)별 도메인 데이터 조회를 담당하는 핸들러 인터페이스 - * - * 새 스킬 추가 시: - * 1. 이 인터페이스를 구현하는 XxxHandler 클래스 생성 (@Component) - * 2. getIntentType()에 intentType 문자열 반환 - * 3. fetchDomainData()에 도메인 서비스 호출 로직 작성 - * → AgentOrchestratorService 수정 불필요 - */ -public interface IntentHandler { - - /** - * 처리할 의도 타입 반환 (KakaoIntentApp이 전달하는 intentType과 일치해야 함) - * 예: "FARE_SEARCH", "REST_AREA_PRICE" - */ - String getIntentType(); - - /** - * 슬롯 파라미터를 받아 도메인 서비스에서 DB 데이터 조회 - * - * @param params KakaoIntentApp 슬롯에서 추출한 파라미터 Map - * @return DB 조회 결과 (RAG 프롬프트에 포함되어 LLM에 전달됨) - */ - Map fetchDomainData(Map params); -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/intent/FetchOwner.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/intent/FetchOwner.java deleted file mode 100644 index 77392a9..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/intent/FetchOwner.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.ex.chatbotapi.domain.agent.intent; - -public enum FetchOwner { - WAS, - WEB, - NONE -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/intent/IntentDefinition.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/intent/IntentDefinition.java deleted file mode 100644 index 29ed9a7..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/intent/IntentDefinition.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.ex.chatbotapi.domain.agent.intent; - -import lombok.Data; - -import java.util.Collections; -import java.util.List; -import java.util.Map; - -@Data -public class IntentDefinition { - private String intentType; - private String description; - private FetchOwner fetchOwner = FetchOwner.WAS; - private String uiType; - private List requiredParams = Collections.emptyList(); - private List optionalParams = Collections.emptyList(); - private List requiredParamDefinitions = Collections.emptyList(); - private List optionalParamDefinitions = Collections.emptyList(); - private Double minConfidence; - private Clarification clarification = new Clarification(); - - public double minConfidenceOrDefault(double defaultValue) { - return minConfidence != null ? minConfidence : defaultValue; - } - - public String clarificationFor(List missingParams) { - if (clarification == null) { - return "조회에 필요한 정보를 조금 더 알려주세요."; - } - if (missingParams != null && missingParams.size() == 1 && clarification.getMissing() != null) { - String message = clarification.getMissing().get(missingParams.get(0)); - if (message != null && !message.trim().isEmpty()) { - return message; - } - } - return clarification.getDefaultMessage() != null - ? clarification.getDefaultMessage() - : "조회에 필요한 정보를 조금 더 알려주세요."; - } - - @Data - public static class Clarification { - private String defaultMessage; - private Map missing = Collections.emptyMap(); - } - - @Data - public static class ParamDefinition { - private String name; - private String description; - private List examples = Collections.emptyList(); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/intent/IntentDefinitionRegistry.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/intent/IntentDefinitionRegistry.java deleted file mode 100644 index c30cdff..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/intent/IntentDefinitionRegistry.java +++ /dev/null @@ -1,199 +0,0 @@ -package com.ex.chatbotapi.domain.agent.intent; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.core.io.ClassPathResource; -import org.springframework.stereotype.Component; -import org.yaml.snakeyaml.Yaml; - -import javax.annotation.PostConstruct; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -@Slf4j -@Component -public class IntentDefinitionRegistry { - private static final String INTENT_DEFINITIONS_FILE = "intent-definitions.yml"; - private static final Path EXTERNAL_INTENT_DEFINITIONS_PATH = Paths.get("config", INTENT_DEFINITIONS_FILE); - - private final Map definitions = new LinkedHashMap<>(); - - @PostConstruct - @SuppressWarnings("unchecked") - public void load() { - try (InputStream inputStream = openIntentDefinitions()) { - Map root = new Yaml().load(inputStream); - Map intents = (Map) root.get("intents"); - definitions.clear(); - - for (Map.Entry entry : intents.entrySet()) { - String intentType = entry.getKey(); - Map raw = (Map) entry.getValue(); - IntentDefinition definition = toDefinition(intentType, raw); - definitions.put(intentType, definition); - } - log.info("intent definitions loaded: {}", definitions.keySet()); - } catch (Exception e) { - throw new IllegalStateException("intent-definitions.yml 로드 실패", e); - } - } - - private InputStream openIntentDefinitions() throws IOException { - if (Files.exists(EXTERNAL_INTENT_DEFINITIONS_PATH)) { - log.info("intent definitions loaded from external file: {}", EXTERNAL_INTENT_DEFINITIONS_PATH.toAbsolutePath()); - return Files.newInputStream(EXTERNAL_INTENT_DEFINITIONS_PATH); - } - - log.info("external intent definitions not found. using classpath resource: {}", INTENT_DEFINITIONS_FILE); - return new ClassPathResource(INTENT_DEFINITIONS_FILE).getInputStream(); - } - - public boolean supports(String intentType) { - return intentType != null && definitions.containsKey(intentType); - } - - public IntentDefinition get(String intentType) { - return definitions.get(intentType); - } - - public Map all() { - return Collections.unmodifiableMap(definitions); - } - - public List> toAnalysisDefinitions() { - List> result = new ArrayList<>(); - for (IntentDefinition definition : definitions.values()) { - Map item = new LinkedHashMap<>(); - item.put("intentType", definition.getIntentType()); - item.put("description", definition.getDescription()); - item.put("params", mergeParamDefinitions(definition)); - result.add(item); - } - return result; - } - - private List> mergeParamDefinitions(IntentDefinition definition) { - List> params = new ArrayList<>(); - appendParamDefinitions(params, definition.getRequiredParamDefinitions(), true); - appendParamDefinitions(params, definition.getOptionalParamDefinitions(), false); - return params; - } - - private void appendParamDefinitions( - List> params, - List definitions, - boolean required - ) { - for (IntentDefinition.ParamDefinition definition : definitions) { - Map item = new LinkedHashMap<>(); - item.put("name", definition.getName()); - item.put("description", definition.getDescription()); - item.put("required", required); - item.put("examples", definition.getExamples()); - params.add(item); - } - } - - private IntentDefinition toDefinition(String intentType, Map raw) { - IntentDefinition definition = new IntentDefinition(); - definition.setIntentType(intentType); - definition.setDescription(asString(raw.get("description"))); - definition.setFetchOwner(FetchOwner.valueOf(asString(raw.get("fetchOwner")))); - definition.setUiType(asString(raw.get("uiType"))); - List requiredParamDefinitions = asParamDefinitions(raw.get("requiredParams")); - List optionalParamDefinitions = asParamDefinitions(raw.get("optionalParams")); - definition.setRequiredParamDefinitions(requiredParamDefinitions); - definition.setOptionalParamDefinitions(optionalParamDefinitions); - definition.setRequiredParams(toParamNames(requiredParamDefinitions)); - definition.setOptionalParams(toParamNames(optionalParamDefinitions)); - definition.setMinConfidence(asDouble(raw.get("minConfidence"))); - definition.setClarification(toClarification(raw.get("clarification"))); - return definition; - } - - @SuppressWarnings("unchecked") - private IntentDefinition.Clarification toClarification(Object value) { - IntentDefinition.Clarification clarification = new IntentDefinition.Clarification(); - if (!(value instanceof Map)) { - return clarification; - } - - Map raw = (Map) value; - clarification.setDefaultMessage(asString(raw.get("default"))); - - Map missing = new LinkedHashMap<>(); - Object missingObject = raw.get("missing"); - if (missingObject instanceof Map) { - Map rawMissing = (Map) missingObject; - for (Map.Entry entry : rawMissing.entrySet()) { - missing.put(entry.getKey(), asString(entry.getValue())); - } - } - clarification.setMissing(missing); - return clarification; - } - - private String asString(Object value) { - return value != null ? String.valueOf(value) : null; - } - - private Double asDouble(Object value) { - if (value instanceof Number) { - return ((Number) value).doubleValue(); - } - if (value == null) { - return null; - } - return Double.valueOf(String.valueOf(value)); - } - - @SuppressWarnings("unchecked") - private List asParamDefinitions(Object value) { - if (!(value instanceof List)) { - return Collections.emptyList(); - } - List result = new ArrayList<>(); - for (Object item : (List) value) { - IntentDefinition.ParamDefinition paramDefinition = new IntentDefinition.ParamDefinition(); - if (item instanceof Map) { - Map raw = (Map) item; - paramDefinition.setName(asString(raw.get("name"))); - paramDefinition.setDescription(asString(raw.get("description"))); - paramDefinition.setExamples(asStringList(raw.get("examples"))); - } else { - paramDefinition.setName(asString(item)); - } - if (paramDefinition.getName() != null && !paramDefinition.getName().trim().isEmpty()) { - result.add(paramDefinition); - } - } - return result; - } - - private List toParamNames(List paramDefinitions) { - List result = new ArrayList<>(); - for (IntentDefinition.ParamDefinition paramDefinition : paramDefinitions) { - result.add(paramDefinition.getName()); - } - return result; - } - - @SuppressWarnings("unchecked") - private List asStringList(Object value) { - if (!(value instanceof List)) { - return Collections.emptyList(); - } - List result = new ArrayList<>(); - for (Object item : (List) value) { - result.add(asString(item)); - } - return result; - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/AgentOrchestratorService.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/AgentOrchestratorService.java deleted file mode 100644 index 59837ac..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/AgentOrchestratorService.java +++ /dev/null @@ -1,131 +0,0 @@ -package com.ex.chatbotapi.domain.agent.service; - -import com.ex.chatbotapi.domain.agent.dto.AgentQueryRequest; -import com.ex.chatbotapi.domain.agent.dto.AgentQueryResponse; -import com.ex.chatbotapi.domain.agent.formatter.LlmDomainDataFormatter; -import com.ex.chatbotapi.domain.agent.handler.IntentHandler; -import com.ex.chatbotapi.domain.rag.dto.RagQueryRequest; -import com.ex.chatbotapi.domain.rag.service.RagService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; - -/** - * 에이전트 오케스트레이터 - * - * 처리 흐름: - * 1. intentType으로 등록된 IntentHandler 조회 - * 2. 핸들러가 있으면 DB 도메인 데이터 조회 - * 3. 질문 + 도메인 데이터를 RAG 서버(exAiChatBot)로 전달 - * 4. LLM 최종 응답 + 구조화 도메인 데이터를 함께 반환 - * - * 새 스킬 추가 방법: - * IntentHandler 인터페이스 구현체를 @Component로 등록하기만 하면 됨 - * 이 클래스 수정 불필요 - * - * LLM용 요약(llmSummary) 추가: - * LlmDomainDataFormatter 구현체를 @Component로 등록 (fare 등 도메인별) - */ -@Slf4j -@Service -public class AgentOrchestratorService { - - private final RagService ragService; - private final Map handlerMap; - private final Map llmFormatterMap; - - public AgentOrchestratorService(List handlers, - List llmFormatters, - RagService ragService) { - this.ragService = ragService; - this.handlerMap = handlers.stream() - .collect(Collectors.toMap(IntentHandler::getIntentType, Function.identity())); - this.llmFormatterMap = llmFormatters.stream() - .collect(Collectors.toMap(LlmDomainDataFormatter::getIntentType, Function.identity())); - log.info("등록된 IntentHandler ({}개): {}", handlerMap.size(), handlerMap.keySet()); - log.info("등록된 LlmDomainDataFormatter ({}개): {}", llmFormatterMap.size(), llmFormatterMap.keySet()); - } - - /** - * 사용자 질문 처리 메인 메서드 - */ - @SuppressWarnings("unchecked") - public AgentQueryResponse processUserQuery(AgentQueryRequest request) { - log.info("Agent Orchestrator - 질문: '{}', intentType: {}", - request.getQuestion(), request.getIntentType()); - - try { - Map domainData = fetchDomainData(request); - enrichDomainDataForLlm(request.getIntentType(), domainData); - - RagQueryRequest ragRequest = new RagQueryRequest(); - ragRequest.setQuestion(request.getQuestion()); - ragRequest.setBotId(request.getBotId()); - ragRequest.setDomainData(domainData); - ragRequest.setIntentType(request.getIntentType()); - - Map ragResult = ragService.processQuestion(ragRequest); - - return AgentQueryResponse.builder() - .status(true) - .intentType(request.getIntentType()) - .llmAnswer((String) ragResult.get("answer")) - .domainData(domainData) - .faqUrls((List) ragResult.get("faqUrls")) - .references((List>) ragResult.get("references")) - .statusMsg("정상응답") - .build(); - - } catch (Exception e) { - log.error("Agent Orchestrator 처리 오류: {}", e.getMessage(), e); - return AgentQueryResponse.builder() - .status(false) - .statusMsg("처리 중 오류 발생: " + e.getMessage()) - .build(); - } - } - - /** - * intentType에 매핑된 핸들러로 DB 데이터 조회 - * 등록된 핸들러가 없으면 null 반환 → RAG 단독 처리 - */ - private Map fetchDomainData(AgentQueryRequest request) { - // 클라이언트(skill)가 이미 domainData를 수집해서 전달한 경우 handler 생략 - if (request.getDomainData() != null && !request.getDomainData().isEmpty()) { - log.info("domainData pre-populated by web (intentType: {}), handler 생략", request.getIntentType()); - return request.getDomainData(); - } - - String intentType = request.getIntentType(); - if (intentType == null || request.getParams() == null) { - log.debug("intentType 또는 params 없음 → RAG 단독 처리"); - return null; - } - - IntentHandler handler = handlerMap.get(intentType); - if (handler == null) { - log.debug("등록된 핸들러 없음 (intentType: {}) → RAG 단독 처리", intentType); - return null; - } - - log.info("도메인 데이터 조회 시작 - intentType: {}, params: {}", intentType, request.getParams()); - return handler.fetchDomainData(request.getParams()); - } - - /** - * intentType에 등록된 포매터가 있으면 domainData에 llmSummary 등을 추가 (RAG 프롬프트 가독성). - */ - private void enrichDomainDataForLlm(String intentType, Map domainData) { - if (domainData == null || intentType == null) { - return; - } - LlmDomainDataFormatter formatter = llmFormatterMap.get(intentType); - if (formatter != null) { - formatter.enrich(domainData); - } - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/CarNoParamNormalizer.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/CarNoParamNormalizer.java deleted file mode 100644 index 0f02617..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/CarNoParamNormalizer.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.ex.chatbotapi.domain.agent.service; - -import com.ex.chatbotapi.domain.agent.dto.CarNoParamNormalizationResult; -import org.springframework.stereotype.Component; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@Component -public class CarNoParamNormalizer { - private static final List CAR_NO_INTENTS = Arrays.asList( - "FARE_UNPAID", - "FARE_REFUND" - ); - - public CarNoParamNormalizationResult normalize(String intentType, Map params) { - Map normalizedParams = new HashMap<>(); - if (params != null) { - normalizedParams.putAll(params); - } - - if (!CAR_NO_INTENTS.contains(intentType) || params == null || params.isEmpty()) { - return CarNoParamNormalizationResult.builder() - .valid(true) - .normalizedParams(normalizedParams) - .build(); - } - - String carNo = normalizedParams.get("carNo"); - if (carNo == null || carNo.trim().isEmpty()) { - return invalid(intentType, normalizedParams); - } - - String normalizedCarNo = normalizeCarNo(carNo); - if (!isFullCarNo(normalizedCarNo)) { - return invalid(intentType, normalizedParams); - } - - normalizedParams.put("carNo", normalizedCarNo); - return CarNoParamNormalizationResult.builder() - .valid(true) - .normalizedParams(normalizedParams) - .build(); - } - - private String normalizeCarNo(String carNo) { - return carNo.trim() - .replaceAll("[\\s\\-]", "") - .toUpperCase(); - } - - private boolean isFullCarNo(String carNo) { - return carNo.matches("\\d{2,3}[가-힣]\\d{4}"); - } - - private CarNoParamNormalizationResult invalid(String intentType, Map params) { - return CarNoParamNormalizationResult.builder() - .valid(false) - .normalizedParams(params) - .clarificationQuestion(clarificationQuestion(intentType)) - .build(); - } - - private String clarificationQuestion(String intentType) { - if ("FARE_UNPAID".equals(intentType)) { - return "챗봇에서 미납 통행료 조회가 가능합니다. 조회하려면 전체 차량번호를 입력해 주세요. 예: 12가3456 미납 조회"; - } - if ("FARE_REFUND".equals(intentType)) { - return "챗봇에서 환불 내역 조회가 가능합니다. 조회하려면 전체 차량번호를 입력해 주세요. 예: 12가3456 환불 조회"; - } - return "조회하려면 전체 차량번호를 입력해 주세요. 예: 12가3456"; - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/DomainOnlyService.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/DomainOnlyService.java deleted file mode 100644 index d18f107..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/DomainOnlyService.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.ex.chatbotapi.domain.agent.service; - -import com.ex.chatbotapi.domain.agent.dto.DomainOnlyRequest; -import com.ex.chatbotapi.domain.agent.dto.DomainOnlyResponse; -import com.ex.chatbotapi.domain.agent.handler.IntentHandler; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; - -/** - * 도메인 데이터 전용 서비스 (RAG/LLM 호출 없음) - * - * 전화번호 조회(IC_TEL) 등 LLM 응답이 불필요한 인텐트에 사용. - * AgentOrchestratorService와 동일한 IntentHandler 레지스트리를 재사용. - */ -@Slf4j -@Service -public class DomainOnlyService { - - private final Map handlerMap; - - public DomainOnlyService(List handlers) { - this.handlerMap = handlers.stream() - .collect(Collectors.toMap(IntentHandler::getIntentType, Function.identity())); - log.info("DomainOnlyService 초기화 - 등록된 핸들러 ({}개): {}", handlerMap.size(), handlerMap.keySet()); - } - - /** - * 도메인 데이터만 조회 (LLM 없음) - */ - public DomainOnlyResponse fetchDomainData(DomainOnlyRequest request) { - String intentType = request.getIntentType(); - log.info("DomainOnly 요청 - intentType: {}, params: {}", intentType, request.getParams()); - - try { - if (intentType == null || request.getParams() == null) { - log.warn("intentType 또는 params 누락"); - return DomainOnlyResponse.builder() - .status(false) - .statusMsg("intentType과 params는 필수입니다.") - .build(); - } - - IntentHandler handler = handlerMap.get(intentType); - if (handler == null) { - log.warn("등록된 핸들러 없음 - intentType: {}", intentType); - return DomainOnlyResponse.builder() - .status(false) - .intentType(intentType) - .statusMsg("처리 가능한 핸들러가 없습니다: " + intentType) - .build(); - } - - Map domainData = handler.fetchDomainData(request.getParams()); - log.info("DomainOnly 조회 완료 - intentType: {}", intentType); - - return DomainOnlyResponse.builder() - .status(true) - .intentType(intentType) - .domainData(domainData) - .statusMsg("정상응답") - .build(); - - } catch (Exception e) { - log.error("DomainOnly 처리 오류 - intentType: {}, 오류: {}", intentType, e.getMessage(), e); - return DomainOnlyResponse.builder() - .status(false) - .intentType(intentType) - .statusMsg("처리 중 오류 발생: " + e.getMessage()) - .build(); - } - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/IcParamNormalizer.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/IcParamNormalizer.java deleted file mode 100644 index ce71689..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/IcParamNormalizer.java +++ /dev/null @@ -1,149 +0,0 @@ -package com.ex.chatbotapi.domain.agent.service; - -import com.ex.chatbotapi.domain.agent.dto.IcNameCandidate; -import com.ex.chatbotapi.domain.agent.dto.IcParamNormalizationResult; -import com.ex.chatbotapi.domain.fare.repository.FareRepository; -import lombok.RequiredArgsConstructor; -import org.bson.Document; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -@Component -@RequiredArgsConstructor -public class IcParamNormalizer { - private static final int CANDIDATE_LIMIT = 5; - private static final List IC_INTENTS = Arrays.asList( - "FARE_SEARCH", - "TROAD_SEARCH_ROUTE", - "IC_TEL" - ); - - private final FareRepository fareRepository; - - public IcParamNormalizationResult normalize(String intentType, Map params) { - Map normalizedParams = new HashMap<>(); - if (params != null) { - normalizedParams.putAll(params); - } - - if (!IC_INTENTS.contains(intentType) || params == null || params.isEmpty()) { - return IcParamNormalizationResult.builder() - .normalizedParams(normalizedParams) - .build(); - } - - for (String paramName : icParamNames(intentType)) { - String originalValue = normalizedParams.get(paramName); - if (originalValue == null || originalValue.trim().isEmpty()) { - continue; - } - - String normalizedValue = normalizeInput(originalValue); - Document exact = fareRepository.findIcInfoByName(normalizedValue); - if (exact != null) { - String icName = exact.getString("ic_name"); - String icCode = formatIcCode(exact.get("ic_code")); - normalizedParams.put(paramName, icName); - normalizedParams.put(paramName + "Code", icCode); - continue; - } - - List candidates = findCandidates(normalizedValue); - if (!candidates.isEmpty()) { - return IcParamNormalizationResult.builder() - .ambiguous(true) - .ambiguousParam(paramName) - .originalValue(originalValue) - .normalizedValue(normalizedValue) - .normalizedParams(normalizedParams) - .candidates(candidates) - .clarificationQuestion(buildClarificationQuestion(paramName, normalizedValue, candidates)) - .build(); - } - - normalizedParams.put(paramName, normalizedValue); - } - - return IcParamNormalizationResult.builder() - .normalizedParams(normalizedParams) - .build(); - } - - private List icParamNames(String intentType) { - if ("IC_TEL".equals(intentType)) { - return Arrays.asList("icName"); - } - return Arrays.asList("fromIc", "toIc"); - } - - private String normalizeInput(String value) { - String normalized = value.trim().replaceAll("\\s+", ""); - String previous; - do { - previous = normalized; - normalized = normalized - .replaceAll("(?i)IC$", "") - .replaceAll("(영업소|톨게이트|요금소)$", ""); - } while (!previous.equals(normalized)); - return normalized.trim(); - } - - private List findCandidates(String icName) { - List documents = fareRepository.findIcInfoByNameContains(icName, CANDIDATE_LIMIT); - - Map unique = new LinkedHashMap<>(); - for (Document document : documents) { - String name = document.getString("ic_name"); - if (name == null || name.isEmpty()) { - continue; - } - unique.putIfAbsent(name, IcNameCandidate.builder() - .icName(name) - .icCode(formatIcCode(document.get("ic_code"))) - .build()); - } - return new ArrayList<>(unique.values()); - } - - private String buildClarificationQuestion( - String paramName, - String normalizedValue, - List candidates - ) { - StringBuilder sb = new StringBuilder(); - sb.append(normalizedValue).append("와 정확히 일치하는 영업소를 찾지 못했습니다. "); - sb.append(displayParamName(paramName)).append(" 영업소를 아래 후보 중에서 선택해 주세요.\n"); - for (IcNameCandidate candidate : candidates) { - sb.append("- ").append(candidate.getIcName()).append("\n"); - } - return sb.toString().trim(); - } - - private String displayParamName(String paramName) { - if ("fromIc".equals(paramName)) { - return "출발"; - } - if ("toIc".equals(paramName)) { - return "도착"; - } - return "조회할"; - } - - private String formatIcCode(Object icCode) { - if (icCode == null) { - return null; - } - - String code = String.valueOf(icCode).trim(); - if (code.matches("\\d{1,2}")) { - return String.format("%03d", Integer.parseInt(code)); - } - return code; - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/IntentAnalysisParamNormalizer.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/IntentAnalysisParamNormalizer.java deleted file mode 100644 index 4f4bb06..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/IntentAnalysisParamNormalizer.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.ex.chatbotapi.domain.agent.service; - -import org.springframework.stereotype.Component; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * LLM tool arguments(Object/List 등)를 validator가 처리 가능한 Map<String, String>으로 변환한다. - */ -@Component -public class IntentAnalysisParamNormalizer { - - public Map toStringMap(Map rawParams) { - Map normalized = new HashMap<>(); - if (rawParams == null) { - return normalized; - } - - for (Map.Entry entry : rawParams.entrySet()) { - String value = stringify(entry.getValue()); - if (value != null && !value.trim().isEmpty()) { - normalized.put(entry.getKey(), value); - } - } - return normalized; - } - - private String stringify(Object value) { - if (value == null) { - return null; - } - if (value instanceof Collection) { - return ((Collection) value).stream() - .map(String::valueOf) - .map(String::trim) - .filter(s -> !s.isEmpty()) - .collect(Collectors.joining(",")); - } - String text = String.valueOf(value).trim(); - if (text.startsWith("[") && text.endsWith("]")) { - text = text.substring(1, text.length() - 1) - .replace("\"", "") - .replace("'", "") - .trim(); - } - return text.isEmpty() || "null".equalsIgnoreCase(text) ? null : text; - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/IntentParamValidator.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/IntentParamValidator.java deleted file mode 100644 index da67724..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/IntentParamValidator.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.ex.chatbotapi.domain.agent.service; - -import com.ex.chatbotapi.domain.agent.dto.IntentValidationResult; -import com.ex.chatbotapi.domain.agent.intent.IntentDefinition; -import com.ex.chatbotapi.domain.agent.intent.IntentDefinitionRegistry; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -@Component -@RequiredArgsConstructor -public class IntentParamValidator { - private static final Set INVALID_PLACEHOLDERS = new HashSet<>(Arrays.asList( - "여기", "거기", "저기", "현재위치", "현재 위치", "내위치", "내 위치", "이곳", "그곳", "저곳" - )); - - private final IntentDefinitionRegistry intentDefinitionRegistry; - - public boolean supports(String intentType) { - return intentDefinitionRegistry.supports(intentType); - } - - public IntentValidationResult validate(String intentType, Map params) { - Map cleaned = cleanParams(params); - List missing = new ArrayList<>(); - - IntentDefinition definition = intentDefinitionRegistry.get(intentType); - if (definition == null) { - return IntentValidationResult.builder() - .valid(false) - .cleanedParams(cleaned) - .missingParams(Collections.emptyList()) - .clarificationQuestion("처리할 수 있는 업무 유형인지 확인이 필요합니다.") - .build(); - } - - for (String key : definition.getRequiredParams()) { - if (!hasUsableValue(cleaned.get(key))) { - missing.add(key); - } - } - - return IntentValidationResult.builder() - .valid(missing.isEmpty()) - .cleanedParams(cleaned) - .missingParams(missing) - .clarificationQuestion(definition.clarificationFor(missing)) - .build(); - } - - public Map cleanParams(Map params) { - Map cleaned = new HashMap<>(); - if (params == null) { - return cleaned; - } - - for (Map.Entry entry : params.entrySet()) { - String value = normalize(entry.getValue()); - if (hasUsableValue(value)) { - cleaned.put(entry.getKey(), value); - } - } - return cleaned; - } - - private boolean hasUsableValue(String value) { - if (value == null || value.trim().isEmpty()) { - return false; - } - return !INVALID_PLACEHOLDERS.contains(normalize(value)); - } - - private String normalize(String value) { - if (value == null) { - return null; - } - return value.trim() - .replaceAll("(에서|부터|까지|으로|로|은|는|이|가|을|를)$", "") - .trim(); - } - -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/LlmAgentRouterService.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/LlmAgentRouterService.java deleted file mode 100644 index 633aa64..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/LlmAgentRouterService.java +++ /dev/null @@ -1,473 +0,0 @@ -package com.ex.chatbotapi.domain.agent.service; - -import com.ex.chatbotapi.domain.agent.dto.AgentQueryRequest; -import com.ex.chatbotapi.domain.agent.dto.AgentQueryResponse; -import com.ex.chatbotapi.domain.agent.dto.CarNoParamNormalizationResult; -import com.ex.chatbotapi.domain.agent.dto.IcParamNormalizationResult; -import com.ex.chatbotapi.domain.agent.dto.IntentValidationResult; -import com.ex.chatbotapi.domain.agent.dto.LlmAgentRequest; -import com.ex.chatbotapi.domain.agent.dto.LlmAgentResponse; -import com.ex.chatbotapi.domain.agent.dto.PendingIntentState; -import com.ex.chatbotapi.domain.agent.dto.RestAreaParamNormalizationResult; -import com.ex.chatbotapi.domain.agent.intent.FetchOwner; -import com.ex.chatbotapi.domain.agent.intent.IntentDefinition; -import com.ex.chatbotapi.domain.agent.intent.IntentDefinitionRegistry; -import com.ex.chatbotapi.domain.rag.client.RagServerClient; -import com.ex.chatbotapi.domain.rag.dto.IntentAnalysisRequest; -import com.ex.chatbotapi.domain.rag.dto.IntentAnalysisResponse; -import com.ex.chatbotapi.domain.rag.dto.RagQueryRequest; -import com.ex.chatbotapi.domain.rag.service.RagService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.regex.Pattern; - -@Slf4j -@Service -@RequiredArgsConstructor -public class LlmAgentRouterService { - private static final double DOMAIN_CONFIDENCE_THRESHOLD = 0.60; - private static final String FARE_UNPAID_INTENT = "FARE_UNPAID"; - private static final Pattern FULL_CAR_NO_PATTERN = Pattern.compile(".*\\d{2,3}\\s*[가-힣]\\s*\\d{4}.*"); - - private final RagServerClient ragServerClient; - private final RagService ragService; - private final AgentOrchestratorService agentOrchestratorService; - private final IntentParamValidator intentParamValidator; - private final CarNoParamNormalizer carNoParamNormalizer; - private final IcParamNormalizer icParamNormalizer; - private final RestAreaParamNormalizer restAreaParamNormalizer; - private final PendingIntentService pendingIntentService; - private final IntentDefinitionRegistry intentDefinitionRegistry; - - public LlmAgentResponse process(LlmAgentRequest request) { - validateRequest(request); - long startedAt = System.currentTimeMillis(); - - PendingIntentState pending = pendingIntentService.get(request.getBotId()); - IntentAnalysisResponse analysis = analyze(request, pending); - - LlmAgentResponse response; - if (pending != null) { - LlmAgentResponse pendingResponse = tryResolvePending(request, pending, analysis); - if (pendingResponse != null) { - response = pendingResponse; - logRouteDecision(request, analysis, response, startedAt); - return response; - } - } - - response = processFreshIntent(request, analysis); - logRouteDecision(request, analysis, response, startedAt); - return response; - } - - private IntentAnalysisResponse analyze(LlmAgentRequest request, PendingIntentState pending) { - IntentAnalysisRequest analysisRequest = IntentAnalysisRequest.builder() - .query(request.getQuestion()) - .botId(request.getBotId()) - .pendingIntentType(pending != null ? pending.getIntentType() : null) - .pendingParams(pending != null ? pending.getCollectedParams() : null) - .intentDefinitions(intentDefinitionRegistry.toAnalysisDefinitions()) - .build(); - - IntentAnalysisResponse response = ragServerClient.analyzeIntent(analysisRequest); - if (response == null) { - response = new IntentAnalysisResponse(); - } - if (response.getParams() == null) { - response.setParams(new HashMap()); - } - - log.info("LLM intent 분석 결과 - intentType: {}, confidence: {}, params: {}, reason: {}", - response.getIntentType(), response.getConfidence(), response.getParams(), response.getReason()); - return response; - } - - private LlmAgentResponse tryResolvePending( - LlmAgentRequest request, - PendingIntentState pending, - IntentAnalysisResponse analysis - ) { - boolean sameIntent = pending.getIntentType().equals(analysis.getIntentType()); - boolean simpleSlotAnswer = isSimpleSlotAnswer(request.getQuestion(), pending); - - if (!sameIntent && intentParamValidator.supports(analysis.getIntentType()) - && analysis.confidenceOrZero() >= DOMAIN_CONFIDENCE_THRESHOLD) { - log.info("새 intent 감지로 기존 pending 취소 - old: {}, new: {}", - pending.getIntentType(), analysis.getIntentType()); - pendingIntentService.clear(request.getBotId()); - return processFreshIntent(request, analysis); - } - - if (!sameIntent && !simpleSlotAnswer) { - log.info("pending과 무관한 질문으로 판단, RAG로 전환 - pending: {}", pending.getIntentType()); - pendingIntentService.clear(request.getBotId()); - return processRagOnly(request); - } - - if (isUnpaidGuidanceQuestion(request.getQuestion()) && !containsFullCarNo(request.getQuestion())) { - log.info("미납 안내성 질문으로 판단, pending 취소 후 RAG로 전환 - pending: {}", pending.getIntentType()); - pendingIntentService.clear(request.getBotId()); - return processRagOnly(request); - } - - Map merged = new HashMap<>(pending.getCollectedParams()); - merged.putAll(intentParamValidator.cleanParams(analysis.getParams())); - if (simpleSlotAnswer && pending.getMissingParams() != null && pending.getMissingParams().size() == 1) { - merged.put(pending.getMissingParams().get(0), request.getQuestion().trim()); - } - - IntentValidationResult validation = intentParamValidator.validate(pending.getIntentType(), merged); - if (validation.isValid()) { - CarNoParamNormalizationResult carNoNormalization = carNoParamNormalizer.normalize( - pending.getIntentType(), - validation.getCleanedParams()); - if (!carNoNormalization.isValid()) { - savePendingForParam(request.getBotId(), pending.getIntentType(), carNoNormalization.getNormalizedParams(), "carNo"); - return paramClarificationResponse( - pending.getIntentType(), - carNoNormalization.getNormalizedParams(), - "carNo", - carNoNormalization.getClarificationQuestion()); - } - - IcParamNormalizationResult normalization = icParamNormalizer.normalize( - pending.getIntentType(), - carNoNormalization.getNormalizedParams()); - if (normalization.isAmbiguous()) { - saveIcCandidatePending(request.getBotId(), pending.getIntentType(), normalization); - return icCandidateClarificationResponse(pending.getIntentType(), normalization); - } - - RestAreaParamNormalizationResult restAreaNormalization = restAreaParamNormalizer.normalize( - pending.getIntentType(), - normalization.getNormalizedParams()); - - pendingIntentService.clear(request.getBotId()); - return processByFetchOwner( - request, - pending.getIntentType(), - restAreaNormalization.getNormalizedParams(), - "pending_filled"); - } - - savePending(request.getBotId(), pending.getIntentType(), validation); - return clarificationResponse(pending.getIntentType(), validation); - } - - private LlmAgentResponse processFreshIntent(LlmAgentRequest request, IntentAnalysisResponse analysis) { - String intentType = analysis.getIntentType(); - IntentDefinition definition = intentDefinitionRegistry.get(intentType); - if (definition != null - && analysis.confidenceOrZero() >= definition.minConfidenceOrDefault(DOMAIN_CONFIDENCE_THRESHOLD)) { - if (FARE_UNPAID_INTENT.equals(intentType) - && isUnpaidGuidanceQuestion(request.getQuestion()) - && !containsFullCarNo(request.getQuestion())) { - pendingIntentService.clear(request.getBotId()); - log.info("미납 안내성 질문으로 판단, 차량번호 보완질문 대신 RAG로 전환 - question: {}", request.getQuestion()); - return processRagOnly(request); - } - - IntentValidationResult validation = intentParamValidator.validate(intentType, analysis.getParams()); - if (validation.isValid()) { - CarNoParamNormalizationResult carNoNormalization = carNoParamNormalizer.normalize( - intentType, - validation.getCleanedParams()); - if (!carNoNormalization.isValid()) { - savePendingForParam(request.getBotId(), intentType, carNoNormalization.getNormalizedParams(), "carNo"); - return paramClarificationResponse( - intentType, - carNoNormalization.getNormalizedParams(), - "carNo", - carNoNormalization.getClarificationQuestion()); - } - - IcParamNormalizationResult normalization = icParamNormalizer.normalize( - intentType, - carNoNormalization.getNormalizedParams()); - if (normalization.isAmbiguous()) { - saveIcCandidatePending(request.getBotId(), intentType, normalization); - return icCandidateClarificationResponse(intentType, normalization); - } - - RestAreaParamNormalizationResult restAreaNormalization = restAreaParamNormalizer.normalize( - intentType, - normalization.getNormalizedParams()); - - pendingIntentService.clear(request.getBotId()); - return processByFetchOwner(request, intentType, restAreaNormalization.getNormalizedParams(), "domain"); - } - - savePending(request.getBotId(), intentType, validation); - return clarificationResponse(intentType, validation); - } - - pendingIntentService.clear(request.getBotId()); - return processRagOnly(request); - } - - private LlmAgentResponse processByFetchOwner( - LlmAgentRequest request, - String intentType, - Map params, - String routeType - ) { - IntentDefinition definition = intentDefinitionRegistry.get(intentType); - if (definition == null) { - return processRagOnly(request); - } - - if (definition.getFetchOwner() == FetchOwner.WEB) { - return webFetchRequiredResponse(intentType, params, definition); - } - if (definition.getFetchOwner() == FetchOwner.NONE) { - return processRagOnly(request); - } - return processDomain(request, intentType, params, routeType, definition); - } - - private LlmAgentResponse processDomain( - LlmAgentRequest request, - String intentType, - Map params, - String routeType, - IntentDefinition definition - ) { - AgentQueryRequest agentRequest = new AgentQueryRequest( - request.getQuestion(), - intentType, - params, - request.getBotId(), - null - ); - AgentQueryResponse agentResponse = agentOrchestratorService.processUserQuery(agentRequest); - String answer = agentResponse.getLlmAnswer() != null - ? agentResponse.getLlmAnswer() - : agentResponse.getStatusMsg(); - - return LlmAgentResponse.builder() - .status(agentResponse.isStatus()) - .statusMsg(agentResponse.getStatusMsg()) - .routeType(routeType) - .intentType(intentType) - .fetchOwner(definition.getFetchOwner().name()) - .uiType(definition.getUiType()) - .answer(answer) - .llmAnswer(agentResponse.getLlmAnswer()) - .domainData(agentResponse.getDomainData()) - .faqUrls(agentResponse.getFaqUrls()) - .references(agentResponse.getReferences()) - .needsClarification(false) - .params(params) - .build(); - } - - private LlmAgentResponse webFetchRequiredResponse( - String intentType, - Map params, - IntentDefinition definition - ) { - return LlmAgentResponse.builder() - .status(true) - .statusMsg("클라이언트 도메인 데이터 조회 필요") - .routeType("web_fetch_required") - .intentType(intentType) - .fetchOwner(definition.getFetchOwner().name()) - .uiType(definition.getUiType()) - .needsClarification(false) - .params(params) - .build(); - } - - @SuppressWarnings("unchecked") - private LlmAgentResponse processRagOnly(LlmAgentRequest request) { - RagQueryRequest ragRequest = new RagQueryRequest(request.getQuestion(), request.getBotId(), null, null); - Map ragResult = ragService.processQuestion(ragRequest); - String answer = String.valueOf(ragResult.get("answer")); - - return LlmAgentResponse.builder() - .status(Boolean.TRUE.equals(ragResult.get("status"))) - .statusMsg((String) ragResult.get("statusMsg")) - .routeType("rag") - .fetchOwner(FetchOwner.NONE.name()) - .answer(answer) - .llmAnswer(answer) - .faqUrls((List) ragResult.get("faqUrls")) - .references((List>) ragResult.get("references")) - .needsClarification(false) - .build(); - } - - private LlmAgentResponse clarificationResponse(String intentType, IntentValidationResult validation) { - String question = validation.getClarificationQuestion(); - return LlmAgentResponse.builder() - .status(true) - .statusMsg("필수 파라미터 추가 입력 필요") - .routeType("clarify") - .intentType(intentType) - .fetchOwner(resolveFetchOwner(intentType)) - .answer(question) - .llmAnswer(question) - .needsClarification(true) - .clarificationQuestion(question) - .pendingIntentType(intentType) - .missingParams(validation.getMissingParams()) - .params(validation.getCleanedParams()) - .build(); - } - - private LlmAgentResponse icCandidateClarificationResponse( - String intentType, - IcParamNormalizationResult normalization - ) { - String question = normalization.getClarificationQuestion(); - return LlmAgentResponse.builder() - .status(true) - .statusMsg("영업소 후보 선택 필요") - .routeType("clarify") - .intentType(intentType) - .fetchOwner(resolveFetchOwner(intentType)) - .answer(question) - .llmAnswer(question) - .needsClarification(true) - .clarificationQuestion(question) - .pendingIntentType(intentType) - .missingParams(Collections.singletonList(normalization.getAmbiguousParam())) - .params(normalization.getNormalizedParams()) - .icCandidates(normalization.getCandidates()) - .build(); - } - - private LlmAgentResponse paramClarificationResponse( - String intentType, - Map params, - String missingParam, - String question - ) { - return LlmAgentResponse.builder() - .status(true) - .statusMsg("파라미터 추가 입력 필요") - .routeType("clarify") - .intentType(intentType) - .fetchOwner(resolveFetchOwner(intentType)) - .answer(question) - .llmAnswer(question) - .needsClarification(true) - .clarificationQuestion(question) - .pendingIntentType(intentType) - .missingParams(Collections.singletonList(missingParam)) - .params(params) - .build(); - } - - private String resolveFetchOwner(String intentType) { - IntentDefinition definition = intentDefinitionRegistry.get(intentType); - return definition != null && definition.getFetchOwner() != null - ? definition.getFetchOwner().name() - : null; - } - - private boolean isUnpaidGuidanceQuestion(String question) { - if (question == null) { - return false; - } - String normalized = question.replaceAll("\\s+", ""); - if (!normalized.contains("미납")) { - return false; - } - return normalized.contains("방법") - || normalized.contains("어떻게") - || normalized.contains("어디서") - || normalized.contains("어디") - || normalized.contains("절차") - || normalized.contains("경로") - || normalized.contains("사이트") - || normalized.contains("홈페이지") - || normalized.contains("앱"); - } - - private boolean containsFullCarNo(String question) { - return question != null && FULL_CAR_NO_PATTERN.matcher(question).matches(); - } - - private void savePending(String botId, String intentType, IntentValidationResult validation) { - PendingIntentState state = PendingIntentState.builder() - .botId(botId) - .intentType(intentType) - .collectedParams(validation.getCleanedParams()) - .missingParams(validation.getMissingParams()) - .build(); - pendingIntentService.save(state); - } - - private void saveIcCandidatePending( - String botId, - String intentType, - IcParamNormalizationResult normalization - ) { - PendingIntentState state = PendingIntentState.builder() - .botId(botId) - .intentType(intentType) - .collectedParams(normalization.getNormalizedParams()) - .missingParams(Collections.singletonList(normalization.getAmbiguousParam())) - .build(); - pendingIntentService.save(state); - } - - private void savePendingForParam( - String botId, - String intentType, - Map params, - String missingParam - ) { - PendingIntentState state = PendingIntentState.builder() - .botId(botId) - .intentType(intentType) - .collectedParams(params) - .missingParams(Collections.singletonList(missingParam)) - .build(); - pendingIntentService.save(state); - } - - private boolean isSimpleSlotAnswer(String question, PendingIntentState pending) { - if (question == null || pending.getMissingParams() == null || pending.getMissingParams().size() != 1) { - return false; - } - String text = question.trim(); - if (text.length() > 20 || text.contains("?") || text.contains("알려") || text.contains("조회")) { - return false; - } - return !text.contains(" "); - } - - private void validateRequest(LlmAgentRequest request) { - if (request == null || request.getQuestion() == null || request.getQuestion().trim().isEmpty()) { - throw new IllegalArgumentException("질문이 비어있습니다."); - } - } - - private void logRouteDecision( - LlmAgentRequest request, - IntentAnalysisResponse analysis, - LlmAgentResponse response, - long startedAt - ) { - long elapsedMs = System.currentTimeMillis() - startedAt; - log.info("[LLM_ROUTER] botId={}, query='{}', intentType={}, confidence={}, routeType={}, fetchOwner={}, uiType={}, missingParams={}, elapsedMs={}", - request.getBotId(), - request.getQuestion(), - analysis.getIntentType(), - analysis.getConfidence(), - response.getRouteType(), - response.getFetchOwner(), - response.getUiType(), - response.getMissingParams(), - elapsedMs); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/PendingIntentService.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/PendingIntentService.java deleted file mode 100644 index 95479dc..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/PendingIntentService.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.ex.chatbotapi.domain.agent.service; - -import com.ex.chatbotapi.domain.agent.dto.PendingIntentState; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.time.Duration; -import java.time.Instant; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -@Slf4j -@Service -public class PendingIntentService { - private static final Duration TTL = Duration.ofMinutes(5); - - private final Map states = new ConcurrentHashMap<>(); - - public PendingIntentState get(String botId) { - if (botId == null || botId.trim().isEmpty()) { - return null; - } - - PendingIntentState state = states.get(botId); - if (state == null) { - return null; - } - - if (state.getCreatedAt() == null || state.getCreatedAt().plus(TTL).isBefore(Instant.now())) { - states.remove(botId); - log.info("pending intent 만료 - botId: {}, intentType: {}", botId, state.getIntentType()); - return null; - } - return state; - } - - public void save(PendingIntentState state) { - if (state == null || state.getBotId() == null || state.getBotId().trim().isEmpty()) { - return; - } - state.setCreatedAt(Instant.now()); - states.put(state.getBotId(), state); - log.info("pending intent 저장 - botId: {}, intentType: {}, missing: {}", - state.getBotId(), state.getIntentType(), state.getMissingParams()); - } - - public void clear(String botId) { - if (botId == null || botId.trim().isEmpty()) { - return; - } - PendingIntentState removed = states.remove(botId); - if (removed != null) { - log.info("pending intent 삭제 - botId: {}, intentType: {}", botId, removed.getIntentType()); - } - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/RestAreaParamNormalizer.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/RestAreaParamNormalizer.java deleted file mode 100644 index e09fa5a..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/RestAreaParamNormalizer.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.ex.chatbotapi.domain.agent.service; - -import com.ex.chatbotapi.domain.agent.dto.RestAreaParamNormalizationResult; -import org.springframework.stereotype.Component; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@Component -public class RestAreaParamNormalizer { - private static final List REST_AREA_INTENTS = Arrays.asList( - "GASSTATION", - "BRAND_SHOP", - "REST_AREA_FOOD_LIST", - "REST_AREA_PRICE" - ); - - public RestAreaParamNormalizationResult normalize(String intentType, Map params) { - Map normalizedParams = new HashMap<>(); - if (params != null) { - normalizedParams.putAll(params); - } - - if (!REST_AREA_INTENTS.contains(intentType) || params == null || params.isEmpty()) { - return RestAreaParamNormalizationResult.builder() - .normalizedParams(normalizedParams) - .build(); - } - - String restAreaName = normalizedParams.get("restAreaName"); - if (restAreaName != null && !restAreaName.trim().isEmpty()) { - String normalizedValue = normalizeRestAreaName(restAreaName); - normalizedParams.put("restAreaName", normalizedValue); - } - - return RestAreaParamNormalizationResult.builder() - .normalizedParams(normalizedParams) - .build(); - } - - private String normalizeRestAreaName(String value) { - String normalized = value.trim().replaceAll("\\s+", ""); - String previous; - do { - previous = normalized; - normalized = normalized.replaceAll("(휴게소|하이웨이파크)$", ""); - } while (!previous.equals(normalized)); - return normalized.trim(); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/ToolDefinitionService.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/ToolDefinitionService.java deleted file mode 100644 index 61b5278..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/ToolDefinitionService.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.ex.chatbotapi.domain.agent.service; - -import com.ex.chatbotapi.domain.agent.intent.FetchOwner; -import com.ex.chatbotapi.domain.agent.intent.IntentDefinition; -import com.ex.chatbotapi.domain.agent.intent.IntentDefinitionRegistry; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * intent-definitions.yml → OpenAI function(tool) 스키마 생성. - */ -@Service -@RequiredArgsConstructor -public class ToolDefinitionService { - private final IntentDefinitionRegistry intentDefinitionRegistry; - - public List> listToolDefinitions() { - List> tools = new ArrayList<>(); - for (IntentDefinition definition : intentDefinitionRegistry.all().values()) { - if (definition.getFetchOwner() == FetchOwner.NONE) { - continue; - } - tools.add(toOpenAiTool(definition)); - } - return tools; - } - - private Map toOpenAiTool(IntentDefinition definition) { - Map properties = new LinkedHashMap<>(); - List required = new ArrayList<>(); - - appendParamProperties(properties, required, definition.getRequiredParamDefinitions(), true); - appendParamProperties(properties, required, definition.getOptionalParamDefinitions(), false); - - Map parameters = new LinkedHashMap<>(); - parameters.put("type", "object"); - parameters.put("properties", properties); - parameters.put("required", required); - - Map function = new LinkedHashMap<>(); - function.put("name", definition.getIntentType()); - function.put( - "description", - definition.getDescription() - + " [fetchOwner=" - + definition.getFetchOwner() - + ", uiType=" - + definition.getUiType() - + "]" - ); - function.put("parameters", parameters); - - Map tool = new LinkedHashMap<>(); - tool.put("type", "function"); - tool.put("function", function); - return tool; - } - - private void appendParamProperties( - Map properties, - List required, - List params, - boolean markRequired - ) { - if (params == null) { - return; - } - for (IntentDefinition.ParamDefinition param : params) { - Map schema = new LinkedHashMap<>(); - schema.put("type", "string"); - schema.put("description", param.getDescription()); - if (param.getExamples() != null && !param.getExamples().isEmpty()) { - schema.put("examples", param.getExamples()); - } - properties.put(param.getName(), schema); - if (markRequired) { - required.add(param.getName()); - } - } - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/ToolExecutionService.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/ToolExecutionService.java deleted file mode 100644 index dd69252..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/ToolExecutionService.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.ex.chatbotapi.domain.agent.service; - -import com.ex.chatbotapi.domain.agent.client.KakaoSkillToolClient; -import com.ex.chatbotapi.domain.agent.dto.ToolExecuteRequest; -import com.ex.chatbotapi.domain.agent.dto.ToolExecuteResponse; -import com.ex.chatbotapi.domain.agent.dto.ToolParamPipelineResult; -import com.ex.chatbotapi.domain.agent.formatter.LlmDomainDataFormatter; -import com.ex.chatbotapi.domain.agent.handler.IntentHandler; -import com.ex.chatbotapi.domain.agent.intent.FetchOwner; -import com.ex.chatbotapi.domain.agent.intent.IntentDefinition; -import com.ex.chatbotapi.domain.agent.intent.IntentDefinitionRegistry; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; - -@Slf4j -@Service -public class ToolExecutionService { - private final IntentDefinitionRegistry intentDefinitionRegistry; - private final Map handlerMap; - private final Map llmFormatterMap; - private final KakaoSkillToolClient kakaoSkillToolClient; - private final ToolParamPipeline toolParamPipeline; - - public ToolExecutionService( - IntentDefinitionRegistry intentDefinitionRegistry, - List handlers, - List llmFormatters, - KakaoSkillToolClient kakaoSkillToolClient, - ToolParamPipeline toolParamPipeline - ) { - this.intentDefinitionRegistry = intentDefinitionRegistry; - this.handlerMap = handlers.stream() - .collect(Collectors.toMap(IntentHandler::getIntentType, Function.identity())); - this.llmFormatterMap = llmFormatters.stream() - .collect(Collectors.toMap(LlmDomainDataFormatter::getIntentType, Function.identity())); - this.kakaoSkillToolClient = kakaoSkillToolClient; - this.toolParamPipeline = toolParamPipeline; - } - - public ToolExecuteResponse execute(ToolExecuteRequest request) { - String toolName = request.getToolName(); - if (toolName == null || toolName.trim().isEmpty()) { - return ToolExecuteResponse.builder() - .status(false) - .statusMsg("toolName is required") - .build(); - } - - IntentDefinition definition = intentDefinitionRegistry.get(toolName); - if (definition == null) { - return ToolExecuteResponse.builder() - .status(false) - .statusMsg("unknown tool: " + toolName) - .build(); - } - - ToolParamPipelineResult pipeline = toolParamPipeline.process(toolName, request.getArguments()); - if (!pipeline.isValid()) { - return ToolExecuteResponse.builder() - .status(false) - .statusMsg(pipeline.isNeedsClarification() - ? "clarification required" - : "parameter validation failed") - .intentType(toolName) - .fetchOwner(definition.getFetchOwner().name()) - .uiType(definition.getUiType()) - .params(pipeline.getParams()) - .needsClarification(pipeline.isNeedsClarification()) - .clarificationQuestion(pipeline.getClarificationQuestion()) - .missingParams(pipeline.getMissingParams()) - .icCandidates(pipeline.getIcCandidates()) - .build(); - } - - Map params = pipeline.getParams(); - FetchOwner fetchOwner = definition.getFetchOwner(); - Map domainData; - - try { - if (fetchOwner == FetchOwner.WAS) { - domainData = fetchWasDomainData(toolName, params); - } else if (fetchOwner == FetchOwner.WEB) { - domainData = kakaoSkillToolClient.fetchDataPortal( - toolName, - params, - request.getUserInput() - ); - } else { - return ToolExecuteResponse.builder() - .status(false) - .statusMsg("tool not executable: " + toolName) - .intentType(toolName) - .fetchOwner(fetchOwner.name()) - .uiType(definition.getUiType()) - .params(params) - .build(); - } - } catch (Exception e) { - log.error("tool execution failed - tool: {}, error: {}", toolName, e.getMessage(), e); - return ToolExecuteResponse.builder() - .status(false) - .statusMsg("tool execution failed: " + e.getMessage()) - .intentType(toolName) - .fetchOwner(fetchOwner.name()) - .uiType(definition.getUiType()) - .params(params) - .build(); - } - - boolean ok = domainData != null && Boolean.TRUE.equals(domainData.get("status")); - String statusMsg = ok - ? "정상응답" - : String.valueOf(domainData != null ? domainData.get("statusMsg") : "no data"); - - return ToolExecuteResponse.builder() - .status(ok) - .statusMsg(statusMsg) - .intentType(toolName) - .fetchOwner(fetchOwner.name()) - .uiType(definition.getUiType()) - .params(params) - .domainData(domainData) - .build(); - } - - private Map fetchWasDomainData(String intentType, Map params) { - IntentHandler handler = handlerMap.get(intentType); - if (handler == null) { - Map fallback = new HashMap<>(); - fallback.put("status", false); - fallback.put("statusMsg", "WAS handler not registered: " + intentType); - return fallback; - } - Map domainData = handler.fetchDomainData(params); - LlmDomainDataFormatter formatter = llmFormatterMap.get(intentType); - if (formatter != null && domainData != null) { - formatter.enrich(domainData); - } - return domainData; - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/ToolParamPipeline.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/ToolParamPipeline.java deleted file mode 100644 index bd71cf9..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/agent/service/ToolParamPipeline.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.ex.chatbotapi.domain.agent.service; - -import com.ex.chatbotapi.domain.agent.dto.CarNoParamNormalizationResult; -import com.ex.chatbotapi.domain.agent.dto.IcParamNormalizationResult; -import com.ex.chatbotapi.domain.agent.dto.IntentValidationResult; -import com.ex.chatbotapi.domain.agent.dto.RestAreaParamNormalizationResult; -import com.ex.chatbotapi.domain.agent.dto.ToolParamPipelineResult; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Map; - -/** - * LlmAgentRouterService와 동일한 파라미터 검증·정규화 체인 (tool execute용). - */ -@Component -@RequiredArgsConstructor -public class ToolParamPipeline { - private final IntentAnalysisParamNormalizer intentAnalysisParamNormalizer; - private final IntentParamValidator intentParamValidator; - private final CarNoParamNormalizer carNoParamNormalizer; - private final IcParamNormalizer icParamNormalizer; - private final RestAreaParamNormalizer restAreaParamNormalizer; - - public ToolParamPipelineResult process(String intentType, Map rawArguments) { - Map params = intentAnalysisParamNormalizer.toStringMap(rawArguments); - - IntentValidationResult validation = intentParamValidator.validate(intentType, params); - if (!validation.isValid()) { - return ToolParamPipelineResult.clarify( - validation.getCleanedParams(), - validation.getClarificationQuestion(), - validation.getMissingParams() - ); - } - - CarNoParamNormalizationResult carNoResult = carNoParamNormalizer.normalize( - intentType, - validation.getCleanedParams() - ); - if (!carNoResult.isValid()) { - return ToolParamPipelineResult.clarify( - carNoResult.getNormalizedParams(), - carNoResult.getClarificationQuestion(), - java.util.Collections.singletonList("carNo") - ); - } - - IcParamNormalizationResult icResult = icParamNormalizer.normalize( - intentType, - carNoResult.getNormalizedParams() - ); - if (icResult.isAmbiguous()) { - return ToolParamPipelineResult.icAmbiguous( - icResult.getNormalizedParams(), - icResult.getClarificationQuestion(), - icResult.getCandidates() - ); - } - - RestAreaParamNormalizationResult restAreaResult = restAreaParamNormalizer.normalize( - intentType, - icResult.getNormalizedParams() - ); - - return ToolParamPipelineResult.success(restAreaResult.getNormalizedParams()); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/api/FareApiController.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/api/FareApiController.java deleted file mode 100644 index a778c60..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/api/FareApiController.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.ex.chatbotapi.domain.fare.api; - -import com.ex.chatbotapi.domain.fare.service.FareService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.Map; - -/** - * 통행요금 관련 API 컨트롤러 - * HTTP 요청 처리만 담당하고, 비즈니스 로직은 FareService에 위임 - */ -@Slf4j -@RestController -@RequiredArgsConstructor -public class FareApiController { - - private final FareService fareService; - - /** - * Description : 환불내역 조회 메소드 - */ - @GetMapping("/api/fare/refund") - public ResponseEntity> queryRefund(String carNo) { - log.info("API 호출 : queryRefund(GET) - carNo: {}", carNo); - return ResponseEntity.ok(fareService.getRefundInfo(carNo)); - } - - /** - * Description : 미납내역 조회 메소드 - */ - @GetMapping("/api/fare/unpaid") - public ResponseEntity> queryUnpaid(String carNo) { - log.info("API 호출 : queryUnpaid(GET) - carNo: {}", carNo); - return ResponseEntity.ok(fareService.getUnpaidInfo(carNo)); - } - - /** - * Description : 영업소 전화번호 조회 메소드 - */ - @GetMapping("/api/call/icTel") - public ResponseEntity> queryIcTel(String icName) { - log.info("API 호출 : queryIcTel(GET) - icName: {}", icName); - return ResponseEntity.ok(fareService.getIcTelInfo(icName)); - } - - /** - * Description : 통행요금 조회 메소드 - */ - @GetMapping("/api/fare/searchFare") - public ResponseEntity> querySearchFare(String fromIc, String toIc) { - log.info("API 호출 : querySearchFare(GET) - fromIc: {}, toIc: {}", fromIc, toIc); - return ResponseEntity.ok(fareService.searchFare(fromIc, toIc)); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/formatter/FareRefundLlmFormatter.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/formatter/FareRefundLlmFormatter.java deleted file mode 100644 index c033d3b..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/formatter/FareRefundLlmFormatter.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.ex.chatbotapi.domain.fare.formatter; - -import com.ex.chatbotapi.domain.agent.formatter.LlmDomainDataFormatter; -import org.springframework.stereotype.Component; - -import java.util.Map; - -/** - * FARE_REFUND — 환불 가능 통행료 조회 결과 요약. - */ -@Component -public class FareRefundLlmFormatter implements LlmDomainDataFormatter { - - @Override - public String getIntentType() { - return "FARE_REFUND"; - } - - @Override - public void enrich(Map domainData) { - if (domainData == null || domainData.isEmpty()) { - return; - } - - Object st = domainData.get("status"); - boolean ok = st instanceof Boolean && (Boolean) st; - String carNo = domainData.get("carNo") != null ? String.valueOf(domainData.get("carNo")) : "(차량번호 없음)"; - - if (!ok) { - domainData.put("llmSummary", - "차량번호 " + carNo + " 에 대해 환불 대상 통행료가 조회되지 않았거나, " - + "일부 노선에서는 환불 내역 확인이 제한될 수 있습니다."); - return; - } - - domainData.put("llmSummary", - "차량번호 " + carNo + " 에 환불 가능한 통행료 정보가 조회되었습니다.\n" - + "환불 신청 및 상세 내역은 한국도로공사 공식 홈페이지 또는 고객센터(1588-2504)를 이용해 주세요."); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/formatter/FareSearchLlmFormatter.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/formatter/FareSearchLlmFormatter.java deleted file mode 100644 index 91c5382..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/formatter/FareSearchLlmFormatter.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.ex.chatbotapi.domain.fare.formatter; - -import com.ex.chatbotapi.domain.agent.formatter.LlmDomainDataFormatter; -import org.springframework.stereotype.Component; - -import java.util.List; -import java.util.Map; - -/** - * FARE_SEARCH — 통행요금 조회 결과를 LLM용 요약 문자열로 정리한다. - */ -@Component -public class FareSearchLlmFormatter implements LlmDomainDataFormatter { - - @Override - public String getIntentType() { - return "FARE_SEARCH"; - } - - @Override - public void enrich(Map domainData) { - if (domainData == null || domainData.isEmpty()) { - return; - } - - Object st = domainData.get("status"); - boolean ok = st instanceof Boolean && (Boolean) st; - - if (!ok) { - String msg = domainData.get("statusMsg") != null - ? String.valueOf(domainData.get("statusMsg")) - : "통행요금 조회에 실패했습니다."; - domainData.put("llmSummary", msg); - return; - } - - @SuppressWarnings("unchecked") - Map fare = (Map) domainData.get("fareData"); - if (fare == null || fare.isEmpty()) { - domainData.put("llmSummary", "통행요금 조회는 성공했으나 fareData가 없습니다."); - return; - } - - String type = String.valueOf(fare.getOrDefault("type", "")); - StringBuilder sb = new StringBuilder(); - - switch (type) { - case "exact": - sb.append("구간 통행요금(정액 산정 가능):\n"); - sb.append("- 출발: ").append(fare.get("fromIc")).append(", 도착: ").append(fare.get("toIc")).append("\n"); - appendVehicleClassLines(sb, fare); - break; - case "open_both": - sb.append("개방식 영업소(출발·도착 모두 개방식으로 판단):\n"); - sb.append("- 기준 영업소: ").append(fare.get("icName")).append("\n"); - sb.append("- 개방식 요금은 해당 영업소 통과 기준 단일 구간 요금입니다.\n"); - appendVehicleClassLines(sb, fare); - break; - case "open_from": - sb.append("개방식 영업소(출발이 개방식):\n"); - sb.append("- 출발 영업소: ").append(fare.get("icName")).append(", 목적지 쪽 영업소: ") - .append(fare.get("toIc")).append("\n"); - Object outs = fare.get("outIcs"); - if (outs instanceof List && !((List) outs).isEmpty()) { - sb.append("- 출구 후보(일부): ").append(outs).append("\n"); - } - appendVehicleClassLines(sb, fare); - break; - case "open_to": - sb.append("개방식 영업소(도착이 개방식):\n"); - sb.append("- 도착 영업소: ").append(fare.get("icName")).append(", 출발 쪽 영업소: ") - .append(fare.get("fromIc")).append("\n"); - Object outs2 = fare.get("outIcs"); - if (outs2 instanceof List && !((List) outs2).isEmpty()) { - sb.append("- 출구 후보(일부): ").append(outs2).append("\n"); - } - appendVehicleClassLines(sb, fare); - break; - default: - sb.append("통행요금 조회(type=").append(type).append("):\n"); - appendVehicleClassLines(sb, fare); - } - - domainData.put("llmSummary", sb.toString().trim()); - } - - private static void appendVehicleClassLines(StringBuilder sb, Map fare) { - sb.append("- 1종: ").append(won(fare.get("fare1"))) - .append(" (경차 1종: ").append(won(fare.get("fare1Small"))).append(")\n"); - sb.append("- 2종: ").append(won(fare.get("fare2"))).append("\n"); - sb.append("- 3종: ").append(won(fare.get("fare3"))).append("\n"); - sb.append("- 4종: ").append(won(fare.get("fare4"))).append("\n"); - sb.append("- 5종: ").append(won(fare.get("fare5"))).append("\n"); - } - - private static String won(Object v) { - if (v == null) { - return "-"; - } - if (v instanceof Number) { - return ((Number) v).intValue() + "원"; - } - return v + "원"; - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/formatter/FareUnpaidLlmFormatter.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/formatter/FareUnpaidLlmFormatter.java deleted file mode 100644 index c314a5b..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/formatter/FareUnpaidLlmFormatter.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.ex.chatbotapi.domain.fare.formatter; - -import com.ex.chatbotapi.domain.agent.formatter.LlmDomainDataFormatter; -import org.springframework.stereotype.Component; - -import java.util.Map; - -/** - * FARE_UNPAID — 미납 통행료 조회 결과 요약. - */ -@Component -public class FareUnpaidLlmFormatter implements LlmDomainDataFormatter { - - @Override - public String getIntentType() { - return "FARE_UNPAID"; - } - - @Override - public void enrich(Map domainData) { - if (domainData == null || domainData.isEmpty()) { - return; - } - - Object st = domainData.get("status"); - boolean ok = st instanceof Boolean && (Boolean) st; - String carNo = domainData.get("carNo") != null ? String.valueOf(domainData.get("carNo")) : "(차량번호 없음)"; - long sum = toLong(domainData.get("sumAmt")); - - if (!ok) { - String message = domainData.get("statusMsg") != null - ? String.valueOf(domainData.get("statusMsg")) - : "미납 통행료 조회에 실패했습니다."; - domainData.put("llmSummary", message); - return; - } - - if (sum < 100) { - domainData.put("llmSummary", - "차량번호 " + carNo + " 기준 조회된 미납 통행료는 없습니다.\n" - + "고객에게 미납금액이 없다고 명확히 안내하세요."); - return; - } - - domainData.put("llmSummary", - "차량번호 " + carNo + " 기준 미납 통행료가 조회되었습니다.\n" - + "총 미납금액은 " + sum + "원입니다.\n" - + "상세 납부는 하이패스/홈페이지 등 공식 채널을 이용해야 합니다."); - } - - private static long toLong(Object o) { - if (o == null) { - return 0L; - } - if (o instanceof Number) { - return ((Number) o).longValue(); - } - try { - return Long.parseLong(String.valueOf(o)); - } catch (NumberFormatException e) { - return 0L; - } - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/formatter/IcTelLlmFormatter.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/formatter/IcTelLlmFormatter.java deleted file mode 100644 index f19a494..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/formatter/IcTelLlmFormatter.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.ex.chatbotapi.domain.fare.formatter; - -import com.ex.chatbotapi.domain.agent.formatter.LlmDomainDataFormatter; -import org.springframework.stereotype.Component; - -import java.util.Map; - -/** - * IC_TEL — 영업소 대표 전화번호 조회 결과 요약. - */ -@Component -public class IcTelLlmFormatter implements LlmDomainDataFormatter { - - @Override - public String getIntentType() { - return "IC_TEL"; - } - - @Override - public void enrich(Map domainData) { - if (domainData == null || domainData.isEmpty()) { - return; - } - - Object st = domainData.get("status"); - boolean ok = st instanceof Boolean && (Boolean) st; - - if (!ok) { - String msg = domainData.get("statusMsg") != null - ? String.valueOf(domainData.get("statusMsg")) - : "영업소 연락처를 조회하지 못했습니다."; - domainData.put("llmSummary", msg); - return; - } - - String icName = domainData.get("icName") != null ? String.valueOf(domainData.get("icName")) : ""; - String tel = domainData.get("tel") != null ? String.valueOf(domainData.get("tel")) : ""; - String code = domainData.get("tolofCd") != null ? String.valueOf(domainData.get("tolofCd")) : ""; - - StringBuilder sb = new StringBuilder(); - sb.append("고속도로 영업소 연락처 조회 결과:\n"); - sb.append("- 영업소명: ").append(icName).append("\n"); - sb.append("- 대표전화: ").append(tel).append("\n"); - if (!code.isEmpty()) { - sb.append("- 영업소코드(참고): ").append(code); - } - domainData.put("llmSummary", sb.toString().trim()); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/handler/FareRefundHandler.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/handler/FareRefundHandler.java deleted file mode 100644 index 4371475..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/handler/FareRefundHandler.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.ex.chatbotapi.domain.fare.handler; - -import com.ex.chatbotapi.domain.fare.service.FareService; -import com.ex.chatbotapi.domain.agent.handler.IntentHandler; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Map; - -@Component -@RequiredArgsConstructor -public class FareRefundHandler implements IntentHandler { - - private final FareService fareService; - - @Override - public String getIntentType() { - return "FARE_REFUND"; - } - - @Override - public Map fetchDomainData(Map params) { - return fareService.getRefundInfo(params.get("carNo")); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/handler/FareSearchHandler.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/handler/FareSearchHandler.java deleted file mode 100644 index 5621646..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/handler/FareSearchHandler.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.ex.chatbotapi.domain.fare.handler; - -import com.ex.chatbotapi.domain.fare.service.FareService; -import com.ex.chatbotapi.domain.agent.handler.IntentHandler; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Map; - -@Component -@RequiredArgsConstructor -public class FareSearchHandler implements IntentHandler { - - private final FareService fareService; - - @Override - public String getIntentType() { - return "FARE_SEARCH"; - } - - @Override - public Map fetchDomainData(Map params) { - return fareService.searchFare(params.get("fromIc"), params.get("toIc")); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/handler/FareUnpaidHandler.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/handler/FareUnpaidHandler.java deleted file mode 100644 index 5254ed2..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/handler/FareUnpaidHandler.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.ex.chatbotapi.domain.fare.handler; - -import com.ex.chatbotapi.domain.fare.service.FareService; -import com.ex.chatbotapi.domain.agent.handler.IntentHandler; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Map; - -@Component -@RequiredArgsConstructor -public class FareUnpaidHandler implements IntentHandler { - - private final FareService fareService; - - @Override - public String getIntentType() { - return "FARE_UNPAID"; - } - - @Override - public Map fetchDomainData(Map params) { - return fareService.getUnpaidInfo(params.get("carNo")); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/handler/IcTelHandler.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/handler/IcTelHandler.java deleted file mode 100644 index 61e5eba..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/handler/IcTelHandler.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.ex.chatbotapi.domain.fare.handler; - -import com.ex.chatbotapi.domain.fare.service.FareService; -import com.ex.chatbotapi.domain.agent.handler.IntentHandler; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Map; - -@Component -@RequiredArgsConstructor -public class IcTelHandler implements IntentHandler { - - private final FareService fareService; - - @Override - public String getIntentType() { - return "IC_TEL"; - } - - @Override - public Map fetchDomainData(Map params) { - return fareService.getIcTelInfo(params.get("icName")); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/repository/FareRepository.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/repository/FareRepository.java deleted file mode 100644 index 5204cc6..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/repository/FareRepository.java +++ /dev/null @@ -1,171 +0,0 @@ -package com.ex.chatbotapi.domain.fare.repository; - -import com.mongodb.BasicDBObject; -import com.mongodb.client.MongoCollection; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.io.IOUtils; -import org.bson.Document; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.json.JsonParser; -import org.springframework.boot.json.JsonParserFactory; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.stereotype.Repository; - -import javax.annotation.PostConstruct; -import javax.sql.DataSource; -import java.io.IOException; -import java.io.InputStream; -import java.io.StringWriter; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.regex.Pattern; - -/** - * 통행요금 관련 데이터 접근 레이어 - */ -@Slf4j -@Repository -@RequiredArgsConstructor -public class FareRepository { - private static final String IC_TEL_FILE = "icTel.json"; - private static final Path EXTERNAL_IC_TEL_PATH = Paths.get("config", IC_TEL_FILE); - - @Qualifier("bpstlDataSource") - private final DataSource bpstlDataSource; - private final MongoTemplate mongoTemplate; - - private Map icTelMap; - - @PostConstruct - public void loadIctelData() throws IOException { - JsonParser parser = JsonParserFactory.getJsonParser(); - try (InputStream inputStream = openIcTelInputStream()) { - StringWriter writer = new StringWriter(); - IOUtils.copy(inputStream, writer, "UTF-8"); - String json = writer.toString(); - icTelMap = parser.parseMap(json); - log.info("icTel.json 로드 완료"); - } - } - - private InputStream openIcTelInputStream() throws IOException { - if (Files.exists(EXTERNAL_IC_TEL_PATH)) { - log.info("icTel.json loaded from external file: {}", EXTERNAL_IC_TEL_PATH.toAbsolutePath()); - return Files.newInputStream(EXTERNAL_IC_TEL_PATH); - } - - log.info("external icTel.json not found. using classpath resource: {}", IC_TEL_FILE); - InputStream classpathResource = this.getClass().getClassLoader().getResourceAsStream(IC_TEL_FILE); - if (classpathResource == null) { - throw new IOException("icTel.json 파일을 찾을 수 없습니다. external/classpath 모두 로드 실패"); - } - return classpathResource; - } - - /** - * 차량번호로 환불내역 조회 - */ - public List> findRefundByCarNo(String carNo) { - String query = "SELECT TOT_RFND_AMT, PECARD_RFND_RSLT_CD " + - "FROM T_STLF_INTG_RFND_VHCL01P1 A " + - "WHERE A.PECARD_RFND_RSLT_CD IN ('G','C','L') " + - "AND A.RFND_VHNO = ? " + - "AND rownum < 2 "; - - JdbcTemplate jdbcTemplate = new JdbcTemplate(bpstlDataSource); - return jdbcTemplate.queryForList(query, carNo); - } - - /** - * 차량번호로 미납내역 조회 - */ - public List> findUnpaidByCarNo(String carNo) { - String query = "WITH INTERCHANGE AS(SELECT IC.HOINST_CD AS BIZ_UNIT, IC.TOLOF_CD AS IC_CODE, IC.KOR_TOLOF_NM AS IC_NAME, IC.USE_YN, BC.ODN5_HOINST_GRP_YN AS BIZ_UNIT_GRP05, CASE WHEN BC.HOINST_CD IN ('00','01','02','08','09','10','11','18','28','34','37','45','46','62') THEN 'Y' ELSE 'N' END AS BIZ_UNIT_GRP_CRDT FROM T_STLA_HOINST01C1 BC, T_STLA_TOLOF_OP01M1 IC WHERE BC.HOINST_CD = IC.HOINST_CD ) SELECT RN_MAX - RNUM ROW_NUM, UR2.*, '0' IN_COL_AMT, '0' IN_COL_EXTRA_AMT, biz_unit OUT_BIZ_UNIT, '0' OUT_COL_AMT, '0' OUT_COL_EXTRA_AMT, '0' WILL_AMT, in_biz_unit IN_BIZ_UNIT, '' WAY_BIZ_UNIT, '0' WAY_COL_AMT, '0' WAY_COL_EXTRA_AMT, '0' WILL_EXTRA_AMT FROM (SELECT ROW_NUMBER() OVER ( ORDER BY WW DESC) -1 AS RNUM, SUM(UNPAID_AMT) OVER() AS TOTAL, COUNT(*) OVER () AS RN_MAX, '' AS DELEAGTE_CODE, SUM(COL_EXTRA_WILL_AMT) OVER() AS TOTAL_EXTRA, SUM(COL_EXTRA_YN) OVER() AS TOTAL_EXTRA_CNT, UR.* FROM (SELECT WW, CAR_NO, CAR_KIND, NVL(IC.IC_NAME, '-') IN_IC, OC.IC_NAME OUT_IC, SAYU, OC.USE_YN OUT_IC_USE_YN, UNPAID_AMT, DG_UNPAID_AMT, ETC_UNPAID_AMT, COL_WILL_AMT, SEPAR_WILL_AMT, COL_EXTRA_WILL_AMT, SEPAR_EXTRA_WILL_AMT, DIV_WILL_AMT, NVL(IC.BIZ_UNIT, '-') IN_BIZ_UNIT, OC.BIZ_UNIT, CASE WHEN UNPAID_AMT >= DIV_WILL_AMT AND OC.BIZ_UNIT_GRP05 = 'Y' THEN DECODE(SAYU, NULL, 'N', 'R') ELSE 'X' END AS R_ACC_YN, CASE WHEN UNPAID_AMT >= DIV_WILL_AMT AND OC.BIZ_UNIT_GRP_CRDT = 'Y' THEN DECODE(SAYU, NULL, 'N', 'R') ELSE 'X' END AS R_CRDT_YN, PAY.IN_IC_CODE, PAY.GEN_IC_CODE AS OUT_IC_CODE, PAY.IC_CODE, WORK_DATE, WORK_NO, SER_NO, LANETYPE, OUT_DATE, OUT_TIME, CAR_CLASS, USER_NAME, STATUS_CODE, PROC_CODE, CASE WHEN COL_EXTRA_WILL_AMT > 0 THEN '1' ELSE '0' END AS COL_EXTRA_YN FROM (SELECT TO_DATE(ED.WOUTP_DATES || ED.WOUTP_HMS , 'YYYYMMDDHH24MISS') WW, ED.NTPD_VHNO CAR_NO, ED.OBU_FRKNCR_CD CAR_KIND, ED.ENRC_TOLOF_CD AS IN_IC_CODE, ED.NTPD_OCRN_TOLOF_CD AS GEN_IC_CODE, ED.NTPD_AFFC_TOLOF_CD AS IC_CODE, CC.CMMN_CD_NM AS SAYU, ED.NTPD_AFFC_TOLOF_CD AS OUT_IC_USE_YN, EC.UNPAID_AMT, NVL(EC.DG_UNPAID_AMT, 0) AS DG_UNPAID_AMT, DECODE(ED.CRGW_SYSTM_CLSS_CD, '0', NVL(EC.ETC_UNPAID_AMT, 0), 0) AS ETC_UNPAID_AMT, EC.COL_WILL_AMT, EC.SEPAR_WILL_AMT, EC.COL_EXTRA_WILL_AMT, EC.SEPAR_EXTRA_WILL_AMT, DECODE(ED.CRGW_SYSTM_CLSS_CD, '1', NVL(EC.ETC_UNPAID_AMT, 0), 0) AS DIV_WILL_AMT, ED.NTPD_AFFC_WORK_DATES AS WORK_DATE, ED.NTPD_AFFC_WORK_NO AS WORK_NO, ED.NTPD_AFFC_VHCL_PROS_NO AS SER_NO, ED.CRGW_SYSTM_CLSS_CD AS LANETYPE, ED.WOUTP_DATES AS OUT_DATE, ED.WOUTP_HMS AS OUT_TIME, ED.PRCPT_FRKNCR_CD AS CAR_CLASS, (SELECT NTPD_CUST_NM FROM T_STLE_NPAFV_RSR01P1 CU WHERE CU.NTPD_AFFC_WORK_DATES = ED.NTPD_AFFC_WORK_DATES AND CU.NTPD_AFFC_TOLOF_CD = ED.NTPD_AFFC_TOLOF_CD AND CU.NTPD_AFFC_WORK_NO = ED.NTPD_AFFC_WORK_NO AND CU.NTPD_AFFC_VHCL_PROS_NO = ED.NTPD_AFFC_VHCL_PROS_NO ) AS USER_NAME, ED.NTPD_AFFC_STAT_CD AS STATUS_CODE, ED.NTPD_AFFC_PROS_CD AS PROC_CODE FROM T_STLE_NPAFV_PATH01P1 ED, (SELECT A.NTPD_AFFC_WORK_DATES AS WORK_DATE, A.NTPD_AFFC_TOLOF_CD AS IC_CODE, A.NTPD_AFFC_WORK_NO AS WORK_NO, A.NTPD_AFFC_VHCL_PROS_NO AS SER_NO, A.CRGW_SYSTM_CLSS_CD AS LANE_TYPE, SUM(B.MNRC_SCDL_TOLL + B.MNRC_SCDL_ATOLL) UNPAID_AMT, SUM(DECODE(B.HOINST_CD, '00', B.MNRC_SCDL_TOLL + B.MNRC_SCDL_ATOLL, 0)) DG_UNPAID_AMT, SUM(DECODE(B.HOINST_CD, '00', 0, B.MNRC_SCDL_TOLL + B.MNRC_SCDL_ATOLL)) ETC_UNPAID_AMT, SUM(B.MNRC_SCDL_TOLL) COL_WILL_AMT, SUM(B.MNRC_SCDL_TOLL) SEPAR_WILL_AMT, SUM(B.MNRC_SCDL_ATOLL) COL_EXTRA_WILL_AMT, SUM(B.MNRC_SCDL_ATOLL) SEPAR_EXTRA_WILL_AMT FROM T_STLE_NPAFV_PATH01P1 A, T_STLF_NTMNRC01L1 B WHERE A.NTPD_AFFC_WORK_DATES = B.NTPD_AFFC_WORK_DATES AND A.NTPD_AFFC_TOLOF_CD = B.NTPD_AFFC_TOLOF_CD AND A.NTPD_AFFC_WORK_NO = B.NTPD_AFFC_WORK_NO AND A.NTPD_AFFC_VHCL_PROS_NO = B.NTPD_AFFC_VHCL_PROS_NO AND A.NTPD_AFFC_STAT_CD < '70' AND A.CRGW_SYSTM_CLSS_CD != '9' AND A.NTPD_VHNO = ? GROUP BY A.NTPD_AFFC_WORK_DATES, A.NTPD_AFFC_TOLOF_CD, A.NTPD_AFFC_WORK_NO, A.NTPD_AFFC_VHCL_PROS_NO, A.CRGW_SYSTM_CLSS_CD ) EC, T_STLA_CMMN_CD01M1 CC WHERE ED.NTPD_AFFC_WORK_DATES = EC.WORK_DATE AND ED.NTPD_AFFC_TOLOF_CD = EC.IC_CODE AND ED.NTPD_AFFC_WORK_NO = EC.WORK_NO AND ED.NTPD_AFFC_VHCL_PROS_NO = EC.SER_NO AND ED.CRGW_SYSTM_CLSS_CD = EC.LANE_TYPE AND ED.NTPD_CD = CC.CMMN_CD AND CC.CMMN_GRP_CD = 'NTPD_CD' AND NOT (ED.NTPD_AFFC_TOLOF_CD = '094' AND ED.NTPD_AFFC_STAT_CD = '60') UNION ALL SELECT /*+ USE_NL(A B C)*/ TO_DATE(A.OUT_DATE||A.OUT_HHMISS, 'YYYYMMDDHH24MISS') WW , A.VIO_CAR_NO CAR_NO , A.OBU_CAR_CLASS CAR_KIND , A.IN_IC_CODE , A.IC_CODE AS GEN_IC_CODE , A.IC_CODE , (SELECT CC.CMMN_CD_NM FROM T_STLA_CMMN_CD01M1 CC WHERE CC.CMMN_GRP_CD ='HIPAS_VIOL_CD' AND A.VIO_CODE = CC.CMMN_CD ) AS SAYU , A.IC_CODE AS OUT_IC_USE_YN , A.AMT AS UNPAID_AMT , DG_UNPAID_AMT , 0 as ETC_UNPAID_AMT , A.AMT AS COL_WILL_AMT , A.AMT AS SEPAR_WILL_AMT , 0 AS COL_EXTRA_WILL_AMT , 0 as SEPAR_EXTRA_WILL_AMT , A.ETC_AMT as DIV_WILL_AMT , A.WORK_DATE , A.WORK_NO , A.SER_NO , 'V' AS LANETYPE , A.OUT_DATE , A.OUT_HHMISS as OUT_TIME , A.TRD_CAR_CLASS as CAR_CLASS , A.VIO_NAME as USER_NAME , '10' as STATUS_CODE , '10' as PROC_CODE FROM (SELECT A.WORK_DATES AS WORK_DATE , A.TOLOF_CD AS IC_CODE , A.WORK_NO , A.VHCL_PROS_NO AS SER_NO , A.VIOL_VHNO AS VIO_CAR_NO , A.WOUTP_DATES AS PROC_DATEHHMISS , replace('���ᱳ', '%', '') VIO_NAME , A.HIPAS_VIOL_CD AS VIO_CODE , B.ENRC_TOLOF_CD AS IN_IC_CODE , B.WOUTP_DATES AS OUT_DATE , B.WOUTP_HMS AS OUT_HHMISS , C.PRCPT_FRKNCR_CD AS TRD_CAR_CLASS , C.OBU_FRKNCR_CD AS OBU_CAR_CLASS , NVL((SELECT SUM(DECODE(X.HOINST_CD, '00', X.MNRC_SCDL_TOLL, 0)) FROM T_STLK_WHVIOL_PROS_AMT01P1 X WHERE A.WORK_DATES = X.WORK_DATES AND A.TOLOF_CD = X.TOLOF_CD AND A.WORK_NO = X.WORK_NO AND A.VHCL_PROS_NO = X.VHCL_PROS_NO), 0) AS DG_UNPAID_AMT , NVL((SELECT sum(X.MNRC_SCDL_TOLL) FROM T_STLK_WHVIOL_PROS_AMT01P1 X WHERE A.WORK_DATES = X.WORK_DATES AND A.TOLOF_CD = X.TOLOF_CD AND A.WORK_NO = X.WORK_NO AND A.VHCL_PROS_NO = X.VHCL_PROS_NO), 0) AS AMT , NVL((SELECT sum(X.MNRC_SCDL_TOLL) FROM T_STLK_WHVIOL_PROS_AMT01P1 X WHERE A.WORK_DATES = X.WORK_DATES AND A.TOLOF_CD = X.TOLOF_CD AND A.WORK_NO = X.WORK_NO AND A.VHCL_PROS_NO = X.VHCL_PROS_NO AND x.HOINST_CD != '00'), 0) AS ETC_AMT FROM T_STLK_WHVIOL_PROS01P1 A , T_STLK_WHVIOL_PROS_PATH01P1 B , T_STLK_HIPASW_VIOL01P1 C , T_STLA_TOLOF_OP01M1 D WHERE 1 = 1 AND A.VIOL_VHNO = ? AND A.WORK_DATES = TO_CHAR(SYSDATE, 'YYYYMMDD') AND A.NTPD_MNRC_STAT_CD IN ('10', '20') AND A.TOLOF_CD = D.TOLOF_CD AND D.HOINST_CD IN (SELECT HOINST_CD FROM T_STLA_HOINST01C1 WHERE ODN5_HOINST_GRP_YN = 'Y') AND A.WORK_DATES = B.WORK_DATES AND A.TOLOF_CD = B.TOLOF_CD AND A.WORK_NO = B.WORK_NO AND A.VHCL_PROS_NO = B.VHCL_PROS_NO AND B.PATH_NO = '1' AND A.WORK_DATES = C.WORK_DATES(+) AND A.TOLOF_CD = C.TOLOF_CD(+) AND A.WORK_NO = C.WORK_NO(+) AND A.VHCL_PROS_NO = C.VHCL_PROS_NO(+) ) A ) PAY, INTERCHANGE IC, INTERCHANGE OC WHERE PAY.IN_IC_CODE = IC.IC_CODE(+) AND PAY.IC_CODE = OC.IC_CODE ORDER BY WW ) UR WHERE UNPAID_AMT > 0 ) UR2 ORDER BY WW DESC "; - - JdbcTemplate jdbcTemplate = new JdbcTemplate(bpstlDataSource); - return jdbcTemplate.queryForList(query, carNo, carNo); - } - - /** - * 영업소 이름으로 전화번호 조회 - */ - public Map findIcTelByName(String icName) { - String query = "SELECT TOLOF_CD, KOR_TOLOF_NM, TOLOF_TELNO " + - "FROM T_STLA_TOLOF_OP01M1 " + - "WHERE KOR_TOLOF_NM LIKE ? " + - "ORDER BY " + - " CASE WHEN KOR_TOLOF_NM = ? THEN 0 " + - " WHEN KOR_TOLOF_NM LIKE ? THEN 1 " + - " ELSE 2 " + - " END"; - - JdbcTemplate jdbcTemplate = new JdbcTemplate(bpstlDataSource); - List> results = jdbcTemplate.queryForList( - query, - "%" + icName + "%", - icName, - icName + "%"); - - if (!results.isEmpty()) { - return results.get(0); - } - return null; - } - - /** - * MongoDB에서 영업소 코드 조회 - */ - public Document findIcInfoByName(String icName) { - BasicDBObject condition = new BasicDBObject(); - condition.put("ic_name", icName); - MongoCollection icInfoCol = mongoTemplate.getCollection("ic_info"); - return icInfoCol.find(condition).first(); - } - - /** - * MongoDB에서 영업소 이름 prefix 후보 조회 - */ - public List findIcInfoByNamePrefix(String icName, int limit) { - BasicDBObject condition = new BasicDBObject(); - condition.put("ic_name", new BasicDBObject("$regex", "^" + Pattern.quote(icName))); - MongoCollection icInfoCol = mongoTemplate.getCollection("ic_info"); - return icInfoCol.find(condition).limit(limit).into(new ArrayList<>()); - } - - /** - * MongoDB에서 영업소 이름 포함 후보 조회 - */ - public List findIcInfoByNameContains(String icName, int limit) { - BasicDBObject condition = new BasicDBObject(); - condition.put("ic_name", new BasicDBObject("$regex", Pattern.quote(icName))); - MongoCollection icInfoCol = mongoTemplate.getCollection("ic_info"); - return icInfoCol.find(condition).limit(limit).into(new ArrayList<>()); - } - - /** - * MongoDB에서 통행요금 조회 - */ - public Document findFareByIcNames(String fromIc, String toIc) { - BasicDBObject condition = new BasicDBObject(); - condition.put("src_name", fromIc); - condition.put("dst_name", toIc); - condition.put("table_type", "01"); - condition.put("system_type", "1"); - - MongoCollection trsAmtCol = mongoTemplate.getCollection("trs_amt"); - return trsAmtCol.find(condition).first(); - } - - /** - * icTel.json 데이터 조회 - */ - public Map getIcTelMap() { - return icTelMap; - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/service/FareAmountHelper.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/service/FareAmountHelper.java deleted file mode 100644 index fb9fc48..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/service/FareAmountHelper.java +++ /dev/null @@ -1,163 +0,0 @@ -package com.ex.chatbotapi.domain.fare.service; - -import org.bson.Document; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * MongoDB trs_amt.amt / sum 배열에서 차종별·구간별 요금과 웹용 breakdown을 추출한다. - */ -final class FareAmountHelper { - - private static final String[] VEHICLE_LABELS = {"1종", "2종", "3종", "4종", "5종", "경차"}; - private static final String[] VEHICLE_DESCRIPTIONS = { - "승용차, 소형승합차/화물차, 경차", - "중형승합차, 중형화물차", - "대형승합차, 2축 대형화물차", - "3축 대형화물차", - "4축 이상 특수 화물차", - "1종(경차)" - }; - - private FareAmountHelper() { - } - - static void applyExactFareAmounts(Map fareData, Document trsDoc) { - List totalList = firstNonNullList( - readIntegerList(trsDoc.get("sum")), - readAmtList(trsDoc, "sum") - ); - if (totalList == null || totalList.size() < 6) { - return; - } - - List kecList = firstNonNullList( - readAmtList(trsDoc, "00"), - totalList - ); - List privateList = subtractLists(totalList, kecList); - - putLegacyFareFields(fareData, totalList); - putSectionFareFields(fareData, "kec", kecList); - putSectionFareFields(fareData, "private", privateList); - - fareData.put("webBreakdown", buildWebBreakdown( - String.valueOf(fareData.get("fromIc")), - String.valueOf(fareData.get("toIc")), - totalList, - kecList, - privateList - )); - } - - private static Map buildWebBreakdown( - String fromIc, - String toIc, - List totalList, - List kecList, - List privateList - ) { - Map breakdown = new HashMap<>(); - breakdown.put("fromIc", fromIc); - breakdown.put("toIc", toIc); - breakdown.put("routeLabel", fromIc + " → " + toIc); - breakdown.put("introText", - "요청하신 경로의 통행요금입니다. 도로공사 구간 요금·민자도로 구간 요금 조회 결과입니다."); - breakdown.put("totalFare", totalList.get(0)); - breakdown.put("kecFare", kecList.get(0)); - breakdown.put("privateFare", privateList.get(0)); - breakdown.put("compactTotalFare", totalList.get(5)); - breakdown.put("compactKecFare", kecList.get(5)); - breakdown.put("compactPrivateFare", privateList.get(5)); - - List> vehicles = new ArrayList<>(); - for (int i = 0; i < 5; i++) { - vehicles.add(vehicleEntry(i, totalList, kecList, privateList, false)); - } - vehicles.add(vehicleEntry(5, totalList, kecList, privateList, true)); - breakdown.put("vehicles", vehicles); - return breakdown; - } - - private static Map vehicleEntry( - int index, - List totalList, - List kecList, - List privateList, - boolean compact - ) { - Map vehicle = new HashMap<>(); - vehicle.put("typeLabel", VEHICLE_LABELS[index]); - vehicle.put("description", VEHICLE_DESCRIPTIONS[index]); - vehicle.put("total", totalList.get(index)); - vehicle.put("kec", kecList.get(index)); - vehicle.put("private", privateList.get(index)); - vehicle.put("compact", compact); - return vehicle; - } - - private static void putLegacyFareFields(Map fareData, List totalList) { - fareData.put("fare1", totalList.get(0)); - fareData.put("fare2", totalList.get(1)); - fareData.put("fare3", totalList.get(2)); - fareData.put("fare4", totalList.get(3)); - fareData.put("fare5", totalList.get(4)); - fareData.put("fare1Small", totalList.get(5)); - } - - private static void putSectionFareFields( - Map fareData, - String prefix, - List values - ) { - fareData.put(prefix + "Fare1", values.get(0)); - fareData.put(prefix + "Fare2", values.get(1)); - fareData.put(prefix + "Fare3", values.get(2)); - fareData.put(prefix + "Fare4", values.get(3)); - fareData.put(prefix + "Fare5", values.get(4)); - fareData.put(prefix + "Fare1Small", values.get(5)); - } - - private static List readAmtList(Document trsDoc, String key) { - Object amtObj = trsDoc.get("amt"); - if (!(amtObj instanceof Document)) { - return null; - } - return readIntegerList(((Document) amtObj).get(key)); - } - - @SuppressWarnings("unchecked") - private static List readIntegerList(Object value) { - if (!(value instanceof List)) { - return null; - } - List result = new ArrayList<>(); - for (Object item : (List) value) { - if (item instanceof Number) { - result.add(((Number) item).intValue()); - } - } - return result.isEmpty() ? null : result; - } - - private static List subtractLists(List totalList, List kecList) { - List privateList = new ArrayList<>(totalList.size()); - for (int i = 0; i < totalList.size(); i++) { - privateList.add(Math.max(0, totalList.get(i) - kecList.get(i))); - } - return privateList; - } - - @SafeVarargs - private static List firstNonNullList(List... lists) { - for (List list : lists) { - if (list != null && !list.isEmpty()) { - return list; - } - } - return null; - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/service/FareService.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/service/FareService.java deleted file mode 100644 index eb0b165..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/fare/service/FareService.java +++ /dev/null @@ -1,322 +0,0 @@ -package com.ex.chatbotapi.domain.fare.service; - -import com.ex.chatbotapi.domain.fare.repository.FareRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.bson.Document; -import org.springframework.stereotype.Service; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * 통행요금 관련 비즈니스 로직 처리 서비스 - */ -@Slf4j -@Service -@RequiredArgsConstructor -public class FareService { - - private final FareRepository fareRepository; - - /** - * 환불내역 조회 - */ - public Map getRefundInfo(String carNo) { - log.info("{} 차량 환불내역 조회 시작", carNo); - - Map response = new HashMap<>(); - - try { - List> list = fareRepository.findRefundByCarNo(carNo); - int refundCnt = list.size(); - - if (refundCnt == 0) { - log.info("{} 차량 환불내역 존재하지 않음", carNo); - response.put("status", false); - response.put("statusMsg", "환불 내역이 존재하지 않습니다."); - response.put("carNo", carNo); - } else { - log.info("{} 차량 환불내역이 존재합니다.", carNo); - response.put("status", true); - response.put("statusMsg", "환불 내역이 존재합니다."); - response.put("carNo", carNo); - response.put("refundData", list.get(0)); - } - - return response; - - } catch (Exception e) { - log.error("환불내역 조회 에러 : {}", e.getMessage(), e); - response.put("status", false); - response.put("statusMsg", "서비스에 일시적인 장애가 발생하였습니다."); - response.put("errorMsg", e.getMessage()); - return response; - } - } - - /** - * 미납내역 조회 - */ - public Map getUnpaidInfo(String carNo) { - log.info("{} 차량 미납내역 조회 시작", carNo); - - Map response = new HashMap<>(); - - try { - List> list = fareRepository.findUnpaidByCarNo(carNo); - - long sumAmt = 0; - for (Map map : list) { - if (map.get("USER_NAME") != null) { - sumAmt += ((java.math.BigDecimal) map.get("UNPAID_AMT")).toBigInteger().longValue(); - log.info("미납금액 : {}", map.get("UNPAID_AMT")); - - map.remove("COL_WILL_AMT"); - map.remove("COL_EXTRA_WILL_AMT"); - map.remove("WORK_NO"); - map.remove("SER_NO"); - map.remove("LANETYPE"); - map.remove("OUT_IC_CODE"); - } - } - - if (sumAmt < 100) { - log.info("{} 차량 미납내역 존재하지 않음", carNo); - response.put("status", true); - response.put("statusMsg", "미납 내역이 존재하지 않습니다."); - response.put("carNo", carNo); - response.put("sumAmt", sumAmt); - response.put("hasUnpaid", false); - } else { - log.info("{} 차량 미납내역이 존재합니다. 총 금액: {}", carNo, sumAmt); - response.put("status", true); - response.put("statusMsg", "미납 내역이 존재합니다."); - response.put("carNo", carNo); - response.put("sumAmt", sumAmt); - response.put("hasUnpaid", true); - response.put("unpaidList", list); - } - - return response; - - } catch (Exception e) { - log.error("미납내역 조회 에러 : {}", e.getMessage(), e); - response.put("status", false); - response.put("statusMsg", "서비스에 일시적인 장애가 발생하였습니다."); - response.put("errorMsg", e.getMessage()); - return response; - } - } - - /** - * 영업소 전화번호 조회 - */ - public Map getIcTelInfo(String icName) { - log.info("{} 영업소 연락처 조회를 시작합니다", icName); - - Map response = new HashMap<>(); - - try { - if (icName == null || icName.isEmpty()) { - log.warn("영업소 이름이 없습니다"); - response.put("status", false); - response.put("statusMsg", "영업소 이름이 없습니다."); - return response; - } - - Map icTelData = fareRepository.findIcTelByName(icName); - - if (icTelData == null) { - log.info("{} 영업소 정보를 찾을 수 없습니다.", icName); - response.put("status", false); - response.put("statusMsg", icName + " 영업소는 제가 모르는 영업소입니다."); - return response; - } - - String tolofCd = (String) icTelData.get("TOLOF_CD"); - String korIcName = (String) icTelData.get("KOR_TOLOF_NM"); - String tel = (String) icTelData.get("TOLOF_TELNO"); - - log.info("영업소 정보 조회 성공: 코드={}, 이름={}, 전화번호={}", tolofCd, korIcName, tel); - - if (tel == null || tel.equals("")) { - log.warn("{} 영업소는 전화번호 정보가 없습니다.", icName); - response.put("status", false); - response.put("statusMsg", icName + " 영업소는 전화번호 정보가 없습니다."); - return response; - } - - log.info("{} 영업소 연락처 조회 성공", icName); - response.put("status", true); - response.put("statusMsg", "정상응답"); - response.put("icName", korIcName); - response.put("tel", tel); - response.put("tolofCd", tolofCd); - - return response; - - } catch (Exception e) { - log.error("영업소 전화번호 조회 에러 : {}", e.getMessage(), e); - response.put("status", false); - response.put("statusMsg", "서비스에 일시적인 장애가 발생하였습니다."); - response.put("errorMsg", e.getMessage()); - return response; - } - } - - /** - * 통행요금 조회 - */ - public Map searchFare(String fromIc, String toIc) { - log.info("{}와 {} 간의 요금을 계산합니다", fromIc, toIc); - - Map response = new HashMap<>(); - - try { - Document fromIcInfo = fareRepository.findIcInfoByName(fromIc); - - if (fromIcInfo == null) { - log.warn("{} 영업소를 찾을 수 없습니다", fromIc); - response.put("status", false); - response.put("statusMsg", fromIc + " : 제가 알지 못하는 영업소 입니다."); - return response; - } - - Document toIcInfo = fareRepository.findIcInfoByName(toIc); - - if (toIcInfo == null) { - log.warn("{} 영업소를 찾을 수 없습니다", toIc); - response.put("status", false); - response.put("statusMsg", toIc + " : 제가 알지 못하는 영업소 입니다."); - return response; - } - - String fromCode = fromIcInfo.get("ic_code").toString(); - String toCode = toIcInfo.get("ic_code").toString(); - - Document trsUnique = fareRepository.findFareByIcNames(fromIc, toIc); - - if (trsUnique != null && !fromIc.equals("대구") && !toIc.equals("대구")) { - Map fareData = new HashMap<>(); - fareData.put("type", "exact"); - fareData.put("fromIc", fromIc); - fareData.put("toIc", toIc); - FareAmountHelper.applyExactFareAmounts(fareData, trsUnique); - - response.put("status", true); - response.put("statusMsg", "정상응답"); - response.put("fareData", fareData); - return response; - } - - int fromCodeInt = Integer.parseInt(fromCode); - int toCodeInt = Integer.parseInt(toCode); - - Map icTelMap = fareRepository.getIcTelMap(); - - if (fromCodeInt < 100 && toCodeInt < 100) { - @SuppressWarnings("unchecked") - Map fromIcTel = (Map) icTelMap.get(fromIc); - if (fromIcTel == null || fromIcTel.get("amt") == null) { - response.put("status", false); - response.put("statusMsg", "해당구간의 통행요금을 찾지 못했습니다."); - return response; - } - - @SuppressWarnings("unchecked") - List fromAmt = (List) fromIcTel.get("amt"); - Map fareData = new HashMap<>(); - fareData.put("type", "open_both"); - fareData.put("icName", fromIc); - fareData.put("fare1", fromAmt.get(0)); - fareData.put("fare1Small", fromAmt.get(5)); - fareData.put("fare2", fromAmt.get(1)); - fareData.put("fare3", fromAmt.get(2)); - fareData.put("fare4", fromAmt.get(3)); - fareData.put("fare5", fromAmt.get(4)); - - response.put("status", true); - response.put("statusMsg", "정상응답"); - response.put("fareData", fareData); - return response; - } - else if (fromCodeInt < 100) { - @SuppressWarnings("unchecked") - Map fromIcTel = (Map) icTelMap.get(fromIc); - if (fromIcTel == null || fromIcTel.get("amt") == null) { - response.put("status", false); - response.put("statusMsg", "해당구간의 통행요금을 찾지 못했습니다."); - return response; - } - - @SuppressWarnings("unchecked") - List fromAmt = (List) fromIcTel.get("amt"); - @SuppressWarnings("unchecked") - List outIcs = (List) fromIcTel.get("out"); - - Map fareData = new HashMap<>(); - fareData.put("type", "open_from"); - fareData.put("icName", fromIc); - fareData.put("toIc", toIc); - fareData.put("fare1", fromAmt.get(0)); - fareData.put("fare1Small", fromAmt.get(5)); - fareData.put("fare2", fromAmt.get(1)); - fareData.put("fare3", fromAmt.get(2)); - fareData.put("fare4", fromAmt.get(3)); - fareData.put("fare5", fromAmt.get(4)); - fareData.put("outIcs", outIcs); - - response.put("status", true); - response.put("statusMsg", "정상응답"); - response.put("fareData", fareData); - return response; - } - else if (toCodeInt < 100) { - @SuppressWarnings("unchecked") - Map toIcTel = (Map) icTelMap.get(toIc); - if (toIcTel == null || toIcTel.get("amt") == null) { - response.put("status", false); - response.put("statusMsg", "해당구간의 통행요금을 찾지 못했습니다."); - return response; - } - - @SuppressWarnings("unchecked") - List toAmt = (List) toIcTel.get("amt"); - @SuppressWarnings("unchecked") - List outIcs = (List) toIcTel.get("out"); - - Map fareData = new HashMap<>(); - fareData.put("type", "open_to"); - fareData.put("icName", toIc); - fareData.put("fromIc", fromIc); - fareData.put("fare1", toAmt.get(0)); - fareData.put("fare1Small", toAmt.get(5)); - fareData.put("fare2", toAmt.get(1)); - fareData.put("fare3", toAmt.get(2)); - fareData.put("fare4", toAmt.get(3)); - fareData.put("fare5", toAmt.get(4)); - fareData.put("outIcs", outIcs); - - response.put("status", true); - response.put("statusMsg", "정상응답"); - response.put("fareData", fareData); - return response; - } - else { - log.warn("{}에서 {} 통행요금을 찾을 수 없습니다", fromIc, toIc); - response.put("status", false); - response.put("statusMsg", "해당구간의 통행요금을 찾지 못했습니다."); - return response; - } - - } catch (Exception e) { - log.error("통행요금 조회 에러 : {}", e.getMessage(), e); - response.put("status", false); - response.put("statusMsg", "서비스에 일시적인 장애가 발생하였습니다."); - response.put("errorMsg", e.getMessage()); - return response; - } - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/api/RagApiController.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/api/RagApiController.java deleted file mode 100644 index d145a0c..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/api/RagApiController.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.ex.chatbotapi.domain.rag.api; - -import com.ex.chatbotapi.domain.rag.dto.RagQueryRequest; -import com.ex.chatbotapi.domain.rag.service.RagService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.Map; - -/** - * RAG(Retrieval-Augmented Generation) 관련 API 컨트롤러 - * HTTP 요청 처리만 담당하고, 비즈니스 로직은 RagService에 위임 - */ -@Slf4j -@RestController -@RequestMapping("/api/v1/rag-only") -@RequiredArgsConstructor -public class RagApiController { - private final RagService ragService; - - /** - * 챗봇 사용자 질문을 RAG 서버로 전달하고 응답을 반환하는 엔드포인트 - */ - @PostMapping - public ResponseEntity> queryRag(@RequestBody RagQueryRequest request) { - log.info("API 호출 : queryRag(POST) - 질문: {}, 봇ID: {}", - request.getQuestion(), request.getBotId()); - return ResponseEntity.ok(ragService.processQuestion(request)); - } - - /** - * RAG 서버(Docker 컨테이너) 헬스체크 엔드포인트 - */ - @GetMapping("/health") - public ResponseEntity> checkHealth() { - log.info("API 호출 : checkHealth(GET)"); - return ResponseEntity.ok(ragService.checkHealth()); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/client/RagServerClient.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/client/RagServerClient.java deleted file mode 100644 index 6a2a09a..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/client/RagServerClient.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.ex.chatbotapi.domain.rag.client; - -import com.ex.chatbotapi.config.RagConfig; -import com.ex.chatbotapi.domain.rag.dto.IntentAnalysisRequest; -import com.ex.chatbotapi.domain.rag.dto.IntentAnalysisResponse; -import com.ex.chatbotapi.domain.rag.dto.RagServerRequest; -import com.ex.chatbotapi.domain.rag.dto.RagServerResponse; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Component; -import org.springframework.web.client.RestClientException; -import org.springframework.web.client.RestTemplate; - -/** - * RAG 서버와의 통신을 담당하는 클라이언트 - */ -@Slf4j -@Component -@RequiredArgsConstructor -public class RagServerClient { - private final RestTemplate restTemplate; - private final RagConfig ragConfig; - - /** - * RAG 서버에 질문을 전송하고 응답을 받아오는 메소드 - */ - public RagServerResponse sendQuestion(RagServerRequest request) { - String url = ragConfig.getFullUrl(); - log.info("RAG 서버 요청 시작 - URL: {}, 질문: {}", url, request.getQuery()); - - try { - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - - HttpEntity entity = new HttpEntity<>(request, headers); - - ResponseEntity response = restTemplate.postForEntity( - url, entity, RagServerResponse.class); - - log.info("RAG 서버 응답 성공 - Status: {}", response.getStatusCode()); - return response.getBody(); - - } catch (RestClientException e) { - log.error("RAG 서버 통신 실패 - URL: {}, 에러: {}", url, e.getMessage(), e); - throw e; - } - } - - /** - * RAG 서버의 LLM intent 분석 엔드포인트 호출. - */ - public IntentAnalysisResponse analyzeIntent(IntentAnalysisRequest request) { - String url = ragConfig.getIntentAnalyzeUrl(); - log.info("RAG intent 분석 요청 시작 - URL: {}, 질문: {}", url, request.getQuery()); - - try { - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - - HttpEntity entity = new HttpEntity<>(request, headers); - ResponseEntity response = restTemplate.postForEntity( - url, entity, IntentAnalysisResponse.class); - - log.info("RAG intent 분석 응답 성공 - Status: {}", response.getStatusCode()); - return response.getBody(); - - } catch (RestClientException e) { - log.error("RAG intent 분석 통신 실패 - URL: {}, 에러: {}", url, e.getMessage(), e); - throw e; - } - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/dto/IntentAnalysisRequest.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/dto/IntentAnalysisRequest.java deleted file mode 100644 index 462b6fe..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/dto/IntentAnalysisRequest.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.ex.chatbotapi.domain.rag.dto; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.Map; -import java.util.List; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class IntentAnalysisRequest { - private String query; - private String botId; - private String pendingIntentType; - private Map pendingParams; - private List> intentDefinitions; -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/dto/IntentAnalysisResponse.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/dto/IntentAnalysisResponse.java deleted file mode 100644 index 32b8068..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/dto/IntentAnalysisResponse.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.ex.chatbotapi.domain.rag.dto; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.HashMap; -import java.util.Map; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class IntentAnalysisResponse { - private String intentType; - private Double confidence; - private Map params = new HashMap<>(); - private String routeType; - private Boolean needsClarification; - private String clarificationQuestion; - private String reason; - - public double confidenceOrZero() { - return confidence != null ? confidence : 0.0; - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/dto/RagQueryRequest.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/dto/RagQueryRequest.java deleted file mode 100644 index 0cdbca0..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/dto/RagQueryRequest.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.ex.chatbotapi.domain.rag.dto; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.Map; - -/** - * 클라이언트(챗봇 웹)로부터 받는 RAG 질의 요청 DTO - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class RagQueryRequest { - /** 사용자 질문 원본 */ - private String question; - /** 사용자 봇 ID */ - private String botId; - /** - * 도메인 서비스에서 조회한 DB 데이터 (intentType에 따라 설정) - * null이면 RAG 단독 처리 - */ - private Map domainData; - /** 의도 타입 (프롬프트 컨텍스트 제공용) */ - private String intentType; -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/dto/RagServerRequest.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/dto/RagServerRequest.java deleted file mode 100644 index a834cd7..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/dto/RagServerRequest.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.ex.chatbotapi.domain.rag.dto; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.Map; - -/** - * RAG 서버(Docker)로 전송하는 요청 DTO - * API 스펙: POST /ask - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class RagServerRequest { - /** 사용자 질문 (Docker API 필드명: "query") */ - private String query; - /** 봇 ID */ - private String botId; - /** - * 도메인 서비스 DB 조회 결과 (null이면 전송 안 함) - * Python 측 field: domain_data (alias: domainData) - */ - private Map domainData; - /** 의도 타입 (Python 측 field: intent_type) */ - private String intentType; -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/dto/RagServerResponse.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/dto/RagServerResponse.java deleted file mode 100644 index 2074881..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/dto/RagServerResponse.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.ex.chatbotapi.domain.rag.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; -import java.util.Map; - -/** - * RAG 서버(Docker)로부터 받는 응답 DTO - * API 스펙: POST /ask 응답 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class RagServerResponse { - /** LLM이 생성한 답변 */ - private String answer; - /** 가장 유사한 질문 목록 */ - @JsonProperty("matched_questions") - private List matchedQuestions; - /** 유사도 점수 배열 */ - private List scores; - /** 참조한 문서/질문 개수 */ - @JsonProperty("num_references") - private Integer numReferences; - /** 공식 FAQ 상세 URL 목록 */ - @JsonProperty("faq_urls") - private List faqUrls; - /** 참조 문서 메타데이터 */ - private List> references; -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/service/RagService.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/service/RagService.java deleted file mode 100644 index 22e9349..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/rag/service/RagService.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.ex.chatbotapi.domain.rag.service; - -import com.ex.chatbotapi.config.RagConfig; -import com.ex.chatbotapi.domain.rag.client.RagServerClient; -import com.ex.chatbotapi.domain.rag.dto.RagQueryRequest; -import com.ex.chatbotapi.domain.rag.dto.RagServerRequest; -import com.ex.chatbotapi.domain.rag.dto.RagServerResponse; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestClientException; -import org.springframework.web.client.RestTemplate; - -import java.util.HashMap; -import java.util.Map; - -/** - * RAG 관련 비즈니스 로직을 처리하는 서비스 - */ -@Slf4j -@Service -@RequiredArgsConstructor -public class RagService { - private final RagServerClient ragServerClient; - private final RagConfig ragConfig; - private final RestTemplate restTemplate; - - /** - * 사용자 질문을 처리하고 RAG 서버로부터 응답을 받아오는 메소드 - */ - public Map processQuestion(RagQueryRequest request) { - log.info("RAG 질문 처리 시작 - 질문: {}, 봇ID: {}", - request.getQuestion(), request.getBotId()); - - Map result = new HashMap<>(); - - try { - validateRequest(request); - - RagServerRequest serverRequest = RagServerRequest.builder() - .query(request.getQuestion()) - .botId(request.getBotId()) - .domainData(request.getDomainData()) - .intentType(request.getIntentType()) - .build(); - - RagServerResponse serverResponse = ragServerClient.sendQuestion(serverRequest); - - result.put("status", true); - result.put("statusMsg", "정상응답"); - result.put("answer", serverResponse.getAnswer()); - result.put("matchedQuestions", serverResponse.getMatchedQuestions()); - result.put("scores", serverResponse.getScores()); - result.put("numReferences", serverResponse.getNumReferences()); - result.put("faqUrls", serverResponse.getFaqUrls()); - result.put("references", serverResponse.getReferences()); - - } catch (IllegalArgumentException e) { - log.warn("입력 검증 실패 - {}", e.getMessage()); - result.put("status", false); - result.put("statusMsg", "입력값 오류: " + e.getMessage()); - - } catch (RestClientException e) { - log.error("RAG 서버 통신 실패 - {}", e.getMessage()); - result.put("status", false); - result.put("statusMsg", "RAG 서버 통신 실패"); - - } catch (Exception e) { - log.error("RAG 질문 처리 중 예외 발생 - {}", e.getMessage(), e); - result.put("status", false); - result.put("statusMsg", "처리 중 오류 발생"); - } - - return result; - } - - /** - * RAG 서버 헬스체크 - */ - @SuppressWarnings({"unchecked", "rawtypes"}) - public Map checkHealth() { - log.info("RAG 서버 헬스체크 시작"); - - Map result = new HashMap<>(); - - try { - String healthUrl = ragConfig.getHealthUrl(); - log.info("헬스체크 URL: {}", healthUrl); - - ResponseEntity response = restTemplate.getForEntity(healthUrl, Map.class); - Map healthData = (Map) response.getBody(); - - result.put("status", true); - result.put("statusMsg", "헬스체크 성공"); - result.put("healthCheck", healthData); - - if (healthData != null) { - log.info("RAG 서버 상태: {}, 벡터 개수: {}", - healthData.get("status"), healthData.get("vector_count")); - } - - } catch (Exception e) { - log.error("RAG 서버 헬스체크 실패 - {}", e.getMessage(), e); - result.put("status", false); - result.put("statusMsg", "헬스체크 실패: " + e.getMessage()); - } - - return result; - } - - private void validateRequest(RagQueryRequest request) { - if (request.getQuestion() == null || request.getQuestion().trim().isEmpty()) { - throw new IllegalArgumentException("질문이 비어있습니다."); - } - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/api/RestAreaApiController.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/api/RestAreaApiController.java deleted file mode 100644 index c60c514..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/api/RestAreaApiController.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.ex.chatbotapi.domain.restarea.api; - -import com.ex.chatbotapi.domain.restarea.service.RestAreaService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.Map; - -/** - * 휴게소 관련 API 컨트롤러 - * HTTP 요청 처리만 담당하고, 비즈니스 로직은 RestAreaService에 위임 - */ -@Slf4j -@RestController -@RequiredArgsConstructor -public class RestAreaApiController { - - private final RestAreaService restAreaService; - - /** - * Description : 휴게소 가격정보 조회 메소드 - */ - @GetMapping("/api/restarea/priceInfo") - public ResponseEntity> queryPriceInfo(String restAreaName) { - log.info("API 호출 : queryPriceInfo(GET) - restAreaName: {}", restAreaName); - return ResponseEntity.ok(restAreaService.getPriceInfo(restAreaName)); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/formatter/BrandShopLlmFormatter.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/formatter/BrandShopLlmFormatter.java deleted file mode 100644 index eb3cf24..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/formatter/BrandShopLlmFormatter.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.ex.chatbotapi.domain.restarea.formatter; - -import com.ex.chatbotapi.domain.agent.formatter.LlmDomainDataFormatter; -import org.springframework.stereotype.Component; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * BRAND_SHOP — 휴게소 브랜드 매장 목록을 LLM용 요약으로 정리한다. - */ -@Component -public class BrandShopLlmFormatter implements LlmDomainDataFormatter { - - private static final int MAX_BRANDS = 20; - - @Override - public String getIntentType() { - return "BRAND_SHOP"; - } - - @Override - @SuppressWarnings("unchecked") - public void enrich(Map domainData) { - if (domainData == null) return; - - Object st = domainData.get("status"); - boolean ok = st instanceof Boolean && (Boolean) st; - - if (!ok) { - String msg = domainData.get("statusMsg") != null - ? String.valueOf(domainData.get("statusMsg")) - : "브랜드 매장 정보를 조회할 수 없습니다."; - domainData.put("llmSummary", msg); - return; - } - - String restAreaName = String.valueOf(domainData.getOrDefault("restAreaName", "")); - Object totalCountObj = domainData.get("totalCount"); - int totalCount = totalCountObj instanceof Number ? ((Number) totalCountObj).intValue() : 0; - - List> list = (List>) domainData.get("list"); - if (list == null || list.isEmpty()) { - domainData.put("llmSummary", restAreaName + " 휴게소 매장 정보가 없습니다."); - return; - } - - // 브랜드명 목록 (중복 제거) - List brands = list.stream() - .map(b -> String.valueOf(b.getOrDefault("brdName", ""))) - .distinct() - .limit(MAX_BRANDS) - .collect(Collectors.toList()); - - StringBuilder sb = new StringBuilder(); - sb.append(restAreaName).append(" 휴게소 브랜드 매장 (총 ").append(totalCount).append("개):\n"); - sb.append(String.join(", ", brands)); - if (totalCount > MAX_BRANDS) { - sb.append(" 외 ").append(totalCount - MAX_BRANDS).append("개"); - } - - domainData.put("llmSummary", sb.toString().trim()); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/formatter/FoodListLlmFormatter.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/formatter/FoodListLlmFormatter.java deleted file mode 100644 index b4aef12..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/formatter/FoodListLlmFormatter.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.ex.chatbotapi.domain.restarea.formatter; - -import com.ex.chatbotapi.domain.agent.formatter.LlmDomainDataFormatter; -import org.springframework.stereotype.Component; - -import java.util.Map; - -/** - * REST_AREA_FOOD_LIST — 휴게소 음식 목록(전체 메뉴)을 LLM용 요약으로 정리한다. - */ -@Component -public class FoodListLlmFormatter implements LlmDomainDataFormatter { - - private static final int MAX_CHARS = 800; - - @Override - public String getIntentType() { - return "REST_AREA_FOOD_LIST"; - } - - @Override - @SuppressWarnings("unchecked") - public void enrich(Map domainData) { - if (domainData == null) return; - - Object st = domainData.get("status"); - boolean ok = st instanceof Boolean && (Boolean) st; - - if (!ok) { - String msg = domainData.get("statusMsg") != null - ? String.valueOf(domainData.get("statusMsg")) - : "음식 목록을 조회할 수 없습니다."; - domainData.put("llmSummary", msg); - return; - } - - String restAreaName = String.valueOf(domainData.getOrDefault("restAreaName", "")); - Map msgMap = (Map) domainData.get("msgMap"); - - if (msgMap == null || msgMap.isEmpty()) { - domainData.put("llmSummary", restAreaName + " 휴게소 메뉴 정보가 없습니다."); - return; - } - - StringBuilder sb = new StringBuilder(); - sb.append(restAreaName).append(" 휴게소 메뉴:\n"); - for (Map.Entry entry : msgMap.entrySet()) { - String restaurant = entry.getKey().replace("휴게소", "").trim(); - sb.append("\n[").append(restaurant).append("]\n"); - sb.append(entry.getValue()).append("\n"); - if (sb.length() > MAX_CHARS) { - sb.append("...(이하 생략)"); - break; - } - } - - domainData.put("llmSummary", sb.toString().trim()); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/formatter/GasstationLlmFormatter.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/formatter/GasstationLlmFormatter.java deleted file mode 100644 index 32ed416..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/formatter/GasstationLlmFormatter.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.ex.chatbotapi.domain.restarea.formatter; - -import com.ex.chatbotapi.domain.agent.formatter.LlmDomainDataFormatter; -import org.springframework.stereotype.Component; - -import java.util.List; -import java.util.Map; - -/** - * GASSTATION — 휴게소 주유소 현황을 LLM용 요약으로 정리한다. - */ -@Component -public class GasstationLlmFormatter implements LlmDomainDataFormatter { - - @Override - public String getIntentType() { - return "GASSTATION"; - } - - @Override - @SuppressWarnings("unchecked") - public void enrich(Map domainData) { - if (domainData == null) return; - - Object st = domainData.get("status"); - boolean ok = st instanceof Boolean && (Boolean) st; - - if (!ok) { - String msg = domainData.get("statusMsg") != null - ? String.valueOf(domainData.get("statusMsg")) - : "주유소 정보를 조회할 수 없습니다."; - domainData.put("llmSummary", msg); - return; - } - - String restAreaName = String.valueOf(domainData.getOrDefault("restAreaName", "")); - - List> list = (List>) domainData.get("list"); - if (list == null || list.isEmpty()) { - domainData.put("llmSummary", restAreaName + " 휴게소 주유 정보가 없습니다."); - return; - } - - StringBuilder sb = new StringBuilder(); - for (Map g : list) { - String svcNm = gasstationDisplayName(restAreaName, g); - String gasoline = String.valueOf(g.getOrDefault("gasolinePrice", "X")); - String diesel = String.valueOf(g.getOrDefault("diselPrice", "X")); - String lpgYn = String.valueOf(g.getOrDefault("lpgYn", "N")); - String lpgPrice = String.valueOf(g.getOrDefault("lpgPrice", "X")); - - sb.append(svcNm).append(" 주유 현황:\n"); - sb.append("- 휘발유: ").append("X".equals(gasoline) ? "정보 없음" : gasoline + "원").append("\n"); - sb.append("- 경유: ").append("X".equals(diesel) ? "정보 없음" : diesel + "원").append("\n"); - sb.append("- LPG: ").append("Y".equals(lpgYn) ? lpgPrice + "원" : "미운영").append("\n"); - } - - domainData.put("llmSummary", sb.toString().trim()); - } - - private String gasstationDisplayName(String restAreaName, Map gasInfo) { - Object serviceAreaName = gasInfo.get("serviceAreaName"); - if (serviceAreaName != null) { - return String.valueOf(serviceAreaName) - .replace("주유소", "") - .replace("휴게소", ""); - } - Object directionObj = gasInfo.get("direction"); - if (directionObj != null && restAreaName != null && !restAreaName.trim().isEmpty()) { - return restAreaName + "(" + String.valueOf(directionObj).trim() + ")"; - } - return "주유소"; - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/formatter/RestAreaPriceLlmFormatter.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/formatter/RestAreaPriceLlmFormatter.java deleted file mode 100644 index fc8851a..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/formatter/RestAreaPriceLlmFormatter.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.ex.chatbotapi.domain.restarea.formatter; - -import com.ex.chatbotapi.domain.agent.formatter.LlmDomainDataFormatter; -import org.springframework.stereotype.Component; - -import java.util.List; -import java.util.Map; - -/** - * REST_AREA_PRICE — 휴게소 간식·메뉴 가격 공시(구간별 min/max/avg)를 LLM용 요약으로 정리한다. - */ -@Component -public class RestAreaPriceLlmFormatter implements LlmDomainDataFormatter { - - @Override - public String getIntentType() { - return "REST_AREA_PRICE"; - } - - @Override - public void enrich(Map domainData) { - if (domainData == null || domainData.isEmpty()) { - return; - } - - Object st = domainData.get("status"); - boolean ok = st instanceof Boolean && (Boolean) st; - - if (!ok) { - String msg = domainData.get("statusMsg") != null - ? String.valueOf(domainData.get("statusMsg")) - : "휴게소 가격정보 조회에 실패했습니다."; - domainData.put("llmSummary", msg); - return; - } - - @SuppressWarnings("unchecked") - Map>> priceInfo = - (Map>>) domainData.get("priceInfo"); - if (priceInfo == null || priceInfo.isEmpty()) { - domainData.put("llmSummary", "가격 공시(priceInfo) 데이터가 없습니다."); - return; - } - - StringBuilder sb = new StringBuilder(); - sb.append("휴게소 메뉴·간식 가격 공시 요약:\n"); - - int restLimit = 5; - int itemLimitPerRest = 12; - int restCount = 0; - - for (Map.Entry>> e : priceInfo.entrySet()) { - if (restCount >= restLimit) { - sb.append("- …외 휴게소 ").append(priceInfo.size() - restLimit).append("곳 더 있음\n"); - break; - } - String restNm = e.getKey(); - List> rows = e.getValue(); - sb.append("- ").append(restNm).append("\n"); - int n = 0; - for (Map row : rows) { - if (n >= itemLimitPerRest) { - sb.append(" · …외 ").append(rows.size() - itemLimitPerRest).append("개 항목\n"); - break; - } - String gb = row.getOrDefault("snackGb", "?"); - String min = row.getOrDefault("minCost", "?"); - String max = row.getOrDefault("maxCost", "?"); - String avg = row.getOrDefault("avgCost", "?"); - sb.append(" · ").append(gb) - .append(": 최저 ").append(min).append("원, 최고 ").append(max) - .append("원, 평균 ").append(avg).append("원\n"); - n++; - } - restCount++; - } - - domainData.put("llmSummary", sb.toString().trim()); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/handler/RestAreaPriceHandler.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/handler/RestAreaPriceHandler.java deleted file mode 100644 index c20d6e0..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/handler/RestAreaPriceHandler.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.ex.chatbotapi.domain.restarea.handler; - -import com.ex.chatbotapi.domain.agent.handler.IntentHandler; -import com.ex.chatbotapi.domain.restarea.service.RestAreaService; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Map; - -@Component -@RequiredArgsConstructor -public class RestAreaPriceHandler implements IntentHandler { - - private final RestAreaService restAreaService; - - @Override - public String getIntentType() { - return "REST_AREA_PRICE"; - } - - @Override - public Map fetchDomainData(Map params) { - return restAreaService.getPriceInfo(params.get("restAreaName")); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/repository/RestAreaRepository.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/repository/RestAreaRepository.java deleted file mode 100644 index cda9834..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/repository/RestAreaRepository.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.ex.chatbotapi.domain.restarea.repository; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.dao.EmptyResultDataAccessException; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.stereotype.Repository; - -import javax.sql.DataSource; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -/** - * 휴게소 관련 데이터 접근 레이어 - */ -@Slf4j -@Repository -@RequiredArgsConstructor -public class RestAreaRepository { - - @Qualifier("restAreaDataSource") - private final DataSource restAreaDataSource; - - /** - * 휴게소 가격정보 조회 - */ - public List> findPriceInfoByRestAreaName(String restAreaName) { - String query = "SELECT REST_NM, NM_REP as SNACK_GB, MIN_COST, MAX_COST, AVG_COST " + - "FROM EXREST.RESTCM_SNACKRESTAVG_VW A " + - "INNER JOIN EXREST.RESTCM_SNACKGB_VW B " + - "ON A.SNACK_GB = B.NM_CD " + - "WHERE REST_NM LIKE ?"; - - JdbcTemplate jdbcTemplate = new JdbcTemplate(restAreaDataSource); - - try { - String searchPattern = "%" + restAreaName + "%"; - return jdbcTemplate.queryForList(query, searchPattern); - } catch (EmptyResultDataAccessException e) { - return Collections.emptyList(); - } - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/service/RestAreaService.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/service/RestAreaService.java deleted file mode 100644 index fa9d246..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/restarea/service/RestAreaService.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.ex.chatbotapi.domain.restarea.service; - -import com.ex.chatbotapi.domain.restarea.repository.RestAreaRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * 휴게소 관련 비즈니스 로직 처리 서비스 - */ -@Slf4j -@Service -@RequiredArgsConstructor -public class RestAreaService { - - private final RestAreaRepository restAreaRepository; - - /** - * 휴게소 가격정보 조회 - */ - public Map getPriceInfo(String restAreaName) { - log.info("{} 휴게소 가격공시 조회를 시작합니다", restAreaName); - - Map response = new HashMap<>(); - - try { - List> restAreaInfo = restAreaRepository - .findPriceInfoByRestAreaName(restAreaName); - - if (!restAreaInfo.isEmpty()) { - Map>> priceInfoByRestArea = new HashMap<>(); - - for (Map map : restAreaInfo) { - String restNm = (String) map.get("REST_NM"); - String snackGb = (String) map.get("SNACK_GB"); - int minCost = ((BigDecimal) map.get("MIN_COST")).intValue(); - int maxCost = ((BigDecimal) map.get("MAX_COST")).intValue(); - int avgCost = ((BigDecimal) map.get("AVG_COST")).intValue(); - - if (!priceInfoByRestArea.containsKey(restNm)) { - priceInfoByRestArea.put(restNm, new ArrayList<>()); - } - - Map priceItem = new HashMap<>(); - priceItem.put("snackGb", snackGb); - priceItem.put("minCost", String.valueOf(minCost)); - priceItem.put("maxCost", String.valueOf(maxCost)); - priceItem.put("avgCost", String.valueOf(avgCost)); - - priceInfoByRestArea.get(restNm).add(priceItem); - } - - response.put("status", true); - response.put("statusMsg", "정상응답"); - response.put("priceInfo", priceInfoByRestArea); - } else { - log.info("{} 휴게소의 메뉴 및 가격정보가 없습니다.", restAreaName); - response.put("status", false); - response.put("statusMsg", restAreaName + " 휴게소의 메뉴 및 가격정보가 없습니다."); - } - - return response; - - } catch (Exception e) { - log.error("가격정보 조회 에러 : {}", e.getMessage(), e); - response.put("status", false); - response.put("statusMsg", "서비스에 일시적인 장애가 발생하였습니다."); - response.put("errorMsg", e.getMessage()); - return response; - } - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/api/TroadApiController.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/api/TroadApiController.java deleted file mode 100644 index 607b9cc..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/api/TroadApiController.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.ex.chatbotapi.domain.troad.api; - -import com.ex.chatbotapi.domain.troad.service.TroadService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.io.IOException; -import java.util.Map; - -/** - * 도로교통 관련 API 컨트롤러 (경로 조회, 혼잡구간, 교통속보) - * HTTP 요청 처리만 담당하고, 비즈니스 로직은 TroadService에 위임 - */ -@Slf4j -@RestController -@RequiredArgsConstructor -public class TroadApiController { - - private final TroadService troadService; - - @GetMapping("/api/troad/searchRoute") - public ResponseEntity> querySearchRoute(String fromIc, String toIc) throws IOException { - log.info("API 호출 : querySearchRoute(GET) - fromIc: {}, toIc: {}", fromIc, toIc); - return ResponseEntity.ok(troadService.searchRoute(fromIc, toIc)); - } - - @GetMapping("/api/troad/congestion") - public ResponseEntity> queryCongestion(String routeName) { - log.info("API 호출 : queryCongestion(GET) - routeName: {}", routeName); - return ResponseEntity.ok(troadService.getCongestion(routeName)); - } - - @GetMapping("/api/troad/breakingNews") - public ResponseEntity> queryBreakingNews(String routeName) { - log.info("API 호출 : queryBreakingNews(GET) - routeName: {}", routeName); - return ResponseEntity.ok(troadService.getBreakingNews(routeName)); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/formatter/TroadBreakingNewsLlmFormatter.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/formatter/TroadBreakingNewsLlmFormatter.java deleted file mode 100644 index ce21b1f..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/formatter/TroadBreakingNewsLlmFormatter.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.ex.chatbotapi.domain.troad.formatter; - -import com.ex.chatbotapi.domain.agent.formatter.LlmDomainDataFormatter; -import org.springframework.stereotype.Component; - -import java.util.List; -import java.util.Map; - -/** - * TROAD_BREAKING_NEWS — 노선별 교통속보(상·하행)를 LLM용 요약으로 정리한다. - */ -@Component -public class TroadBreakingNewsLlmFormatter implements LlmDomainDataFormatter { - - private static final String REGEX_DIST = "\\(\\d+(\\.\\d+)?K\\)"; - - @Override - public String getIntentType() { - return "TROAD_BREAKING_NEWS"; - } - - @Override - public void enrich(Map domainData) { - if (domainData == null || domainData.isEmpty()) { - return; - } - - Object st = domainData.get("status"); - boolean ok = st instanceof Boolean && (Boolean) st; - - if (!ok) { - String msg = domainData.get("statusMsg") != null - ? String.valueOf(domainData.get("statusMsg")) - : "교통속보 조회에 실패했습니다."; - domainData.put("llmSummary", msg); - return; - } - - @SuppressWarnings("unchecked") - Map routeInfo = (Map) domainData.get("RouteInfo"); - @SuppressWarnings("unchecked") - List> newsS = (List>) domainData.get("RouteBreakingNewsS"); - @SuppressWarnings("unchecked") - List> newsE = (List>) domainData.get("RouteBreakingNewsE"); - - if (routeInfo == null || routeInfo.isEmpty()) { - domainData.put("llmSummary", "노선 기본 정보(RouteInfo)가 없습니다."); - return; - } - - String routeNm = String.valueOf(routeInfo.getOrDefault("ROUTENM", "?")); - StringBuilder sb = new StringBuilder(); - sb.append("노선 교통속보 요약 — ").append(routeNm).append("\n"); - - int cap = 10; - appendNewsBlock(sb, "상행(S)", newsS, cap); - appendNewsBlock(sb, "하행(E)", newsE, cap); - - domainData.put("llmSummary", sb.toString().trim()); - } - - private static void appendNewsBlock(StringBuilder sb, String label, List> rows, int cap) { - sb.append("\n【").append(label).append("】\n"); - if (rows == null || rows.isEmpty()) { - sb.append("- 속보 없음\n"); - return; - } - int n = 0; - for (Map map : rows) { - if (n >= cap) { - sb.append("- …외 ").append(rows.size() - cap).append("건\n"); - break; - } - String inciTp = String.valueOf(map.getOrDefault("INCI_TP", "")); - String when = String.valueOf(map.getOrDefault("INCIDENT_DATE", "")); - String pos = String.valueOf(map.getOrDefault("INCIDENT_POS_NAME", "")); - pos = pos.replaceAll(REGEX_DIST, "").trim(); - String msg = String.valueOf(map.getOrDefault("MSG", "")); - msg = msg.replaceAll("\\s+", " ").trim(); - - sb.append("- "); - if (!when.isEmpty() && !"null".equals(when)) { - sb.append(when).append(" "); - } - if (!inciTp.isEmpty() && !"null".equals(inciTp)) { - sb.append("[").append(inciTp).append("] "); - } - if (!pos.isEmpty() && !"null".equals(pos)) { - sb.append(pos).append(" — "); - } - sb.append(msg.isEmpty() || "null".equals(msg) ? "(내용참조)" : msg).append("\n"); - n++; - } - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/formatter/TroadCongestionLlmFormatter.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/formatter/TroadCongestionLlmFormatter.java deleted file mode 100644 index 103f33e..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/formatter/TroadCongestionLlmFormatter.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.ex.chatbotapi.domain.troad.formatter; - -import com.ex.chatbotapi.domain.agent.formatter.LlmDomainDataFormatter; -import org.springframework.stereotype.Component; - -import java.util.List; -import java.util.Map; - -/** - * TROAD_CONGESTION — 노선별 혼잡 구간(상·하행)을 LLM용 요약으로 정리한다. - */ -@Component -public class TroadCongestionLlmFormatter implements LlmDomainDataFormatter { - - @Override - public String getIntentType() { - return "TROAD_CONGESTION"; - } - - @Override - public void enrich(Map domainData) { - if (domainData == null || domainData.isEmpty()) { - return; - } - - Object st = domainData.get("status"); - boolean ok = st instanceof Boolean && (Boolean) st; - - if (!ok) { - String msg = domainData.get("statusMsg") != null - ? String.valueOf(domainData.get("statusMsg")) - : "노선 혼잡정보 조회에 실패했습니다."; - domainData.put("llmSummary", msg); - return; - } - - @SuppressWarnings("unchecked") - Map routeInfo = (Map) domainData.get("RouteInfo"); - @SuppressWarnings("unchecked") - List> routeInfoS = (List>) domainData.get("RouteInfoS"); - @SuppressWarnings("unchecked") - List> routeInfoE = (List>) domainData.get("RouteInfoE"); - - if (routeInfo == null || routeInfo.isEmpty()) { - domainData.put("llmSummary", "노선 기본 정보(RouteInfo)가 없습니다."); - return; - } - - String routeNm = String.valueOf(routeInfo.getOrDefault("ROUTENM", "?")); - StringBuilder sb = new StringBuilder(); - sb.append("노선 혼잡 구간 요약 — ").append(routeNm).append("\n"); - - int cap = 12; - if (routeInfoS != null && !routeInfoS.isEmpty()) { - appendDirectionBlock(sb, "상행 등(S)", routeInfoS, cap); - } else { - sb.append("\n【상행 등혼잡 구간】\n- 해당 방향 데이터 없음\n"); - } - - if (routeInfoE != null && !routeInfoE.isEmpty()) { - appendDirectionBlock(sb, "하행 등(E)", routeInfoE, cap); - } else { - sb.append("\n【하행 등혼잡 구간】\n- 해당 방향 데이터 없음\n"); - } - - domainData.put("llmSummary", sb.toString().trim()); - } - - private static void appendDirectionBlock(StringBuilder sb, String label, List> rows, int cap) { - sb.append("\n【").append(label).append("】\n"); - int n = 0; - for (Map map : rows) { - if (n >= cap) { - sb.append("- …외 ").append(rows.size() - cap).append("건\n"); - break; - } - String node = String.valueOf(map.getOrDefault("NODE_NM", "?")); - String grade = gradeToKo(map.get("TRAFFIC_GRADE")); - double len = toDouble(map.get("LINK_LEN_KM")); - double speed = toDouble(map.get("SPEED")); - String dirNm = String.valueOf(map.getOrDefault("ROUTE_DIR_NM", "")); - sb.append("- ").append(node).append(" (").append(String.format("%.1f", len)).append("km) — ") - .append(grade).append(", 평균속도 ").append(String.format("%.0f", speed)).append("km/h"); - if (!dirNm.isEmpty() && !"?".equals(dirNm)) { - sb.append(", ").append(dirNm); - } - sb.append("\n"); - n++; - } - if (rows.isEmpty()) { - sb.append("- 해당 방향 데이터 없음\n"); - } - } - - private static String gradeToKo(Object code) { - if (code == null) { - return "정보없음"; - } - String g = code.toString(); - switch (g) { - case "G": - return "원활"; - case "L": - return "서행"; - case "R": - return "정체"; - default: - return g; - } - } - - private static double toDouble(Object v) { - if (v == null) { - return 0; - } - if (v instanceof Number) { - return ((Number) v).doubleValue(); - } - try { - return Double.parseDouble(v.toString()); - } catch (NumberFormatException e) { - return 0; - } - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/formatter/TroadSearchRouteLlmFormatter.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/formatter/TroadSearchRouteLlmFormatter.java deleted file mode 100644 index 38d12f2..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/formatter/TroadSearchRouteLlmFormatter.java +++ /dev/null @@ -1,149 +0,0 @@ -package com.ex.chatbotapi.domain.troad.formatter; - -import com.ex.chatbotapi.domain.agent.formatter.LlmDomainDataFormatter; -import com.google.gson.Gson; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * TROAD_SEARCH_ROUTE — 시터스 영업소 간 경로(Summary, 정체/사고/작업 Icdt 등)를 LLM용 요약으로 정리한다. - */ -@Component -public class TroadSearchRouteLlmFormatter implements LlmDomainDataFormatter { - - private static final Gson GSON = new Gson(); - private static final String REGEX_DIST = "\\(\\d+(\\.\\d+)?K\\)"; - private static final String REGEX_TIME = "\\[\\d{2}:\\d{2}-\\s*:\\s*\\]"; - - @Override - public String getIntentType() { - return "TROAD_SEARCH_ROUTE"; - } - - @Override - public void enrich(Map domainData) { - if (domainData == null || domainData.isEmpty()) { - return; - } - - Object st = domainData.get("status"); - boolean ok = st instanceof Boolean && (Boolean) st; - - if (!ok) { - String msg = domainData.get("statusMsg") != null - ? String.valueOf(domainData.get("statusMsg")) - : "영업소 간 교통경로 조회에 실패했습니다."; - domainData.put("llmSummary", msg); - return; - } - - @SuppressWarnings("unchecked") - Map summary = (Map) domainData.get("Summary"); - @SuppressWarnings("unchecked") - Map bunch = (Map) domainData.get("Bunch"); - @SuppressWarnings("unchecked") - Map icdt = (Map) domainData.get("Icdt"); - - String srcName = String.valueOf(domainData.getOrDefault("SrcName", "")); - String dstName = String.valueOf(domainData.getOrDefault("DstName", "")); - - StringBuilder sb = new StringBuilder(); - sb.append("영업소 구간 교통요약\n"); - if (!srcName.isEmpty() || !dstName.isEmpty()) { - sb.append("- 구간: ").append(srcName).append(" ⇒ ").append(dstName).append("\n"); - } - - if (summary != null && !summary.isEmpty()) { - double routeKm = toDouble(summary.get("RouteLength")) / 1000.0; - double travelSec = toDouble(summary.get("TravelTime")); - int hh = (int) (travelSec / 3600); - int mm = (int) ((travelSec % 3600) / 60); - sb.append("- 총거리: 약 ").append(Math.round(routeKm)).append("km\n"); - sb.append("- 소요시간(엔진 추정): ").append(hh).append("시간 ").append(mm).append("분\n"); - } - - if (bunch != null && bunch.containsKey("BunchItems")) { - sb.append("- 구간내 이벤트·구간 묶음 건수(BunchItems): ").append((int) toDouble(bunch.get("BunchItems"))).append("건\n"); - } - - sb.append("\n【정체】\n"); - appendIcdtSection(sb, icdt, "정체"); - sb.append("\n【사고】\n"); - appendIcdtSection(sb, icdt, "사고"); - sb.append("\n【작업】\n"); - appendIcdtSection(sb, icdt, "작업"); - - domainData.put("llmSummary", sb.toString().trim()); - } - - private static void appendIcdtSection(StringBuilder sb, Map icdt, String type) { - List lines = extractIcdtMessages(icdt, type, 25); - if (lines.isEmpty()) { - sb.append("- 없음\n"); - return; - } - for (String line : lines) { - sb.append("» ").append(line).append("\n"); - } - } - - /** - * Kakao 스킬과 동일한 IcdtData·IcdtInfo 구조를 사용해 타입별 메시지 문자열을 수집한다. - */ - @SuppressWarnings("unchecked") - private static List extractIcdtMessages(Map icdt, String incidentType, int maxLines) { - List out = new ArrayList<>(); - if (icdt == null) { - return out; - } - Object raw = icdt.get("IcdtData"); - if (!(raw instanceof List)) { - return out; - } - - List icdtDatas = (List) raw; - for (Object icdtData : icdtDatas) { - if (out.size() >= maxLines) { - break; - } - Map icdtDataMap = GSON.fromJson(GSON.toJson(icdtData), Map.class); - Object ii = icdtDataMap.get("IcdtInfo"); - if (!(ii instanceof List)) { - continue; - } - List icdtInfo = (List) ii; - if (icdtInfo.size() < 4) { - continue; - } - String category = String.valueOf(icdtInfo.get(2)); - if (!incidentType.equals(category)) { - continue; - } - String infoMsg = String.valueOf(icdtInfo.get(3)); - infoMsg = infoMsg.replaceAll(REGEX_DIST, ""); - infoMsg = infoMsg.replaceAll(REGEX_TIME, ""); - infoMsg = infoMsg.trim(); - if (!infoMsg.isEmpty()) { - out.add(infoMsg); - } - } - return out; - } - - private static double toDouble(Object v) { - if (v == null) { - return 0; - } - if (v instanceof Number) { - return ((Number) v).doubleValue(); - } - try { - return Double.parseDouble(v.toString()); - } catch (NumberFormatException e) { - return 0; - } - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/handler/TroadBreakingNewsHandler.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/handler/TroadBreakingNewsHandler.java deleted file mode 100644 index 9b7d439..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/handler/TroadBreakingNewsHandler.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.ex.chatbotapi.domain.troad.handler; - -import com.ex.chatbotapi.domain.agent.handler.IntentHandler; -import com.ex.chatbotapi.domain.troad.service.TroadService; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Map; - -@Component -@RequiredArgsConstructor -public class TroadBreakingNewsHandler implements IntentHandler { - - private final TroadService troadService; - - @Override - public String getIntentType() { - return "TROAD_BREAKING_NEWS"; - } - - @Override - public Map fetchDomainData(Map params) { - return troadService.getBreakingNews(params.get("routeName")); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/handler/TroadCongestionHandler.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/handler/TroadCongestionHandler.java deleted file mode 100644 index 55f00a2..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/handler/TroadCongestionHandler.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.ex.chatbotapi.domain.troad.handler; - -import com.ex.chatbotapi.domain.agent.handler.IntentHandler; -import com.ex.chatbotapi.domain.troad.service.TroadService; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Map; - -@Component -@RequiredArgsConstructor -public class TroadCongestionHandler implements IntentHandler { - - private final TroadService troadService; - - @Override - public String getIntentType() { - return "TROAD_CONGESTION"; - } - - @Override - public Map fetchDomainData(Map params) { - return troadService.getCongestion(params.get("routeName")); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/handler/TroadSearchRouteHandler.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/handler/TroadSearchRouteHandler.java deleted file mode 100644 index 44fea18..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/handler/TroadSearchRouteHandler.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.ex.chatbotapi.domain.troad.handler; - -import com.ex.chatbotapi.domain.agent.handler.IntentHandler; -import com.ex.chatbotapi.domain.troad.service.TroadService; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.io.IOException; -import java.util.Map; - -@Component -@RequiredArgsConstructor -public class TroadSearchRouteHandler implements IntentHandler { - - private final TroadService troadService; - - @Override - public String getIntentType() { - return "TROAD_SEARCH_ROUTE"; - } - - @Override - public Map fetchDomainData(Map params) { - try { - String fromIc = params.get("fromIcCode") != null ? params.get("fromIcCode") : params.get("fromIc"); - String toIc = params.get("toIcCode") != null ? params.get("toIcCode") : params.get("toIc"); - return troadService.searchRoute(fromIc, toIc); - } catch (IOException e) { - throw new RuntimeException("시터스 엔진 조회 실패: " + e.getMessage(), e); - } - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/service/TroadService.java b/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/service/TroadService.java deleted file mode 100644 index 3777a15..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/java/com/ex/chatbotapi/domain/troad/service/TroadService.java +++ /dev/null @@ -1,255 +0,0 @@ -package com.ex.chatbotapi.domain.troad.service; - -import com.google.gson.Gson; -import com.google.gson.internal.LinkedTreeMap; -import com.mongodb.BasicDBObject; -import com.mongodb.client.MongoCollection; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.bson.Document; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.dao.EmptyResultDataAccessException; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.stereotype.Service; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.util.*; - -@Slf4j -@Service -@RequiredArgsConstructor -public class TroadService { - - private final MongoTemplate mongoTemplate; - - @Qualifier("troadTemplate") - private final JdbcTemplate troadTemplate; - - private final List exceptionRoute = Arrays.asList("남해선", "서울문산선", "중부내륙선", "호남선", "수도권제2순환선"); - - /** - * 영업소(톨게이트) 간 경로 조회 - */ - public Map searchRoute(String fromIc, String toIc) throws IOException { - BasicDBObject condition = new BasicDBObject(); - condition.put("src", fromIc); - condition.put("dst", toIc); - Gson gson = new Gson(); - - MongoCollection icRoadInfo = mongoTemplate.getCollection("icRoadInfo"); - Document icMinGRS = icRoadInfo.find(condition).first(); - - Map response = new HashMap<>(); - if (icMinGRS != null) { - String srcGrs = icMinGRS.getString("srcGrs80"); - String srcName = icMinGRS.getString("srcName"); - String dstGrs = icMinGRS.getString("dstGrs80"); - String dstName = icMinGRS.getString("dstName"); - - log.info("시터스 엔진 조회"); - String result = extractRouteInfo(srcGrs, dstGrs); - Map resMap = gson.fromJson(result, Map.class); - - @SuppressWarnings("unchecked") - Map summary = (LinkedTreeMap) resMap.get("Summary"); - @SuppressWarnings("unchecked") - Map icdt = (LinkedTreeMap) resMap.get("Icdt"); - @SuppressWarnings("unchecked") - Map bunch = (LinkedTreeMap) resMap.get("Bunch"); - - response.put("status", true); - response.put("statusMsg", "정상응답"); - response.put("Summary", summary); - response.put("Icdt", icdt); - response.put("Bunch", bunch); - response.put("SrcName", srcName); - response.put("SrcGrs", srcGrs); - response.put("DstName", dstName); - response.put("DstGrs", dstGrs); - } else { - response.put("status", false); - response.put("statusMsg", "몽고db에 해당구간 존재안함"); - log.error("시터스 엔진 조회 에러(몽고db에 해당구간 존재안함) : {} - {}", fromIc, toIc); - } - return response; - } - - /** - * 노선별 혼잡구간 조회 - */ - public Map getCongestion(String routeName) { - String queryGetRouteInfo = "SELECT ROUTENM, ROUTECD, GRPID FROM TROAD_APP.V_ROUTE_NEW_NM WHERE ROUTENM = ? GROUP BY ROUTENM, ROUTECD, GRPID"; - String queryGetRouteQuickReplyInfo = "SELECT ROUTENM, ROUTECD, GRPID FROM TROAD_APP.V_ROUTE_NEW_NM WHERE ROUTENM LIKE ? GROUP BY ROUTENM, ROUTECD, GRPID"; - String queryGetRouteCongestionInfoS = "SELECT RPRS_ROTNM, ROUTE_DIR_NM, NODE_NM, TRAFFIC_GRADE, LINK_LEN_KM, SPEED FROM ( SELECT ROW_NUMBER() OVER( PARTITION BY A.DRIVE_DRCT_CD, A.ROUTE_CD, A.GRP_ID ORDER BY A.DRIVE_DRCT_CD, A.ROUTE_CD, A.SECT_SQNO ASC ) AS STNUMB , ROW_NUMBER() OVER( PARTITION BY A.DRIVE_DRCT_CD, A.ROUTE_CD, A.GRP_ID ORDER BY A.DRIVE_DRCT_CD, A.ROUTE_CD, A.SECT_SQNO DESC ) AS EDNUMB , ROW_NUMBER() OVER( PARTITION BY A.CONZONE_ID, A.DRIVE_DRCT_CD ORDER BY CASE WHEN A.ROUTE_CD='3010' THEN A.SECT_SQNO END DESC, CASE WHEN A.ROUTE_CD='1000' THEN A.SECT_SQNO END , CASE WHEN MOD(B.ROUTESHORTNO,2)=1 THEN A.SECT_SQNO END, CASE WHEN MOD(B.ROUTESHORTNO,2)=0 THEN A.SECT_SQNO END DESC ) AS DETAIL_NO , ROW_NUMBER() OVER( PARTITION BY A.CONZONE_ID, A.DRIVE_DRCT_CD ORDER BY CASE WHEN A.ROUTE_CD='3010' THEN A.SECT_SQNO END, CASE WHEN A.ROUTE_CD='1000' THEN A.SECT_SQNO END DESC, CASE WHEN MOD(B.ROUTESHORTNO,2)=1 THEN A.SECT_SQNO END DESC, CASE WHEN MOD(B.ROUTESHORTNO,2)=0 THEN A.SECT_SQNO END ) AS DETAIL_NO2 , A.ROUTESORT, A.SECT_SQNO, B.ROUTEDIRNM AS ROUTE_DIR_NM , B.ROUTENM AS RPRS_ROTNM, A.ROUTE_CD, A.GRP_ID, A.DRIVE_DRCT_CD, A.NODE_NM, A.CONZONE_ID, A.ROUTE_NO_LIST, A.LINK_LEN_KM, A.BUS_YN, A.SPEED, A.TRAFFIC_GRADE, A.TRAFFIC_GRADE_BUS, A.CCTV_ID, A.CCTV_AREA, A.HLS_URL, A.FILEURL_IMG, A.INCIDENT_TYPE_CD, A.INCIDENT_DESC, A.MSG, A.INCIDENT_POS_NAME, A.DETR_ID, A.SPEED_DETR, A.TRAFFIC_GRADE_DETR, A.RESTAREA_ID, A.RESTAREA_NM, A.CONZONE_X, A.CONZONE_Y, A.WORK_CD, A.VVALUE01, A.VVALUE02, A.VVALUE03, A.VVALUE04, A.VVALUE05, A.VVALUE06, A.VVALUE07, A.VVALUE08, A.VVALUE09, CASE WHEN A.ROUTE_CD='3010' THEN 0 WHEN A.ROUTE_CD='1000' THEN 1 ELSE MOD(B.ROUTESHORTNO,2) END AS BGUBUN FROM V_ROUTE_TRFC_VER001 A LEFT OUTER JOIN V_ROUTE_NEW_NM B ON B.ROUTECD = A.ROUTE_CD AND B.GRPID = A.GRP_ID AND B.UPDOWNDIR = A.DRIVE_DRCT_CD WHERE 1=1 AND A.GRP_ID = ? ) WHERE 1=1 AND ROUTE_CD = ? AND DRIVE_DRCT_CD = 'S' AND GRP_ID = ? AND TRAFFIC_GRADE IN ('R','L') AND DETAIL_NO = '1' ORDER BY CASE WHEN BGUBUN=1 THEN STNUMB END, CASE WHEN BGUBUN=0 THEN STNUMB END DESC "; - String queryGetRouteCongestionInfoE = "SELECT RPRS_ROTNM, ROUTE_DIR_NM, NODE_NM, TRAFFIC_GRADE, LINK_LEN_KM, SPEED FROM ( SELECT ROW_NUMBER() OVER( PARTITION BY A.DRIVE_DRCT_CD, A.ROUTE_CD, A.GRP_ID ORDER BY A.DRIVE_DRCT_CD, A.ROUTE_CD, A.SECT_SQNO ASC ) AS STNUMB , ROW_NUMBER() OVER( PARTITION BY A.DRIVE_DRCT_CD, A.ROUTE_CD, A.GRP_ID ORDER BY A.DRIVE_DRCT_CD, A.ROUTE_CD, A.SECT_SQNO DESC ) AS EDNUMB , ROW_NUMBER() OVER( PARTITION BY A.CONZONE_ID, A.DRIVE_DRCT_CD ORDER BY CASE WHEN A.ROUTE_CD='3010' THEN A.SECT_SQNO END, CASE WHEN A.ROUTE_CD='1000' THEN A.SECT_SQNO END DESC, CASE WHEN MOD(B.ROUTESHORTNO,2)=1 THEN A.SECT_SQNO END DESC, CASE WHEN MOD(B.ROUTESHORTNO,2)=0 THEN A.SECT_SQNO END ) AS DETAIL_NO , ROW_NUMBER() OVER( PARTITION BY A.CONZONE_ID, A.DRIVE_DRCT_CD ORDER BY CASE WHEN A.ROUTE_CD='3010' THEN A.SECT_SQNO END DESC, CASE WHEN A.ROUTE_CD='1000' THEN A.SECT_SQNO END , CASE WHEN MOD(B.ROUTESHORTNO,2)=1 THEN A.SECT_SQNO END, CASE WHEN MOD(B.ROUTESHORTNO,2)=0 THEN A.SECT_SQNO END DESC ) AS DETAIL_NO2 , A.ROUTESORT, A.SECT_SQNO, B.ROUTEDIRNM AS ROUTE_DIR_NM , B.ROUTENM AS RPRS_ROTNM, A.ROUTE_CD, A.GRP_ID, A.DRIVE_DRCT_CD, A.NODE_NM, A.CONZONE_ID, A.ROUTE_NO_LIST, A.LINK_LEN_KM, A.BUS_YN, A.SPEED, A.TRAFFIC_GRADE, A.TRAFFIC_GRADE_BUS, A.CCTV_ID, A.CCTV_AREA, A.HLS_URL, A.FILEURL_IMG, A.INCIDENT_TYPE_CD, A.INCIDENT_DESC, A.MSG, A.INCIDENT_POS_NAME, A.DETR_ID, A.SPEED_DETR, A.TRAFFIC_GRADE_DETR, A.RESTAREA_ID, A.RESTAREA_NM, A.CONZONE_X, A.CONZONE_Y, A.WORK_CD, A.VVALUE01, A.VVALUE02, A.VVALUE03, A.VVALUE04, A.VVALUE05, A.VVALUE06, A.VVALUE07, A.VVALUE08, A.VVALUE09, CASE WHEN A.ROUTE_CD='3010' THEN 0 WHEN A.ROUTE_CD='1000' THEN 1 ELSE MOD(B.ROUTESHORTNO,2) END AS BGUBUN FROM V_ROUTE_TRFC_VER001 A LEFT OUTER JOIN V_ROUTE_NEW_NM B ON B.ROUTECD = A.ROUTE_CD AND B.GRPID = A.GRP_ID AND B.UPDOWNDIR = A.DRIVE_DRCT_CD WHERE 1=1 AND A.GRP_ID = ? ) WHERE 1=1 AND ROUTE_CD = ? AND DRIVE_DRCT_CD = 'E' AND GRP_ID = ? AND TRAFFIC_GRADE IN ('R','L') AND DETAIL_NO = '1' ORDER BY CASE WHEN BGUBUN=1 THEN STNUMB END DESC, CASE WHEN BGUBUN=0 THEN STNUMB END "; - - Map response = new HashMap<>(); - Map routeInfo; - - try { - routeInfo = troadTemplate.queryForMap(queryGetRouteInfo, routeName); - } catch (EmptyResultDataAccessException e) { - routeInfo = Collections.emptyMap(); - } - - String quickReplyRouteName = exceptionRoute.contains(routeName) - ? routeName.substring(0, routeName.length() - 1) - : routeName; - log.info(quickReplyRouteName); - - List> routeQuickReplyInfo; - if (routeName.equals("수도권순환선")) { - routeQuickReplyInfo = new ArrayList<>(); - Map firstMap = new HashMap<>(); - firstMap.put("ROUTENM", "수도권제1순환선"); - firstMap.put("ROUTECD", ""); - firstMap.put("GRPID", ""); - Map secondMap = new HashMap<>(); - secondMap.put("ROUTENM", "수도권제2순환선"); - secondMap.put("ROUTECD", ""); - secondMap.put("GRPID", ""); - routeQuickReplyInfo.add(firstMap); - routeQuickReplyInfo.add(secondMap); - } else { - routeQuickReplyInfo = troadTemplate.queryForList(queryGetRouteQuickReplyInfo, - "%" + quickReplyRouteName + "%"); - } - response.put("RouteQuickReplyInfo", routeQuickReplyInfo); - - if (!routeInfo.isEmpty()) { - String routeNm = (String) routeInfo.get("ROUTENM"); - String routeCd = (String) routeInfo.get("ROUTECD"); - String grpId = (String) routeInfo.get("GRPID"); - - log.info("{} 혼잡정보 조회", routeNm); - log.info("노선코드 : {}, 그룹 식별자 : {}", routeCd, grpId); - - List> routeInfoS = troadTemplate.queryForList(queryGetRouteCongestionInfoS, grpId, - routeCd, grpId); - List> routeInfoE = troadTemplate.queryForList(queryGetRouteCongestionInfoE, grpId, - routeCd, grpId); - - response.put("status", true); - response.put("statusMsg", "정상응답"); - response.put("RouteInfo", routeInfo); - response.put("RouteInfoS", routeInfoS); - response.put("RouteInfoE", routeInfoE); - } else { - response.put("status", false); - response.put("statusMsg", "유효하지 않은 노선입니다."); - log.error("유효하지 않은 노선입니다. - {}", routeName); - } - return response; - } - - /** - * 노선별 교통속보 조회 - */ - public Map getBreakingNews(String routeName) { - String queryGetRouteInfo = "SELECT ROUTENM, ROUTECD, GRPID FROM TROAD_APP.V_ROUTE_NEW_NM WHERE ROUTENM = ? GROUP BY ROUTENM, ROUTECD, GRPID"; - String queryGetRouteQuickReplyInfo = "SELECT ROUTENM, ROUTECD, GRPID FROM TROAD_APP.V_ROUTE_NEW_NM WHERE ROUTENM LIKE ? GROUP BY ROUTENM, ROUTECD, GRPID"; - String queryGetBreakingNewsS = "SELECT ROUTECD, GRPID, BOUND AS BND, Y.CMMN_CD_NM AS ROADNM, CASE WHEN BOUND = 'S' THEN SUBSTR(Y.DETL_CTNT, 1, INSTR(Y.DETL_CTNT, '$')-1) WHEN BOUND = 'E' THEN SUBSTR(Y.DETL_CTNT, INSTR(Y.DETL_CTNT, '$')+1) END BOUNDNM, ROUTESORT, ROADSHORTNO, INCIDENT_ID, '[' || TO_CHAR(TO_DATE(INCIDENT_ID, 'YYYYMMDDHH24MISS'), 'YYYY-MM-DD HH24:MI') || ']' AS INCIDENT_DATE, INCI_TP_ORDER, INCI_TP, MSG, INCIDENT_POS_NAME, OCRN_DSTNC, CNGS_LNGT, LAT, LON, CCTV_ID, CCTV_NM, THUMBIMAGE, HLS_URL, ST_D, EN_D, RANK() OVER( PARTITION BY ROUTESORT, Y.CMMN_CD_NM, BOUND ORDER BY ROUTESORT ASC, DECODE(INCI_TP_ORDER, '00','1', '04', '2', '03', '3', '4') ASC, ST_D DESC, INCIDENT_ID DESC ) AS STNUMB , RANK() OVER( PARTITION BY ROUTESORT, Y.CMMN_CD_NM, BOUND ORDER BY ROUTESORT ASC, DECODE(INCI_TP_ORDER, '00','1', '04', '2', '03', '3', '4') DESC, ST_D ASC, INCIDENT_ID ASC ) AS EDNUMB , ' ' AS VVALUE01, ' ' AS VVALUE02, ' ' AS VVALUE03, ' ' AS VVALUE04, ' ' AS VVALUE05 FROM V_ACDT_BRNWS_VER001 X LEFT OUTER JOIN TLISDEV.T_TRMB_CD_INFO01M1 Y ON ( X.ROUTECD || X.GRPID = Y.CMMN_CD AND Y.HGRN_CD_ID = 'COM064' AND Y.USE_XN = 'Y' ) WHERE 1=1 AND ROUTECD = ? AND ROADNM LIKE ? AND BOUND = 'S' AND INCI_TP_ORDER IN ('00','04','03','01')"; - String queryGetBreakingNewsE = "SELECT ROUTECD, GRPID, BOUND AS BND, Y.CMMN_CD_NM AS ROADNM, CASE WHEN BOUND = 'S' THEN SUBSTR(Y.DETL_CTNT, 1, INSTR(Y.DETL_CTNT, '$')-1) WHEN BOUND = 'E' THEN SUBSTR(Y.DETL_CTNT, INSTR(Y.DETL_CTNT, '$')+1) END BOUNDNM, ROUTESORT, ROADSHORTNO, INCIDENT_ID, '[' || TO_CHAR(TO_DATE(INCIDENT_ID, 'YYYYMMDDHH24MISS'), 'YYYY-MM-DD HH24:MI') || ']' AS INCIDENT_DATE, INCI_TP_ORDER, INCI_TP, MSG, INCIDENT_POS_NAME, OCRN_DSTNC, CNGS_LNGT, LAT, LON, CCTV_ID, CCTV_NM, THUMBIMAGE, HLS_URL, ST_D, EN_D, RANK() OVER( PARTITION BY ROUTESORT, Y.CMMN_CD_NM, BOUND ORDER BY ROUTESORT ASC, DECODE(INCI_TP_ORDER, '00','1', '04', '2', '03', '3', '4') ASC, ST_D DESC, INCIDENT_ID DESC ) AS STNUMB , RANK() OVER( PARTITION BY ROUTESORT, Y.CMMN_CD_NM, BOUND ORDER BY ROUTESORT ASC, DECODE(INCI_TP_ORDER, '00','1', '04', '2', '03', '3', '4') DESC, ST_D ASC, INCIDENT_ID ASC ) AS EDNUMB , ' ' AS VVALUE01, ' ' AS VVALUE02, ' ' AS VVALUE03, ' ' AS VVALUE04, ' ' AS VVALUE05 FROM V_ACDT_BRNWS_VER001 X LEFT OUTER JOIN TLISDEV.T_TRMB_CD_INFO01M1 Y ON ( X.ROUTECD || X.GRPID = Y.CMMN_CD AND Y.HGRN_CD_ID = 'COM064' AND Y.USE_XN = 'Y' ) WHERE 1=1 AND ROUTECD = ? AND ROADNM LIKE ? AND BOUND = 'E' AND INCI_TP_ORDER IN ('00','04','03','01')"; - - Map response = new HashMap<>(); - Map routeInfo; - - try { - routeInfo = troadTemplate.queryForMap(queryGetRouteInfo, routeName); - } catch (EmptyResultDataAccessException e) { - routeInfo = Collections.emptyMap(); - } - - String quickReplyRouteName = exceptionRoute.contains(routeName) - ? routeName.substring(0, routeName.length() - 1) - : routeName; - log.info(quickReplyRouteName); - - List> routeQuickReplyInfo; - if (routeName.equals("수도권순환선")) { - routeQuickReplyInfo = new ArrayList<>(); - Map firstMap = new HashMap<>(); - firstMap.put("ROUTENM", "수도권제1순환선"); - firstMap.put("ROUTECD", ""); - firstMap.put("GRPID", ""); - Map secondMap = new HashMap<>(); - secondMap.put("ROUTENM", "수도권제2순환선"); - secondMap.put("ROUTECD", ""); - secondMap.put("GRPID", ""); - routeQuickReplyInfo.add(firstMap); - routeQuickReplyInfo.add(secondMap); - } else { - routeQuickReplyInfo = troadTemplate.queryForList(queryGetRouteQuickReplyInfo, - "%" + quickReplyRouteName + "%"); - } - response.put("RouteQuickReplyInfo", routeQuickReplyInfo); - - if (!routeInfo.isEmpty()) { - String routeNm = (String) routeInfo.get("ROUTENM"); - String routeCd = (String) routeInfo.get("ROUTECD"); - String grpId = (String) routeInfo.get("GRPID"); - - log.info("{} 교통속보 조회", routeNm); - log.info("노선코드 : {}, 그룹 식별자 : {}", routeCd, grpId); - - List> routeBreakingNewsS = troadTemplate.queryForList(queryGetBreakingNewsS, routeCd, - "%" + routeNm + "%"); - List> routeBreakingNewsE = troadTemplate.queryForList(queryGetBreakingNewsE, routeCd, - "%" + routeNm + "%"); - - response.put("status", true); - response.put("statusMsg", "정상응답"); - response.put("RouteInfo", routeInfo); - response.put("RouteBreakingNewsS", routeBreakingNewsS); - response.put("RouteBreakingNewsE", routeBreakingNewsE); - } else { - response.put("status", false); - response.put("statusMsg", "유효하지 않은 노선입니다."); - log.error("유효하지 않은 노선입니다. - {}", routeName); - } - return response; - } - - /** - * 시터스 엔진 소켓 통신 - */ - private String extractRouteInfo(String srcGrs, String dstGrs) throws IOException { - String query = "CoordType=WGS84GEO,WGS84GEO&StartPoint=" + srcGrs - + "&EndPoint=" + dstGrs + "&MidPoint=" + "" - + "&Result=" + "" + "&SearchOption=12,0,1\n"; - - InetSocketAddress ipep = new InetSocketAddress("172.16.162.17", 8088); - Socket client = new Socket(); - client.connect(ipep); - - StringBuffer sb = new StringBuffer(); - try (OutputStream sender = client.getOutputStream(); - InputStream receiver = client.getInputStream()) { - - byte[] data = query.getBytes("UTF-8"); - sender.write(data, 0, data.length); - - data = new byte[1024000]; - int i; - while ((i = receiver.read(data)) != -1) { - sb.append(new String(data, 0, i, "UTF-8")); - if (sb.toString().endsWith("\n.\n\n")) - break; - } - } catch (Throwable e) { - e.printStackTrace(); - } - - String result = sb.toString(); - if (result.endsWith("\n.\n\n")) { - result = result.replace("\n.\n\n", ""); - } - client.close(); - return result; - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/resources/application.yml b/chatbotApi-chatbot2.0-agent/src/main/resources/application.yml deleted file mode 100644 index 6c73b6e..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/resources/application.yml +++ /dev/null @@ -1,72 +0,0 @@ -# ============================================================================= -# chatbotApi (WAS) — 172.16.180.130 -# 동일 서버: exAiChatBot :28012, chatbotAdmin :8087, Qdrant, MongoDB -# kakaoChatbotSkill: 별도 웹 서버 (SKILL_INTERNAL_BASE_URL 로 지정) -# LLM 게이트웨이: 172.16.163.96 -# ============================================================================= - -server: - port: 8086 - http2: - enabled: true - -spring: - datasource: - bpstl: - driver-class-name: oracle.jdbc.OracleDriver - url: jdbc:oracle:thin:@localhost:1521/orcl - username: OPR_MAIN - password: test -# url: jdbc:oracle:thin:@172.16.181.22:1681/BPSTL -# username: stl_app -# password: "wjdtksdpdlvlvl!20" - troad: - driver-class-name: oracle.jdbc.OracleDriver - url: jdbc:oracle:thin:@localhost:1521/orcl2 - username: OPR_MAIN - password: test -# url: jdbc:oracle:thin:@172.16.163.76:1522/TROAD -# username: TROAD_APP -# password: troad_app!2017 - restarea: - driver-class-name: oracle.jdbc.OracleDriver - url: jdbc:oracle:thin:@localhost:1521/orcl - username: OPR_MAIN - password: test - data: - mongodb: - uri: ${MONGODB_URI:mongodb://exDev:ehfhrhdtk!!@127.0.0.1:27017/chatbot?authSource=chatbot&heartbeatFrequencyMS=30000&minPoolSize=3&maxIdleTimeMS=1800000} -# uri: mongodb://exDev:ehfhrhdtk!!@exfareqc.ex.co.kr:27017/chatbot?heartbeatFrequencyMS=30000&minPoolSize=3&maxIdleTimeMS=1800000 - -restTemplate: - factory: - readTimeout: 5000 - connectTimeout: 3000 - httpClient: - maxConnTotal: 100 - maxConnPerRoute: 5 - -# WAS → RAG (legacy /llm-agent, /intent/analyze) — RAG는 동일 서버 localhost -rag: - server: - host: ${RAG_SERVER_HOST:127.0.0.1} - port: ${RAG_SERVER_PORT:28012} - ask-endpoint: /ask - health-endpoint: /health - intent-analyze-endpoint: /intent/analyze - connect-timeout: 5000 - read-timeout: 30000 - -# Agent tool API (모드 분기는 Skill에서만 — WAS는 tool executor) -agent: - tool: - internal-api-key: ${INTERNAL_TOOL_API_KEY:} - execute-timeout-ms: ${TOOL_EXECUTE_TIMEOUT_MS:30000} - -# WAS → kakaoChatbotSkill (WEB tool internal) — 웹 서버 IP/호스트명 설정 -kakao-skill: - base-url: ${SKILL_INTERNAL_BASE_URL:http://128.200.100.61:8083} - data-portal-tool-endpoint: /internal/tools/data-portal - internal-api-key: ${INTERNAL_TOOL_API_KEY:} - connect-timeout: 5000 - read-timeout: 30000 diff --git a/chatbotApi-chatbot2.0-agent/src/main/resources/icTel.json b/chatbotApi-chatbot2.0-agent/src/main/resources/icTel.json deleted file mode 100644 index 3aa815b..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/resources/icTel.json +++ /dev/null @@ -1,2240 +0,0 @@ -{ - "중랑": { - "tel": "070-4220-6490", - "code": "004" - }, - "화성비봉": { - "tel": "1877-1504", - "code": "005" - }, - "서별내": { - "tel": "031-515-6091", - "code": "006" - }, - "동별내": { - "tel": "031-515-6092", - "code": "007" - }, - "인천": { - "tel": "032-718-2190", - "code": "011", - "amt": [900, 900, 900, 1000, 1000, 450], - "out": ["서시흥", "군자", "서서울", "서울", "동서울"] - }, - "인천계양": { - "tel": "032-545-0127", - "code": "011" - }, - "순천만": { - "tel": "061-882-2626", - "code": "013", - "amt": [900, 900, 900, 1000, 1100, 450], - "out": ["남순천", "동순천"] - }, - "소태": { - "tel": "062-220-8880", - "code": "016" - }, - "송암": { - "tel": "062-670-7100", - "code": "017" - }, - "유덕": { - "tel": "062-370-0517", - "code": "018" - }, - "금천": { - "tel": "070-4435-0641", - "code": "019" - }, - "고모": { - "tel": "053-790-1600", - "code": "023" - }, - "삼덕": { - "tel": "053-791-6633", - "code": "024" - }, - "동부산": { - "tel": "051-722-7515", - "code": "025" - }, - "호원": { - "tel": "031-894-7960", - "code": "026" - }, - "예전": { - "tel": "052-710-3066", - "code": "027" - }, - "염포산": { - "tel": "052-710-3066", - "code": "028" - }, - "용마터널": { - "tel": "031-566-5300", - "code": "030" - }, - "의왕": { - "tel": "031-458-3091", - "code": "031" - }, - "일산": { - "tel": "없음", - "code": "032" - }, - "고양": { - "tel": "031-994-6300", - "code": "033" - }, - "통일로": { - "tel": "031-994-6400", - "code": "034" - }, - "송추": { - "tel": "031-894-6200", - "code": "036" - }, - "별내": { - "tel": "031-522-6400", - "code": "037" - }, - "불암산": { - "tel": "031-522-6300", - "code": "038" - }, - "안양과천": { - "tel": "031-756-6982", - "code": "040" - }, - "북판교": { - "tel": "031-756-6982", - "code": "041" - }, - "북청계": { - "tel": "031-756-6982", - "code": "042" - }, - "북의왕": { - "tel": "031-756-6982", - "code": "043" - }, - "일산대교": { - "tel": "031-988-6585", - "code": "044" - }, - "한밭": { - "tel": "042-670-2700", - "code": "047" - }, - "대화": { - "tel": "042-670-2700", - "code": "048" - }, - "앞산": { - "tel": "1661-5388", - "code": "049" - }, - "앞산터널": { - "tel": "1661-5388", - "code": "049" - }, - "파동": { - "tel": "1661-5388", - "code": "050" - }, - "파동고가교": { - "tel": "1661-5388", - "code": "050" - }, - "남인천": { - "tel": "032-718-2191", - "code": "053", - "amt": [900, 900, 900, 1000, 1100, 450], - "out": ["서시흥", "군자", "서서울", "서울", "동서울"] - }, - "팔룡터널": { - "tel": "055-251-8599", - "code": "055" - }, - "일로": { - "tel": "061-882-2617", - "code": "056", - "amt": [800, 800, 800, 800, 800, 400], - "out": ["목포", "서영암"] - }, - "다사": { - "tel": "053-717-2784", - "code": "057", - "amt": [1100, 1100, 1100, 1100, 1100, 550], - "out": ["북달성"] - }, - "선암": { - "tel": "070-4437-1715", - "code": "060" - }, - "성남": { - "tel": "031-327-2136", - "code": "061", - "amt": [1000, 1000, 1000, 1100, 1200, 500], - "out": ["서시흥", "군자", "서서울", "서울", "동서울"] - }, - "구리남양주": { - "tel": "031-327-2137", - "code": "062", - "amt": [900, 900, 900, 1000, 1100, 450], - "out": ["서시흥", "군자", "서서울", "서울", "동서울"] - }, - "토평": { - "tel": "031-327-2138", - "code": "063", - "amt": [800, 800, 800, 800, 800, 400], - "out": ["서시흥", "군자", "서서울", "서울", "동서울"] - }, - "청계": { - "tel": "031-327-2139", - "code": "064", - "amt": [1000, 1000, 1000, 1100, 1200, 500], - "out": ["서시흥", "군자", "서서울", "서울", "동서울"] - }, - "판교": { - "tel": "031-327-2129", - "code": "065", - "amt": [1000, 1000, 1000, 1100, 1200, 500], - "out": ["서시흥", "군자", "서서울", "서울", "동서울"] - }, - "하남": { - "tel": "031-327-2131", - "code": "066", - "amt": [800, 900, 900, 900, 900, 400], - "out": ["서시흥", "군자", "서서울", "서울", "동서울"] - }, - "시흥": { - "tel": "031-327-2104", - "code": "067", - "amt": [900, 900, 900, 900, 1000, 450], - "out": ["서시흥", "군자", "서서울", "서울", "동서울"] - }, - "김포": { - "tel": "031-327-2101", - "code": "068", - "amt": [900, 900, 900, 1000, 1100, 450], - "out": ["서시흥", "군자", "서서울", "서울", "동서울"] - }, - "고촌": { - "tel": "031-985-0456", - "code": "068" - }, - "대왕판교": { - "tel": "031-327-2130", - "code": "069", - "amt": [1000, 1000, 1000, 1100, 1200, 500], - "out": ["서울"] - }, - "미시령터널": { - "tel": "033-635-7338", - "code": "070" - }, - "이패": { - "tel": "031-511-7676", - "code": "071" - }, - "덕소삼패": { - "tel": "031-577-2733", - "code": "074" - }, - "명지": { - "tel": "051-271-8585", - "code": "075" - }, - "을숙도대교": { - "tel": "051-271-8585", - "code": "075" - }, - "가덕": { - "tel": "051-714-6873", - "code": "076" - }, - "거제": { - "tel": "055-736-6891", - "code": "077" - }, - "창원": { - "tel": "055-213-7200", - "code": "078" - }, - "녹산": { - "tel": "055-213-7266", - "code": "079" - }, - "수정산터널": { - "tel": "051-893-5011", - "code": "080" - }, - "금토분기점": { - "tel": "1599-2505", - "code": "082" - }, - "인천대교": { - "tel": "032-745-8200", - "code": "083" - }, - "서수지": { - "tel": "1599-2505", - "code": "084" - }, - "금토": { - "tel": "1599-2505", - "code": "085" - }, - "물왕": { - "tel": "031-8084-8940", - "code": "086" - }, - "연성": { - "tel": "031-313-3013", - "code": "087" - }, - "고잔": { - "tel": "032-422-3015", - "code": "088" - }, - "부산항대교": { - "tel": "1544-3888", - "code": "090" - }, - "내서": { - "tel": "055-711-2831", - "code": "092", - "amt": [1000, 1000, 1000, 1100, 1100, 500], - "out": ["산인", "칠원", "마산"] - }, - "개금": { - "tel": "051-790-7052", - "code": "093" - }, - "광안대로": { - "tel": "051-780-0042", - "code": "094" - }, - "백양터널": { - "tel": "051-309-2600", - "code": "095" - }, - "마창대교": { - "tel": "055-710-2000", - "code": "096" - }, - "인천공항": { - "tel": "032-560-6200", - "code": "097" - }, - "북인천": { - "tel": "032-560-6300", - "code": "098" - }, - "청라": { - "tel": "032-560-6333", - "code": "099" - }, - "서울": { - "tel": "031-327-2124", - "code": "101" - }, - "동수원": { - "tel": "031-327-2108", - "code": "102" - }, - "수원신갈": { - "tel": "031-327-2125", - "code": "103" - }, - "지곡": { - "tel": "055-711-2846", - "code": "104" - }, - "기흥": { - "tel": "031-327-2126", - "code": "105" - }, - "오산": { - "tel": "031-327-2128", - "code": "106" - }, - "안성": { - "tel": "031-327-2151", - "code": "107" - }, - "천안": { - "tel": "041-411-2426", - "code": "108" - }, - "계룡": { - "tel": "041-411-2400", - "code": "109" - }, - "두계": { - "tel": "042-841-2504", - "code": "109" - }, - "목천": { - "tel": "041-411-2427", - "code": "110" - }, - "청주": { - "tel": "043-756-2323", - "code": "111" - }, - "남청주": { - "tel": "043-756-2324", - "code": "112" - }, - "신탄진": { - "tel": "042-714-2499", - "code": "113" - }, - "남대전": { - "tel": "042-714-2492", - "code": "114" - }, - "대전": { - "tel": "042-714-2498", - "code": "115" - }, - "옥천": { - "tel": "043-756-2325", - "code": "116" - }, - "금강": { - "tel": "043-756-2326", - "code": "117" - }, - "영동": { - "tel": "043-756-2327", - "code": "118" - }, - "판암": { - "tel": "042-714-2493", - "code": "119" - }, - "황간": { - "tel": "043-756-2328", - "code": "120" - }, - "추풍령": { - "tel": "054-706-2714", - "code": "121" - }, - "김천": { - "tel": "054-706-2711", - "code": "122" - }, - "구미": { - "tel": "054-706-2708", - "code": "123" - }, - "상주": { - "tel": "054-706-2719", - "code": "124" - }, - "남구미": { - "tel": "054-706-2707", - "code": "125" - }, - "왜관": { - "tel": "054-706-2741", - "code": "126" - }, - "서대구": { - "tel": "053-717-2797", - "code": "127" - }, - "금왕꽃동네": { - "tel": "043-756-2308", - "code": "128" - }, - "북대구": { - "tel": "053-717-2796", - "code": "129" - }, - "동김천": { - "tel": "054-706-2712", - "code": "130" - }, - "경산": { - "tel": "054-706-2700", - "code": "131" - }, - "영천": { - "tel": "054-706-2731", - "code": "132" - }, - "건천": { - "tel": "054-706-2701", - "code": "133" - }, - "선산": { - "tel": "054-706-2709", - "code": "134" - }, - "경주": { - "tel": "054-706-2702", - "code": "135" - }, - "음성": { - "tel": "043-756-2306", - "code": "136" - }, - "통도사": { - "tel": "052-701-2893", - "code": "137" - }, - "양산": { - "tel": "055-711-2822", - "code": "138" - }, - "서울산": { - "tel": "052-701-2858", - "code": "139" - }, - "부산": { - "tel": "051-793-2882", - "code": "140" - }, - "군북": { - "tel": "055-711-2841", - "code": "142" - }, - "장지": { - "tel": "055-711-2840", - "code": "143" - }, - "함안": { - "tel": "055-711-2839", - "code": "144" - }, - "산인": { - "tel": "055-711-2842", - "code": "145" - }, - "동창원": { - "tel": "055-711-2834", - "code": "146" - }, - "진례": { - "tel": "055-711-2812", - "code": "147" - }, - "서김해": { - "tel": "055-711-2806", - "code": "148" - }, - "동김해": { - "tel": "055-711-2808", - "code": "149" - }, - "북부산": { - "tel": "051-793-2881", - "code": "150" - }, - "북대전": { - "tel": "042-714-2497", - "code": "151" - }, - "유성": { - "tel": "042-714-2496", - "code": "152" - }, - "서대전": { - "tel": "042-714-2495", - "code": "153" - }, - "안영": { - "tel": "042-714-2494", - "code": "154" - }, - "논산": { - "tel": "041-411-2407", - "code": "155" - }, - "익산": { - "tel": "063-712-2518", - "code": "156" - }, - "삼례": { - "tel": "063-712-2523", - "code": "157" - }, - "전주": { - "tel": "063-712-2527", - "code": "158" - }, - "서전주": { - "tel": "063-712-2522", - "code": "159" - }, - "김제": { - "tel": "063-712-2505", - "code": "160" - }, - "금산사": { - "tel": "063-712-2506", - "code": "161" - }, - "태인": { - "tel": "063-712-2531", - "code": "162" - }, - "정읍": { - "tel": "063-712-2529", - "code": "163" - }, - "무주": { - "tel": "063-712-2513", - "code": "164" - }, - "백양사": { - "tel": "061-882-2632", - "code": "165" - }, - "장성": { - "tel": "061-882-2634", - "code": "166" - }, - "광주": { - "tel": "061-882-2631", - "code": "167" - }, - "덕유산": { - "tel": "063-712-2514", - "code": "168" - }, - "장수": { - "tel": "063-712-2526", - "code": "169" - }, - "서상": { - "tel": "055-711-2845", - "code": "170" - }, - "마성": { - "tel": "031-327-2113", - "code": "171" - }, - "용인": { - "tel": "031-327-2140", - "code": "172" - }, - "양지": { - "tel": "031-327-2141", - "code": "173" - }, - "춘천": { - "tel": "033-812-2212", - "code": "174" - }, - "덕평": { - "tel": "031-327-2142", - "code": "175" - }, - "이천": { - "tel": "031-327-2143", - "code": "176" - }, - "여주": { - "tel": "031-327-2148", - "code": "177" - }, - "문막": { - "tel": "033-812-2200", - "code": "178" - }, - "홍천": { - "tel": "033-812-2211", - "code": "179" - }, - "원주": { - "tel": "033-812-2201", - "code": "180" - }, - "새말": { - "tel": "033-812-2202", - "code": "181" - }, - "둔내": { - "tel": "033-812-2203", - "code": "182" - }, - "면온": { - "tel": "033-812-2204", - "code": "183" - }, - "대관령": { - "tel": "033-812-2208", - "code": "184" - }, - "평창": { - "tel": "033-812-2205", - "code": "185" - }, - "속사": { - "tel": "033-812-2206", - "code": "186" - }, - "진부": { - "tel": "033-812-2207", - "code": "187" - }, - "서충주": { - "tel": "043-756-2307", - "code": "188" - }, - "강릉": { - "tel": "033-812-2216", - "code": "189" - }, - "동서울": { - "tel": "031-327-2132", - "code": "190" - }, - "경기광주": { - "tel": "031-327-2133", - "code": "191" - }, - "곤지암": { - "tel": "031-327-2134", - "code": "192" - }, - "일죽": { - "tel": "031-327-2150", - "code": "193" - }, - "서이천": { - "tel": "031-327-2135", - "code": "194" - }, - "대소": { - "tel": "043-756-2314", - "code": "195" - }, - "진천": { - "tel": "043-756-2315", - "code": "196" - }, - "증평": { - "tel": "043-756-2316", - "code": "197" - }, - "서청주": { - "tel": "043-756-2318", - "code": "198" - }, - "오창": { - "tel": "043-756-2317", - "code": "199" - }, - "금산": { - "tel": "041-411-2405", - "code": "200" - }, - "칠곡": { - "tel": "053-717-2795", - "code": "201" - }, - "다부": { - "tel": "054-706-2740", - "code": "202" - }, - "가산": { - "tel": "054-706-2739", - "code": "203" - }, - "추부": { - "tel": "041-411-2406", - "code": "204" - }, - "군위": { - "tel": "054-706-2710", - "code": "205" - }, - "의성": { - "tel": "054-706-2736", - "code": "206" - }, - "남안동": { - "tel": "054-706-2725", - "code": "207" - }, - "서안동": { - "tel": "054-706-2726", - "code": "208" - }, - "풍산": { - "tel": "054-853-9925", - "code": "208" - }, - "예천": { - "tel": "054-706-2733", - "code": "209" - }, - "영주": { - "tel": "054-706-2729", - "code": "210" - }, - "풍기": { - "tel": "054-706-2728", - "code": "211" - }, - "북수원": { - "tel": "031-327-2109", - "code": "212" - }, - "부곡": { - "tel": "031-327-2110", - "code": "213" - }, - "동군포": { - "tel": "031-327-2111", - "code": "214" - }, - "군포": { - "tel": "031-327-2112", - "code": "215" - }, - "횡성": { - "tel": "033-812-2210", - "code": "216" - }, - "북원주": { - "tel": "033-812-2209", - "code": "217" - }, - "남원주": { - "tel": "033-812-2230", - "code": "218" - }, - "신림": { - "tel": "033-812-2231", - "code": "219" - }, - "생초": { - "tel": "055-711-2819", - "code": "221" - }, - "산청": { - "tel": "055-711-2817", - "code": "222" - }, - "단성": { - "tel": "055-711-2818", - "code": "223" - }, - "함양": { - "tel": "055-711-2847", - "code": "224" - }, - "서진주": { - "tel": "055-711-2825", - "code": "225" - }, - "군산": { - "tel": "063-712-2504", - "code": "227" - }, - "동군산": { - "tel": "063-712-2503", - "code": "228" - }, - "북천안": { - "tel": "041-411-2428", - "code": "229" - }, - "팔공산": { - "tel": "053-717-2794", - "code": "230" - }, - "화원옥포": { - "tel": "053-717-2796", - "code": "231" - }, - "화원": { - "tel": "053-614-8800", - "code": "231" - }, - "달성": { - "tel": "053-717-2791", - "code": "232" - }, - "현풍": { - "tel": "053-717-2792", - "code": "233" - }, - "서천": { - "tel": "041-411-2421", - "code": "234" - }, - "창녕": { - "tel": "055-711-2830", - "code": "235" - }, - "영산": { - "tel": "055-711-2829", - "code": "236" - }, - "남지": { - "tel": "055-711-2828", - "code": "237" - }, - "칠서": { - "tel": "055-711-2843", - "code": "238" - }, - "칠원": { - "tel": "055-711-2844", - "code": "239" - }, - "제천": { - "tel": "043-756-2310", - "code": "240" - }, - "동고령": { - "tel": "054-706-2705", - "code": "241" - }, - "성산": { - "tel": "054-954-0529", - "code": "241" - }, - "고령": { - "tel": "054-706-2706", - "code": "242" - }, - "해인사": { - "tel": "055-711-2848", - "code": "243" - }, - "서부산": { - "tel": "055-711-2807", - "code": "244" - }, - "장유": { - "tel": "055-711-2811", - "code": "245" - }, - "가락": { - "tel": "051-793-2880", - "code": "246" - }, - "남원": { - "tel": "063-712-2512", - "code": "247" - }, - "순창": { - "tel": "063-712-2517", - "code": "248" - }, - "담양": { - "tel": "061-882-2614", - "code": "249" - }, - "남양산": { - "tel": "055-711-2820", - "code": "250" - }, - "물금": { - "tel": "055-711-2821", - "code": "251" - }, - "대동": { - "tel": "055-711-2809", - "code": "252" - }, - "서서울": { - "tel": "031-327-2103", - "code": "253" - }, - "군자": { - "tel": "031-327-2105", - "code": "254" - }, - "서안산": { - "tel": "031-327-2106", - "code": "255" - }, - "안산": { - "tel": "031-327-2107", - "code": "256" - }, - "홍성": { - "tel": "041-411-2430", - "code": "257" - }, - "광천": { - "tel": "041-411-2412", - "code": "258" - }, - "대천": { - "tel": "041-411-2413", - "code": "259" - }, - "춘장대": { - "tel": "041-411-2420", - "code": "260" - }, - "동광주": { - "tel": "062-719-2692", - "code": "261" - }, - "창평": { - "tel": "061-882-2615", - "code": "262" - }, - "옥과": { - "tel": "061-882-2604", - "code": "263" - }, - "동남원": { - "tel": "063-712-2509", - "code": "264" - }, - "곡성": { - "tel": "061-882-2602", - "code": "265" - }, - "석곡": { - "tel": "061-882-2603", - "code": "266" - }, - "주암송광사": { - "tel": "061-882-2627", - "code": "267" - }, - "송광사": { - "tel": "061-882-2627", - "code": "267" - }, - "승주": { - "tel": "061-882-2625", - "code": "268" - }, - "지리산": { - "tel": "063-712-2511", - "code": "269" - }, - "서순천": { - "tel": "061-882-2623", - "code": "270" - }, - "순천": { - "tel": "061-882-2624", - "code": "271" - }, - "광양": { - "tel": "061-882-2606", - "code": "272" - }, - "동광양": { - "tel": "061-882-2608", - "code": "273" - }, - "가조": { - "tel": "055-711-2801", - "code": "274" - }, - "옥곡": { - "tel": "061-882-2607", - "code": "275" - }, - "진월": { - "tel": "061-882-2609", - "code": "276" - }, - "하동": { - "tel": "055-711-2837", - "code": "277" - }, - "진교": { - "tel": "055-711-2838", - "code": "278" - }, - "곤양": { - "tel": "055-711-2814", - "code": "279" - }, - "거창": { - "tel": "055-711-2802", - "code": "280" - }, - "매송": { - "tel": "031-327-2114", - "code": "281" - }, - "비봉": { - "tel": "031-327-2115", - "code": "282" - }, - "발안": { - "tel": "031-327-2116", - "code": "283" - }, - "남양양": { - "tel": "033-812-2214", - "code": "284" - }, - "서평택": { - "tel": "031-327-2117", - "code": "285" - }, - "송악": { - "tel": "041-411-2410", - "code": "287" - }, - "당진": { - "tel": "041-411-2409", - "code": "288" - }, - "마산": { - "tel": "055-711-2832", - "code": "289" - }, - "축동": { - "tel": "055-711-2816", - "code": "290" - }, - "사천": { - "tel": "055-711-2815", - "code": "291" - }, - "진주": { - "tel": "055-711-2823", - "code": "292" - }, - "문산": { - "tel": "055-711-2824", - "code": "293" - }, - "북창원": { - "tel": "055-711-2833", - "code": "294" - }, - "진성": { - "tel": "055-711-2827", - "code": "295" - }, - "지수": { - "tel": "055-711-2826", - "code": "296" - }, - "동충주": { - "tel": "043-756-2309", - "code": "297" - }, - "서산": { - "tel": "041-411-2417", - "code": "298" - }, - "해미": { - "tel": "041-411-2419", - "code": "299" - }, - "남여주": { - "tel": "031-327-2144", - "code": "500" - }, - "서김제": { - "tel": "063-712-2507", - "code": "501" - }, - "부안": { - "tel": "063-712-2515", - "code": "502" - }, - "줄포": { - "tel": "063-712-2516", - "code": "503" - }, - "선운산": { - "tel": "063-712-2502", - "code": "504" - }, - "고창": { - "tel": "063-712-2501", - "code": "505" - }, - "영광": { - "tel": "061-882-2629", - "code": "506" - }, - "함평": { - "tel": "061-882-2637", - "code": "507" - }, - "무안": { - "tel": "061-882-2616", - "code": "508" - }, - "목포": { - "tel": "061-882-2618", - "code": "509" - }, - "양양": { - "tel": "033-812-2228", - "code": "510" - }, - "무창포": { - "tel": "041-411-2411", - "code": "511" - }, - "연화산": { - "tel": "055-711-2805", - "code": "512" - }, - "고성": { - "tel": "055-711-2804", - "code": "513" - }, - "동고성": { - "tel": "055-711-2803", - "code": "514" - }, - "북통영": { - "tel": "055-711-2835", - "code": "515" - }, - "통영": { - "tel": "055-711-2836", - "code": "516" - }, - "양평": { - "tel": "031-327-2147", - "code": "517" - }, - "칠곡물류": { - "tel": "054-706-2742", - "code": "518" - }, - "남대구": { - "tel": "053-717-2790", - "code": "519" - }, - "평택": { - "tel": "031-327-2122", - "code": "520" - }, - "단양": { - "tel": "043-756-2313", - "code": "521" - }, - "북단양": { - "tel": "043-756-2312", - "code": "522" - }, - "남제천": { - "tel": "043-756-2311", - "code": "523" - }, - "청통와촌": { - "tel": "054-706-2732", - "code": "524" - }, - "북영천": { - "tel": "054-706-2730", - "code": "525" - }, - "서포항": { - "tel": "054-706-2745", - "code": "526" - }, - "포항": { - "tel": "054-706-2743", - "code": "527" - }, - "기흥동탄": { - "tel": "031-327-2127", - "code": "528" - }, - "노포": { - "tel": "051-793-2883", - "code": "529" - }, - "내장산": { - "tel": "063-712-2530", - "code": "530" - }, - "문의": { - "tel": "043-756-2319", - "code": "531" - }, - "회인": { - "tel": "043-756-2320", - "code": "532" - }, - "보은": { - "tel": "043-756-2321", - "code": "533" - }, - "속리산": { - "tel": "043-756-2322", - "code": "534" - }, - "화서": { - "tel": "054-706-2721", - "code": "535" - }, - "남상주": { - "tel": "054-706-2716", - "code": "536" - }, - "남김천": { - "tel": "054-706-2713", - "code": "537" - }, - "성주": { - "tel": "054-706-2722", - "code": "538" - }, - "남성주": { - "tel": "054-706-2723", - "code": "539" - }, - "청북": { - "tel": "031-327-2118", - "code": "540" - }, - "송탄": { - "tel": "031-327-2119", - "code": "541" - }, - "서안성": { - "tel": "031-327-2120", - "code": "542" - }, - "남안성": { - "tel": "031-327-2121", - "code": "543" - }, - "완주": { - "tel": "063-712-2521", - "code": "544" - }, - "소양": { - "tel": "063-712-2520", - "code": "545" - }, - "진안": { - "tel": "063-712-2532", - "code": "546" - }, - "북무안": { - "tel": "061-454-7202", - "code": "548" - }, - "현경": { - "tel": "061-454-7202", - "code": "548" - }, - "동함평": { - "tel": "061-882-2636", - "code": "549" - }, - "문평": { - "tel": "061-882-2612", - "code": "550" - }, - "나주": { - "tel": "061-882-2611", - "code": "551" - }, - "서광산": { - "tel": "062-719-2691", - "code": "552" - }, - "동광산": { - "tel": "062-719-2690", - "code": "553" - }, - "동홍천": { - "tel": "033-812-2213", - "code": "554" - }, - "북진천": { - "tel": "043-756-2305", - "code": "555" - }, - "동순천": { - "tel": "061-882-2605", - "code": "556" - }, - "황전": { - "tel": "061-882-2628", - "code": "557" - }, - "구례화엄사": { - "tel": "061-882-2610", - "code": "558" - }, - "서남원": { - "tel": "063-712-2510", - "code": "559" - }, - "감곡": { - "tel": "043-756-2300", - "code": "560" - }, - "북충주": { - "tel": "043-756-2301", - "code": "561" - }, - "중앙탑": { - "tel": "준비중", - "code": "762" - }, - "충주": { - "tel": "043-756-2302", - "code": "562" - }, - "괴산": { - "tel": "043-756-2303", - "code": "563" - }, - "연풍": { - "tel": "043-756-2304", - "code": "564" - }, - "문경새재": { - "tel": "054-706-2715", - "code": "565" - }, - "점촌함창": { - "tel": "054-706-2720", - "code": "566" - }, - "남고창": { - "tel": "063-712-2500", - "code": "567" - }, - "장성물류": { - "tel": "061-882-2633", - "code": "568" - }, - "북광주": { - "tel": "061-882-2613", - "code": "569" - }, - "북상주": { - "tel": "054-706-2717", - "code": "570" - }, - "면천": { - "tel": "041-411-2408", - "code": "571" - }, - "고덕": { - "tel": "041-411-2423", - "code": "572" - }, - "예산수덕사": { - "tel": "041-411-2425", - "code": "573" - }, - "신양": { - "tel": "041-411-2424", - "code": "574" - }, - "유구": { - "tel": "041-411-2404", - "code": "575" - }, - "마곡사": { - "tel": "041-411-2401", - "code": "576" - }, - "공주": { - "tel": "041-411-2402", - "code": "577" - }, - "서세종": { - "tel": "044-410-2480", - "code": "578" - }, - "남세종": { - "tel": "044-410-2481", - "code": "579" - }, - "북강릉": { - "tel": "033-812-2215", - "code": "580" - }, - "옥계": { - "tel": "033-812-2218", - "code": "581" - }, - "망상": { - "tel": "033-812-2219", - "code": "582" - }, - "동해": { - "tel": "033-812-2220", - "code": "583" - }, - "남강릉": { - "tel": "033-812-2217", - "code": "584" - }, - "하조대": { - "tel": "033-812-2229", - "code": "585" - }, - "북남원": { - "tel": "063-712-2508", - "code": "586" - }, - "오수": { - "tel": "063-712-2525", - "code": "587" - }, - "임실": { - "tel": "063-712-2524", - "code": "588" - }, - "상관": { - "tel": "063-712-2519", - "code": "589" - }, - "동전주": { - "tel": "063-712-2528", - "code": "590" - }, - "동서천": { - "tel": "041-411-2422", - "code": "591" - }, - "서부여": { - "tel": "041-411-2416", - "code": "592" - }, - "부여": { - "tel": "041-411-2414", - "code": "593" - }, - "청양": { - "tel": "041-411-2429", - "code": "594" - }, - "서공주": { - "tel": "041-411-2403", - "code": "595" - }, - "가락2": { - "tel": "055-329-3621", - "code": "596" - }, - "서여주": { - "tel": "031-327-2145", - "code": "597" - }, - "북여주": { - "tel": "031-327-2146", - "code": "598" - }, - "양촌": { - "tel": "042-841-2504", - "code": "599" - }, - "남천안": { - "tel": "041-850-6763", - "code": "601" - }, - "풍세": { - "tel": "041-850-6768", - "code": "1000" - }, - "풍세상": { - "tel": "041-850-6768", - "code": "602" - }, - "정안": { - "tel": "041-850-6786", - "code": "603" - }, - "남공주": { - "tel": "041-850-6805", - "code": "604" - }, - "탄천": { - "tel": "041-850-6808", - "code": "605" - }, - "서논산": { - "tel": "041-850-6827", - "code": "606" - }, - "연무": { - "tel": "041-850-6859", - "code": "607" - }, - "남논산하": { - "tel": "041-850-6871", - "code": "608" - }, - "남논산": { - "tel": "041-850-6871", - "code": "1000" - }, - "낙동2": { - "tel": "1877-0111", - "code": "610" - }, - "도개": { - "tel": "041-850-6793", - "code": "611" - }, - "풍세하": { - "tel": "041-850-6768", - "code": "612" - }, - "남풍세": { - "tel": "041-850-6793", - "code": "613" - }, - "서군위": { - "tel": "041-850-6793", - "code": "614" - }, - "동군위": { - "tel": "041-850-6793", - "code": "615" - }, - "신녕": { - "tel": "1877-0111", - "code": "616" - }, - "남논산상": { - "tel": "041-850-6871", - "code": "618" - }, - "영천JC": { - "tel": "1877-0111", - "code": "620" - }, - "동대구": { - "tel": "053-961-0302", - "code": "621" - }, - "대구": { - "tel": "053-791-5388", - "code": "622" - }, - "수성": { - "tel": "053-791-7330", - "code": "623" - }, - "청도": { - "tel": "054-373-5680", - "code": "624" - }, - "밀양": { - "tel": "055-355-0924", - "code": "625" - }, - "남밀양": { - "tel": "055-355-5451", - "code": "626" - }, - "삼랑진": { - "tel": "055-351-0502", - "code": "627" - }, - "상동": { - "tel": "055-323-1690", - "code": "628" - }, - "김해부산": { - "tel": "055-323-3805", - "code": "629" - }, - "울산": { - "tel": "052-254-8800", - "code": "641" - }, - "문수": { - "tel": "052-277-7501", - "code": "642" - }, - "청량": { - "tel": "052-260-6501", - "code": "643" - }, - "온양": { - "tel": "052-237-1170", - "code": "644" - }, - "장안": { - "tel": "051-727-3520", - "code": "645" - }, - "기장일광": { - "tel": "051-721-2554", - "code": "646" - }, - "해운대송정": { - "tel": "051-722-2514", - "code": "647" - }, - "해운대": { - "tel": "051-722-2514", - "code": "648" - }, - "군위출구JC": { - "tel": "1877-0111", - "code": "650" - }, - "남양주": { - "tel": "031-576-3545", - "code": "651" - }, - "화도": { - "tel": "031-559-2731", - "code": "652" - }, - "서종": { - "tel": "031-771-6712", - "code": "653" - }, - "설악": { - "tel": "031-584-2427", - "code": "654" - }, - "강촌": { - "tel": "033-264-1057", - "code": "655" - }, - "남춘천": { - "tel": "033-261-1662", - "code": "656" - }, - "동산": { - "tel": "033-261-1093", - "code": "657" - }, - "조양": { - "tel": "033-261-1093", - "code": "658" - }, - "초월": { - "tel": "070-8031-5376", - "code": "660" - }, - "동곤지암": { - "tel": "070-8032-4140", - "code": "661" - }, - "흥천이포": { - "tel": "031-775-2665", - "code": "662" - }, - "대신": { - "tel": "031-775-7306", - "code": "663" - }, - "동여주": { - "tel": "070-8032-4140", - "code": "664" - }, - "동양평": { - "tel": "070-8031-5399", - "code": "665" - }, - "서원주": { - "tel": "033-731-8068", - "code": "666" - }, - "경기광주JC": { - "tel": "070-8031-5376", - "code": "667" - }, - "신평JC": { - "tel": "033-731-8068", - "code": "668" - }, - "원주JC": { - "tel": "033-731-8068", - "code": "669" - }, - "봉담": { - "tel": "070-8031-4294", - "code": "671" - }, - "정남": { - "tel": "070-8031-4600", - "code": "672" - }, - "북오산": { - "tel": "070-8031-4227", - "code": "673" - }, - "동탄": { - "tel": "070-8031-5254", - "code": "836" - }, - "서오산": { - "tel": "070-8031-4226", - "code": "675" - }, - "향남": { - "tel": "070-8031-4228", - "code": "676" - }, - "북평택": { - "tel": "070-8031-4229", - "code": "677" - }, - "화산출구JC": { - "tel": "1877-0111", - "code": "680" - }, - "조암": { - "tel": "031-494-0393", - "code": "682" - }, - "송산마도": { - "tel": "031-494-0427", - "code": "683" - }, - "남안산": { - "tel": "031-494-0436", - "code": "684" - }, - "서시흥": { - "tel": "031-494-0430", - "code": "685" - }, - "동영천": { - "tel": "054-536-9159", - "code": "686" - }, - "금곡": { - "tel": "070-8031-5010", - "code": "691" - }, - "동안산당수": { - "tel": "070-8031-5025", - "code": "692" - }, - "남군포": { - "tel": "070-8031-5060", - "code": "693" - }, - "동시흥": { - "tel": "070-8031-5078", - "code": "694" - }, - "목감": { - "tel": "070-8031-5078", - "code": "694" - }, - "남광명": { - "tel": "070-8031-5100", - "code": "695" - }, - "남진례": { - "tel": "070-5143-6377", - "code": "696" - }, - "대청": { - "tel": "070-5143-6375", - "code": "697" - }, - "진해": { - "tel": "070-5143-6371", - "code": "698" - }, - "진해본선": { - "tel": "070-5143-6371", - "code": "699" - }, - "북양양": { - "tel": "033-812-2227", - "code": "701" - }, - "속초": { - "tel": "033-812-2223", - "code": "702" - }, - "삼척": { - "tel": "033-812-2221", - "code": "703" - }, - "근덕": { - "tel": "033-812-2222", - "code": "704" - }, - "남이천": { - "tel": "031-327-2149", - "code": "705" - }, - "남경주": { - "tel": "054-706-2704", - "code": "706" - }, - "남포항": { - "tel": "054-706-2744", - "code": "707" - }, - "동경주": { - "tel": "054-706-2703", - "code": "708" - }, - "삽교": { - "tel": "없음", - "code": "709" - }, - "동상주": { - "tel": "054-706-2718", - "code": "712" - }, - "서의성": { - "tel": "054-706-2734", - "code": "713" - }, - "북의성": { - "tel": "054-706-2735", - "code": "714" - }, - "동안동": { - "tel": "054-706-2724", - "code": "715" - }, - "청송": { - "tel": "054-706-2738", - "code": "716" - }, - "활천": { - "tel": "052-701-2890", - "code": "722" - }, - "임고": { - "tel": "054-336-9600", - "code": "725" - }, - "신둔": { - "tel": "031-636-7095", - "code": "729" - }, - "유천": { - "tel": "053-591-2504", - "code": "730" - }, - "북현풍": { - "tel": "053-611-4532", - "code": "731" - }, - "행담도": { - "tel": "041-357-0878", - "code": "732" - }, - "옥산": { - "tel": "043-232-0554", - "code": "733" - }, - "동둔내": { - "tel": "033-334-3005", - "code": "734" - }, - "남양평": { - "tel": "031-771-0390", - "code": "735" - }, - "삼성": { - "tel": "043-882-2504", - "code": "736" - }, - "평택고덕": { - "tel": "031-327-2123", - "code": "741" - }, - "기장일광입구": { - "tel": "051-721-2554", - "code": "746" - }, - "범서": { - "tel": "052-701-2891", - "code": "747" - }, - "동청송영양": { - "tel": "054-706-2737", - "code": "748" - }, - "영덕": { - "tel": "054-706-2727", - "code": "749" - }, - "기장": { - "tel": "051-721-2554", - "code": "751" - }, - "기장철마": { - "tel": "051-793-2850", - "code": "752" - }, - "금정": { - "tel": "051-793-2884", - "code": "753" - }, - "광재": { - "tel": "055-711-2810", - "code": "755" - }, - "진영": { - "tel": "055-711-2813", - "code": "757" - }, - "내촌": { - "tel": "033-812-2224", - "code": "765" - }, - "인제": { - "tel": "033-812-2225", - "code": "766" - }, - "구병산": { - "tel": "043-544-0141", - "code": "767" - }, - "서양양": { - "tel": "033-812-2226", - "code": "768" - }, - "서오창동JC": { - "tel": "070-4219-5716", - "code": "840" - }, - "서오창서JC": { - "tel": "070-4219-5716", - "code": "850" - }, - "서오창": { - "tel": "070-4219-5716", - "code": "851" - }, - "북중랑": { - "tel": "없음", - "code": "860" - }, - "갈매동구릉": { - "tel": "070-4220-6490", - "code": "861" - }, - "동구릉": { - "tel": "070-4220-6490", - "code": "861" - }, - "남별내": { - "tel": "070-4173-7383", - "code": "862" - }, - "동의정부": { - "tel": "070-4220-6524", - "code": "863" - }, - "민락": { - "tel": "070-4220-6549", - "code": "864" - }, - "소흘": { - "tel": "070-4220-6530", - "code": "865" - }, - "선단": { - "tel": "070-4220-6569", - "code": "866" - }, - "포천": { - "tel": "070-4220-6573", - "code": "867" - }, - "신북": { - "tel": "070-4220-6583", - "code": "869" - }, - "옥정": { - "tel": "070-4220-6590", - "code": "870" - }, - "양주": { - "tel": "070-4220-6590", - "code": "879" - }, - "청라원창": { - "tel": "032-292-7236", - "code": "886" - }, - "남청라": { - "tel": "032-292-7150", - "code": "887" - }, - "북청라": { - "tel": "032-292-7249", - "code": "889" - }, - "검단양촌": { - "tel": "032-292-7198", - "code": "890" - }, - "대곶": { - "tel": "032-292-7211", - "code": "891" - }, - "서김포통진": { - "tel": "032-292-7224", - "code": "892" - }, - "서영암": { - "tel": "061-882-2630", - "code": "981" - }, - "강진무위사": { - "tel": "061-882-2600", - "code": "982" - }, - "강진": { - "tel": "061-434-9912", - "code": "982" - }, - "장흥": { - "tel": "061-882-2635", - "code": "983" - }, - "보성": { - "tel": "061-882-2620", - "code": "984" - }, - "벌교": { - "tel": "061-882-2621", - "code": "985" - }, - "고흥": { - "tel": "061-882-2601", - "code": "986" - }, - "남순천": { - "tel": "061-882-2622", - "code": "987" - }, - "무안공항": { - "tel": "061-882-2619", - "code": "547" - }, - "한림": { - "tel": "055-711-2800", - "code": "756" - }, - "배내골": { - "tel": "055-711-2849", - "code": "771" - }, - "남봉담": { - "tel": "", - "code": "801" - }, - "청요서": { - "tel": "", - "code": "812" - }, - "청요동": { - "tel": "", - "code": "802" - }, - "남비봉팔탄": { - "tel": "", - "code": "803" - }, - "화성": { - "tel": "", - "code": "804" - }, - "마도": { - "tel": "", - "code": "805" - }, - "마도서": { - "tel": "", - "code": "806" - }, - "마도동": { - "tel": "", - "code": "816" - }, - "장안본선": { - "tel": "", - "code": "681" - }, - "도척": { - "tel": "070-8031-5272", - "code": "831" - }, - "포곡": { - "tel": "070-8031-5264", - "code": "832" - }, - "서용인": { - "tel": "070-8031-5260", - "code": "835" - }, - "북달성": { - "tel": "053-717-2785", - "code": "772" - }, - "북다사": { - "tel": "053-717-2786", - "code": "773" - }, - "남칠곡": { - "tel": "053-706-2746", - "code": "774" - }, - "지천": { - "tel": "054-706-2747", - "code": "058" - }, - "동명동호": { - "tel": "053-706-2748", - "code": "775" - }, - "연경": { - "tel": "053-717-2787", - "code": "776" - }, - "파군재": { - "tel": "053-717-2788", - "code": "777" - }, - "둔산": { - "tel": "053-717-2789", - "code": "778" - }, - "율암": { - "tel": "053-717-2783", - "code": "779" - }, - "남사진위": { - "tel": "031-327-2152", - "code": "742" - }, - "남광산": { - "tel": "061-882-2693", - "code": "718" - }, - "남장성": { - "tel": "061-882-2638", - "code": "719" - }, - "두물머리": { - "tel": "031-327-2153", - "code": "763" - }, - "조안": { - "tel": "031-327-2153", - "code": "764" - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/main/resources/intent-definitions.yml b/chatbotApi-chatbot2.0-agent/src/main/resources/intent-definitions.yml deleted file mode 100644 index 31e6a93..0000000 --- a/chatbotApi-chatbot2.0-agent/src/main/resources/intent-definitions.yml +++ /dev/null @@ -1,245 +0,0 @@ -{ - "version": 1, - "intents": { - "FARE_SEARCH": { - "description": "출발 IC와 도착 IC 사이 통행요금 조회", - "fetchOwner": "WAS", - "uiType": "FARE_CARD", - "requiredParams": [ - { - "name": "fromIc", - "description": "출발 IC명. IC, 영업소, 톨게이트, 요금소 같은 접미사와 공백은 제거한 짧은 한글명으로 추출합니다. 사용자가 '여기', '현재 위치'처럼 말하면 실제 IC명으로 보지 않습니다.", - "examples": ["판교", "서울", "대전"] - }, - { - "name": "toIc", - "description": "도착 IC명. IC, 영업소, 톨게이트, 요금소 같은 접미사와 공백은 제거한 짧은 한글명으로 추출합니다.", - "examples": ["신갈", "부산", "광주"] - } - ], - "optionalParams": [], - "clarification": { - "default": "통행요금 조회를 위해 출발 IC와 도착 IC를 알려주세요. 예: 판교에서 신갈까지", - "missing": { - "fromIc": "출발 IC를 알려주세요. 예: 판교에서 신갈까지의 통행요금", - "toIc": "도착 IC를 알려주세요. 예: 판교에서 신갈까지의 통행요금" - } - } - }, - "FARE_UNPAID": { - "description": "차량의 미납 통행료를 실제로 조회하려는 의도. 전체 차량번호가 있으면 carNo로 추출하고, 조회 의사는 있지만 차량번호가 없으면 carNo를 null로 둡니다. 미납 조회 방법, 납부 방법, 조회 가능 경로만 묻는 안내성 질문은 이 intent가 아니라 일반 RAG로 처리합니다.", - "fetchOwner": "WAS", - "uiType": "TEXT_ONLY", - "requiredParams": [ - { - "name": "carNo", - "description": "조회할 전체 차량번호입니다. 공백이나 하이픈은 제거해서 추출합니다. 끝자리만 있는 부분 차량번호는 사용하지 않습니다. 사용자가 실제 조회 의사를 말했지만 차량번호를 말하지 않으면 null로 둡니다.", - "examples": ["12가3456", "123가4567"] - } - ], - "optionalParams": [], - "minConfidence": 0.85, - "clarification": { - "default": "챗봇에서 미납 통행료 조회가 가능합니다. 조회하려면 전체 차량번호를 입력해 주세요. 예: 12가3456 미납 조회", - "missing": { - "carNo": "챗봇에서 미납 통행료 조회가 가능합니다. 조회하려면 전체 차량번호를 입력해 주세요. 예: 12가3456 미납 조회" - } - } - }, - "FARE_REFUND": { - "description": "차량번호 기반 환불 조회", - "fetchOwner": "WAS", - "uiType": "TEXT_ONLY", - "requiredParams": [ - { - "name": "carNo", - "description": "환불 내역을 조회할 전체 차량번호입니다. 공백이나 하이픈은 제거해서 추출합니다. 끝자리만 있는 부분 차량번호는 사용하지 않습니다.", - "examples": ["12가3456", "123가4567"] - } - ], - "optionalParams": [], - "minConfidence": 0.85, - "clarification": { - "default": "환불 조회를 위해 차량번호를 알려주세요.", - "missing": { - "carNo": "조회할 차량번호를 알려주세요." - } - } - }, - "IC_TEL": { - "description": "IC 또는 영업소 전화번호 조회", - "fetchOwner": "WAS", - "uiType": "PHONE_CARD", - "requiredParams": [ - { - "name": "icName", - "description": "전화번호를 확인할 IC명 또는 영업소명입니다. IC, 영업소, 톨게이트, 요금소 같은 접미사와 공백은 제거한 짧은 한글명으로 추출합니다.", - "examples": ["판교", "서울", "신갈"] - } - ], - "optionalParams": [], - "clarification": { - "default": "전화번호를 확인할 IC 또는 영업소명을 알려주세요.", - "missing": { - "icName": "전화번호를 확인할 IC 또는 영업소명을 알려주세요." - } - } - }, - "GASSTATION": { - "description": "휴게소 주유소 유가/운영 정보 조회", - "fetchOwner": "WEB", - "uiType": "GASSTATION_CARD", - "requiredParams": [ - { - "name": "restAreaName", - "description": "주유소 정보를 조회할 휴게소명입니다. 휴게소/주유소 접미사, 방향 표현(서울방향, 부산방향, 익산방향, 포항방향 등), 공백은 제외하고 짧은 한글명만 추출합니다.", - "examples": ["죽전", "망향", "안성", "영천"] - } - ], - "optionalParams": [ - { - "name": "restAreaDirection", - "description": "사용자가 명시한 휴게소 방향 또는 상행/하행 구분입니다. '영천 주유소 익산방향'처럼 휴게소명 뒤에 붙은 방향도 반드시 분리해서 추출합니다. 방향을 말하지 않았으면 null로 둡니다.", - "examples": ["서울방향", "부산방향", "익산방향", "포항방향", "상행", "하행"] - } - ], - "clarification": { - "default": "조회할 휴게소명을 알려주세요.", - "missing": { - "restAreaName": "주유소 정보를 확인할 휴게소명을 알려주세요." - } - } - }, - "BRAND_SHOP": { - "description": "휴게소 브랜드 매장 조회", - "fetchOwner": "WEB", - "uiType": "BRAND_SHOP_CARD", - "requiredParams": [ - { - "name": "restAreaName", - "description": "브랜드 매장을 조회할 휴게소명입니다. 휴게소 접미사와 공백은 제거한 짧은 한글명으로 추출합니다.", - "examples": ["죽전", "망향", "안성"] - } - ], - "optionalParams": [ - { - "name": "brand", - "description": "찾고 싶은 브랜드 또는 매장명입니다.", - "examples": ["스타벅스", "탐앤탐스", "롯데리아"] - }, - { - "name": "startIndex", - "description": "목록 조회 시작 위치입니다. 사용자가 직접 말하지 않으면 추출하지 않습니다.", - "examples": ["1", "10"] - } - ], - "clarification": { - "default": "조회할 휴게소명을 알려주세요.", - "missing": { - "restAreaName": "브랜드 매장을 확인할 휴게소명을 알려주세요." - } - } - }, - "REST_AREA_FOOD_LIST": { - "description": "휴게소 음식/메뉴 목록 조회", - "fetchOwner": "WEB", - "uiType": "FOOD_LIST_CARD", - "requiredParams": [ - { - "name": "restAreaName", - "description": "음식/메뉴 목록을 조회할 휴게소명입니다. 휴게소 접미사와 공백은 제거한 짧은 한글명으로 추출합니다.", - "examples": ["죽전", "망향", "안성"] - } - ], - "optionalParams": [], - "clarification": { - "default": "조회할 휴게소명을 알려주세요.", - "missing": { - "restAreaName": "음식 정보를 확인할 휴게소명을 알려주세요." - } - } - }, - "REST_AREA_PRICE": { - "description": "휴게소 음식 가격 조회", - "fetchOwner": "WAS", - "uiType": "PRICE_CARD", - "requiredParams": [ - { - "name": "restAreaName", - "description": "음식 가격을 확인할 휴게소명입니다. 휴게소 접미사와 공백은 제거한 짧은 한글명으로 추출합니다.", - "examples": ["죽전", "망향", "안성"] - } - ], - "optionalParams": [], - "clarification": { - "default": "음식 가격을 확인할 휴게소명을 알려주세요.", - "missing": { - "restAreaName": "음식 가격을 확인할 휴게소명을 알려주세요." - } - } - }, - "TROAD_SEARCH_ROUTE": { - "description": "출발 IC와 도착 IC 기준 교통 경로 조회", - "fetchOwner": "WAS", - "uiType": "TEXT_ONLY", - "requiredParams": [ - { - "name": "fromIc", - "description": "교통 경로 조회의 출발 IC명입니다. IC, 영업소, 톨게이트, 요금소 같은 접미사와 공백은 제거한 짧은 한글명으로 추출합니다.", - "examples": ["판교", "서울", "대전"] - }, - { - "name": "toIc", - "description": "교통 경로 조회의 도착 IC명입니다. IC, 영업소, 톨게이트, 요금소 같은 접미사와 공백은 제거한 짧은 한글명으로 추출합니다.", - "examples": ["신갈", "부산", "광주"] - } - ], - "optionalParams": [], - "clarification": { - "default": "교통 경로 조회를 위해 출발 IC와 도착 IC를 알려주세요.", - "missing": { - "fromIc": "출발 IC를 알려주세요.", - "toIc": "도착 IC를 알려주세요." - } - } - }, - "TROAD_CONGESTION": { - "description": "노선 정체 조회", - "fetchOwner": "WAS", - "uiType": "TEXT_ONLY", - "requiredParams": [ - { - "name": "routeName", - "description": "정체 정보를 조회할 고속도로 노선명입니다.", - "examples": ["경부선", "영동선", "서해안선"] - } - ], - "optionalParams": [], - "clarification": { - "default": "조회할 노선명을 알려주세요. 예: 경부선, 영동선", - "missing": { - "routeName": "조회할 노선명을 알려주세요. 예: 경부선, 영동선" - } - } - }, - "TROAD_BREAKING_NEWS": { - "description": "노선 사고/공사/돌발정보 조회", - "fetchOwner": "WAS", - "uiType": "TEXT_ONLY", - "requiredParams": [ - { - "name": "routeName", - "description": "사고/공사/돌발정보를 조회할 고속도로 노선명입니다.", - "examples": ["경부선", "영동선", "서해안선"] - } - ], - "optionalParams": [], - "clarification": { - "default": "조회할 노선명을 알려주세요. 예: 경부선, 영동선", - "missing": { - "routeName": "조회할 노선명을 알려주세요. 예: 경부선, 영동선" - } - } - } - } -} \ No newline at end of file diff --git a/chatbotApi-chatbot2.0-agent/src/test/java/com/ex/chatbotapi/ChatbotapiApplicationTests.java b/chatbotApi-chatbot2.0-agent/src/test/java/com/ex/chatbotapi/ChatbotapiApplicationTests.java deleted file mode 100644 index 25b89a7..0000000 --- a/chatbotApi-chatbot2.0-agent/src/test/java/com/ex/chatbotapi/ChatbotapiApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.ex.chatbotapi; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class ChatbotapiApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/chatbotApi-chatbot2.0-agent/src/test/java/com/ex/chatbotapi/domain/agent/service/IntentParamValidatorTest.java b/chatbotApi-chatbot2.0-agent/src/test/java/com/ex/chatbotapi/domain/agent/service/IntentParamValidatorTest.java deleted file mode 100644 index cc59ded..0000000 --- a/chatbotApi-chatbot2.0-agent/src/test/java/com/ex/chatbotapi/domain/agent/service/IntentParamValidatorTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.ex.chatbotapi.domain.agent.service; - -import com.ex.chatbotapi.domain.agent.dto.IntentValidationResult; -import com.ex.chatbotapi.domain.agent.intent.IntentDefinitionRegistry; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.HashMap; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; - -class IntentParamValidatorTest { - - private IntentParamValidator validator; - - @BeforeEach - void setUp() { - IntentDefinitionRegistry registry = new IntentDefinitionRegistry(); - registry.load(); - validator = new IntentParamValidator(registry); - } - - @Test - void fareSearchRequiresRealDepartureAndDestination() { - Map params = new HashMap<>(); - params.put("fromIc", "여기"); - params.put("toIc", "신갈"); - - IntentValidationResult result = validator.validate("FARE_SEARCH", params); - - assertThat(result.isValid()).isFalse(); - assertThat(result.getMissingParams()).containsExactly("fromIc"); - assertThat(result.getCleanedParams()).containsEntry("toIc", "신갈"); - assertThat(result.getCleanedParams()).doesNotContainKey("fromIc"); - } - - @Test - void fareSearchPassesWithBothIcParams() { - Map params = new HashMap<>(); - params.put("fromIc", "판교"); - params.put("toIc", "신갈"); - - IntentValidationResult result = validator.validate("FARE_SEARCH", params); - - assertThat(result.isValid()).isTrue(); - assertThat(result.getMissingParams()).isEmpty(); - assertThat(result.getCleanedParams()).containsEntry("fromIc", "판교"); - assertThat(result.getCleanedParams()).containsEntry("toIc", "신갈"); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/test/java/com/ex/chatbotapi/domain/agent/service/ToolDefinitionServiceTest.java b/chatbotApi-chatbot2.0-agent/src/test/java/com/ex/chatbotapi/domain/agent/service/ToolDefinitionServiceTest.java deleted file mode 100644 index 96dd527..0000000 --- a/chatbotApi-chatbot2.0-agent/src/test/java/com/ex/chatbotapi/domain/agent/service/ToolDefinitionServiceTest.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.ex.chatbotapi.domain.agent.service; - -import com.ex.chatbotapi.domain.agent.intent.FetchOwner; -import com.ex.chatbotapi.domain.agent.intent.IntentDefinition; -import com.ex.chatbotapi.domain.agent.intent.IntentDefinitionRegistry; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.List; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class ToolDefinitionServiceTest { - - @Mock - private IntentDefinitionRegistry intentDefinitionRegistry; - - private ToolDefinitionService toolDefinitionService; - - @BeforeEach - void setUp() { - toolDefinitionService = new ToolDefinitionService(intentDefinitionRegistry); - } - - @Test - void listToolDefinitions_excludesNoneFetchOwner() { - IntentDefinition fare = definition("FARE_SEARCH", FetchOwner.WAS, "FARE_CARD"); - IntentDefinition ragOnly = definition("PURE_RAG", FetchOwner.NONE, "TEXT_ONLY"); - - when(intentDefinitionRegistry.all()).thenReturn(Map.of( - "FARE_SEARCH", fare, - "PURE_RAG", ragOnly - )); - - List> tools = toolDefinitionService.listToolDefinitions(); - - assertThat(tools).hasSize(1); - @SuppressWarnings("unchecked") - Map function = (Map) tools.get(0).get("function"); - assertThat(function.get("name")).isEqualTo("FARE_SEARCH"); - assertThat(function.get("description").toString()).contains("fetchOwner=WAS"); - } - - private IntentDefinition definition(String name, FetchOwner owner, String uiType) { - IntentDefinition definition = new IntentDefinition(); - definition.setIntentType(name); - definition.setDescription("desc for " + name); - definition.setFetchOwner(owner); - definition.setUiType(uiType); - - IntentDefinition.ParamDefinition fromIc = new IntentDefinition.ParamDefinition(); - fromIc.setName("fromIc"); - fromIc.setDescription("출발 IC"); - fromIc.setExamples(List.of("판교")); - - IntentDefinition.ParamDefinition toIc = new IntentDefinition.ParamDefinition(); - toIc.setName("toIc"); - toIc.setDescription("도착 IC"); - - definition.setRequiredParamDefinitions(List.of(fromIc, toIc)); - definition.setRequiredParams(List.of("fromIc", "toIc")); - return definition; - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/test/java/com/ex/chatbotapi/domain/agent/service/ToolExecutionServiceTest.java b/chatbotApi-chatbot2.0-agent/src/test/java/com/ex/chatbotapi/domain/agent/service/ToolExecutionServiceTest.java deleted file mode 100644 index c8c432a..0000000 --- a/chatbotApi-chatbot2.0-agent/src/test/java/com/ex/chatbotapi/domain/agent/service/ToolExecutionServiceTest.java +++ /dev/null @@ -1,136 +0,0 @@ -package com.ex.chatbotapi.domain.agent.service; - -import com.ex.chatbotapi.domain.agent.client.KakaoSkillToolClient; -import com.ex.chatbotapi.domain.agent.dto.ToolExecuteRequest; -import com.ex.chatbotapi.domain.agent.dto.ToolExecuteResponse; -import com.ex.chatbotapi.domain.agent.dto.ToolParamPipelineResult; -import com.ex.chatbotapi.domain.agent.handler.IntentHandler; -import com.ex.chatbotapi.domain.agent.intent.FetchOwner; -import com.ex.chatbotapi.domain.agent.intent.IntentDefinition; -import com.ex.chatbotapi.domain.agent.intent.IntentDefinitionRegistry; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class ToolExecutionServiceTest { - - @Mock - private IntentDefinitionRegistry intentDefinitionRegistry; - @Mock - private IntentHandler fareSearchHandler; - @Mock - private KakaoSkillToolClient kakaoSkillToolClient; - @Mock - private ToolParamPipeline toolParamPipeline; - - private ToolExecutionService toolExecutionService; - - @BeforeEach - void setUp() { - when(fareSearchHandler.getIntentType()).thenReturn("FARE_SEARCH"); - toolExecutionService = new ToolExecutionService( - intentDefinitionRegistry, - List.of(fareSearchHandler), - List.of(), - kakaoSkillToolClient, - toolParamPipeline - ); - } - - @Test - void execute_returnsClarifyWhenPipelineFails() { - IntentDefinition definition = new IntentDefinition(); - definition.setIntentType("FARE_SEARCH"); - definition.setFetchOwner(FetchOwner.WAS); - definition.setUiType("FARE_CARD"); - when(intentDefinitionRegistry.get("FARE_SEARCH")).thenReturn(definition); - when(toolParamPipeline.process(eq("FARE_SEARCH"), any())) - .thenReturn(ToolParamPipelineResult.clarify( - Map.of("toIc", "신갈"), - "출발 IC를 알려주세요.", - List.of("fromIc") - )); - - ToolExecuteResponse response = toolExecutionService.execute( - ToolExecuteRequest.builder() - .toolName("FARE_SEARCH") - .arguments(Map.of("toIc", "신갈")) - .build() - ); - - assertThat(response.isStatus()).isFalse(); - assertThat(response.isNeedsClarification()).isTrue(); - assertThat(response.getClarificationQuestion()).contains("출발"); - assertThat(response.getMissingParams()).contains("fromIc"); - } - - @Test - void execute_wasHandlerSuccess() { - IntentDefinition definition = new IntentDefinition(); - definition.setIntentType("FARE_SEARCH"); - definition.setFetchOwner(FetchOwner.WAS); - definition.setUiType("FARE_CARD"); - when(intentDefinitionRegistry.get("FARE_SEARCH")).thenReturn(definition); - when(toolParamPipeline.process(eq("FARE_SEARCH"), any())) - .thenReturn(ToolParamPipelineResult.success(Map.of( - "fromIc", "판교", - "toIc", "신갈" - ))); - - Map domainData = new HashMap<>(); - domainData.put("status", true); - domainData.put("statusMsg", "정상응답"); - when(fareSearchHandler.fetchDomainData(any())).thenReturn(domainData); - - ToolExecuteResponse response = toolExecutionService.execute( - ToolExecuteRequest.builder() - .toolName("FARE_SEARCH") - .arguments(Map.of("fromIc", "판교", "toIc", "신갈")) - .build() - ); - - assertThat(response.isStatus()).isTrue(); - assertThat(response.getIntentType()).isEqualTo("FARE_SEARCH"); - assertThat(response.getFetchOwner()).isEqualTo("WAS"); - assertThat(response.getDomainData()).containsEntry("status", true); - } - - @Test - void execute_webFetchViaSkillClient() { - IntentDefinition definition = new IntentDefinition(); - definition.setIntentType("GASSTATION"); - definition.setFetchOwner(FetchOwner.WEB); - definition.setUiType("GASSTATION_CARD"); - when(intentDefinitionRegistry.get("GASSTATION")).thenReturn(definition); - when(toolParamPipeline.process(eq("GASSTATION"), any())) - .thenReturn(ToolParamPipelineResult.success(Map.of("restAreaName", "죽전"))); - - Map domainData = new HashMap<>(); - domainData.put("status", true); - when(kakaoSkillToolClient.fetchDataPortal(eq("GASSTATION"), any(), any())) - .thenReturn(domainData); - - ToolExecuteResponse response = toolExecutionService.execute( - ToolExecuteRequest.builder() - .toolName("GASSTATION") - .arguments(Map.of("restAreaName", "죽전")) - .userInput("죽전 주유소") - .build() - ); - - assertThat(response.isStatus()).isTrue(); - assertThat(response.getFetchOwner()).isEqualTo("WEB"); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/test/java/com/ex/chatbotapi/domain/fare/service/FareAmountHelperTest.java b/chatbotApi-chatbot2.0-agent/src/test/java/com/ex/chatbotapi/domain/fare/service/FareAmountHelperTest.java deleted file mode 100644 index dba7c90..0000000 --- a/chatbotApi-chatbot2.0-agent/src/test/java/com/ex/chatbotapi/domain/fare/service/FareAmountHelperTest.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.ex.chatbotapi.domain.fare.service; - -import org.bson.Document; -import org.junit.jupiter.api.Test; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; - -class FareAmountHelperTest { - - @Test - void applyExactFareAmounts_buildsWebBreakdown() { - Document trsDoc = new Document(); - trsDoc.put("sum", Arrays.asList(3200, 3500, 4000, 4500, 5000, 2800)); - Document amt = new Document(); - amt.put("00", Arrays.asList(3000, 3300, 3800, 4300, 4800, 2700)); - trsDoc.put("amt", amt); - - Map fareData = new HashMap<>(); - fareData.put("type", "exact"); - fareData.put("fromIc", "판교"); - fareData.put("toIc", "신갈"); - - FareAmountHelper.applyExactFareAmounts(fareData, trsDoc); - - assertThat(fareData.get("fare1")).isEqualTo(3200); - assertThat(fareData.get("fare1Small")).isEqualTo(2800); - - @SuppressWarnings("unchecked") - Map breakdown = (Map) fareData.get("webBreakdown"); - assertThat(breakdown).isNotNull(); - assertThat(breakdown.get("fromIc")).isEqualTo("판교"); - assertThat(breakdown.get("toIc")).isEqualTo("신갈"); - assertThat(breakdown.get("totalFare")).isEqualTo(3200); - assertThat(breakdown.get("kecFare")).isEqualTo(3000); - assertThat(breakdown.get("privateFare")).isEqualTo(200); - - @SuppressWarnings("unchecked") - List> vehicles = (List>) breakdown.get("vehicles"); - assertThat(vehicles).hasSize(6); - assertThat(vehicles.get(0).get("typeLabel")).isEqualTo("1종"); - assertThat(vehicles.get(5).get("compact")).isEqualTo(true); - } -} diff --git a/chatbotApi-chatbot2.0-agent/src/test/java/com/ex/chatbotapi/domain/rag/dto/RagServerResponseTest.java b/chatbotApi-chatbot2.0-agent/src/test/java/com/ex/chatbotapi/domain/rag/dto/RagServerResponseTest.java deleted file mode 100644 index 595ce41..0000000 --- a/chatbotApi-chatbot2.0-agent/src/test/java/com/ex/chatbotapi/domain/rag/dto/RagServerResponseTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.ex.chatbotapi.domain.rag.dto; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.Test; - -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; - -class RagServerResponseTest { - - @Test - void shouldDeserializeFaqUrlsAndReferencesFromRagServerResponse() throws Exception { - String json = "{" - + "\"answer\":\"하이패스 FAQ 안내입니다.\"," - + "\"matched_questions\":[\"하이패스 단말기 등록은 어떻게 하나요?\"]," - + "\"scores\":[0.95]," - + "\"num_references\":1," - + "\"faq_urls\":[\"https://www.hipass.co.kr/board/FaqDetail.do?ser_no=186\"]," - + "\"references\":[{\"question\":\"하이패스 단말기 등록은 어떻게 하나요?\"," - + "\"source\":\"hipass_faq\"," - + "\"url\":\"https://www.hipass.co.kr/board/FaqDetail.do?ser_no=186\"}]" - + "}"; - - RagServerResponse response = new ObjectMapper().readValue(json, RagServerResponse.class); - - assertThat(response.getMatchedQuestions()).containsExactly("하이패스 단말기 등록은 어떻게 하나요?"); - assertThat(response.getFaqUrls()).containsExactly("https://www.hipass.co.kr/board/FaqDetail.do?ser_no=186"); - assertThat(response.getReferences()).hasSize(1); - - Map reference = response.getReferences().get(0); - assertThat(reference.get("source")).isEqualTo("hipass_faq"); - assertThat(reference.get("url")).isEqualTo("https://www.hipass.co.kr/board/FaqDetail.do?ser_no=186"); - } -} diff --git a/chatbotApp-chatbot2.0-agent b/chatbotApp-chatbot2.0-agent new file mode 160000 index 0000000..8d68a16 --- /dev/null +++ b/chatbotApp-chatbot2.0-agent @@ -0,0 +1 @@ +Subproject commit 8d68a16231925d55e3adf7f3def831f8542597de diff --git a/chatbotApp-chatbot2.0-agent/README.md b/chatbotApp-chatbot2.0-agent/README.md deleted file mode 100644 index 9c79f1c..0000000 --- a/chatbotApp-chatbot2.0-agent/README.md +++ /dev/null @@ -1,109 +0,0 @@ -# chatbotApp — 카카오 인앱·브라우저 단독 챗봇 UI (Next.js + SSE) - -`chatbotAdmin`의 챗봇 테스트 화면과 **별개**인, 고속도로 이용객 대상 **단독 웹 챗봇**입니다. - -| 구분 | 경로 | 기술 | -|------|------|------| -| **chatbotApp (기존)** | `/chatbot/app/` | Next.js 14 · 카카오톡 스타일 · SSE | -| **chatbotApp (AI 모드)** | `/chatbot/ai/` | Next.js 14 · 정부24 AI 스타일 · SSE | -| **chatbotApp (배너)** | `/chatbot/banner/` | 검색창 + 드롭다운 · iframe/embed.js 임베드 | -| **chatbotApp (샘플)** | `/chatbot/sample/` | 국민참여로 UI + banner iframe 데모 | -| chatbotAdmin ChatPage | `/chatbot/admin/` | Admin RAG 디버그 | - -## 운영 URL - -``` -https://chatbot.exdev.co.kr/chatbot/app/ ← 기존 UI -https://chatbot.exdev.co.kr/chatbot/ai/ ← AI 모드 UI -https://chatbot.exdev.co.kr/chatbot/banner/ ← 배너 검색 (임베드) -https://chatbot.exdev.co.kr/chatbot/sample/ ← 국민참여로 샘플 -``` - -## 아키텍처 - -```mermaid -flowchart TD - APP[브라우저/카카오 인앱] -->|/chatbot/ask/stream SSE, /chatbot/ask JSON| SK[kakaoChatbotSkill /web/ask +/stream] - SK --> RAG[RAG :28012 /agent/chat] - RAG --> OUT([answer + 카드 + faqUrls]) -``` - -- **웹 입력 전용 경로**: chatbotApp 질의는 kakaoChatbotSkill의 `/web/ask` 또는 `/web/ask/stream`을 거쳐 RAG Agent(`/agent/chat`)로 전달됩니다. -- **속도**: SSE는 kakao-skill에서 전체 답변 완료 후 `done` 이벤트로 전달 (카카오 callback과 유사). -- **안정성**: JSON API도 유지해 스트림 실패 시 일반 응답을 받을 수 있습니다. -- **FAQ/세션**: `/chatbot/faq/*`, `/chatbot/session/*`는 웹 UI 보조 API입니다. - -### botId · 세션 정책 (현재) - -- 웹앱은 `sessionStorage`에 저장한 랜덤 `botId`(`web-...`, `src/lib/session.ts`)를 사용하며, **대화마다 새 botId로 시작하는 무상태 정책**입니다. -- 카카오톡과 **세션을 연계하지 않으며**, 웹 진입 시 **과거 대화를 화면에 복원하지 않습니다**(`useChatSession`은 빈 상태로 시작, `/web/ask`만 호출). -- 서버(MongoDB `chat_history`)에는 botId별 이력이 남지만, botId가 매번 새로 발급되므로 사실상 새 세션입니다. 정책 배경·향후 세션 연계(서명 토큰) 옵션: [`../SYSTEM_ARCHITECTURE.md`](../SYSTEM_ARCHITECTURE.md), [`../IMPROVEMENTS.md`](../IMPROVEMENTS.md). - -## 로컬 단독 실행 (nginx :8089) - -> **포트:** `8089` = chatbotApp 전용. Caddy HTTP는 `9089` (`CADDY_HTTP_PORT`). -> GoldenChart 로그인이 보이면 `docker stop ex-chatbot-caddy` 후 `./start.sh` 재실행. - -```bash -cd chatbotApp -./start.sh -# → http://localhost:8089/chatbot/app/ -``` - -RAG API(`:28012`)가 기동되어 있어야 질의·SSE가 동작합니다. - -로컬 UI + **원격 API**(기본): `./start.sh` — nginx가 `/chatbot/*`를 `https://chatbot.exdev.co.kr`로 프록시합니다. 로컬 LLM/RAG는 필요 없습니다. - -로컬 RAG로 테스트하려면: - -```bash -CHATBOT_API_UPSTREAM=http://host.docker.internal:28012/chatbot/ \ -CHATBOT_API_HOST=host.docker.internal \ -./start.sh -``` - -## 개발 - -```bash -cd chatbotApp/web -npm install -npm run dev # http://localhost:3000 — /chatbot/* 는 exdev.co.kr 로 프록시 (next.config rewrites) -``` - -## 배포 빌드 - -```bash -cd chatbotApp -chmod +x build.sh -./build.sh # → public/ (VERSION 3.0.0) -``` - -프로젝트 루트에서 RAG 반영: - -```bash -./scripts/reload-chatbot-app.sh -# 또는 -./remote-start-chatbot.sh -``` - -## API - -| Method | Path | 설명 | -|--------|------|------| -| POST | `/chatbot/ask/stream` | SSE 스트리밍 질의 (웹앱 기본) | -| POST | `/chatbot/ask` | JSON 응답 | -| GET | `/chatbot/faq/popular` | FAQ | -| POST | `/chatbot/session/start` | 세션 추적 | - -## 파일 구조 - -``` -chatbotApp/ -├── web/ # Next.js 소스 -│ ├── src/ -│ └── scripts/export-public.mjs -├── public/ # 빌드 산출물 (배포) -├── build.sh -├── nginx/ -└── docker-compose.yml -``` diff --git a/chatbotApp-chatbot2.0-agent/build.sh b/chatbotApp-chatbot2.0-agent/build.sh deleted file mode 100755 index 12d213e..0000000 --- a/chatbotApp-chatbot2.0-agent/build.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bash -# chatbotApp Next.js 빌드 → public/ + public-ai/ + public-banner/ -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -WEB_DIR="${SCRIPT_DIR}/web" -EMBED_SRC="${SCRIPT_DIR}/embed/chatbot-banner.js" - -build_variant() { - local base_path="$1" - local ui_mode="$2" - local export_dir="$3" - - echo "[build] NEXT_PUBLIC_BASE_PATH=${base_path} NEXT_PUBLIC_UI_MODE=${ui_mode} → ${export_dir}/" - ( - cd "$WEB_DIR" - export NEXT_PUBLIC_BASE_PATH="$base_path" - export NEXT_PUBLIC_UI_MODE="$ui_mode" - export NEXT_PUBLIC_CHATBOT_API_ROOT="${NEXT_PUBLIC_CHATBOT_API_ROOT:-/chatbot}" - export EXPORT_TARGET_DIR="$export_dir" - npm run build:deploy - ) -} - -build_variant "/chatbot/app" "app" "public" -build_variant "/chatbot/ai" "ai" "public-ai" -build_variant "/chatbot/banner" "banner" "public-banner" -build_variant "/chatbot/voicebot" "voicebot" "public-voicebot" - -if [[ -f "${EMBED_SRC}" ]]; then - cp "${EMBED_SRC}" "${SCRIPT_DIR}/public-banner/embed.js" - echo "[build] embed.js → public-banner/embed.js" -fi - -SAMPLE_SRC="${SCRIPT_DIR}/sample" -SAMPLE_DEST="${SCRIPT_DIR}/public-sample" -if [[ -d "${SAMPLE_SRC}" ]]; then - rm -rf "${SAMPLE_DEST}" - mkdir -p "${SAMPLE_DEST}" - cp -R "${SAMPLE_SRC}/." "${SAMPLE_DEST}/" - echo "[build] sample/ → public-sample/" -fi - -echo "[build] ✅ chatbotApp 배포 완료" -echo " - /chatbot/app/ → public/" -echo " - /chatbot/ai/ → public-ai/" -echo " - /chatbot/banner/ → public-banner/" -echo " - /chatbot/sample/ → public-sample/" -echo " - /chatbot/voicebot/ → public-voicebot/" diff --git a/chatbotApp-chatbot2.0-agent/docker-compose.yml b/chatbotApp-chatbot2.0-agent/docker-compose.yml deleted file mode 100644 index 744a1a6..0000000 --- a/chatbotApp-chatbot2.0-agent/docker-compose.yml +++ /dev/null @@ -1,29 +0,0 @@ -# chatbotApp 단독 실행 (카카오 인앱·브라우저 UI) -# cd chatbotApp && docker compose up -d -# → http://localhost:8089/chatbot/app/ -# -# 기본: /chatbot/* → https://chatbot.exdev.co.kr (원격 exAiChatBot) -# 로컬 RAG(:28012) 사용: -# CHATBOT_API_UPSTREAM=http://host.docker.internal:28012/chatbot/ \ -# CHATBOT_API_HOST=host.docker.internal docker compose up -d --force-recreate -services: - chatbot-app-web: - image: nginx:1.27-alpine - container_name: ex-chatbot-app-web - restart: unless-stopped - ports: - - "${CHATBOT_APP_PORT:-8089}:80" - extra_hosts: - - "host.docker.internal:host-gateway" - environment: - CHATBOT_API_UPSTREAM: "${CHATBOT_API_UPSTREAM:-https://chatbot.exdev.co.kr/chatbot/}" - CHATBOT_API_HOST: "${CHATBOT_API_HOST:-chatbot.exdev.co.kr}" - volumes: - - ./public:/usr/share/nginx/html:ro - - ./public-ai:/usr/share/nginx/html-ai:ro - - ./public-banner:/usr/share/nginx/html-banner:ro - - ./public-sample:/usr/share/nginx/html-sample:ro - - ./public-voicebot:/usr/share/nginx/html-voicebot:ro - - ./nginx/default.conf.template:/etc/nginx/templates/default.conf.template:ro - - ./nginx/docker-entrypoint.sh:/docker-entrypoint.sh:ro - entrypoint: ["/bin/sh", "/docker-entrypoint.sh"] diff --git a/chatbotApp-chatbot2.0-agent/embed/chatbot-banner.js b/chatbotApp-chatbot2.0-agent/embed/chatbot-banner.js deleted file mode 100644 index dda38fc..0000000 --- a/chatbotApp-chatbot2.0-agent/embed/chatbot-banner.js +++ /dev/null @@ -1,85 +0,0 @@ -/** - * ExChatbotBanner — 타 사이트 임베드용 검색 배너 SDK (iframe) - * @example - *
- * - * - */ -(function (global) { - "use strict"; - - var DEFAULT_BASE = "https://chatbot.exdev.co.kr/chatbot/banner/"; - - function resolveContainer(el) { - if (!el) return null; - if (typeof el === "string") return document.querySelector(el); - return el; - } - - function init(options) { - options = options || {}; - var container = resolveContainer(options.container); - if (!container) { - console.error("[ExChatbotBanner] container not found"); - return null; - } - - var baseUrl = (options.baseUrl || DEFAULT_BASE).replace(/\/?$/, "/"); - var params = new URLSearchParams(); - if (options.embed !== false) params.set("embed", "1"); - if (options.placeholder) params.set("placeholder", String(options.placeholder)); - var qs = params.toString(); - var src = baseUrl + (qs ? "?" + qs : ""); - - var minHeight = Number(options.minHeight) || 56; - var maxHeight = Number(options.maxHeight) || 520; - var height = Number(options.height) || minHeight; - - var wrap = document.createElement("div"); - wrap.className = "ex-chatbot-banner-widget"; - wrap.style.cssText = - "position:relative;width:100%;max-width:" + - (options.maxWidth || "720px") + - ";margin:0 auto;"; - - var iframe = document.createElement("iframe"); - iframe.src = src; - iframe.title = options.title || "한국도로공사 AI 검색"; - iframe.setAttribute("allow", "clipboard-write"); - iframe.loading = options.loading || "lazy"; - iframe.style.cssText = - "display:block;width:100%;border:0;overflow:hidden;min-height:" + - minHeight + - "px;height:" + - height + - "px;transition:height 0.15s ease;"; - - function onMessage(ev) { - if (!ev.data || ev.data.type !== "ex-chatbot-banner-resize") return; - if (ev.source !== iframe.contentWindow) return; - var h = Number(ev.data.height); - if (h > 0) { - iframe.style.height = Math.min(Math.max(h, minHeight), maxHeight) + "px"; - } - } - - global.addEventListener("message", onMessage); - wrap.appendChild(iframe); - container.appendChild(wrap); - - return { - destroy: function () { - global.removeEventListener("message", onMessage); - wrap.remove(); - }, - iframe: iframe, - }; - } - - global.ExChatbotBanner = { - init: init, - version: "1.0.0", - }; -})(typeof window !== "undefined" ? window : globalThis); diff --git a/chatbotApp-chatbot2.0-agent/nginx/default.conf b/chatbotApp-chatbot2.0-agent/nginx/default.conf deleted file mode 100644 index 32ef95f..0000000 --- a/chatbotApp-chatbot2.0-agent/nginx/default.conf +++ /dev/null @@ -1,2 +0,0 @@ -# 이 파일은 참고용입니다. docker compose는 default.conf.template + docker-entrypoint.sh 를 사용합니다. -# 기본 upstream: https://chatbot.exdev.co.kr/chatbot/ diff --git a/chatbotApp-chatbot2.0-agent/nginx/default.conf.template b/chatbotApp-chatbot2.0-agent/nginx/default.conf.template deleted file mode 100644 index 01280cd..0000000 --- a/chatbotApp-chatbot2.0-agent/nginx/default.conf.template +++ /dev/null @@ -1,104 +0,0 @@ -# chatbotApp 단독 웹서버 — Next.js 정적 UI + 챗 API 프록시 -# 기본 upstream: https://chatbot.exdev.co.kr (Caddy → kakaoChatbotSkill /web/ask) -# 로컬 kakao-skill(:8083) 사용 시: -# CHATBOT_API_UPSTREAM=http://host.docker.internal:8083/web/ -# CHATBOT_API_HOST=host.docker.internal -# 레거시 RAG 직접(:28012) 사용 시: -# CHATBOT_API_UPSTREAM=http://host.docker.internal:28012/chatbot/ -# -# Next.js 빌드(basePath=/chatbot/app)의 CSS·JS는 /chatbot/app/_next/ 경로를 사용합니다. -# /chatbot/ API 프록시보다 정적 location을 먼저 두어야 스타일이 적용됩니다. - -map $http_upgrade $connection_upgrade { - default upgrade; - '' close; -} - -server { - listen 80; - server_name _; - root /usr/share/nginx/html; - index index.html; - - client_max_body_size 1m; - - # Next.js 정적 자산 + SPA (운영과 동일 경로 /chatbot/app/, /chatbot/ai/) - location = /chatbot/app { - return 301 /chatbot/app/; - } - - location ^~ /chatbot/app/ { - alias /usr/share/nginx/html/; - index index.html; - add_header Cache-Control "no-cache, no-store, must-revalidate"; - } - - location = /chatbot/ai { - return 301 /chatbot/ai/; - } - - location ^~ /chatbot/ai/ { - alias /usr/share/nginx/html-ai/; - index index.html; - add_header Cache-Control "no-cache, no-store, must-revalidate"; - } - - location = /chatbot/banner { - return 301 /chatbot/banner/; - } - - location ^~ /chatbot/banner/ { - alias /usr/share/nginx/html-banner/; - index index.html; - add_header Cache-Control "no-cache, no-store, must-revalidate"; - add_header Content-Security-Policy "frame-ancestors *" always; - } - - location = /chatbot/sample { - return 301 /chatbot/sample/; - } - - location ^~ /chatbot/sample/ { - alias /usr/share/nginx/html-sample/; - index index.html; - add_header Cache-Control "no-cache, no-store, must-revalidate"; - add_header Permissions-Policy "microphone=*, camera=()" always; - } - - location = /chatbot/voicebot { - return 301 /chatbot/voicebot/; - } - - location ^~ /chatbot/voicebot/ { - alias /usr/share/nginx/html-voicebot/; - index index.html; - add_header Cache-Control "no-cache, no-store, must-revalidate"; - add_header Content-Security-Policy "frame-ancestors *" always; - add_header Permissions-Policy "microphone=*, camera=()" always; - } - - # 루트 접속 → basePath와 동일 URL로 이동 - location = / { - return 302 /chatbot/app/; - } - - # RAG API (ask/stream/faq/session/health 등) — 원격 exAiChatBot 또는 로컬 RAG - location /chatbot/ { - proxy_pass ${CHATBOT_API_UPSTREAM}; - proxy_http_version 1.1; - proxy_ssl_server_name on; - proxy_set_header Host ${CHATBOT_API_HOST}; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_connect_timeout 10s; - proxy_read_timeout 120s; - proxy_send_timeout 120s; - proxy_buffering off; - } - - location / { - try_files $uri $uri/ /index.html; - add_header Cache-Control "no-cache, no-store, must-revalidate"; - } -} diff --git a/chatbotApp-chatbot2.0-agent/nginx/docker-entrypoint.sh b/chatbotApp-chatbot2.0-agent/nginx/docker-entrypoint.sh deleted file mode 100755 index db5b317..0000000 --- a/chatbotApp-chatbot2.0-agent/nginx/docker-entrypoint.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -set -e - -export CHATBOT_API_UPSTREAM="${CHATBOT_API_UPSTREAM:-https://chatbot.exdev.co.kr/chatbot/}" -export CHATBOT_API_HOST="${CHATBOT_API_HOST:-chatbot.exdev.co.kr}" - -envsubst '${CHATBOT_API_UPSTREAM} ${CHATBOT_API_HOST}' \ - < /etc/nginx/templates/default.conf.template \ - > /etc/nginx/conf.d/default.conf - -exec nginx -g 'daemon off;' diff --git a/chatbotApp-chatbot2.0-agent/sample/css/minwon-sample.css b/chatbotApp-chatbot2.0-agent/sample/css/minwon-sample.css deleted file mode 100644 index dad0ae8..0000000 --- a/chatbotApp-chatbot2.0-agent/sample/css/minwon-sample.css +++ /dev/null @@ -1,97 +0,0 @@ -.minwon-page .partcptn-header { - justify-content: space-between; -} - -.minwon-page .partcptn-logo { - text-decoration: none; -} - -.minwon-back-link { - font-size: 13px; - font-weight: 600; - color: #333; - text-decoration: none; -} - -.minwon-back-link:hover { - color: #005bac; -} - -.minwon-main { - flex: 1; - padding: 24px 20px 32px; - max-width: 480px; - margin: 0 auto; - width: 100%; -} - -.minwon-title { - margin: 0 0 10px; - font-size: 24px; - font-weight: 800; - letter-spacing: -0.03em; - color: #111; -} - -.minwon-lead { - margin: 0 0 24px; - font-size: 14px; - line-height: 1.65; - color: #444; -} - -.minwon-cards { - display: flex; - flex-direction: column; - gap: 12px; -} - -.minwon-card { - padding: 18px 16px; - border-radius: 14px; - background: rgba(255, 255, 255, 0.92); - border: 1px solid rgba(0, 0, 0, 0.06); - box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04); -} - -.minwon-card h2 { - margin: 0 0 8px; - font-size: 16px; - font-weight: 700; - color: #111; -} - -.minwon-card p { - margin: 0 0 14px; - font-size: 13px; - line-height: 1.55; - color: #555; -} - -.minwon-card-btn { - border: 1px solid #246beb; - background: #fff; - color: #246beb; - border-radius: 999px; - padding: 8px 16px; - font-size: 13px; - font-weight: 700; - font-family: inherit; - cursor: pointer; -} - -.minwon-card-btn:hover { - background: #eef4ff; -} - -.minwon-note { - margin: 24px 0 0; - font-size: 12px; - line-height: 1.6; - color: #666; -} - -.minwon-note a { - color: #005bac; - font-weight: 600; -} diff --git a/chatbotApp-chatbot2.0-agent/sample/css/partcptn-sample.css b/chatbotApp-chatbot2.0-agent/sample/css/partcptn-sample.css deleted file mode 100644 index 511f074..0000000 --- a/chatbotApp-chatbot2.0-agent/sample/css/partcptn-sample.css +++ /dev/null @@ -1,306 +0,0 @@ -/* 국민참여로 (ex.co.kr/partcptn) 샘플 — 모바일 퍼스트 */ -*, -*::before, -*::after { - box-sizing: border-box; -} - -html, -body { - margin: 0; - padding: 0; - font-family: "Pretendard", "Malgun Gothic", "Apple SD Gothic Neo", sans-serif; - color: #111; - background: #dfe8f0; - line-height: 1.5; - -webkit-font-smoothing: antialiased; -} - -button { - font-family: inherit; -} - -.partcptn-page { - min-height: 100vh; - min-height: 100dvh; - display: flex; - flex-direction: column; - max-width: 480px; - margin: 0 auto; - background: #e8eef4; - box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.04); -} - -.partcptn-header { - position: relative; - z-index: 10; - display: flex; - align-items: center; - justify-content: space-between; - padding: 14px 18px; - background: rgba(255, 255, 255, 0.92); -} - -.partcptn-logo { - display: flex; - align-items: center; - gap: 6px; - font-size: 0; -} - -.partcptn-logo-ex { - font-size: 22px; - font-weight: 900; - letter-spacing: -0.04em; - background: linear-gradient(90deg, #e60012 0%, #005bac 100%); - -webkit-background-clip: text; - background-clip: text; - color: transparent; -} - -.partcptn-logo-text { - font-size: 17px; - font-weight: 800; - color: #111; - letter-spacing: -0.03em; -} - -.partcptn-menu-btn { - display: flex; - align-items: center; - gap: 6px; - border: none; - background: transparent; - font-size: 14px; - font-weight: 600; - color: #333; - cursor: pointer; - padding: 4px 0; -} - -.partcptn-menu-icon { - display: flex; - flex-direction: column; - gap: 4px; - width: 18px; -} - -.partcptn-menu-icon span { - display: block; - height: 2px; - background: #333; - border-radius: 1px; -} - -.partcptn-hero { - position: relative; - flex: 1; - display: flex; - flex-direction: column; - min-height: 0; - overflow: visible; -} - -.partcptn-hero-bg { - position: absolute; - inset: 0; - background: - linear-gradient(180deg, rgba(232, 238, 244, 0.2) 0%, rgba(210, 220, 232, 0.45) 100%), - radial-gradient(ellipse 70% 50% at 50% 55%, rgba(255, 220, 150, 0.35) 0%, transparent 55%), - radial-gradient(ellipse 90% 60% at 50% 40%, rgba(200, 215, 235, 0.9) 0%, rgba(180, 195, 215, 0.6) 100%); - pointer-events: none; -} - -.partcptn-hero-bg::after { - content: ""; - position: absolute; - left: 50%; - top: 42%; - transform: translate(-50%, -50%); - width: 120px; - height: 160px; - background: radial-gradient(ellipse, rgba(255, 240, 200, 0.5) 0%, transparent 70%); - filter: blur(8px); -} - -.partcptn-hero-content { - position: relative; - z-index: 2; - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - padding: 28px 20px 24px; - overflow: visible; -} - -.partcptn-hero-title { - margin: 0 0 16px; - font-size: clamp(22px, 5.5vw, 28px); - font-weight: 800; - line-height: 1.35; - letter-spacing: -0.03em; - color: #111; -} - -.partcptn-hero-sub { - margin: 0 0 28px; - font-size: 14px; - line-height: 1.65; - color: #333; - max-width: 320px; -} - -.partcptn-search-slot { - width: 100%; - max-width: 340px; - background: transparent; - line-height: 0; - overflow: visible; - position: relative; - z-index: 30; -} - -.partcptn-search-slot iframe { - display: block; - width: 100%; - border: 0; - min-height: 48px; - background: transparent !important; - overflow: visible; - vertical-align: top; -} - -.partcptn-search-slot.is-expanded { - margin-bottom: 4px; -} - -.partcptn-page--search-open .partcptn-hero { - flex: 1 1 auto; - min-height: min(72vh, 640px); -} - -.partcptn-bottom-dock { - position: relative; - z-index: 5; - flex-shrink: 0; - display: flex; - align-items: stretch; - justify-content: space-around; - gap: 0; - margin-top: auto; - padding: 18px 12px calc(18px + env(safe-area-inset-bottom, 0px)); - background: rgba(58, 54, 50, 0.62); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border-top: 1px solid rgba(255, 255, 255, 0.06); -} - -.partcptn-dock-item { - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - justify-content: flex-start; - gap: 10px; - min-width: 0; - padding: 4px 8px 2px; - border: none; - background: transparent; - color: #fff; - cursor: pointer; - text-align: center; - transition: opacity 0.15s; -} - -.partcptn-dock-item:hover { - opacity: 0.88; -} - -.partcptn-dock-item:active { - opacity: 0.75; -} - -.partcptn-dock-icon { - width: 44px; - height: 44px; - display: flex; - align-items: center; - justify-content: center; - color: #fff; - flex-shrink: 0; -} - -.partcptn-dock-icon svg { - width: 44px; - height: 44px; - display: block; -} - -.partcptn-dock-label { - display: block; - font-size: 12px; - font-weight: 500; - line-height: 1.35; - letter-spacing: -0.02em; - color: rgba(255, 255, 255, 0.96); - word-break: keep-all; -} - -.sample-chat-overlay { - position: fixed; - inset: 0; - z-index: 9999; - display: none; - align-items: center; - justify-content: center; - padding: 16px; - background: rgba(15, 23, 42, 0.45); -} - -.sample-chat-overlay.is-open { - display: flex; -} - -.sample-chat-panel { - position: relative; - width: min(100%, 420px); - height: min(88vh, 720px); - border-radius: 16px; - overflow: hidden; - background: #fff; - box-shadow: 0 24px 48px rgba(15, 23, 42, 0.2); -} - -.sample-chat-panel iframe { - display: block; - width: 100%; - height: 100%; - border: 0; -} - -.sample-chat-close { - position: absolute; - top: 10px; - right: 10px; - z-index: 2; - width: 36px; - height: 36px; - border: none; - border-radius: 50%; - background: rgba(255, 255, 255, 0.95); - color: #333; - font-size: 22px; - line-height: 1; - cursor: pointer; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12); -} - -@media (min-width: 481px) { - .partcptn-page { - min-height: 100vh; - border-left: 1px solid rgba(0, 0, 0, 0.06); - border-right: 1px solid rgba(0, 0, 0, 0.06); - } -} diff --git a/chatbotApp-chatbot2.0-agent/sample/index.html b/chatbotApp-chatbot2.0-agent/sample/index.html deleted file mode 100644 index 4113e35..0000000 --- a/chatbotApp-chatbot2.0-agent/sample/index.html +++ /dev/null @@ -1,244 +0,0 @@ - - - - - - 국민참여로 - 국민과 함께, 열린 도로, 열린 소통 - - - - -
-
- - -
- -
- - -
-

국민과 함께, 열린 도로, 열린 소통

-

- 국민의 작은 목소리도 정책의 시작이 됩니다.
- 소통하고, 함께 만들어가는 스마트 국민 소통 플랫폼 -

- -
- -
-
-
- - -
- - - - - - diff --git a/chatbotApp-chatbot2.0-agent/sample/minwon/index.html b/chatbotApp-chatbot2.0-agent/sample/minwon/index.html deleted file mode 100644 index 2d64011..0000000 --- a/chatbotApp-chatbot2.0-agent/sample/minwon/index.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - 민원신청 - 국민참여로 - - - - -
-
- - ← 샘플 홈 -
- -
-

민원신청

-

- 고속도로 이용과 관련된 민원을 온라인으로 접수·조회하실 수 있습니다. -

- -
-
-

포장 파손 피해 배상

-

고속도로 포장 파손으로 인한 차량 피해 배상을 신청합니다.

- -
-
-

도로 점용 허가

-

고속도로 구간 내 점용·굴착 등 허가를 신청합니다.

- -
-
-

일반 민원 접수

-

기타 고속도로 관련 민원을 접수합니다.

- -
-
- -

- 본 페이지는 국민참여로 샘플 연동용 데모 화면입니다. 실제 민원 접수는 - 한국도로공사 국민참여로 - 를 이용해 주세요. -

-
-
- - diff --git a/chatbotApp-chatbot2.0-agent/start.sh b/chatbotApp-chatbot2.0-agent/start.sh deleted file mode 100755 index 162a63c..0000000 --- a/chatbotApp-chatbot2.0-agent/start.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env bash -# chatbotApp 단독 웹서버 기동 (nginx + RAG API 프록시) -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR" - -export PATH="/usr/local/bin:/Applications/Docker.app/Contents/Resources/bin:${PATH}" - -PORT="${CHATBOT_APP_PORT:-8089}" -RAG_PORT="${RAG_API_PORT:-28012}" -export CHATBOT_API_UPSTREAM="${CHATBOT_API_UPSTREAM:-https://chatbot.exdev.co.kr/chatbot/}" -export CHATBOT_API_HOST="${CHATBOT_API_HOST:-chatbot.exdev.co.kr}" - -log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; } - -if ! docker info >/dev/null 2>&1; then - log "ERROR: Docker Engine 미기동" - exit 1 -fi - -# :8089 점유 시 Caddy/GoldenChart 등과 충돌 — chatbotApp 전용 포트 확보 -if docker ps --format '{{.Names}} {{.Ports}}' 2>/dev/null | grep -E '8089->8089|:8089->8089' | grep -qv 'ex-chatbot-app-web'; then - log "⚠️ :8089 다른 컨테이너(Caddy 등) 점유 — docker stop ex-chatbot-caddy 후 재시도" -fi - -docker compose up -d --force-recreate - -sleep 2 -APP_URL="http://127.0.0.1:${PORT}/chatbot/app/" -BODY="$(curl -sf --max-time 5 "$APP_URL" 2>/dev/null || true)" - -if echo "$BODY" | grep -q 'data-chatbot-app="next-sse"'; then - log "✅ chatbotApp UI: ${APP_URL}" -elif echo "$BODY" | grep -qi 'GoldenChart'; then - log "❌ GoldenChart 화면 — :${PORT} 포트 충돌" - log " 1) docker stop ex-chatbot-caddy (CADDY_HTTP_PORT는 9089 권장)" - log " 2) cd chatbotApp && docker compose up -d --force-recreate" - log " 3) ${APP_URL} 재접속" - exit 1 -else - log "⚠️ UI 미응답 — docker logs ex-chatbot-app-web" -fi - -if curl -sf --max-time 10 "http://127.0.0.1:${PORT}/chatbot/health" >/dev/null 2>&1; then - log "✅ API 프록시 연결 가능 → ${CHATBOT_API_UPSTREAM}" -else - log "⚠️ API 프록시 미응답 — 원격 ${CHATBOT_API_UPSTREAM} 또는 네트워크 확인" -fi - -if curl -sf --max-time 5 "http://127.0.0.1:${RAG_PORT}/chatbot/health" >/dev/null 2>&1; then - log "ℹ️ 로컬 RAG :${RAG_PORT} 기동 중 (프록시 대상 변경 시 CHATBOT_API_UPSTREAM 사용)" -fi - -log "원격 API: ${CHATBOT_API_UPSTREAM}" -log "운영 URL (Caddy): https://chatbot.exdev.co.kr/chatbot/app/" diff --git a/chatbotApp-chatbot2.0-agent/web/.gitignore b/chatbotApp-chatbot2.0-agent/web/.gitignore deleted file mode 100644 index 7833851..0000000 --- a/chatbotApp-chatbot2.0-agent/web/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules/ -.next/ -out/ -*.tsbuildinfo diff --git a/chatbotApp-chatbot2.0-agent/web/next-env.d.ts b/chatbotApp-chatbot2.0-agent/web/next-env.d.ts deleted file mode 100644 index 40c3d68..0000000 --- a/chatbotApp-chatbot2.0-agent/web/next-env.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -/// -/// - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/chatbotApp-chatbot2.0-agent/web/next.config.mjs b/chatbotApp-chatbot2.0-agent/web/next.config.mjs deleted file mode 100644 index 3ee623c..0000000 --- a/chatbotApp-chatbot2.0-agent/web/next.config.mjs +++ /dev/null @@ -1,29 +0,0 @@ -/** @type {import('next').NextConfig} */ -const basePath = process.env.NEXT_PUBLIC_BASE_PATH || ""; - -const remoteApiOrigin = ( - process.env.CHATBOT_API_UPSTREAM || - process.env.NEXT_PUBLIC_CHATBOT_API_BASE || - "https://chatbot.exdev.co.kr" -).replace(/\/chatbot\/?$/, ""); - -const nextConfig = { - output: "export", - basePath, - assetPrefix: basePath || undefined, - trailingSlash: true, - reactStrictMode: true, - poweredByHeader: false, - images: { unoptimized: true }, - async rewrites() { - if (process.env.NODE_ENV !== "development") return []; - return [ - { - source: "/chatbot/:path*", - destination: `${remoteApiOrigin}/chatbot/:path*`, - }, - ]; - }, -}; - -export default nextConfig; diff --git a/chatbotApp-chatbot2.0-agent/web/package-lock.json b/chatbotApp-chatbot2.0-agent/web/package-lock.json deleted file mode 100644 index fcd5c3c..0000000 --- a/chatbotApp-chatbot2.0-agent/web/package-lock.json +++ /dev/null @@ -1,1993 +0,0 @@ -{ - "name": "chatbot-app-web", - "version": "3.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "chatbot-app-web", - "version": "3.0.0", - "dependencies": { - "next": "^14.2.29", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-markdown": "^9.0.3", - "remark-gfm": "^4.0.1" - }, - "devDependencies": { - "@types/node": "^20.14.0", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "typescript": "^5.4.5" - } - }, - "node_modules/@next/env": { - "version": "14.2.35", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz", - "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==", - "license": "MIT" - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz", - "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz", - "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz", - "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz", - "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz", - "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz", - "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz", - "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz", - "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz", - "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "license": "Apache-2.0" - }, - "node_modules/@swc/helpers": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", - "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.3", - "tslib": "^2.4.0" - } - }, - "node_modules/@types/debug": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", - "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "license": "MIT" - }, - "node_modules/@types/estree-jsx": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", - "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.19.43", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", - "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.31", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", - "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.2.tgz", - "integrity": "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==", - "license": "ISC" - }, - "node_modules/bail": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001799", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", - "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/ccount": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-html4": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", - "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" - }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decode-named-character-reference": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", - "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", - "license": "MIT", - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/estree-util-is-identifier-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", - "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/hast-util-to-jsx-runtime": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", - "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-js": "^1.0.0", - "unist-util-position": "^5.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-whitespace": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", - "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/html-url-attributes": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", - "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/inline-style-parser": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", - "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", - "license": "MIT" - }, - "node_modules/is-alphabetical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-decimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/longest-streak": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/markdown-table": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", - "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/mdast-util-find-and-replace": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", - "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "escape-string-regexp": "^5.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", - "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark": "^4.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", - "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", - "license": "MIT", - "dependencies": { - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-gfm-autolink-literal": "^2.0.0", - "mdast-util-gfm-footnote": "^2.0.0", - "mdast-util-gfm-strikethrough": "^2.0.0", - "mdast-util-gfm-table": "^2.0.0", - "mdast-util-gfm-task-list-item": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-autolink-literal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", - "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "ccount": "^2.0.0", - "devlop": "^1.0.0", - "mdast-util-find-and-replace": "^3.0.0", - "micromark-util-character": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-footnote": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-strikethrough": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", - "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-table": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", - "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "markdown-table": "^3.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-task-list-item": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", - "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", - "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-jsx": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", - "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdxjs-esm": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", - "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-phrasing": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", - "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast": { - "version": "13.2.1", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", - "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@ungap/structured-clone": "^1.0.0", - "devlop": "^1.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "trim-lines": "^3.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", - "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^4.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "unist-util-visit": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", - "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", - "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-destination": "^2.0.0", - "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-title": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-gfm": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", - "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", - "license": "MIT", - "dependencies": { - "micromark-extension-gfm-autolink-literal": "^2.0.0", - "micromark-extension-gfm-footnote": "^2.0.0", - "micromark-extension-gfm-strikethrough": "^2.0.0", - "micromark-extension-gfm-table": "^2.0.0", - "micromark-extension-gfm-tagfilter": "^2.0.0", - "micromark-extension-gfm-task-list-item": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-autolink-literal": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", - "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-footnote": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-strikethrough": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", - "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-table": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", - "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-tagfilter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", - "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-task-list-item": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", - "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-factory-destination": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", - "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", - "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", - "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", - "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-chunked": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", - "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-classify-character": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", - "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", - "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", - "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", - "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", - "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-html-tag-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", - "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", - "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", - "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", - "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", - "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/next": { - "version": "14.2.35", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz", - "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==", - "license": "MIT", - "dependencies": { - "@next/env": "14.2.35", - "@swc/helpers": "0.5.5", - "busboy": "1.6.0", - "caniuse-lite": "^1.0.30001579", - "graceful-fs": "^4.2.11", - "postcss": "8.4.31", - "styled-jsx": "5.1.1" - }, - "bin": { - "next": "dist/bin/next" - }, - "engines": { - "node": ">=18.17.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.33", - "@next/swc-darwin-x64": "14.2.33", - "@next/swc-linux-arm64-gnu": "14.2.33", - "@next/swc-linux-arm64-musl": "14.2.33", - "@next/swc-linux-x64-gnu": "14.2.33", - "@next/swc-linux-x64-musl": "14.2.33", - "@next/swc-win32-arm64-msvc": "14.2.33", - "@next/swc-win32-ia32-msvc": "14.2.33", - "@next/swc-win32-x64-msvc": "14.2.33" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.41.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@playwright/test": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/parse-entities": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", - "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/parse-entities/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/property-information": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", - "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-markdown": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz", - "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "hast-util-to-jsx-runtime": "^2.0.0", - "html-url-attributes": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.0.0", - "unified": "^11.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "@types/react": ">=18", - "react": ">=18" - } - }, - "node_modules/remark-gfm": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", - "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-gfm": "^3.0.0", - "micromark-extension-gfm": "^3.0.0", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-parse": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", - "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-rehype": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", - "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "mdast-util-to-hast": "^13.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-stringify": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", - "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-to-markdown": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/space-separated-tokens": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/stringify-entities": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", - "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", - "license": "MIT", - "dependencies": { - "character-entities-html4": "^2.0.0", - "character-entities-legacy": "^3.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/style-to-js": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", - "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", - "license": "MIT", - "dependencies": { - "style-to-object": "1.0.14" - } - }, - "node_modules/style-to-object": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", - "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", - "license": "MIT", - "dependencies": { - "inline-style-parser": "0.2.7" - } - }, - "node_modules/styled-jsx": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", - "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", - "license": "MIT", - "dependencies": { - "client-only": "0.0.1" - }, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/trim-lines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trough": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/unified": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", - "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "bail": "^2.0.0", - "devlop": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-is": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", - "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", - "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", - "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", - "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/zwitch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - } - } -} diff --git a/chatbotApp-chatbot2.0-agent/web/package.json b/chatbotApp-chatbot2.0-agent/web/package.json deleted file mode 100644 index 96b989d..0000000 --- a/chatbotApp-chatbot2.0-agent/web/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "chatbot-app-web", - "private": true, - "version": "3.0.0", - "scripts": { - "dev": "next dev -p 3000", - "build": "next build", - "build:deploy": "next build && node scripts/export-public.mjs", - "lint": "next lint", - "start": "next start" - }, - "dependencies": { - "next": "^14.2.29", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-markdown": "^9.0.3", - "remark-gfm": "^4.0.1" - }, - "devDependencies": { - "@types/node": "^20.14.0", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "typescript": "^5.4.5" - } -} diff --git a/chatbotApp-chatbot2.0-agent/web/public/images/giltong.jpg b/chatbotApp-chatbot2.0-agent/web/public/images/giltong.jpg deleted file mode 100644 index 9dd0509c5b26e8df809431794421eae0c9b7d34b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 51769 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

m6#0FH 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 diff --git a/chatbotApp-chatbot2.0-agent/web/scripts/export-public.mjs b/chatbotApp-chatbot2.0-agent/web/scripts/export-public.mjs deleted file mode 100644 index e3e27a0..0000000 --- a/chatbotApp-chatbot2.0-agent/web/scripts/export-public.mjs +++ /dev/null @@ -1,48 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const scriptDir = path.dirname(fileURLToPath(import.meta.url)); -const webRoot = path.resolve(scriptDir, ".."); -const outRoot = path.join(webRoot, "out"); -const sourceDir = outRoot; -const targetName = process.env.EXPORT_TARGET_DIR || "public"; -const targetDir = path.join(webRoot, "..", targetName); -const basePath = process.env.NEXT_PUBLIC_BASE_PATH || "/chatbot/app"; -const uiMode = process.env.NEXT_PUBLIC_UI_MODE || "app"; - -if (!fs.existsSync(sourceDir)) { - console.error(`[export-public] 빌드 출력 없음: ${sourceDir}`); - console.error("NEXT_PUBLIC_BASE_PATH=/chatbot/app npm run build:deploy"); - process.exit(1); -} - -function copyRecursive(src, dest) { - fs.mkdirSync(dest, { recursive: true }); - for (const entry of fs.readdirSync(src, { withFileTypes: true })) { - const from = path.join(src, entry.name); - const to = path.join(dest, entry.name); - if (entry.isDirectory()) { - copyRecursive(from, to); - } else { - fs.copyFileSync(from, to); - } - } -} - -for (const name of fs.existsSync(targetDir) ? fs.readdirSync(targetDir) : []) { - const p = path.join(targetDir, name); - if (name === ".gitkeep") continue; - fs.rmSync(p, { recursive: true, force: true }); -} - -copyRecursive(sourceDir, targetDir); - -const versionPath = path.join(targetDir, "VERSION"); -const version = fs.existsSync(versionPath) - ? fs.readFileSync(versionPath, "utf8").trim() - : "3.0.0"; -const stamp = `${version} (${uiMode}@${basePath})`; -fs.writeFileSync(versionPath, `${stamp}\n`, "utf8"); - -console.log(`[export-public] ✅ ${sourceDir} → ${targetDir} (${stamp})`); diff --git a/chatbotApp-chatbot2.0-agent/web/src/app/globals.css b/chatbotApp-chatbot2.0-agent/web/src/app/globals.css deleted file mode 100644 index a9159f4..0000000 --- a/chatbotApp-chatbot2.0-agent/web/src/app/globals.css +++ /dev/null @@ -1,1095 +0,0 @@ -:root { - --kakao-bg: #b2c7d9; - --kakao-yellow: #fee500; - --bot-bubble: #ffffff; - --text: #111111; - --subtext: #555555; - --time: #6b7b8c; - --border: rgba(0, 0, 0, 0.06); - --safe-top: env(safe-area-inset-top, 0px); - --safe-bottom: env(safe-area-inset-bottom, 0px); - --composer-h: 58px; - --bubble-max: min(78vw, 520px); -} - -* { - box-sizing: border-box; -} - -html, -body { - margin: 0; - width: 100%; - height: 100%; - height: -webkit-fill-available; - overflow: hidden; - background: var(--kakao-bg); - font-family: -apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Noto Sans KR", sans-serif; - color: var(--text); - -webkit-text-size-adjust: 100%; -} - -.app { - display: flex; - flex-direction: column; - width: 100%; - max-width: none; - height: 100%; - height: 100vh; - height: 100dvh; - height: -webkit-fill-available; - min-height: 100vh; - min-height: 100dvh; - min-height: -webkit-fill-available; - margin: 0; - background: var(--kakao-bg); -} - -.app-header { - flex-shrink: 0; - display: flex; - align-items: center; - gap: 12px; - padding: calc(10px + var(--safe-top)) clamp(12px, 3vw, 32px) 8px; - background: var(--kakao-bg); -} - -.app-header-brand { - flex: 1; - min-width: 0; - overflow: hidden; -} - -.app-header-actions { - display: flex; - align-items: center; - gap: 8px; - flex-shrink: 0; - margin-left: auto; -} - -.app-btn-close { - display: inline-flex; - align-items: center; - justify-content: center; - width: 36px; - height: 36px; - padding: 0; - border: none; - border-radius: 50%; - background: rgba(255, 255, 255, 0.92); - color: #333; - cursor: pointer; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12); -} - -.app-btn-close:active { - transform: scale(0.96); -} - -.chat { - flex: 1; - min-height: 0; - overflow-y: auto; - overflow-x: hidden; - -webkit-overflow-scrolling: touch; - padding: calc(12px + var(--safe-top)) clamp(12px, 3vw, 32px) 12px; - scroll-behavior: smooth; -} - -.msg-row { - display: flex; - margin-bottom: clamp(10px, 2vh, 16px); -} - -.msg-row.user { - justify-content: flex-end; -} - -.msg-row.bot { - justify-content: flex-start; - align-items: flex-end; - gap: clamp(6px, 1.5vw, 10px); -} - -.bot-avatar { - width: clamp(34px, 8vw, 42px); - height: clamp(34px, 8vw, 42px); - border-radius: 14px; - background: #fff; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - overflow: hidden; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); -} - -.bot-avatar-img { - width: 100%; - height: 100%; - object-fit: cover; - display: block; - border-radius: 14px; -} - -.bot-content { - max-width: min(calc(100% - 52px), calc(100% - clamp(34px, 8vw, 42px) - 16px)); -} - -.bot-name { - margin: 0 0 4px 2px; -} - -.bubble { - position: relative; - padding: clamp(8px, 2vw, 12px) clamp(10px, 2.5vw, 14px); - border-radius: 16px; - font-size: clamp(14px, 3.5vw, 16px); - line-height: 1.55; - white-space: pre-wrap; - word-break: break-word; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); -} - -.bubble.user { - background: var(--kakao-yellow); - border-top-right-radius: 4px; - max-width: var(--bubble-max); -} - -.bubble.bot { - background: var(--bot-bubble); - border-top-left-radius: 4px; - max-width: min(85vw, 560px); -} - -.bubble.bot.bubble--rich { - white-space: normal; - padding: clamp(10px, 2.5vw, 14px) clamp(12px, 3vw, 16px); - line-height: 1.65; -} - -/* ── ChatGPT / Gemini 스타일 마크다운 ── */ -.bubble-body.md-content { - display: flex; - flex-direction: column; - gap: 0.15em; - font-size: clamp(14px, 3.5vw, 16px); - color: #0d0d0d; -} - -.md-content > * + * { - margin-top: 0.1em; -} - -.md-p { - margin: 0 0 0.75em; - line-height: 1.72; - letter-spacing: -0.01em; -} - -.md-p:last-child { - margin-bottom: 0; -} - -.md-p--compact { - margin-bottom: 0.4em; -} - -.md-p--compact:last-child { - margin-bottom: 0; -} - -.md-h { - margin: 1em 0 0.45em; - font-weight: 700; - line-height: 1.35; - letter-spacing: -0.02em; - color: #0d0d0d; -} - -.md-h:first-child { - margin-top: 0; -} - -.md-h1 { font-size: 1.12em; } -.md-h2 { font-size: 1.06em; } -.md-h3, .md-h4 { font-size: 1em; } - -.md-strong { - font-weight: 700; - color: #0d0d0d; -} - -.md-em { - font-style: italic; -} - -.md-inline-code { - font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; - font-size: 0.88em; - padding: 0.12em 0.38em; - border-radius: 5px; - background: #f4f4f4; - color: #c7254e; - word-break: break-word; -} - -.md-link { - color: #007aff; - text-decoration: none; - word-break: break-all; -} - -.md-link:active { - opacity: 0.75; -} - -.md-list { - margin: 0.25em 0 0.75em; - padding-left: 1.4em; -} - -.md-list--bullet { - list-style: disc; -} - -.md-list--ordered { - list-style: decimal; -} - -.md-list--nested { - margin: 0.35em 0 0.15em; - padding-left: 1.15em; -} - -.md-li { - margin: 0.4em 0; - padding-left: 0.15em; - line-height: 1.65; -} - -.md-li::marker { - color: #444; - font-weight: 600; -} - -.md-blockquote { - margin: 0.65em 0; - padding: 0.55em 0 0.55em 0.85em; - border-left: 3px solid #d0d0d0; - color: #444; - background: linear-gradient(90deg, rgba(0, 0, 0, 0.03) 0%, transparent 100%); - border-radius: 0 6px 6px 0; -} - -.md-hr { - border: none; - border-top: 1px solid #e5e5e5; - margin: 0.85em 0; -} - -.md-code-wrap { - margin: 0.65em 0 0.85em; - border-radius: 10px; - overflow: hidden; - background: #f7f7f8; - border: 1px solid #ececec; -} - -.md-code-lang { - padding: 6px 12px; - font-size: 0.72em; - font-weight: 600; - color: #666; - background: #efefef; - border-bottom: 1px solid #e5e5e5; - text-transform: lowercase; -} - -.md-pre { - margin: 0; - padding: 12px 14px; - overflow-x: auto; - -webkit-overflow-scrolling: touch; - font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; - font-size: 0.82em; - line-height: 1.55; - color: #1a1a1a; - white-space: pre-wrap; - word-break: break-word; -} - -.md-pre code { - font-family: inherit; - background: none; - padding: 0; - color: inherit; -} - -.bubble-body { - display: flex; - flex-direction: column; -} - -.msg-time { - margin-top: 4px; - font-size: clamp(10px, 2.2vw, 12px); - color: var(--time); -} - -.msg-row.user .msg-time { - text-align: right; -} - -.msg-row.bot .msg-time { - margin-left: 2px; -} - -.bubble.loading { - color: var(--subtext); -} - -.typing { - display: inline-flex; - gap: 4px; - align-items: center; - margin-left: 4px; -} - -.typing span { - width: 6px; - height: 6px; - border-radius: 50%; - background: #8a96a3; - animation: bounce 1.2s infinite ease-in-out; -} - -.typing span:nth-child(2) { animation-delay: 0.15s; } -.typing span:nth-child(3) { animation-delay: 0.3s; } - -@keyframes bounce { - 0%, 80%, 100% { transform: translateY(0); opacity: 0.45; } - 40% { transform: translateY(-4px); opacity: 1; } -} - -/* ── 입력창 위 관련 질문 (app 모드) ── */ -.related-questions-bar--app { - flex-shrink: 0; - border-top: 1px solid var(--border); - background: #eef2f6; - padding: 10px clamp(10px, 3vw, 24px) 8px; -} - -.related-questions-bar__head { - margin: 0 0 8px; - font-size: 12px; - font-weight: 700; - color: var(--subtext); - letter-spacing: -0.01em; -} - -.related-questions-bar__scroll { - display: flex; - gap: 8px; - overflow-x: auto; - -webkit-overflow-scrolling: touch; - scrollbar-width: none; - padding-bottom: 2px; -} - -.related-questions-bar__scroll::-webkit-scrollbar { - display: none; -} - -.related-questions-bar__chip { - flex-shrink: 0; - border: 1px solid #d8dce3; - background: #fff; - color: var(--text); - border-radius: 999px; - padding: 8px 14px; - font-size: 13px; - line-height: 1.35; - cursor: pointer; - white-space: nowrap; - max-width: min(85vw, 320px); - overflow: hidden; - text-overflow: ellipsis; -} - -.related-questions-bar__chip:hover:not(:disabled) { - background: #f5f7fa; - border-color: #c5cad3; -} - -.related-questions-bar__chip:disabled { - opacity: 0.55; - cursor: not-allowed; -} - -.composer { - position: relative; - z-index: 10; - flex-shrink: 0; - overflow: visible; - padding: clamp(8px, 2vw, 12px) clamp(10px, 3vw, 24px) calc(clamp(8px, 2vw, 12px) + var(--safe-bottom)); - background: #f6f6f6; - border-top: 1px solid var(--border); -} - -.composer form { - display: flex; - gap: clamp(6px, 1.5vw, 10px); - align-items: center; - width: 100%; - max-width: none; -} - -.composer input { - flex: 1; - min-width: 0; - height: clamp(40px, 10vw, 44px); - border: 1.5px solid #007aff; - border-radius: 999px; - padding: 0 clamp(14px, 3.5vw, 18px); - font-size: clamp(14px, 3.5vw, 16px); - background: #fff; - outline: none; -} - -.composer input:focus { - border-color: #007aff; -} - -.composer input:disabled { - background: #fafafa; - border-color: #c8c8c8; -} - -.btn-send { - flex-shrink: 0; - display: inline-flex; - align-items: center; - justify-content: center; - height: clamp(36px, 9vw, 40px); - min-width: clamp(56px, 15vw, 68px); - padding: 0 clamp(16px, 4vw, 20px); - border: none; - border-radius: 999px; - font-size: clamp(14px, 3.5vw, 16px); - font-weight: 500; - font-family: inherit; - line-height: 1; - cursor: default; - transition: background 0.15s ease, color 0.15s ease; - background: #ececec; - color: #a8a8a8; -} - -.btn-send.is-ready { - background: #007aff; - color: #fff; - cursor: pointer; -} - -.btn-send.is-ready:active { - transform: scale(0.97); -} - -.faq-wrap { - position: relative; - flex-shrink: 0; -} - -.btn-faq { - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - width: clamp(40px, 10vw, 44px); - height: clamp(40px, 10vw, 44px); - border: none; - border-radius: 50%; - cursor: pointer; - background: #fff; - color: #007aff; - border: 1.5px solid #007aff; - transition: transform 0.15s ease, background 0.15s ease, color 0.15s ease; -} - -.btn-faq:active, -.btn-faq.is-open { - transform: scale(0.96); - background: #007aff; - color: #fff; -} - -.btn-faq:disabled { - opacity: 0.45; - cursor: not-allowed; -} - -.faq-panel[hidden], -.faq-panel.faq-panel--closed { - display: none !important; -} - -.faq-panel { - position: fixed; - left: 50%; - right: auto; - transform: translateX(-50%); - bottom: calc(72px + var(--safe-bottom)); - width: min(92vw, 360px); - max-height: min(62vh, 520px); - background: #fff; - border-radius: 14px; - box-shadow: 0 8px 28px rgba(0, 0, 0, 0.18); - border: 1px solid rgba(0, 0, 0, 0.08); - display: flex; - flex-direction: column; - overflow: hidden; - z-index: 1000; -} - -.faq-panel-head { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 14px; - border-bottom: 1px solid #eee; - font-size: 0.92rem; - font-weight: 700; - color: #222; - flex-shrink: 0; -} - -.faq-close { - border: none; - background: transparent; - font-size: 1.4rem; - line-height: 1; - color: #888; - cursor: pointer; - padding: 0 4px; -} - -.faq-body { - overflow-y: auto; - -webkit-overflow-scrolling: touch; - padding: 8px 10px 12px; -} - -.faq-loading, -.faq-empty { - margin: 12px 8px; - font-size: 0.85rem; - color: #666; -} - -.faq-section + .faq-section { - margin-top: 10px; -} - -.faq-cat { - margin: 8px 6px 6px; - font-size: 0.78rem; - font-weight: 700; - color: #007aff; - letter-spacing: -0.01em; -} - -.faq-list { - list-style: none; - margin: 0; - padding: 0; -} - -.faq-item { - display: block; - width: 100%; - text-align: left; - border: none; - background: #f7f9fc; - border-radius: 10px; - padding: 10px 12px; - margin-bottom: 6px; - font-size: 0.84rem; - line-height: 1.45; - color: #222; - cursor: pointer; - font-family: inherit; -} - -.faq-item:active { - background: #e8f2ff; -} - -.btn-voice { - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - width: clamp(40px, 10vw, 44px); - height: clamp(40px, 10vw, 44px); - border: none; - border-radius: 50%; - cursor: pointer; - transition: transform 0.15s ease, background 0.15s ease, box-shadow 0.15s ease; - background: #fff; - color: #444; - border: 1px solid #ddd; -} - -.btn-voice:active { - transform: scale(0.96); -} - -.btn-send:disabled, -.btn-voice:disabled, -.composer input:disabled { - opacity: 1; -} - -.btn-voice:disabled, -.composer input:disabled { - opacity: 0.55; - cursor: not-allowed; -} - -.btn-voice.listening { - background: #ff4d4f; - border-color: #ff4d4f; - color: #fff; - box-shadow: 0 0 0 4px rgba(255, 77, 79, 0.25); - animation: pulse-mic 1.2s ease-in-out infinite; -} - -.btn-voice.unsupported { - opacity: 0.35; - cursor: not-allowed; -} - -@keyframes pulse-mic { - 0%, 100% { box-shadow: 0 0 0 4px rgba(255, 77, 79, 0.2); } - 50% { box-shadow: 0 0 0 8px rgba(255, 77, 79, 0.12); } -} - -.welcome-card { - margin: 8px 0 clamp(14px, 3vh, 20px); - padding: clamp(12px, 3vw, 16px); - background: rgba(255, 255, 255, 0.72); - border-radius: 14px; - font-size: clamp(13px, 3.2vw, 15px); - line-height: 1.6; - color: #333; - max-width: 100%; -} - -/* ── Kakao ListCard 스타일 (주유·휴게소 등) ── */ -.list-card-stack { - display: flex; - flex-direction: column; - gap: 10px; - margin-top: 8px; - max-width: min(85vw, 360px); -} - -.action-buttons { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-top: 8px; - max-width: min(85vw, 360px); -} - -.action-button { - display: inline-flex; - align-items: center; - justify-content: center; - min-height: 34px; - padding: 8px 12px; - border: 1px solid rgba(0, 122, 255, 0.28); - border-radius: 999px; - background: #fff; - color: #007aff; - font-size: 13px; - font-weight: 700; - line-height: 1.2; - text-decoration: none; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); -} - -.action-button:active { - transform: scale(0.98); -} - -.list-card { - background: #fff; - border-radius: 14px; - border: 1px solid rgba(0, 0, 0, 0.06); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06); - overflow: hidden; -} - -.list-card__title { - margin: 0; - padding: 12px 14px 8px; - font-size: 15px; - font-weight: 700; - line-height: 1.4; - color: #111; -} - -.list-card__items { - list-style: none; - margin: 0; - padding: 0 10px 10px; -} - -.list-card__row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - padding: 10px 4px; - border-top: 1px solid rgba(0, 0, 0, 0.05); -} - -.list-card__row-link { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - width: 100%; - color: inherit; - text-decoration: none; -} - -.list-card__row-link:active { - opacity: 0.72; -} - -.list-card__row:first-child { - border-top: none; -} - -.list-card__meta { - min-width: 0; - flex: 1; -} - -.list-card__price { - font-size: 17px; - font-weight: 700; - color: #111; - line-height: 1.3; -} - -.list-card__location { - margin-top: 2px; - font-size: 12px; - color: #777; - line-height: 1.3; -} - -.list-card__fuel { - flex-shrink: 0; - width: 52px; - height: 52px; - border-radius: 10px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 2px; - background: #f5f5f5; - color: #333; -} - -.list-card__fuel-icon { - font-size: 16px; - line-height: 1; -} - -.list-card__thumb { - width: 36px; - height: 36px; - object-fit: contain; - display: block; -} - -.list-card__fuel-label { - font-size: 10px; - font-weight: 700; - line-height: 1.1; -} - -.list-card__fuel--gasoline { - background: #1a1a1a; - color: #fff; -} - -.list-card__fuel--diesel { - background: #f58220; - color: #fff; -} - -.list-card__fuel--lpg { - background: #f5c518; - color: #222; -} - -.list-card__fuel--electric { - background: #007aff; - color: #fff; -} - -.list-card__fuel--hydrogen { - background: #34c759; - color: #fff; -} - -.list-card__fuel--food { - background: #ff9500; - color: #fff; -} - -.list-card__fuel--food-soup, -.list-card__fuel--food-rice, -.list-card__fuel--food-meat, -.list-card__fuel--food-fish, -.list-card__fuel--food-noodle, -.list-card__fuel--food-coffee, -.list-card__fuel--food-bread, -.list-card__fuel--food-drink, -.list-card__fuel--food-premium, -.list-card__fuel--food-recommend { - background: #ff9500; - color: #fff; -} - -.list-card__fuel--facility, -.list-card__fuel--facility-nursery, -.list-card__fuel--facility-shower, -.list-card__fuel--facility-pharmacy, -.list-card__fuel--facility-carwash, -.list-card__fuel--facility-repair { - background: #5856d6; - color: #fff; -} - -.list-card__fuel--shop, -.list-card__fuel--theme, -.list-card__fuel--recruit { - background: #007aff; - color: #fff; -} - -.list-card__fuel--road-smooth { - background: #34c759; - color: #fff; -} - -.list-card__fuel--road-slow { - background: #ffcc00; - color: #222; -} - -.list-card__fuel--road-congestion, -.list-card__fuel--road-accident { - background: #ff3b30; - color: #fff; -} - -.list-card__fuel--road-work { - background: #ff9500; - color: #fff; -} - -.list-card__fuel--unknown { - background: #8e8e93; - color: #fff; -} - -/* ── 유사 질문 Quick Reply (카카오톡 스타일) ── */ -.quick-reply-wrap { - margin-top: 10px; - max-width: min(85vw, 360px); -} - -.quick-reply-title { - margin: 0 0 8px; - font-size: 12px; - font-weight: 600; - color: #666; -} - -.quick-reply-list { - display: flex; - flex-wrap: wrap; - gap: 8px; -} - -.quick-reply-chip { - display: inline-flex; - align-items: center; - max-width: 100%; - padding: 8px 12px; - border: 1px solid #d8dce3; - border-radius: 18px; - background: #fff; - color: #222; - font-size: 13px; - line-height: 1.35; - text-align: left; - cursor: pointer; - font-family: inherit; - transition: background 0.15s ease, border-color 0.15s ease; -} - -.quick-reply-chip:hover:not(:disabled) { - background: #fff9db; - border-color: #fee500; -} - -.quick-reply-chip:active:not(:disabled) { - background: #fee500; -} - -.quick-reply-chip:disabled { - opacity: 0.55; - cursor: not-allowed; -} - -@media (min-width: 1024px) { - :root { - --bubble-max: min(520px, 45vw); - } - - .chat { - padding-left: max(32px, 8vw); - padding-right: max(32px, 8vw); - } - - .composer form { - max-width: 1200px; - margin: 0 auto; - } -} - -.bot-avatar-fallback { - font-size: 0.72rem; - font-weight: 700; - color: #007aff; -} - -.loading-text { - color: var(--subtext); -} - -.md-content p { - margin: 0 0 0.75em; - line-height: 1.72; -} - -.md-content p:last-child { - margin-bottom: 0; -} - -.md-content ul, -.md-content ol { - margin: 0.25em 0 0.75em; - padding-left: 1.4em; -} - -.md-content li { - margin: 0.4em 0; - line-height: 1.65; -} - -.suggestion-block { - margin-top: 0.85em; -} - -.suggestion-block__title { - margin: 0 0 0.65em; - font-weight: 500; -} - -.suggestion-block__list { - list-style: none; - margin: 0.25em 0 0.75em; - padding: 0; -} - -.suggestion-block__list li { - margin: 0.28em 0; -} - -.suggestion-block__button { - display: inline-flex; - align-items: flex-start; - gap: 0.35em; - max-width: 100%; - border: 0; - padding: 0.1em 0; - background: transparent; - color: inherit; - font: inherit; - line-height: 1.65; - text-align: left; - cursor: pointer; -} - -.suggestion-block__button:hover { - color: #006ee6; - text-decoration: underline; - text-underline-offset: 3px; -} - -.suggestion-block__button:disabled { - cursor: default; - text-decoration: none; -} - -.suggestion-block__index { - flex-shrink: 0; -} - -.md-content h1, -.md-content h2, -.md-content h3, -.md-content h4 { - margin: 1em 0 0.45em; - font-weight: 700; - line-height: 1.35; -} - -.md-content h1:first-child, -.md-content h2:first-child, -.md-content h3:first-child { - margin-top: 0; -} - -.md-content a { - color: #007aff; - word-break: break-all; -} - -.md-content code { - font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; - font-size: 0.88em; - padding: 0.12em 0.38em; - border-radius: 5px; - background: #f4f4f4; -} - -.faq-category-title { - margin: 8px 6px 6px; - font-size: 0.78rem; - font-weight: 700; - color: #007aff; -} diff --git a/chatbotApp-chatbot2.0-agent/web/src/app/layout.tsx b/chatbotApp-chatbot2.0-agent/web/src/app/layout.tsx deleted file mode 100644 index 76a75e9..0000000 --- a/chatbotApp-chatbot2.0-agent/web/src/app/layout.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import type { Metadata, Viewport } from "next"; -import { getUiMode } from "@/lib/uiMode"; -import "./globals.css"; - -const uiMode = getUiMode(); - -const titles: Record = { - app: "ex AI Chatbot 2.0", - ai: "ex AI Chatbot 2.0", - banner: "ex AI Chatbot 2.0", - voicebot: "ex AI Chatbot 2.0", -}; - -const descriptions: Record = { - app: "고속도로 이용 관련 AI 상담", - ai: "고속도로·휴게소·통행료 AI 상담 (AI 모드)", - banner: "검색창 + 드롭다운 AI 검색 (임베드용)", - voicebot: "음성으로 고속도로 이용 AI 상담", -}; - -export const metadata: Metadata = { - title: titles[uiMode] ?? titles.app, - description: descriptions[uiMode] ?? descriptions.app, -}; - -export const viewport: Viewport = { - width: "device-width", - initialScale: 1, - viewportFit: "cover", - themeColor: uiMode === "app" ? "#b2c7d9" : "#246beb", -}; - -export default function RootLayout({ children }: { children: React.ReactNode }) { - const htmlClass = - uiMode === "ai" - ? "ai-mode" - : uiMode === "banner" - ? "banner-mode" - : uiMode === "voicebot" - ? "voicebot-mode" - : undefined; - const bodyAttr = - uiMode === "ai" - ? "ai-mode" - : uiMode === "banner" - ? "banner-mode" - : uiMode === "voicebot" - ? "voicebot-mode" - : "next-sse"; - - return ( - - {children} - - ); -} diff --git a/chatbotApp-chatbot2.0-agent/web/src/app/page.tsx b/chatbotApp-chatbot2.0-agent/web/src/app/page.tsx deleted file mode 100644 index 2515c8c..0000000 --- a/chatbotApp-chatbot2.0-agent/web/src/app/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { AiChatApp } from "@/components/ai/AiChatApp"; -import { BannerChatApp } from "@/components/banner/BannerChatApp"; -import { ChatApp } from "@/components/ChatApp"; -import { VoicebotChatApp } from "@/components/voicebot/VoicebotChatApp"; -import { getUiMode } from "@/lib/uiMode"; - -export default function Page() { - const mode = getUiMode(); - if (mode === "banner") return ; - if (mode === "ai") return ; - if (mode === "voicebot") return ; - return ; -} diff --git a/chatbotApp-chatbot2.0-agent/web/src/components/ActionButtonList.tsx b/chatbotApp-chatbot2.0-agent/web/src/components/ActionButtonList.tsx deleted file mode 100644 index f7c9cb5..0000000 --- a/chatbotApp-chatbot2.0-agent/web/src/components/ActionButtonList.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import type { WebActionButton } from "@/lib/types"; - -interface ActionButtonListProps { - buttons?: WebActionButton[]; -} - -function resolveHref(button: WebActionButton): string | undefined { - if (button.action === "phone") { - const number = button.phoneNumber?.replace(/[^0-9+]/g, ""); - return number ? `tel:${number}` : button.url; - } - return button.url || button.webUrl || button.pcUrl || button.mobileUrl || button.androidUrl || button.iosUrl; -} - -function isExternalHref(href: string): boolean { - return /^https?:\/\//i.test(href); -} - -export function ActionButtonList({ buttons }: ActionButtonListProps) { - const resolvedButtons = (buttons ?? []) - .map((button) => ({ button, href: resolveHref(button) })) - .filter((item): item is { button: WebActionButton; href: string } => Boolean(item.href)); - - if (resolvedButtons.length === 0) { - return null; - } - - return ( -

- {resolvedButtons.map(({ button, href }, index) => ( - - {button.label || "바로가기"} - - ))} -
- ); -} diff --git a/chatbotApp-chatbot2.0-agent/web/src/components/ChatApp.tsx b/chatbotApp-chatbot2.0-agent/web/src/components/ChatApp.tsx deleted file mode 100644 index e3e93f1..0000000 --- a/chatbotApp-chatbot2.0-agent/web/src/components/ChatApp.tsx +++ /dev/null @@ -1,65 +0,0 @@ -"use client"; - -import { ExBrandTitle } from "@/components/ExBrandTitle"; -import { ChatCloseButton } from "@/components/ChatCloseButton"; -import { Composer } from "@/components/Composer"; -import { FaqPanel } from "@/components/FaqPanel"; -import { MessageList } from "@/components/MessageList"; -import { RelatedQuestionsBar } from "@/components/RelatedQuestionsBar"; -import { useEmbedShell } from "@/hooks/useEmbedShell"; -import { useChatSession } from "@/hooks/useChatSession"; -import { getLatestAssistantQuickReplies } from "@/lib/relatedQuestions"; -import { WELCOME_TEXT } from "@/lib/config"; - -export function ChatApp() { - const { - messages, - busy, - faqOpen, - faqLoading, - faqCategories, - faqSupported, - sendMessage, - toggleFaq, - setFaqOpen, - } = useChatSession(); - - const embedded = useEmbedShell(); - const hasConversation = messages.length > 0; - const relatedQuestions = getLatestAssistantQuickReplies(messages); - - return ( -
-
- - {embedded ? ( -
- -
- ) : null} -
- void sendMessage(text)} /> - {hasConversation && relatedQuestions.length > 0 ? ( - void sendMessage(text)} - /> - ) : null} - void sendMessage(text)} - onFaqToggle={toggleFaq} - /> - setFaqOpen(false)} - onSelect={(question) => void sendMessage(question)} - /> -
- ); -} diff --git a/chatbotApp-chatbot2.0-agent/web/src/components/ChatCloseButton.tsx b/chatbotApp-chatbot2.0-agent/web/src/components/ChatCloseButton.tsx deleted file mode 100644 index ca88ce1..0000000 --- a/chatbotApp-chatbot2.0-agent/web/src/components/ChatCloseButton.tsx +++ /dev/null @@ -1,29 +0,0 @@ -"use client"; - -import { requestChatClose } from "@/lib/embedShell"; - -interface ChatCloseButtonProps { - className?: string; - label?: string; -} - -export function ChatCloseButton({ - className = "ai-btn-close", - label = "종료", -}: ChatCloseButtonProps) { - return ( - - ); -} diff --git a/chatbotApp-chatbot2.0-agent/web/src/components/Composer.tsx b/chatbotApp-chatbot2.0-agent/web/src/components/Composer.tsx deleted file mode 100644 index 6266504..0000000 --- a/chatbotApp-chatbot2.0-agent/web/src/components/Composer.tsx +++ /dev/null @@ -1,137 +0,0 @@ -"use client"; - -import { FormEvent, useCallback, useEffect, useRef, useState } from "react"; - -interface ComposerProps { - busy: boolean; - faqEnabled?: boolean; - onSend: (text: string) => void; - onFaqToggle: () => void; -} - -export function Composer({ busy, faqEnabled = true, onSend, onFaqToggle }: ComposerProps) { - const [value, setValue] = useState(""); - const [listening, setListening] = useState(false); - const recognitionRef = useRef(null); - const voiceBaseRef = useRef(""); - - const speechSupported = - typeof window !== "undefined" && - !!(window.SpeechRecognition || window.webkitSpeechRecognition); - - useEffect(() => { - if (!speechSupported) return; - const Ctor = window.SpeechRecognition || window.webkitSpeechRecognition; - if (!Ctor) return; - const recognition = new Ctor(); - recognition.lang = "ko-KR"; - recognition.interimResults = true; - recognition.continuous = false; - recognition.maxAlternatives = 1; - - recognition.onstart = () => setListening(true); - recognition.onend = () => setListening(false); - recognition.onerror = () => setListening(false); - recognition.onresult = (event: SpeechRecognitionEvent) => { - let transcript = ""; - for (let i = event.resultIndex; i < event.results.length; i += 1) { - transcript += event.results[i][0].transcript; - } - setValue(voiceBaseRef.current + transcript.trim()); - }; - - recognitionRef.current = recognition; - return () => { - try { - recognition.stop(); - } catch { - /* noop */ - } - }; - }, [speechSupported]); - - const submit = useCallback( - (e?: FormEvent) => { - e?.preventDefault(); - const text = value.trim(); - if (!text || busy) return; - onSend(text); - setValue(""); - voiceBaseRef.current = ""; - }, - [busy, onSend, value], - ); - - const toggleVoice = () => { - const recognition = recognitionRef.current; - if (!recognition || busy) return; - if (listening) { - recognition.stop(); - return; - } - voiceBaseRef.current = value.trim() ? `${value.trim()} ` : ""; - try { - recognition.start(); - } catch { - /* noop */ - } - }; - - return ( -
-
- setValue(e.target.value)} - /> -
- {faqEnabled ? ( - - ) : null} -
- -
-
- ); -} - -declare global { - interface Window { - SpeechRecognition: typeof SpeechRecognition; - webkitSpeechRecognition: typeof SpeechRecognition; - } -} diff --git a/chatbotApp-chatbot2.0-agent/web/src/components/ExBrandTitle.tsx b/chatbotApp-chatbot2.0-agent/web/src/components/ExBrandTitle.tsx deleted file mode 100644 index fa2ab8d..0000000 --- a/chatbotApp-chatbot2.0-agent/web/src/components/ExBrandTitle.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { CHATBOT_BRAND_SUFFIX } from "@/lib/brand"; -import "@/styles/ex-brand.css"; - -type ExBrandTitleProps = { - as?: "h1" | "h2" | "div" | "span"; - className?: string; - size?: "md" | "sm"; - variant?: "light" | "dark"; - showDivider?: boolean; -}; - -export function ExBrandTitle({ - as: Tag = "div", - className = "", - size = "md", - variant = "light", - showDivider = true, -}: ExBrandTitleProps) { - const classes = [ - "ex-brand", - variant === "dark" ? "ex-brand--dark" : "", - size === "sm" ? "ex-brand--sm" : "", - className, - ] - .filter(Boolean) - .join(" "); - - return ( - - - {showDivider ? - ); -} diff --git a/chatbotApp-chatbot2.0-agent/web/src/components/FaqPanel.tsx b/chatbotApp-chatbot2.0-agent/web/src/components/FaqPanel.tsx deleted file mode 100644 index 910eb0a..0000000 --- a/chatbotApp-chatbot2.0-agent/web/src/components/FaqPanel.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import type { FaqCategory } from "@/lib/types"; - -interface FaqPanelProps { - open: boolean; - loading: boolean; - categories: FaqCategory[]; - onClose: () => void; - onSelect: (question: string) => void; -} - -export function FaqPanel({ open, loading, categories, onClose, onSelect }: FaqPanelProps) { - return ( - - ); -} diff --git a/chatbotApp-chatbot2.0-agent/web/src/components/FareResultCard.tsx b/chatbotApp-chatbot2.0-agent/web/src/components/FareResultCard.tsx deleted file mode 100644 index 99ef553..0000000 --- a/chatbotApp-chatbot2.0-agent/web/src/components/FareResultCard.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { formatFareWon, type ParsedFareResult } from "@/lib/fareResultCard"; -import "@/styles/fare-result-card.css"; - -interface FareResultCardProps { - data: ParsedFareResult; -} - -function SectionIcon({ kind }: { kind: "kec" | "private" }) { - return ( - - ); -} - -function VehicleIcon({ imageUrl, label }: { imageUrl?: string; label: string }) { - if (imageUrl) { - return ; - } - return ( - - ); -} - -export function FareResultCard({ data }: FareResultCardProps) { - const showPrivateSection = data.privateFare > 0; - - return ( -
-

{data.introText}

- -
- 총 통행료 - {formatFareWon(data.primaryFare)} - {data.routeLabel} -
- -
-
-
- - 도로공사 구간 요금 -
- {formatFareWon(data.kecFare)} -
- {showPrivateSection ? ( -
-
- - 민자도로 구간 요금 -
- - {formatFareWon(data.privateFare)} - -
- ) : null} -
- -
-

차종별 요금 안내

-
    - {data.vehicles.map((vehicle) => ( -
  • -
    -
    - - {vehicle.typeLabel} -
    - - {formatFareWon(vehicle.amount)} - -
    - {vehicle.privateAmount > 0 ? ( -

    - 도로공사 {formatFareWon(vehicle.kec)} · 민자 {formatFareWon(vehicle.privateAmount)} -

    - ) : null} -
  • - ))} -
-
- -

* 차종별 편도 기준 (1종 승용차 기준)

-
- ); -} diff --git a/chatbotApp-chatbot2.0-agent/web/src/components/ListCardStack.tsx b/chatbotApp-chatbot2.0-agent/web/src/components/ListCardStack.tsx deleted file mode 100644 index 52527dd..0000000 --- a/chatbotApp-chatbot2.0-agent/web/src/components/ListCardStack.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { FareResultCard } from "@/components/FareResultCard"; -import { - isFareListCard, - parseFareListCard, -} from "@/lib/fareResultCard"; -import { - LIST_CARD_ICON, - LIST_CARD_LABEL, - listCardItemKindFromImageUrl, - type WebListCard, -} from "@/lib/listCards"; - -interface ListCardPanelProps { - card: WebListCard; -} - -export function ListCardPanel({ card }: ListCardPanelProps) { - if (!card.items?.length) { - return null; - } - - return ( -
- {card.title ?

{card.title}

: null} -
    - {card.items.map((item, index) => { - const kind = listCardItemKindFromImageUrl(item.imageUrl); - const label = LIST_CARD_LABEL[kind]; - const fallbackIcon = LIST_CARD_ICON[kind]; - const key = `${item.title}-${item.description ?? ""}-${index}`; - const content = ( - <> -
    -
    {item.title}
    - {item.description ? ( -
    {item.description}
    - ) : null} -
    - - - ); - - return ( -
  • - {item.linkUrl ? ( - - {content} - - ) : ( - content - )} -
  • - ); - })} -
-
- ); -} - -interface ListCardStackProps { - cards?: WebListCard[]; -} - -export function ListCardStack({ cards }: ListCardStackProps) { - if (!cards?.length) { - return null; - } - - const hasFare = cards.some(isFareListCard); - const stackClass = hasFare ? "list-card-stack list-card-stack--fare" : "list-card-stack"; - - return ( -
- {cards.map((card, index) => { - if (isFareListCard(card)) { - const parsed = parseFareListCard(card); - if (parsed) { - return ; - } - } - return ; - })} -
- ); -} diff --git a/chatbotApp-chatbot2.0-agent/web/src/components/MarkdownBody.tsx b/chatbotApp-chatbot2.0-agent/web/src/components/MarkdownBody.tsx deleted file mode 100644 index 97b394d..0000000 --- a/chatbotApp-chatbot2.0-agent/web/src/components/MarkdownBody.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import ReactMarkdown from "react-markdown"; -import remarkGfm from "remark-gfm"; - -interface MarkdownBodyProps { - text: string; - onSuggestionSelect?: (question: string) => void; -} - -const SUGGESTION_MARKER = "혹시 이런 것을 찾으셨나요"; -const SUGGESTION_LINE = /^\s*(\d+)[.)]\s*(.+?)\s*$/; - -type MarkdownSection = - | { type: "markdown"; text: string } - | { type: "suggestions"; title: string; items: { index: string; question: string }[]; footer?: string }; - -function parseSuggestionSections(text: string): MarkdownSection[] { - const lines = text.split(/\r?\n/); - const sections: MarkdownSection[] = []; - let markdownLines: string[] = []; - let suggestionTitle = ""; - let suggestionItems: { index: string; question: string }[] = []; - let suggestionFooter: string[] = []; - let inSuggestions = false; - - const flushMarkdown = () => { - const value = markdownLines.join("\n").trim(); - if (value) sections.push({ type: "markdown", text: value }); - markdownLines = []; - }; - - const flushSuggestions = () => { - if (suggestionItems.length > 0) { - sections.push({ - type: "suggestions", - title: suggestionTitle || "💡 혹시 이런 것을 찾으셨나요?", - items: suggestionItems, - footer: suggestionFooter.join("\n").trim() || undefined, - }); - } - suggestionTitle = ""; - suggestionItems = []; - suggestionFooter = []; - }; - - for (const line of lines) { - const trimmed = line.trim(); - if (!inSuggestions && trimmed.includes(SUGGESTION_MARKER)) { - flushMarkdown(); - inSuggestions = true; - suggestionTitle = trimmed; - continue; - } - - if (inSuggestions) { - const match = trimmed.match(SUGGESTION_LINE); - if (match) { - suggestionItems.push({ index: match[1], question: match[2] }); - continue; - } - if (trimmed) { - suggestionFooter.push(line); - } - continue; - } - - markdownLines.push(line); - } - - if (inSuggestions) { - flushSuggestions(); - } else { - flushMarkdown(); - } - - return sections; -} - -export function MarkdownBody({ text, onSuggestionSelect }: MarkdownBodyProps) { - const sections = parseSuggestionSections(text); - - return ( -
- {sections.map((section, index) => { - if (section.type === "markdown") { - return ( - - {section.text} - - ); - } - - return ( -
-

{section.title}

-
    - {section.items.map((item) => ( -
  1. - -
  2. - ))} -
- {section.footer ? ( - {section.footer} - ) : null} -
- ); - })} -
- ); -} diff --git a/chatbotApp-chatbot2.0-agent/web/src/components/MessageBubble.tsx b/chatbotApp-chatbot2.0-agent/web/src/components/MessageBubble.tsx deleted file mode 100644 index cc06fa2..0000000 --- a/chatbotApp-chatbot2.0-agent/web/src/components/MessageBubble.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { ListCardStack } from "@/components/ListCardStack"; -import { ExBrandTitle } from "@/components/ExBrandTitle"; -import { hasFareListCards } from "@/lib/fareResultCard"; -import type { ChatMessage } from "@/lib/types"; -import { MarkdownBody } from "./MarkdownBody"; - -interface MessageBubbleProps { - message: ChatMessage; - onSuggestionSelect?: (question: string) => void; -} - -export function MessageBubble({ message, onSuggestionSelect }: MessageBubbleProps) { - if (message.role === "user") { - return ( -
-
-
{message.text}
-
{message.time}
-
-
- ); - } - - const showFareCard = !message.streaming && hasFareListCards(message.listCards); - const showText = Boolean(message.text) && !showFareCard; - - return ( -
- -
- - {(message.streaming && !message.text && !message.listCards?.length) || showText ? ( -
- {message.streaming && !message.text && !message.listCards?.length ? ( - - {message.statusText || "답변준비중…"} -
- ) : null} - {!message.streaming ? : null} -
{message.time}
-
-
- ); -} diff --git a/chatbotApp-chatbot2.0-agent/web/src/components/MessageList.tsx b/chatbotApp-chatbot2.0-agent/web/src/components/MessageList.tsx deleted file mode 100644 index 00b8b98..0000000 --- a/chatbotApp-chatbot2.0-agent/web/src/components/MessageList.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { useEffect, useRef } from "react"; -import type { ChatMessage } from "@/lib/types"; -import { MessageBubble } from "./MessageBubble"; - -interface MessageListProps { - messages: ChatMessage[]; - welcome: string; - onSuggestionSelect?: (question: string) => void; -} - -export function MessageList({ messages, welcome, onSuggestionSelect }: MessageListProps) { - const chatRef = useRef(null); - - useEffect(() => { - const el = chatRef.current; - if (!el) return; - el.scrollTop = el.scrollHeight; - }, [messages]); - - return ( -
-
{welcome}
- {messages.map((message) => ( - - ))} -
- ); -} diff --git a/chatbotApp-chatbot2.0-agent/web/src/components/QuickReplyChips.tsx b/chatbotApp-chatbot2.0-agent/web/src/components/QuickReplyChips.tsx deleted file mode 100644 index 3969828..0000000 --- a/chatbotApp-chatbot2.0-agent/web/src/components/QuickReplyChips.tsx +++ /dev/null @@ -1,34 +0,0 @@ -"use client"; - -import type { WebQuickReply } from "@/lib/types"; - -interface QuickReplyChipsProps { - items?: WebQuickReply[]; - disabled?: boolean; - onSelect: (message: string) => void; -} - -export function QuickReplyChips({ items, disabled, onSelect }: QuickReplyChipsProps) { - if (!items?.length) { - return null; - } - - return ( -
-

이런 질문도 있어요

-
- {items.map((item) => ( - - ))} -
-
- ); -} diff --git a/chatbotApp-chatbot2.0-agent/web/src/components/RelatedQuestionsBar.tsx b/chatbotApp-chatbot2.0-agent/web/src/components/RelatedQuestionsBar.tsx deleted file mode 100644 index ed49fd4..0000000 --- a/chatbotApp-chatbot2.0-agent/web/src/components/RelatedQuestionsBar.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import type { WebQuickReply } from "@/lib/types"; - -interface RelatedQuestionsBarProps { - items: WebQuickReply[]; - busy: boolean; - onSelect: (message: string) => void; - variant?: "ai" | "app"; -} - -export function RelatedQuestionsBar({ - items, - busy, - onSelect, - variant = "ai", -}: RelatedQuestionsBarProps) { - if (!items.length) { - return null; - } - - const isAi = variant === "ai"; - - return ( - - ); -} diff --git a/chatbotApp-chatbot2.0-agent/web/src/components/ai/AiChatApp.tsx b/chatbotApp-chatbot2.0-agent/web/src/components/ai/AiChatApp.tsx deleted file mode 100644 index 38effa4..0000000 --- a/chatbotApp-chatbot2.0-agent/web/src/components/ai/AiChatApp.tsx +++ /dev/null @@ -1,89 +0,0 @@ -"use client"; - -import { useEffect } from "react"; -import "@/styles/ai-mode.css"; -import { FaqPanel } from "@/components/FaqPanel"; -import { useChatSession } from "@/hooks/useChatSession"; -import { ChatCloseButton } from "@/components/ChatCloseButton"; -import { ExBrandTitle } from "@/components/ExBrandTitle"; -import { RelatedQuestionsBar } from "@/components/RelatedQuestionsBar"; -import { useEmbedShell } from "@/hooks/useEmbedShell"; -import { getLatestAssistantQuickReplies } from "@/lib/relatedQuestions"; -import { AiComposer } from "./AiComposer"; -import { AiMessageList } from "./AiMessageList"; - -export function AiChatApp() { - const { - messages, - busy, - faqOpen, - faqLoading, - faqCategories, - faqSupported, - sendMessage, - toggleFaq, - loadFaq, - setFaqOpen, - } = useChatSession(); - - const embedded = useEmbedShell(); - const hasConversation = messages.length > 0; - const relatedQuestions = getLatestAssistantQuickReplies(messages); - - useEffect(() => { - if (faqSupported) void loadFaq(); - }, [faqSupported, loadFaq]); - - return ( -
-
- -
- {faqSupported ? ( - - ) : null} - -
-
- - void sendMessage(text)} - /> - - {hasConversation && relatedQuestions.length > 0 ? ( - void sendMessage(question)} - /> - ) : null} - - void sendMessage(text)} /> - - setFaqOpen(false)} - onSelect={(question) => void sendMessage(question)} - /> -
- ); -} diff --git a/chatbotApp-chatbot2.0-agent/web/src/components/ai/AiComposer.tsx b/chatbotApp-chatbot2.0-agent/web/src/components/ai/AiComposer.tsx deleted file mode 100644 index b7ca612..0000000 --- a/chatbotApp-chatbot2.0-agent/web/src/components/ai/AiComposer.tsx +++ /dev/null @@ -1,127 +0,0 @@ -"use client"; - -import { FormEvent, KeyboardEvent, useCallback, useRef, useState } from "react"; -import { useSpeechRecognition } from "@/hooks/useSpeechRecognition"; -import { AiVoiceOverlay } from "./AiVoiceOverlay"; - -interface AiComposerProps { - busy: boolean; - showFooterNote?: boolean; - onSend: (text: string) => void; -} - -export function AiComposer({ busy, showFooterNote = true, onSend }: AiComposerProps) { - const [value, setValue] = useState(""); - const textareaRef = useRef(null); - - const resizeTextarea = useCallback(() => { - const el = textareaRef.current; - if (!el) return; - el.style.height = "auto"; - el.style.height = `${Math.min(el.scrollHeight, 120)}px`; - }, []); - - const handleVoiceComplete = useCallback( - (text: string) => { - setValue(text); - requestAnimationFrame(resizeTextarea); - textareaRef.current?.focus(); - }, - [resizeTextarea], - ); - - const { active: voiceActive, interimText, speaking, error: voiceError, start, cancel, supported } = - useSpeechRecognition({ onComplete: handleVoiceComplete }); - - const submit = useCallback( - (e?: FormEvent) => { - e?.preventDefault(); - const text = value.trim(); - if (!text || busy) return; - onSend(text); - setValue(""); - requestAnimationFrame(resizeTextarea); - }, - [busy, onSend, resizeTextarea, value], - ); - - const onKeyDown = (e: KeyboardEvent) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - submit(); - } - }; - - const openVoiceInput = () => { - if (busy || voiceActive) return; - start(value); - }; - - const canSend = value.trim().length > 0 && !busy; - - return ( - <> -