#!/bin/bash # ====================================================================== # Golden Chart - 원격 서버 자동 배포 스크립트 # 대상 서버: exdev.co.kr # 동작: 로컬 소스 압축 전송 → 서버에서 docker compose up --build # ====================================================================== set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" # 색상 RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' # ── 서버 설정 ────────────────────────────────────────────────────────── REMOTE_HOST="${REMOTE_HOST:-exdev.co.kr}" REMOTE_USER="${REMOTE_USER:-aidev}" REMOTE_PORT="${REMOTE_PORT:-22}" REMOTE_PROJECT_DIR="${REMOTE_PROJECT_DIR:-/Users/aidev/apps/goldenChart}" FRONTEND_PORT="${FRONTEND_PORT:-80}" BACKEND_PORT="${BACKEND_PORT:-8080}" # ── 옵션 파싱 ────────────────────────────────────────────────────────── DOWN=false LOGS=false STATUS=false SETUP_SSH=false SYNC=true NO_CACHE=true # 기본: --no-cache (항상 최신 코드) FORGET_PASSWORD=false SHELL_ONLY=false SERVICE="" # 특정 서비스만 재빌드 (frontend / backend / all) print_usage() { echo "사용법: $0 [옵션]" echo "" echo " (기본) 소스 동기화 + 전체 서비스 재빌드 + 재시작" echo " --use-cache 캐시 사용 빌드 (빠름)" echo " --no-sync 파일 동기화 생략" echo " --frontend 프론트엔드만 재빌드" echo " --backend 백엔드만 재빌드" echo " --down, -d 모든 서비스 중지" echo " --logs, -l 전체 로그 스트리밍" echo " --status, -s 서비스 상태 확인" echo " --shell 서버 SSH 세션 열기" echo " --forget-password SSH 패스워드 캐시 삭제" echo " --help, -h 도움말" echo "" echo "서버: ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PORT}" echo "경로: ${REMOTE_PROJECT_DIR}" echo "URL: http://${REMOTE_HOST}:${FRONTEND_PORT}" } while [[ $# -gt 0 ]]; do case $1 in --use-cache) NO_CACHE=false; shift ;; --no-sync) SYNC=false; shift ;; --frontend) SERVICE=frontend; shift ;; --backend) SERVICE=backend; shift ;; --down|-d) DOWN=true; shift ;; --logs|-l) LOGS=true; shift ;; --status|-s) STATUS=true; shift ;; --shell) SHELL_ONLY=true; shift ;; --setup-ssh) SETUP_SSH=true; shift ;; --forget-password) FORGET_PASSWORD=true; shift ;; --user) REMOTE_USER="$2"; shift 2 ;; --port-ssh) REMOTE_PORT="$2"; shift 2 ;; --help|-h) print_usage; exit 0 ;; *) echo -e "${RED}알 수 없는 옵션: $1${NC}"; print_usage; exit 1 ;; esac done SSH_TARGET="${REMOTE_USER}@${REMOTE_HOST}" # ── 배너 ─────────────────────────────────────────────────────────────── echo -e "${CYAN}${BOLD}" echo "╔══════════════════════════════════════════════════════════╗" echo "║ GoldenChart 원격 배포 스크립트 ║" echo "╚══════════════════════════════════════════════════════════╝${NC}" echo "" echo -e " 서버: ${YELLOW}${SSH_TARGET}:${REMOTE_PORT}${NC}" echo -e " 경로: ${YELLOW}${REMOTE_PROJECT_DIR}${NC}" echo -e " URL: ${CYAN}http://${REMOTE_HOST}:${FRONTEND_PORT}${NC}" echo "" # ── SSH 패스워드 캐시 ───────────────────────────────────────────────── PASS_CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/golden-chart-deploy" PASS_CACHE_FILE="${PASS_CACHE_DIR}/sshpass-${REMOTE_USER}@${REMOTE_HOST}-${REMOTE_PORT}.txt" if [ "$FORGET_PASSWORD" = true ]; then rm -f "$PASS_CACHE_FILE" && echo -e "${GREEN}✓ 패스워드 캐시 삭제${NC}" || echo -e "${YELLOW}캐시 없음${NC}" fi SERVER_PASSWORD="" PASSWORD_FROM_CACHE=false apply_ssh_cmds() { # IdentitiesOnly=yes: ssh-agent의 모든 키를 자동 시도하지 않도록 제한 # → "Too many authentication failures" 방지 if [ -n "$SERVER_PASSWORD" ]; then export SSHPASS="$SERVER_PASSWORD" SSH_CMD="sshpass -e ssh -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -o PubkeyAuthentication=no -p ${REMOTE_PORT}" SCP_CMD="sshpass -e scp -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -o PubkeyAuthentication=no -P ${REMOTE_PORT}" else unset SSHPASS 2>/dev/null || true SSH_CMD="ssh -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -p ${REMOTE_PORT}" SCP_CMD="scp -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -P ${REMOTE_PORT}" fi } if command -v sshpass &>/dev/null; then if [ -f "$PASS_CACHE_FILE" ]; then IFS= read -r SERVER_PASSWORD < "$PASS_CACHE_FILE" || SERVER_PASSWORD="" [ -n "$SERVER_PASSWORD" ] && PASSWORD_FROM_CACHE=true && echo -e "${GREEN}✓ 저장된 패스워드 사용 (--forget-password 로 초기화)${NC}" fi if [ -z "$SERVER_PASSWORD" ]; then echo -e "${CYAN}SSH 패스워드 (키 인증 시 Enter):${NC}" read -s -p "패스워드: " SERVER_PASSWORD; echo "" PASSWORD_FROM_CACHE=false fi else echo -e "${YELLOW}sshpass 없음 — SSH 키 인증 모드${NC}" fi apply_ssh_cmds # ── SSH 연결 테스트 ─────────────────────────────────────────────────── echo -e "${CYAN}[연결] SSH 테스트 중...${NC}" # set -e 영향 차단을 위해 || true 사용 SSH_ERR=$( { $SSH_CMD $SSH_TARGET "exit"; } 2>&1 ) || true SSH_EC=$( { $SSH_CMD $SSH_TARGET "exit" >/dev/null 2>&1; echo $?; } ) || SSH_EC=1 SSH_EC="${SSH_EC:-1}" if [ "${SSH_EC}" != "0" ] && [ -n "$SERVER_PASSWORD" ] && [ "$PASSWORD_FROM_CACHE" = true ] && command -v sshpass &>/dev/null; then echo -e "${YELLOW}캐시 패스워드 실패 — 재입력:${NC}" rm -f "$PASS_CACHE_FILE" read -s -p "패스워드: " SERVER_PASSWORD; echo "" PASSWORD_FROM_CACHE=false apply_ssh_cmds SSH_ERR=$( { $SSH_CMD $SSH_TARGET "exit"; } 2>&1 ) || true SSH_EC=$( { $SSH_CMD $SSH_TARGET "exit" >/dev/null 2>&1; echo $?; } ) || SSH_EC=1 SSH_EC="${SSH_EC:-1}" fi if [ "${SSH_EC}" != "0" ]; then echo -e "${RED}✗ SSH 연결 실패 (exit code: ${SSH_EC})${NC}" [ -n "$SSH_ERR" ] && echo -e "${RED} 오류: $(echo "$SSH_ERR" | head -3)${NC}" echo "" echo -e "${YELLOW}해결 방법:${NC}" echo " 1) SSH 키 등록: ssh-copy-id -p ${REMOTE_PORT} ${SSH_TARGET}" echo " 2) 직접 테스트: ssh -p ${REMOTE_PORT} ${SSH_TARGET}" echo " 3) sshpass 설치 (패스워드 인증): brew install hudochenkov/sshpass/sshpass" exit 1 fi echo -e "${GREEN}✓ SSH 연결 성공${NC}" # 패스워드 캐시 저장 if [ -n "$SERVER_PASSWORD" ] && [ "$PASSWORD_FROM_CACHE" = false ] && command -v sshpass &>/dev/null; then mkdir -p "$PASS_CACHE_DIR"; chmod 700 "$PASS_CACHE_DIR" 2>/dev/null || true ( umask 077 && printf '%s' "$SERVER_PASSWORD" > "$PASS_CACHE_FILE" ) chmod 600 "$PASS_CACHE_FILE" 2>/dev/null || true echo -e "${GREEN}✓ 패스워드 캐시 저장됨${NC}" fi echo "" # ── 원격 명령 헬퍼 ──────────────────────────────────────────────────── remote_exec() { $SSH_CMD $SSH_TARGET "bash -l -c $(printf '%q' "$*")"; } # ── 특수 모드 처리 ──────────────────────────────────────────────────── if [ "$SHELL_ONLY" = true ]; then echo -e "${CYAN}서버 SSH 세션 (exit 로 종료):${NC}" $SSH_CMD -t $SSH_TARGET "bash -l"; exit 0 fi if [ "$STATUS" = true ]; then echo -e "${CYAN}[상태] 서비스 컨테이너:${NC}" remote_exec "cd ${REMOTE_PROJECT_DIR} && docker compose ps" echo ""; echo -e "${CYAN}[리소스]:${NC}" remote_exec "docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}'" exit 0 fi if [ "$LOGS" = true ]; then echo -e "${CYAN}[로그] 전체 서비스 로그 (Ctrl+C 종료):${NC}" $SSH_CMD $SSH_TARGET "cd ${REMOTE_PROJECT_DIR} && docker compose logs -f --tail 100" exit 0 fi if [ "$DOWN" = true ]; then echo -e "${YELLOW}[중지] 모든 서비스 중지 중...${NC}" remote_exec "cd ${REMOTE_PROJECT_DIR} && docker compose down" echo -e "${GREEN}✓ 서비스 중지 완료${NC}"; exit 0 fi # ── [1/3] 파일 동기화 ───────────────────────────────────────────────── if [ "$SYNC" = true ]; then echo -e "${CYAN}[1/3] 소스 → 서버 동기화 중...${NC}" TEMP_ARCHIVE="/tmp/gc-deploy-$$.tar.gz" echo -e "${YELLOW} 압축 중 (node_modules / .git / dist / target 제외)...${NC}" tar --exclude='*/node_modules' \ --exclude='*/.git' \ --exclude='*/dist' \ --exclude='*/target' \ --exclude='*/.idea' \ --exclude='*/.vscode' \ --exclude='*/.cursor' \ --exclude='*/__MACOSX' \ --exclude='*/.DS_Store' \ --exclude='*.log' \ --exclude='.env.local' \ --exclude='.env.*.local' \ -czf "$TEMP_ARCHIVE" . echo -e "${YELLOW} 서버 전송 중...${NC}" $SCP_CMD "$TEMP_ARCHIVE" "${SSH_TARGET}:/tmp/gc-deploy.tar.gz" echo -e "${YELLOW} 서버 기존 소스 정리 후 압축 해제 중...${NC}" remote_exec "rm -rf ${REMOTE_PROJECT_DIR} && \ mkdir -p ${REMOTE_PROJECT_DIR} && \ cd ${REMOTE_PROJECT_DIR} && \ tar -xzf /tmp/gc-deploy.tar.gz && \ rm -f /tmp/gc-deploy.tar.gz" rm -f "$TEMP_ARCHIVE" echo -e "${GREEN}✓ 파일 동기화 완료${NC}" else echo -e "${CYAN}[1/3] 파일 동기화 생략 — 서버 기존 소스 사용${NC}" fi echo "" # ── Docker 데몬 확인 ────────────────────────────────────────────────── echo -e "${CYAN}[사전] Docker 데몬 확인 중...${NC}" if ! remote_exec "docker info >/dev/null 2>&1"; then echo -e "${YELLOW} Docker Desktop 기동 시도 중...${NC}" remote_exec "[ -d /Applications/Docker.app ] && open -ga Docker || true" for i in $(seq 1 40); do sleep 3 remote_exec "docker info >/dev/null 2>&1" && break echo -ne "${YELLOW} 대기 중 ${i}...${NC}\r" done fi remote_exec "docker info >/dev/null 2>&1" && echo -e "${GREEN}✓ Docker 데몬 준비됨${NC}" || { echo -e "${RED}✗ Docker 데몬 불가${NC}"; exit 1; } echo "" # ── [2/3] 빌드 및 재시작 ────────────────────────────────────────────── [ "$NO_CACHE" = true ] && CACHE_MSG="캐시 없이 (항상 최신 코드 반영)" || CACHE_MSG="캐시 사용 (빠름)" [ "$NO_CACHE" = true ] && NO_CACHE_FLAG="--no-cache" || NO_CACHE_FLAG="" if [ -z "$SERVICE" ]; then # ── 전체 서비스 재시작 ───────────────────────────────────────────── echo -e "${CYAN}[2/3] 전체 서비스 빌드 + 재시작 중... (${CACHE_MSG})${NC}" echo -e "${YELLOW} (mysql, backend, frontend 순서로 시작됨)${NC}" remote_exec "cd ${REMOTE_PROJECT_DIR} && \ docker compose down --remove-orphans && \ docker compose build ${NO_CACHE_FLAG} && \ docker compose up -d" else # ── 특정 서비스만 재빌드 ────────────────────────────────────────── echo -e "${CYAN}[2/3] ${SERVICE} 서비스만 재빌드 중... (${CACHE_MSG})${NC}" remote_exec "cd ${REMOTE_PROJECT_DIR} && \ docker compose build ${NO_CACHE_FLAG} ${SERVICE} && \ docker compose up -d ${SERVICE}" fi echo -e "${GREEN}✓ 빌드 및 재시작 완료${NC}" echo "" # ── [3/3] 서비스 안정화 대기 ────────────────────────────────────────── echo -e "${CYAN}[3/3] 서비스 기동 대기 중 (최대 90초)...${NC}" WAIT=0; MAX_WAIT=90 while [ $WAIT -lt $MAX_WAIT ]; do RUNNING=$(remote_exec "cd ${REMOTE_PROJECT_DIR} && docker compose ps --status running --format json 2>/dev/null | grep -c 'running' || echo 0" 2>/dev/null || echo "0") # 3개 서비스가 모두 running 이면 완료 TOTAL=$(remote_exec "cd ${REMOTE_PROJECT_DIR} && docker compose ps --format json 2>/dev/null | grep -c 'name' || echo 0" 2>/dev/null || echo "0") HTTP_CODE=$(remote_exec "curl -s -o /dev/null -w '%{http_code}' http://localhost:${FRONTEND_PORT}/ 2>/dev/null || echo 000" 2>/dev/null || echo "000") echo -ne " 대기 ${WAIT}s — 컨테이너: ${RUNNING}/${TOTAL}, HTTP: ${HTTP_CODE} \r" if echo "$HTTP_CODE" | grep -qE "^2[0-9][0-9]$"; then echo "" break fi sleep 5; WAIT=$((WAIT + 5)) done echo "" # ── 최종 상태 ───────────────────────────────────────────────────────── echo -e "${CYAN}[완료] 서비스 상태:${NC}" remote_exec "cd ${REMOTE_PROJECT_DIR} && docker compose ps" echo "" HTTP_CODE=$(remote_exec "curl -s -o /dev/null -w '%{http_code}' http://localhost:${FRONTEND_PORT}/ 2>/dev/null || echo 000" 2>/dev/null || echo "000") if echo "$HTTP_CODE" | grep -qE "^2[0-9][0-9]$"; then echo -e "${GREEN}✓ HTTP ${HTTP_CODE} — 프론트엔드 정상 응답${NC}" else echo -e "${YELLOW}⚠ HTTP ${HTTP_CODE} — 아직 초기화 중일 수 있습니다.${NC}" echo -e "${YELLOW} 로그 확인: ./deploy-home.sh --logs${NC}" fi BACKEND_HTTP=$(remote_exec "curl -s -o /dev/null -w '%{http_code}' http://localhost:${BACKEND_PORT}/api/indicators/list 2>/dev/null || echo 000" 2>/dev/null || echo "000") if echo "$BACKEND_HTTP" | grep -qE "^2[0-9][0-9]$"; then echo -e "${GREEN}✓ HTTP ${BACKEND_HTTP} — 백엔드 API 정상 응답${NC}" else echo -e "${YELLOW}⚠ 백엔드 HTTP ${BACKEND_HTTP} (Flyway 마이그레이션 실행 중일 수 있음)${NC}" fi echo "" echo -e "${GREEN}${BOLD}╔══════════════════════════════════════════════════════════╗${NC}" echo -e "${GREEN}${BOLD}║ 배포 완료! 🎉 ║${NC}" echo -e "${GREEN}${BOLD}╚══════════════════════════════════════════════════════════╝${NC}" echo "" echo -e " HTTP: ${CYAN}${BOLD}http://${REMOTE_HOST}:${FRONTEND_PORT}${NC}" echo -e " HTTPS: ${CYAN}${BOLD}https://${REMOTE_HOST}/${NC} ${YELLOW}(인증서: ./scripts/setup-exdev-https.sh)${NC}" echo "" echo -e "${YELLOW}자주 쓰는 명령어:${NC}" echo " ./deploy-home.sh # 전체 재빌드 배포 (기본)" echo " ./deploy-home.sh --use-cache # 캐시 사용 빌드 (빠름)" echo " ./deploy-home.sh --frontend # 프론트엔드만 재빌드" echo " ./deploy-home.sh --backend # 백엔드만 재빌드" echo " ./deploy-home.sh --no-sync # 서버 기존 소스로 재빌드" echo " ./deploy-home.sh --status # 서비스 상태 확인" echo " ./deploy-home.sh --logs # 전체 로그 보기" echo " ./deploy-home.sh --down # 서비스 중지" echo " ./deploy-home.sh --shell # 서버 SSH 접속"