#!/usr/bin/env bash # chatbot.exdev.co.kr — Agent 2.0 전체 스택 기동 # # ./server-dev/start-all-server.sh # ./server-dev/start-all-server.sh --no-build # ./server-dev/start-all-server.sh --skip-desktop # ./server-dev/start-all-server.sh --skip-java # ./server-dev/start-all-server.sh --skip-legacy-stop # ./server-dev/start-all-server.sh --no-pull # git 동기화 생략 # ./server-dev/start-all-server.sh --no-push # Gitea push 생략 # ./server-dev/start-all-server.sh --skip-oracle # TROAD Oracle 기동 생략 # ./server-dev/start-all-server.sh --local # 로컬 Mac 개발 모드 (local-dev 위임) # # 로컬 Mac 실행 시: Gitea(exdev) 최신 merge → 현재 소스 commit/push → 원격 배포. # 공식 GitHub(KoreaExpressWayCorp) push 는 하지 않음 — ./git_fetch_all.sh 전용. set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SERVER_DEV_DIR="${ROOT_DIR}/server-dev" RUN_DIR="${SERVER_DEV_DIR}/run" LOG_DIR="${SERVER_DEV_DIR}/logs" GIT_DIR="${GIT_DIR:-/Volumes/ADATA/gitea/data/gitea-repositories/aidev/exaichatbot_agent.git}" WORK_TREE="${WORK_TREE:-/Users/aidev/dev/exAichatbot_agent}" GIT_BRANCH="${GIT_BRANCH:-main}" 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" DESKTOP_DIR="${ROOT_DIR}/chatbotDesktopApp-chatbot2.0" ENV_TEMPLATE="${SERVER_DEV_DIR}/env.exdev-server.template" RAG_ENV="${RAG_DIR}/.env" COMPOSE_SERVER="${RAG_DIR}/docker-compose.server.yml" COMPOSE_CADDY="${SERVER_DEV_DIR}/docker-compose.caddy.yml" COMPOSE_ORACLE="${SERVER_DEV_DIR}/docker-compose.oracle.yml" LEGACY_STACK_DIR="${LEGACY_STACK_DIR:-/Users/aidev/dev/exAIChatbot2.0}" MONGO_CONTAINER="${MONGO_CONTAINER:-exaichatbot-agent-mongo}" ORACLE_CONTAINER="${ORACLE_CONTAINER:-exaichatbot-agent-oracle-troad}" INTERNAL_TOOL_API_KEY="${INTERNAL_TOOL_API_KEY:-exdev-agent-tool-key}" PUBLIC_BASE_URL="${PUBLIC_BASE_URL:-https://chatbot.exdev.co.kr}" 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:-8092}" NO_BUILD=false SKIP_JAVA=false SKIP_LEGACY_STOP=false SKIP_CADDY=false SKIP_DESKTOP=false SKIP_ORACLE=false NO_PULL=false NO_PUSH=false LOCAL_DEV=false EXDEV_SSH="${EXDEV_SSH:-aidev@chatbot.exdev.co.kr}" for arg in "$@"; do case "$arg" in --no-build) NO_BUILD=true ;; --skip-java) SKIP_JAVA=true ;; --skip-legacy-stop) SKIP_LEGACY_STOP=true ;; --skip-caddy) SKIP_CADDY=true ;; --skip-desktop) SKIP_DESKTOP=true ;; --skip-oracle) SKIP_ORACLE=true ;; --no-pull) NO_PULL=true ;; --no-push) NO_PUSH=true ;; --local) LOCAL_DEV=true ;; -h|--help) sed -n '2,15p' "$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; } warn() { log "⚠️ $*"; } # 공식 GitHub 동기화/push 는 start-all-server 경로에서 절대 수행하지 않음. # (과거: git_fetch_all.sh 호출 → 로컬 수정이 공식 GitHub 로 올라가던 문제 방지) push_source_to_gitea() { if $NO_PUSH; then log "Gitea push 생략 (--no-push)" return 0 fi if is_exdev_server; then log "exdev 서버 — Gitea push 생략 (post-receive/deploy 경로 사용)" return 0 fi local push_script="${SERVER_DEV_DIR}/push-to-gitea.sh" [[ -x "${push_script}" ]] || fail "push-to-gitea.sh 없음: ${push_script}" log "현재 소스 → Gitea (${GIT_BRANCH}) 반영..." "${push_script}" } wait_remote_deploy() { if $NO_PUSH; then return 0 fi local wait_script="${SERVER_DEV_DIR}/wait-remote-deploy.sh" if [[ -x "${wait_script}" ]]; then EXDEV_SSH="${EXDEV_SSH}" PUBLIC_BASE_URL="${PUBLIC_BASE_URL}" "${wait_script}" || \ warn "원격 배포 대기 실패 — ssh ${EXDEV_SSH} tail /Volumes/ADATA/logs/deploy/exAichatbot_agent.log" else warn "wait-remote-deploy.sh 없음 — https://chatbot.exdev.co.kr/chatbot/ai/ 수동 확인" fi } sync_submodules_to_server() { local rsync_script="${SERVER_DEV_DIR}/sync-submodules-rsync.sh" [[ -x "${rsync_script}" ]] || fail "sync-submodules-rsync.sh 없음" log "submodule 소스 → chatbot.exdev.co.kr rsync..." EXDEV_SSH="${EXDEV_SSH}" "${rsync_script}" } trigger_remote_deploy() { log "원격 monorepo checkout..." ssh -o BatchMode=yes "${EXDEV_SSH}" \ "GIT_DIR=/Volumes/ADATA/gitea/data/gitea-repositories/aidev/exaichatbot_agent.git; \ WT=/Users/aidev/dev/exAichatbot_agent; \ git --work-tree=\"\${WT}\" --git-dir=\"\${GIT_DIR}\" checkout -f main; \ rm -f \"\${WT}/.git\" /tmp/exAichatbot_agent_deploy.lock" sync_submodules_to_server log "원격 deploy (빌드 + 기동)..." # SSH 세션에서 deploy 가 끝나므로, 실패 시 즉시 중단 (wait 900s 방지) if ! ssh -o BatchMode=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=120 "${EXDEV_SSH}" \ "cp /Users/aidev/dev/exAichatbot_agent/server-dev/deploy.sh /Volumes/ADATA/git/deploy-exAichatbot_agent.sh 2>/dev/null || true; \ chmod +x /Volumes/ADATA/git/deploy-exAichatbot_agent.sh; \ export PATH=\"/opt/homebrew/bin:/Applications/Docker.app/Contents/Resources/bin:\$PATH\"; \ bash /Volumes/ADATA/git/deploy-exAichatbot_agent.sh --start-only"; then fail "원격 deploy 실패 — ssh ${EXDEV_SSH} 'tail -80 /Volumes/ADATA/logs/deploy/exAichatbot_agent.log'" fi } build_chatbot_app_for_deploy() { log "chatbotApp 로컬 빌드 (서버 Next.js 대신 public/ 생성)..." chmod -R u+w "${APP_DIR}/public-sample" 2>/dev/null || true rm -rf "${APP_DIR}/public-sample" 2>/dev/null || true ( cd "${APP_DIR}/web" npm ci --prefer-offline 2>/dev/null || npm install ) NEXT_PUBLIC_FARE_QUERY_MOCK=true bash "${APP_DIR}/build.sh" [[ -f "${APP_DIR}/public/index.html" ]] || fail "chatbotApp public/ 빌드 실패" [[ -f "${APP_DIR}/public-ai/index.html" ]] || fail "chatbotApp public-ai/ 빌드 실패" build_desktop_app } build_desktop_app() { if $NO_BUILD || $SKIP_DESKTOP; then log "데스크톱 앱 빌드 생략 (--no-build/--skip-desktop)" return 0 fi [[ -d "${DESKTOP_DIR}" ]] || fail "데스크톱 앱 디렉터리 없음: ${DESKTOP_DIR}" chmod +x "${DESKTOP_DIR}/build-and-publish.sh" log "chatbotDesktopApp Electron 재빌드 + downloads 배포..." # 서버/원격 배포용: Mac + Windows 모두 생성 SKIP_WIN="${SKIP_WIN:-0}" SKIP_MAC="${SKIP_MAC:-0}" \ bash "${DESKTOP_DIR}/build-and-publish.sh" } deploy_from_local_mac() { log "로컬 Mac → Gitea(exdev) sync(merge) → push → chatbot.exdev.co.kr 배포" log "공식 GitHub push 생략 (필요 시 ./git_fetch_all.sh)" build_chatbot_app_for_deploy push_source_to_gitea trigger_remote_deploy wait_remote_deploy log "==========================================" log "원격 배포 요청 완료" log " UI ${PUBLIC_BASE_URL}/chatbot/ai/" log " 로그 ssh ${EXDEV_SSH} tail -f /Volumes/ADATA/logs/deploy/exAichatbot_agent.log" log "==========================================" exit 0 } sync_source_from_git() { if $NO_PULL; then log "git 동기화 생략 (--no-pull)" return 0 fi local tree="${WORK_TREE}" local branch="${GIT_BRANCH}" if [[ -d "${GIT_DIR}" ]]; then mkdir -p "${tree}" log "Gitea 최신 소스 동기화 (${branch})..." if ! git --git-dir="${GIT_DIR}" show-ref --verify --quiet "refs/heads/${branch}" 2>/dev/null; then fail "브랜치 없음: ${branch} (GIT_DIR=${GIT_DIR})" fi git --work-tree="${tree}" --git-dir="${GIT_DIR}" checkout -f "${branch}" # Gitea work tree 만 사용 — 공식 GitHub fetch/push 없음 if [[ -f "${tree}/.gitmodules" ]]; then log "Gitea submodule 포인터 checkout..." git -C "${tree}" submodule update --init --recursive 2>/dev/null || \ warn "submodule update 실패 — rsync/배포 산출물로 계속" fi local commit commit="$(git --git-dir="${GIT_DIR}" rev-parse --short HEAD)" log "✅ 소스 동기화(Gitea): ${commit} (${branch})" elif [[ -d "${ROOT_DIR}/.git" ]]; then log "git pull origin ${branch} (Gitea/monorepo)..." ( cd "${ROOT_DIR}" git fetch origin "${branch}" git checkout "${branch}" git pull --ff-only origin "${branch}" ) log "✅ git pull 완료: $(git -C "${ROOT_DIR}" rev-parse --short HEAD)" tree="${ROOT_DIR}" else log "Git 저장소 없음 — 현재 디렉터리 소스 사용 (${ROOT_DIR})" return 0 fi chmod +x "${tree}/server-dev/"*.sh 2>/dev/null || true chmod +x "${tree}/git_fetch_all.sh" 2>/dev/null || true chmod +x "${tree}/chatbotApp-chatbot2.0-agent/build.sh" 2>/dev/null || true chmod +x "${tree}/chatbotDesktopApp-chatbot2.0/build-and-publish.sh" 2>/dev/null || true chmod +x "${tree}/start-all-server.sh" "${tree}/stop-all-server.sh" "${tree}/start-all.sh" 2>/dev/null || true } is_exdev_server() { [[ -d "${GIT_DIR}" ]] \ && [[ -f "${LEGACY_STACK_DIR}/scripts/exdev/start-host-ai.sh" ]] } run_local_dev() { log "로컬 Mac — local-dev/start-all.sh 로 위임 (exdev AI SSH 터널 사용)" local -a args=() $NO_BUILD && args+=(--no-build) $SKIP_JAVA && args+=(--skip-java) $SKIP_DESKTOP && args+=(--skip-desktop) if ((${#args[@]} > 0)); then exec "${ROOT_DIR}/local-dev/start-all.sh" "${args[@]}" else exec "${ROOT_DIR}/local-dev/start-all.sh" fi } 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 } host_ai_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 } ensure_host_ai() { if host_ai_ready; then log "호스트 AI 이미 실행 중 (:${LLM_PORT}~:${RERANK_PORT})" return 0 fi local ai_script="${LEGACY_STACK_DIR}/scripts/exdev/start-host-ai.sh" if [[ -x "${ai_script}" ]] || [[ -f "${ai_script}" ]]; then log "호스트 AI 기동..." bash "${ai_script}" || log "⚠️ start-host-ai.sh 일부 실패 — 기존 프로세스 확인" fi if host_ai_ready; then return 0 fi log "호스트 AI 기동 대기 (최대 120s)..." local i=0 while (( i < 120 )); do if host_ai_ready; then log "✅ 호스트 AI 준비 완료" return 0 fi sleep 5 i=$((i + 5)) done if curl -sf --max-time 3 "http://127.0.0.1:${LLM_PORT}/v1/models" >/dev/null 2>&1; then log "⚠️ LLM만 응답 (Embed/Rerank 미기동) — degraded 모드로 계속" return 0 fi fail "호스트 AI 미응답 (:${LLM_PORT})" } stop_legacy_stack() { if $SKIP_LEGACY_STOP; then return 0; fi local stop_script="${LEGACY_STACK_DIR}/remote-stop-chatbot.sh" if [[ -f "${stop_script}" ]]; then log "레거시 exAIChatbot2.0 스택 중지 (포트 충돌 방지)..." bash "${stop_script}" --destroy-stack 2>/dev/null || bash "${stop_script}" 2>/dev/null || true docker rm -f ex-chatbot-caddy 2>/dev/null || true fi } ensure_rag_env() { # .env 키=값 갱신 (없으면 append) patch_env() { local key="$1" value="$2" tmp tmp="$(mktemp)" if [[ -f "${RAG_ENV}" ]]; then grep -vE "^${key}=" "${RAG_ENV}" > "${tmp}" || true else : > "${tmp}" fi printf '%s\n%s=%s\n' "$(cat "${tmp}")" "${key}" "${value}" > "${RAG_ENV}" rm -f "${tmp}" } if [[ ! -f "${RAG_ENV}" ]]; then log ".env 없음 → env.exdev-server.template 복사" cp "${ENV_TEMPLATE}" "${RAG_ENV}" fi # RAG(Docker) → 호스트 chatbotApi (exdev 로컬 WAS). 운영 IP(172.16.180.130) 사용 금지. # GitHub 커밋 기본값은 운영 IP 유지 — git_fetch_all.sh 가 push 전 복원. patch_env "EXDEV_LOCAL" "true" patch_env "CHATBOT_API_BASE_URL" "http://host.docker.internal:${WAS_PORT}/api" patch_env "INTERNAL_TOOL_API_KEY" "${INTERNAL_TOOL_API_KEY}" patch_env "CHATBOT_TOOL_TIMEOUT" "60" if grep -qE 'CHATBOT_API_BASE_URL=.*172\.16\.180\.130' "${RAG_ENV}" 2>/dev/null; then fail "exdev RAG .env 에 운영 WAS(172.16.180.130) 가 남아 있습니다" fi log "RAG .env WAS: host.docker.internal:${WAS_PORT} (로컬 chatbotApi, EXDEV_LOCAL=true)" } ensure_mongo() { if docker ps -a --format '{{.Names}}' | grep -qx "${MONGO_CONTAINER}"; then docker start "${MONGO_CONTAINER}" >/dev/null 2>&1 || true log "MongoDB 재사용: ${MONGO_CONTAINER}" wait_port 27017 "MongoDB" 20 || true return 0 fi if lsof -iTCP:27017 -sTCP:LISTEN >/dev/null 2>&1; then log "MongoDB :27017 이미 사용 중" return 0 fi log "MongoDB 컨테이너 생성: ${MONGO_CONTAINER}" docker run -d --name "${MONGO_CONTAINER}" -p 27017:27017 mongo:7 >/dev/null wait_port 27017 "MongoDB" 30 || true } oracle_container_running() { docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "${ORACLE_CONTAINER}" } wait_oracle_ready() { local max="${1:-300}" i=0 log "Oracle TROAD 준비 대기 (최대 ${max}s)..." while (( i < max )); do if oracle_container_running && docker exec "${ORACLE_CONTAINER}" healthcheck.sh >/dev/null 2>&1; then log "✅ Oracle TROAD ready (localhost:1521/orcl2, OPR_MAIN)" return 0 fi sleep 5 i=$((i + 5)) done warn "Oracle TROAD 타임아웃 — docker logs ${ORACLE_CONTAINER}" docker logs --tail 40 "${ORACLE_CONTAINER}" 2>/dev/null | sed 's/^/ /' || true return 1 } # 교통정보 뷰가 비어 있으면 oracle/apply-sql.sh 로 적용한다. # (기존 chatbotApi SQL: TROAD_APP.V_ROUTE_NEW_NM / V_ROUTE_TRFC_VER001 등 — 앱 로직 변경 없음) ensure_troad_schema() { local apply="${SERVER_DEV_DIR}/oracle/apply-sql.sh" [[ -x "$apply" ]] || return 0 local rows route_rows rows="$(docker exec -e NLS_LANG=.AL32UTF8 "${ORACLE_CONTAINER}" bash -lc \ "sqlplus -S -L OPR_MAIN/${ORACLE_TROAD_PASSWORD:-test}@localhost:1521/ORCL2 <<'EOSQL' SET HEADING OFF SET FEEDBACK OFF SET PAGESIZE 0 SELECT COUNT(*) FROM V_INCIDENT_FTMS; EXIT EOSQL" 2>/dev/null | tr -dc '0-9')" route_rows="$(docker exec -e NLS_LANG=.AL32UTF8 "${ORACLE_CONTAINER}" bash -lc \ "sqlplus -S -L OPR_MAIN/${ORACLE_TROAD_PASSWORD:-test}@localhost:1521/ORCL2 <<'EOSQL' SET HEADING OFF SET FEEDBACK OFF SET PAGESIZE 0 SELECT COUNT(*) FROM TROAD_APP.V_ROUTE_NEW_NM; EXIT EOSQL" 2>/dev/null | tr -dc '0-9')" if [[ -n "$rows" && "$rows" -gt 0 && -n "$route_rows" && "$route_rows" -gt 0 ]]; then log "✅ TROAD 교통정보 뷰 확인 (V_INCIDENT_FTMS ${rows}건, V_ROUTE_NEW_NM ${route_rows}건)" return 0 fi log "TROAD 교통정보 뷰/데이터 적용 (oracle/apply-sql.sh)..." "$apply" >/dev/null 2>&1 \ && log "✅ TROAD 교통정보 뷰 적용 완료" \ || warn "TROAD 스키마 적용 실패 — server-dev/oracle/apply-sql.sh 를 직접 실행해 확인하세요" } ensure_oracle() { if $SKIP_ORACLE; then log "Oracle TROAD 기동 생략 (--skip-oracle)" return 0 fi [[ -f "${COMPOSE_ORACLE}" ]] || fail "server-dev/docker-compose.oracle.yml 없음" # 이미 다른 프로세스가 1521 을 쓰는 경우(비-Oracle 컨테이너)만 경고 if ! oracle_container_running && lsof -iTCP:1521 -sTCP:LISTEN >/dev/null 2>&1; then if docker ps --format '{{.Names}} {{.Ports}}' 2>/dev/null | grep -qE '1521'; then log "Oracle :1521 이미 Docker 로 사용 중 — 기존 인스턴스 재사용" wait_oracle_ready 120 && ensure_troad_schema || true return 0 fi warn "호스트 :1521 이 이미 점유됨 — TROAD Oracle 기동 실패 가능" fi log "Oracle TROAD 기동 (docker-compose.oracle.yml)..." ( cd "${SERVER_DEV_DIR}" docker compose -f docker-compose.oracle.yml up -d ) || fail "Oracle TROAD docker compose up 실패" if wait_oracle_ready 360; then ensure_troad_schema else warn "Oracle 미준비 — chatbotApi TROAD 조회가 실패할 수 있음" fi } bootstrap_qdrant_if_empty() { local collection="${QDRANT_COLLECTION:-qa_vectors_v2}" if [[ -f "${RAG_ENV}" ]]; then local from_env from_env="$(grep -E '^QDRANT_COLLECTION=' "${RAG_ENV}" 2>/dev/null | tail -1 | cut -d= -f2- | tr -d ' "\r' || true)" [[ -n "${from_env}" ]] && collection="${from_env}" fi local vc vc="$(curl -sf "http://127.0.0.1:6333/collections/${collection}" 2>/dev/null \ | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('result',{}).get('points_count',0))" 2>/dev/null || echo 0)" if [[ "${vc}" -gt 0 ]]; then log "Qdrant 기존 데이터 ${vc} points" return 0 fi local bootstrap="${SERVER_DEV_DIR}/bootstrap-qdrant-from-legacy.sh" if [[ -x "${bootstrap}" ]]; then log "Qdrant 비어 있음 → 레거시 데이터 이전" "${bootstrap}" || log "⚠️ Qdrant bootstrap 실패" fi } ensure_rag_compose() { local template="${SERVER_DEV_DIR}/docker-compose.rag.server.yml" [[ -f "${template}" ]] || fail "server-dev/docker-compose.rag.server.yml 없음" cp "${template}" "${COMPOSE_SERVER}" } start_rag() { ensure_rag_env ensure_rag_compose [[ -f "${COMPOSE_SERVER}" ]] || fail "docker-compose.server.yml 없음" # compose environment 보간 + 컨테이너 env (로컬 WAS) export EXDEV_LOCAL=true export CHATBOT_API_BASE_URL="http://host.docker.internal:${WAS_PORT}/api" export INTERNAL_TOOL_API_KEY="${INTERNAL_TOOL_API_KEY}" export CHATBOT_TOOL_TIMEOUT="${CHATBOT_TOOL_TIMEOUT:-60}" cd "${RAG_DIR}" docker compose -f "${COMPOSE_SERVER}" down --remove-orphans 2>/dev/null || true local -a up_flags=(-d) if $NO_BUILD; then up_flags+=(--no-build); else up_flags+=(--build); fi log "RAG 스택 기동 (docker-compose.server.yml)..." log " EXDEV_LOCAL=${EXDEV_LOCAL} CHATBOT_API_BASE_URL=${CHATBOT_API_BASE_URL}" docker compose -f "${COMPOSE_SERVER}" up "${up_flags[@]}" wait_http "http://127.0.0.1:6333/collections" "Qdrant" 30 || true bootstrap_qdrant_if_empty (cd "${RAG_DIR}" && docker compose -f "${COMPOSE_SERVER}" up -d) || true wait_http "http://127.0.0.1:${RAG_PORT}/health" "RAG API" 180 || true wait_port "${ADMIN_PORT}" "Vector Admin" 30 || true ensure_mongo_schema } # 새 소스에만 있는 Mongo 컬렉션(예: usage_sessions)이 배포 대상 DB 에 없을 수 있다. # 기동 시 API 가 스스로 만들지만, 배포 로그에 결과를 남겨 누락을 바로 확인한다. ensure_mongo_schema() { local container="exaichatbot-agent-api-1" if ! docker ps --format '{{.Names}}' | grep -qx "${container}"; then warn "Mongo 스키마 점검 생략 — ${container} 미기동" return 0 fi log "Mongo 컬렉션·인덱스 점검..." if ! docker exec "${container}" python /app/scripts/mongo_schema.py 2>&1 | sed 's/^/ /'; then warn "Mongo 컬렉션·인덱스 일부 누락 — 위 로그 확인 (챗봇 기동은 계속됨)" fi } ensure_java() { local home="" for candidate in \ "/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home" \ "/usr/local/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home"; 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 [[ -n "${home}" ]] || fail "Java 17 필요 — brew install openjdk@17" 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 local port="" case "${name}" in chatbotApi) port="${WAS_PORT}" ;; kakaoChatbotSkill) port="${SKILL_PORT}" ;; esac if [[ -f "${pid_file}" ]]; then local old_pid old_pid="$(cat "${pid_file}")" if kill -0 "$old_pid" 2>/dev/null; then if [[ -n "${port}" ]] && lsof -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1; then log "${name} 이미 실행 중 (PID ${old_pid}, :${port})" return 0 fi # pid 는 있으나 포트 미개방 → 좀비/실패 프로세스 정리 kill "$old_pid" 2>/dev/null || true fi rm -f "${pid_file}" fi [[ -n "${JAVA_HOME}" ]] || ensure_java chmod +x "${dir}/gradlew" # release=8 프로젝트는 Java 8 API만 허용. 기동 전 컴파일로 실패를 즉시 드러낸다. log "${name} 컴파일 확인 (Java ${JAVA_HOME})..." if ! ( cd "${dir}" export JAVA_HOME ./gradlew -Dorg.gradle.java.home="${JAVA_HOME}" compileJava --quiet ); then fail "${name} 컴파일 실패 — ${dir} 소스/JDK 확인 (Java 8 API만 사용, isBlank/List.of 금지)" fi 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}" if [[ -n "${port}" ]]; then if ! wait_port "${port}" "${name}" 240; then warn "${name} :${port} 미개방 — 최근 로그:" tail -40 "${log_file}" 2>/dev/null | sed 's/^/ /' || true return 1 fi fi return 0 } build_chatbot_app() { if $NO_BUILD && [[ -f "${APP_DIR}/public/index.html" ]]; then log "chatbotApp 빌드 생략 (--no-build)" return 0 fi log "chatbotApp Next.js 빌드..." ( cd "${APP_DIR}/web" npm ci --prefer-offline 2>/dev/null || npm install ) NEXT_PUBLIC_FARE_QUERY_MOCK=true bash "${APP_DIR}/build.sh" build_desktop_app } start_chatbot_app() { build_chatbot_app docker rm -f ex-chatbot-app-web 2>/dev/null || true ( cd "${APP_DIR}" export CHATBOT_APP_PORT="${APP_PORT}" 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/ai/" "chatbotApp UI" 60 || true } start_caddy() { if $SKIP_CADDY; then return 0; fi [[ -f "${COMPOSE_CADDY}" ]] || return 0 local cert_dir="${LEGACY_STACK_DIR}/docker/nginx/certs" [[ -f "${cert_dir}/fullchain.pem" ]] || fail "TLS 인증서 없음: ${cert_dir}/fullchain.pem" log "Caddy HTTPS 기동 (443 → chatbotApp:${APP_PORT})..." ( cd "${SERVER_DEV_DIR}" export CHATBOT_APP_PORT="${APP_PORT}" export RAG_API_PORT="${RAG_PORT}" export SKILL_PORT="${SKILL_PORT}" export ADMIN_PORT="${ADMIN_PORT}" export LEGACY_CERTS_DIR="${cert_dir}" docker compose -f docker-compose.caddy.yml up -d --force-recreate ) wait_http "https://chatbot.exdev.co.kr/chatbot/ai/" "HTTPS UI" 30 || true } verify_stack() { local ok=true host_ai_ready || { log "❌ 호스트 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/ai/" | grep -q 'data-chatbot-app="ai-mode"' || { log "❌ chatbotApp UI"; ok=false; } # exdev 는 운영 WAS IP 를 절대 참조하지 않아야 한다 local rag_was rag_was="$(docker exec exaichatbot-agent-api-1 printenv CHATBOT_API_BASE_URL 2>/dev/null || true)" if [[ "${rag_was}" == *172.16.180.130* ]]; then log "❌ RAG 가 운영 WAS(172.16.180.130) 를 참조 중: ${rag_was}" ok=false fi # 로컬 WAS tool 정의 로드 (음식메뉴 등 WEB tool 전제) local tools_json tools_json="$(curl -sf -m 8 -H "X-Internal-Tool-Key: ${INTERNAL_TOOL_API_KEY}" \ "http://127.0.0.1:${WAS_PORT}/api/v1/tools/definitions" 2>/dev/null || true)" if ! echo "${tools_json}" | grep -q 'REST_AREA_FOOD_LIST'; then log "❌ chatbotApi tool 정의에 REST_AREA_FOOD_LIST 없음 (WAS 미기동/연동 실패)" ok=false else log "✅ chatbotApi domain tools OK" fi if $ok; then log "✅ 전체 스택 정상"; return 0; fi log "⚠️ 일부 서비스 미응답 — ${LOG_DIR}/ 확인" return 1 } mkdir -p "${RUN_DIR}" "${LOG_DIR}" log "==========================================" log "exAichatbot Agent 2.0 기동" log "==========================================" if ! is_exdev_server; then if $LOCAL_DEV; then sync_source_from_git run_local_dev else deploy_from_local_mac fi fi sync_source_from_git docker info >/dev/null 2>&1 || fail "Docker Engine 미기동" log "exdev 서버 모드" stop_legacy_stack ensure_host_ai ensure_mongo ensure_oracle start_rag if ! $SKIP_JAVA; then ensure_java # Skill 먼저 (WAS WEB tool 이 Skill data-portal 을 역호출) start_java_service "kakaoChatbotSkill" "${SKILL_DIR}" \ "${RUN_DIR}/kakao-skill.pid" "${LOG_DIR}/kakao-skill.log" \ "EXDEV_LOCAL=true" \ "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=${PUBLIC_BASE_URL}/chatbot/ai/" \ || warn "kakaoChatbotSkill 기동 실패" start_java_service "chatbotApi" "${API_DIR}" \ "${RUN_DIR}/chatbot-api.pid" "${LOG_DIR}/chatbot-api.log" \ "INTERNAL_TOOL_API_KEY=${INTERNAL_TOOL_API_KEY}" \ "SKILL_INTERNAL_BASE_URL=http://127.0.0.1:${SKILL_PORT}" \ "RAG_SERVER_HOST=127.0.0.1" \ "RAG_SERVER_PORT=${RAG_PORT}" \ || warn "chatbotApi 기동 실패" fi start_chatbot_app start_caddy log "==========================================" log "기동 완료" log " HTTPS UI ${PUBLIC_BASE_URL}/chatbot/ai/" log " RAG http://127.0.0.1:${RAG_PORT}/health" log " Admin http://127.0.0.1:${ADMIN_PORT}/" log " WAS http://127.0.0.1:${WAS_PORT}/" log " Skill http://127.0.0.1:${SKILL_PORT}/web/ask" log " Oracle localhost:1521/orcl2 (OPR_MAIN / TROAD)" log " Kakao ${PUBLIC_BASE_URL}/kakao/skill" log " 로그 ${LOG_DIR}/" log " 종료 ./server-dev/stop-all-server.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