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 033e24c..0000000 Binary files a/chatbotApi-chatbot2.0-agent/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/chatbotApi-chatbot2.0-agent/gradle/wrapper/gradle-wrapper.properties b/chatbotApi-chatbot2.0-agent/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 9355b41..0000000 --- a/chatbotApi-chatbot2.0-agent/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/chatbotApi-chatbot2.0-agent/gradlew b/chatbotApi-chatbot2.0-agent/gradlew deleted file mode 100755 index fcb6fca..0000000 --- a/chatbotApi-chatbot2.0-agent/gradlew +++ /dev/null @@ -1,248 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - 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 9dd0509..0000000 Binary files a/chatbotApp-chatbot2.0-agent/web/public/images/giltong.jpg and /dev/null differ 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 ( - <> -