#!/usr/bin/env bash # 로컬 전체 스택 기동 (exdev AI SSH 터널 + 원격 Mongo/Qdrant + RAG + WAS + Skill + chatbotApp) # Mongo/Qdrant 는 stock.exdev.co.kr 를 사용 (로컬 컨테이너 미기동) # # ./local-dev/start-all.sh # ./local-dev/start-all.sh --no-tunnel # ./local-dev/start-all.sh --skip-java # ./local-dev/start-all.sh --no-build # # 종료: ./local-dev/stop-all.sh set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" LOCAL_DEV_DIR="${ROOT_DIR}/local-dev" RUN_DIR="${LOCAL_DEV_DIR}/run" LOG_DIR="${LOCAL_DEV_DIR}/logs" RAG_DIR="${ROOT_DIR}/exAiChatBot-chatbot2.0-agent" API_DIR="${ROOT_DIR}/chatbotApi-chatbot2.0-agent" SKILL_DIR="${ROOT_DIR}/kakaoChatbotSkill-chatbot2.0-agent" APP_DIR="${ROOT_DIR}/chatbotApp-chatbot2.0-agent" ENV_TEMPLATE="${LOCAL_DEV_DIR}/env.exdev-ai.template" RAG_ENV="${RAG_DIR}/.env" COMPOSE_LOCAL="${RAG_DIR}/docker-compose.local.yml" EXDEV_DATA_HOST="${EXDEV_DATA_HOST:-stock.exdev.co.kr}" INTERNAL_TOOL_API_KEY="${INTERNAL_TOOL_API_KEY:-local-dev-key}" MONGODB_URI="${MONGODB_URI:-mongodb://exDev:ehfhrhdtk!!@${EXDEV_DATA_HOST}:27017/chatbot?authSource=chatbot&heartbeatFrequencyMS=30000&minPoolSize=3&maxIdleTimeMS=1800000}" LLM_PORT="${LLM_PORT:-16000}" EMBED_PORT="${EMBED_PORT:-16001}" RERANK_PORT="${RERANK_PORT:-16002}" RAG_PORT="${RAG_API_PORT:-28012}" ADMIN_PORT="${ADMIN_PORT:-28013}" WAS_PORT="${CHATBOT_API_PORT:-8086}" SKILL_PORT="${KAKAO_SKILL_PORT:-8083}" APP_PORT="${CHATBOT_APP_PORT:-8089}" MONGO_PORT="${MONGO_PORT:-27017}" QDRANT_PORT="${QDRANT_PORT:-6333}" SKIP_TUNNEL=false SKIP_JAVA=false NO_BUILD=false for arg in "$@"; do case "$arg" in --no-tunnel) SKIP_TUNNEL=true ;; --skip-java) SKIP_JAVA=true ;; --no-build) NO_BUILD=true ;; --sync-qdrant) echo "경고: 원격 Qdrant 사용 중이라 --sync-qdrant 는 무시됩니다." ;; -h|--help) sed -n '2,12p' "$0" exit 0 ;; *) echo "알 수 없는 옵션: $arg (--help)" exit 1 ;; esac done export PATH="/opt/homebrew/bin:/usr/local/bin:/Applications/Docker.app/Contents/Resources/bin:${PATH}" log() { echo "[$(date '+%H:%M:%S')] $*"; } fail() { log "ERROR: $*"; exit 1; } wait_http() { local url="$1" label="$2" max="${3:-90}" local i=0 while (( i < max )); do if curl -sf --max-time 3 "$url" >/dev/null 2>&1; then log "✅ ${label}" return 0 fi sleep 2 i=$((i + 2)) done log "⚠️ ${label} 타임아웃 (${url})" return 1 } wait_port() { local port="$1" label="$2" max="${3:-90}" local i=0 while (( i < max )); do if lsof -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1; then log "✅ ${label} (:${port})" return 0 fi sleep 2 i=$((i + 2)) done log "⚠️ ${label} 타임아웃 (:${port})" return 1 } ai_tunnel_ready() { curl -sf --max-time 3 "http://127.0.0.1:${LLM_PORT}/v1/models" >/dev/null 2>&1 \ && curl -sf --max-time 3 "http://127.0.0.1:${EMBED_PORT}/health" >/dev/null 2>&1 \ && curl -sf --max-time 3 -X POST "http://127.0.0.1:${RERANK_PORT}/rerank" \ -H "Content-Type: application/json" \ -d '{"query":"ping","documents":["doc"]}' >/dev/null 2>&1 } start_tunnel() { if ai_tunnel_ready; then log "exdev AI 터널 이미 연결됨 (:${LLM_PORT}~:${RERANK_PORT})" return 0 fi log "exdev AI SSH 터널 백그라운드 기동..." ssh -f -N \ -o ServerAliveInterval=30 \ -o ExitOnForwardFailure=yes \ -L "${LLM_PORT}:127.0.0.1:${LLM_PORT}" \ -L "${EMBED_PORT}:127.0.0.1:${EMBED_PORT}" \ -L "${RERANK_PORT}:127.0.0.1:${RERANK_PORT}" \ "${EXDEV_USER:-aidev}@${EXDEV_HOST:-chatbot.exdev.co.kr}" sleep 1 pgrep -f "ssh -f -N.*${LLM_PORT}:127.0.0.1:${LLM_PORT}" | head -1 > "${RUN_DIR}/tunnel.pid" || true local i=0 while (( i < 60 )); do if ai_tunnel_ready; then log "✅ exdev AI 터널 (LLM/Embed/Rerank)" return 0 fi sleep 2 i=$((i + 2)) done fail "exdev AI 터널 실패 — exdev에서 start-host-ai.sh 실행 여부 확인" } patch_env() { local key="$1" value="$2" local tmp="${RAG_ENV}.tmp.$$" if [[ -f "${RAG_ENV}" ]]; then grep -v "^${key}=" "${RAG_ENV}" > "${tmp}" || true else : > "${tmp}" fi printf '%s\n%s=%s\n' "$(cat "${tmp}")" "${key}" "${value}" > "${RAG_ENV}" rm -f "${tmp}" } ensure_rag_env() { if [[ ! -f "${RAG_ENV}" ]]; then log ".env 없음 → env.exdev-ai.template 복사" cp "${ENV_TEMPLATE}" "${RAG_ENV}" fi # Docker 컨테이너 → Mac 호스트(SSH 터널) 접근 patch_env "LLM_BASE_URL" "http://host.docker.internal:${LLM_PORT}" patch_env "TEI_EMBED_URL" "http://host.docker.internal:${EMBED_PORT}" patch_env "TEI_RERANK_URL" "http://host.docker.internal:${RERANK_PORT}" patch_env "EMBED_API_STYLE" "tei" patch_env "RERANK_API_STYLE" "legacy" patch_env "CHATBOT_API_BASE_URL" "http://host.docker.internal:${WAS_PORT}/api" patch_env "INTERNAL_TOOL_API_KEY" "${INTERNAL_TOOL_API_KEY}" # Mongo / Qdrant → stock.exdev.co.kr (원격) patch_env "EXDEV_DATA_HOST" "${EXDEV_DATA_HOST}" patch_env "MONGO_HOST" "${EXDEV_DATA_HOST}" patch_env "MONGO_PORT" "${MONGO_PORT}" patch_env "MONGO_USER" "" patch_env "MONGO_PASSWORD" "" patch_env "MONGO_DATABASE" "chat_history" patch_env "QDRANT_HOST" "${EXDEV_DATA_HOST}" patch_env "QDRANT_PORT" "${QDRANT_PORT}" patch_env "QDRANT_COLLECTION" "qa_vectors_v2" } ensure_remote_data() { log "원격 데이터 확인: ${EXDEV_DATA_HOST} (Mongo :${MONGO_PORT}, Qdrant :${QDRANT_PORT})" if ! curl -sf --max-time 5 "http://${EXDEV_DATA_HOST}:${QDRANT_PORT}/collections" >/dev/null 2>&1; then fail "원격 Qdrant 접속 실패 — http://${EXDEV_DATA_HOST}:${QDRANT_PORT}" fi log "✅ 원격 Qdrant (${EXDEV_DATA_HOST}:${QDRANT_PORT})" if command -v nc >/dev/null 2>&1; then if ! nc -z -w 5 "${EXDEV_DATA_HOST}" "${MONGO_PORT}" >/dev/null 2>&1; then fail "원격 MongoDB 접속 실패 — ${EXDEV_DATA_HOST}:${MONGO_PORT}" fi fi log "✅ 원격 MongoDB (${EXDEV_DATA_HOST}:${MONGO_PORT})" } ensure_rag_compose() { local template="${LOCAL_DEV_DIR}/docker-compose.rag.local.yml" [[ -f "${template}" ]] || fail "local-dev/docker-compose.rag.local.yml 없음" cp "${template}" "${COMPOSE_LOCAL}" } start_rag() { ensure_rag_env ensure_rag_compose [[ -f "${COMPOSE_LOCAL}" ]] || fail "docker-compose.local.yml 없음" cd "${RAG_DIR}" local -a up_flags=(-d) if $NO_BUILD; then up_flags+=(--no-build) else up_flags+=(--build) fi # 이전 slim/legacy 잔여 정리 docker compose -f docker-compose-slim.yml down --remove-orphans 2>/dev/null || true # 로컬 Qdrant 잔여 컨테이너 정리 (원격 사용) docker compose -f docker-compose.local.yml stop qdrant 2>/dev/null || true docker compose -f docker-compose.local.yml rm -f qdrant 2>/dev/null || true log "RAG 스택 기동 (docker-compose.local.yml, 원격 Mongo/Qdrant)..." docker compose -f docker-compose.local.yml up "${up_flags[@]}" wait_http "http://${EXDEV_DATA_HOST}:${QDRANT_PORT}/collections" "원격 Qdrant" 30 || true wait_http "http://127.0.0.1:${RAG_PORT}/health" "RAG API" 120 wait_http "http://127.0.0.1:${ADMIN_PORT}/api/stats" "Vector Admin" 60 || \ wait_port "${ADMIN_PORT}" "Vector Admin" 30 || true } ensure_java() { local home="" local candidates=( "/Users/macbook/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home" "/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home" "/usr/local/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home" ) for candidate in "${candidates[@]}"; do if [[ -x "${candidate}/bin/java" ]]; then home="${candidate}" break fi done if [[ -z "${home}" ]] && /usr/libexec/java_home -v 17 >/dev/null 2>&1; then home="$(/usr/libexec/java_home -v 17)" fi if [[ -z "${home}" ]]; then fail "Java 17 필요 — brew install openjdk@17" fi export JAVA_HOME="${home}" export PATH="${JAVA_HOME}/bin:${PATH}" log "Java 17: $("${JAVA_HOME}/bin/java" -version 2>&1 | head -1)" } start_java_service() { local name="$1" dir="$2" pid_file="$3" log_file="$4" shift 4 if [[ -f "${pid_file}" ]]; then local old_pid old_pid="$(cat "${pid_file}")" if kill -0 "$old_pid" 2>/dev/null; then log "${name} 이미 실행 중 (PID ${old_pid})" return 0 fi rm -f "${pid_file}" fi chmod +x "${dir}/gradlew" log "${name} 기동 중..." : > "${log_file}" ( cd "${dir}" export JAVA_HOME for ev in "$@"; do export "${ev?}" done nohup ./gradlew -Dorg.gradle.java.home="${JAVA_HOME}" bootRun \ >> "${log_file}" 2>&1 & echo $! > "${pid_file}" ) log "${name} PID $(cat "${pid_file}") → ${log_file}" } verify_stack() { local ok=true ai_tunnel_ready || { log "❌ exdev AI 터널"; ok=false; } curl -sf "http://127.0.0.1:${RAG_PORT}/health" | grep -q '"status"' || { log "❌ RAG health"; ok=false; } wait_port "${WAS_PORT}" "chatbotApi" 5 || ok=false wait_port "${SKILL_PORT}" "kakaoChatbotSkill" 5 || ok=false curl -sf "http://127.0.0.1:${APP_PORT}/chatbot/app/" | grep -q 'data-chatbot-app' || { log "❌ chatbotApp UI"; ok=false; } if $ok; then log "✅ 전체 스택 정상" return 0 fi log "⚠️ 일부 서비스 미응답 — ${LOG_DIR}/ 로그 확인" return 1 } # ── main ── mkdir -p "${RUN_DIR}" "${LOG_DIR}" docker info >/dev/null 2>&1 || fail "Docker Engine 미기동" log "==========================================" log "exChatbot 로컬 전체 스택 기동" log "==========================================" if ! $SKIP_TUNNEL; then start_tunnel else ai_tunnel_ready || fail "--no-tunnel 이지만 :${LLM_PORT}~:${RERANK_PORT} AI 미응답" fi ensure_remote_data start_rag if ! $SKIP_JAVA; then ensure_java start_java_service "chatbotApi" "${API_DIR}" \ "${RUN_DIR}/chatbot-api.pid" "${LOG_DIR}/chatbot-api.log" \ "INTERNAL_TOOL_API_KEY=${INTERNAL_TOOL_API_KEY}" \ "MONGODB_URI=${MONGODB_URI}" \ "SKILL_INTERNAL_BASE_URL=http://127.0.0.1:${SKILL_PORT}" \ "RAG_SERVER_HOST=127.0.0.1" \ "RAG_SERVER_PORT=${RAG_PORT}" start_java_service "kakaoChatbotSkill" "${SKILL_DIR}" \ "${RUN_DIR}/kakao-skill.pid" "${LOG_DIR}/kakao-skill.log" \ "AGENT_MODE=agent" \ "RAG_AGENT_CHAT_URL=http://127.0.0.1:${RAG_PORT}/agent/chat" \ "CHATBOT_API_BASE_URL=http://127.0.0.1:${WAS_PORT}/api/" \ "INTERNAL_TOOL_API_KEY=${INTERNAL_TOOL_API_KEY}" \ "CHATBOT_APP_URL=http://localhost:${APP_PORT}/chatbot/app/" wait_port "${WAS_PORT}" "chatbotApi" 240 || true wait_port "${SKILL_PORT}" "kakaoChatbotSkill" 240 || true fi log "chatbotApp 기동..." docker rm -f ex-chatbot-app-web 2>/dev/null || true ( cd "${APP_DIR}" export CHATBOT_API_UPSTREAM="http://host.docker.internal:${SKILL_PORT}/web/" export CHATBOT_API_HOST="host.docker.internal" docker compose up -d --force-recreate ) wait_http "http://127.0.0.1:${APP_PORT}/chatbot/app/" "chatbotApp UI" 30 || true log "==========================================" log "기동 완료" log " UI http://127.0.0.1:${APP_PORT}/chatbot/app/" log " RAG http://127.0.0.1:${RAG_PORT}/health" log " Admin http://127.0.0.1:${ADMIN_PORT}/" log " Mongo ${EXDEV_DATA_HOST}:${MONGO_PORT} (원격, RAG+WAS)" log " Qdrant http://${EXDEV_DATA_HOST}:${QDRANT_PORT}/dashboard (원격)" log " WAS http://127.0.0.1:${WAS_PORT}/" log " Skill http://127.0.0.1:${SKILL_PORT}/web/ask" log " exdev AI tunnel :${LLM_PORT} ~ :${RERANK_PORT}" log " 로그 ${LOG_DIR}/" log " 종료 ./local-dev/stop-all.sh" log "==========================================" HEALTH="$(curl -sf "http://127.0.0.1:${RAG_PORT}/health" 2>/dev/null || echo 'unavailable')" log "RAG health: ${HEALTH}" verify_stack || exit 1