#!/bin/bash # ============================================ # CaStAD Production Server - 완전 자동 배포 스크립트 # ============================================ # 도메인: # - castad.ktenterprise.net # - ado2.whitedonkey.kr # 서버: qm391-0282.cafe24.com (Ubuntu 24.04) # ============================================ set -e # 오류 시 중단 # 색상 정의 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' BLUE='\033[0;34m' BOLD='\033[1m' NC='\033[0m' log() { echo -e "${GREEN}[✓]${NC} $1"; } error() { echo -e "${RED}[✗]${NC} $1"; } warn() { echo -e "${YELLOW}[!]${NC} $1"; } info() { echo -e "${CYAN}[i]${NC} $1"; } header() { echo -e "\n${BOLD}${BLUE}═══ $1 ═══${NC}\n"; } # ============================================ # 설정 # ============================================ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" cd "$SCRIPT_DIR" # 포트 설정 (기본값 - 충돌 시 자동으로 다른 포트 탐색) BACKEND_PORT_DEFAULT=3001 INSTAGRAM_PORT_DEFAULT=5003 FRONTEND_PORT=3002 # 빌드 시에만 사용 # 실제 사용할 포트 (find_available_ports에서 설정) BACKEND_PORT=$BACKEND_PORT_DEFAULT INSTAGRAM_PORT=$INSTAGRAM_PORT_DEFAULT # 도메인 DOMAIN_PRIMARY="castad.ktenterprise.net" DOMAIN_SECONDARY="ado2.whitedonkey.kr" # 디렉토리 APP_DIR="$SCRIPT_DIR" NGINX_AVAILABLE="/etc/nginx/sites-available" NGINX_ENABLED="/etc/nginx/sites-enabled" LOG_DIR="$APP_DIR/logs" # 명령어 ACTION=${1:-help} # ============================================ # 배너 출력 # ============================================ show_banner() { echo "" echo -e "${BOLD}${CYAN}╔════════════════════════════════════════════════════════════╗${NC}" echo -e "${BOLD}${CYAN}║ 🍮 CaStAD (카스타드) Production Server ║${NC}" echo -e "${BOLD}${CYAN}║ AI-Powered Pension Marketing Video Platform ║${NC}" echo -e "${BOLD}${CYAN}╠════════════════════════════════════════════════════════════╣${NC}" echo -e "${BOLD}${CYAN}║ 도메인: ${DOMAIN_PRIMARY} ║${NC}" echo -e "${BOLD}${CYAN}║ ${DOMAIN_SECONDARY} ║${NC}" echo -e "${BOLD}${CYAN}╚════════════════════════════════════════════════════════════╝${NC}" echo "" } # ============================================ # 환경 검사 # ============================================ check_environment() { header "환경 검사" local ERRORS=0 # Node.js if command -v node &> /dev/null; then local NODE_VER=$(node -v) log "Node.js: $NODE_VER" else error "Node.js가 설치되지 않았습니다" ERRORS=$((ERRORS + 1)) fi # npm if command -v npm &> /dev/null; then local NPM_VER=$(npm -v) log "npm: v$NPM_VER" else error "npm이 설치되지 않았습니다" ERRORS=$((ERRORS + 1)) fi # PM2 if command -v pm2 &> /dev/null; then local PM2_VER=$(pm2 -v) log "PM2: v$PM2_VER" else warn "PM2가 설치되지 않았습니다. 설치합니다..." npm install -g pm2 log "PM2 설치 완료" fi # Python3 if command -v python3 &> /dev/null; then local PY_VER=$(python3 --version) log "Python: $PY_VER" else error "Python3이 설치되지 않았습니다" ERRORS=$((ERRORS + 1)) fi # pip3 if command -v pip3 &> /dev/null; then log "pip3: 설치됨" else warn "pip3이 설치되지 않았습니다" fi # FFmpeg if command -v ffmpeg &> /dev/null; then log "FFmpeg: 설치됨" else warn "FFmpeg가 설치되지 않았습니다 - 영상 렌더링 불가" info "설치: sudo apt-get install -y ffmpeg" fi # Chromium (Puppeteer용) if command -v chromium-browser &> /dev/null || command -v chromium &> /dev/null; then log "Chromium: 설치됨" else warn "Chromium이 설치되지 않았습니다 - 영상 렌더링 불가" info "설치: sudo apt-get install -y chromium-browser" fi # nginx if command -v nginx &> /dev/null; then local NGINX_VER=$(nginx -v 2>&1 | cut -d'/' -f2) log "nginx: $NGINX_VER" else error "nginx가 설치되지 않았습니다" ERRORS=$((ERRORS + 1)) fi # Git if command -v git &> /dev/null; then log "Git: 설치됨" else error "Git이 설치되지 않았습니다" ERRORS=$((ERRORS + 1)) fi if [ $ERRORS -gt 0 ]; then error "필수 요구사항 $ERRORS 개가 누락되었습니다" return 1 fi log "환경 검사 완료" return 0 } # ============================================ # 포트 사용 가능 여부 확인 # ============================================ is_port_available() { local PORT=$1 # ss 명령어로 포트 사용 여부 확인 if ss -tlnp 2>/dev/null | grep -q ":${PORT} "; then return 1 # 사용 중 fi return 0 # 사용 가능 } # ============================================ # 사용 가능한 포트 찾기 # ============================================ find_free_port() { local START_PORT=$1 local MAX_TRIES=${2:-20} local PORT=$START_PORT for ((i=0; i/dev/null || echo "") local CASTAD_INSTA_PID=$(pm2 pid castad-instagram 2>/dev/null || echo "") # Backend 포트 찾기 if is_port_available $BACKEND_PORT_DEFAULT; then BACKEND_PORT=$BACKEND_PORT_DEFAULT log "Backend 포트: ${BACKEND_PORT} (기본값 사용)" else # 기존 CaStAD 프로세스인지 확인 local PORT_PID=$(ss -tlnp 2>/dev/null | grep ":${BACKEND_PORT_DEFAULT} " | grep -oP 'pid=\K[0-9]+' | head -1) if [ -n "$CASTAD_BACKEND_PID" ] && [ "$PORT_PID" == "$CASTAD_BACKEND_PID" ]; then BACKEND_PORT=$BACKEND_PORT_DEFAULT info "Backend 포트: ${BACKEND_PORT} (기존 CaStAD 프로세스, 재시작 시 교체)" else # 다른 프로세스가 사용 중 - 새 포트 찾기 local NEW_PORT=$(find_free_port $BACKEND_PORT_DEFAULT) if [ -z "$NEW_PORT" ]; then error "사용 가능한 Backend 포트를 찾을 수 없습니다 (${BACKEND_PORT_DEFAULT}~$((BACKEND_PORT_DEFAULT+19)))" return 1 fi BACKEND_PORT=$NEW_PORT warn "Backend 포트 ${BACKEND_PORT_DEFAULT} 사용 중 → ${BACKEND_PORT} 로 변경" fi fi # Instagram 서비스 포트 찾기 if is_port_available $INSTAGRAM_PORT_DEFAULT; then INSTAGRAM_PORT=$INSTAGRAM_PORT_DEFAULT log "Instagram 포트: ${INSTAGRAM_PORT} (기본값 사용)" else # 기존 CaStAD 프로세스인지 확인 local PORT_PID=$(ss -tlnp 2>/dev/null | grep ":${INSTAGRAM_PORT_DEFAULT} " | grep -oP 'pid=\K[0-9]+' | head -1) if [ -n "$CASTAD_INSTA_PID" ] && [ "$PORT_PID" == "$CASTAD_INSTA_PID" ]; then INSTAGRAM_PORT=$INSTAGRAM_PORT_DEFAULT info "Instagram 포트: ${INSTAGRAM_PORT} (기존 CaStAD 프로세스, 재시작 시 교체)" else # 다른 프로세스가 사용 중 - 새 포트 찾기 local NEW_PORT=$(find_free_port $INSTAGRAM_PORT_DEFAULT) if [ -z "$NEW_PORT" ]; then error "사용 가능한 Instagram 포트를 찾을 수 없습니다 (${INSTAGRAM_PORT_DEFAULT}~$((INSTAGRAM_PORT_DEFAULT+19)))" return 1 fi INSTAGRAM_PORT=$NEW_PORT warn "Instagram 포트 ${INSTAGRAM_PORT_DEFAULT} 사용 중 → ${INSTAGRAM_PORT} 로 변경" fi fi echo "" info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" info " 최종 포트 설정:" info " Backend: ${BACKEND_PORT}" info " Instagram: ${INSTAGRAM_PORT}" info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" log "포트 탐색 완료" return 0 } # ============================================ # 의존성 설치 # ============================================ install_dependencies() { header "의존성 설치" # Node.js 의존성 if [ ! -d "node_modules" ] || [ "$1" == "force" ]; then log "Frontend 패키지 설치 중..." npm install --legacy-peer-deps --silent else info "Frontend 패키지 이미 설치됨 (강제 설치: --force)" fi if [ ! -d "server/node_modules" ] || [ "$1" == "force" ]; then log "Backend 패키지 설치 중..." cd server && npm install --legacy-peer-deps --silent && cd .. else info "Backend 패키지 이미 설치됨" fi # Python 의존성 (Instagram 서비스) if [ -f "server/instagram/requirements.txt" ]; then if ! python3 -c "import instagrapi" 2>/dev/null; then log "Instagram 서비스 Python 패키지 설치 중..." pip3 install -q -r server/instagram/requirements.txt 2>/dev/null || \ pip3 install --break-system-packages -q -r server/instagram/requirements.txt 2>/dev/null || true else info "Instagram Python 패키지 이미 설치됨" fi fi log "의존성 설치 완료" } # ============================================ # 디렉토리 설정 # ============================================ setup_directories() { header "디렉토리 설정" mkdir -p "$LOG_DIR" mkdir -p server/downloads mkdir -p server/temp mkdir -p server/uploads log "디렉토리 생성 완료" } # ============================================ # 환경 변수 검사 # ============================================ check_env_file() { header "환경 변수 검사" if [ ! -f ".env" ]; then if [ -f ".env.production.example" ]; then warn ".env 파일이 없습니다. 템플릿에서 생성합니다..." cp .env.production.example .env warn ".env 파일을 편집하여 API 키를 설정하세요!" warn "nano .env" else error ".env 파일이 없습니다!" return 1 fi else log ".env 파일 확인됨" fi # 필수 환경변수 검사 source .env 2>/dev/null || true if [ -z "$JWT_SECRET" ] || [ "$JWT_SECRET" == "your-super-secret-jwt-key-change-this-in-production" ]; then warn "JWT_SECRET이 기본값입니다. 변경하세요!" fi if [ -z "$VITE_GEMINI_API_KEY" ] || [ "$VITE_GEMINI_API_KEY" == "your-gemini-api-key" ]; then warn "VITE_GEMINI_API_KEY가 설정되지 않았습니다" fi # 포트 환경변수 업데이트 local ENV_UPDATED=0 if grep -q "^PORT=" .env; then local OLD_PORT=$(grep "^PORT=" .env | cut -d'=' -f2) if [ "$OLD_PORT" != "${BACKEND_PORT}" ]; then sed -i "s/^PORT=.*/PORT=${BACKEND_PORT}/" .env info ".env PORT: ${OLD_PORT} → ${BACKEND_PORT}" ENV_UPDATED=1 fi else echo "PORT=${BACKEND_PORT}" >> .env info ".env PORT: ${BACKEND_PORT} (추가)" ENV_UPDATED=1 fi if grep -q "^INSTAGRAM_SERVICE_PORT=" .env; then local OLD_PORT=$(grep "^INSTAGRAM_SERVICE_PORT=" .env | cut -d'=' -f2) if [ "$OLD_PORT" != "${INSTAGRAM_PORT}" ]; then sed -i "s/^INSTAGRAM_SERVICE_PORT=.*/INSTAGRAM_SERVICE_PORT=${INSTAGRAM_PORT}/" .env info ".env INSTAGRAM_SERVICE_PORT: ${OLD_PORT} → ${INSTAGRAM_PORT}" ENV_UPDATED=1 fi else echo "INSTAGRAM_SERVICE_PORT=${INSTAGRAM_PORT}" >> .env info ".env INSTAGRAM_SERVICE_PORT: ${INSTAGRAM_PORT} (추가)" ENV_UPDATED=1 fi if grep -q "^INSTAGRAM_SERVICE_URL=" .env; then sed -i "s|^INSTAGRAM_SERVICE_URL=.*|INSTAGRAM_SERVICE_URL=http://localhost:${INSTAGRAM_PORT}|" .env else echo "INSTAGRAM_SERVICE_URL=http://localhost:${INSTAGRAM_PORT}" >> .env fi if [ $ENV_UPDATED -eq 1 ]; then log ".env 파일 포트 설정 업데이트 완료" else log ".env 파일 포트 설정 확인 완료 (변경 없음)" fi } # ============================================ # 프론트엔드 빌드 # ============================================ build_frontend() { header "프론트엔드 빌드" export NODE_ENV=production export VITE_BACKEND_PORT=$BACKEND_PORT log "Vite 빌드 중..." npm run build if [ ! -d "dist" ]; then error "프론트엔드 빌드 실패!" return 1 fi log "프론트엔드 빌드 완료 (dist/)" } # ============================================ # PM2 Ecosystem 파일 생성 # ============================================ create_ecosystem() { header "PM2 설정 생성" cat > ecosystem.config.cjs << ECOSYSTEM module.exports = { apps: [ { name: 'castad-backend', script: 'server/index.js', cwd: '${APP_DIR}', instances: 1, exec_mode: 'fork', autorestart: true, watch: false, max_memory_restart: '1G', env: { NODE_ENV: 'production', PORT: ${BACKEND_PORT} }, error_file: '${LOG_DIR}/backend-error.log', out_file: '${LOG_DIR}/backend-out.log', log_date_format: 'YYYY-MM-DD HH:mm:ss Z' }, { name: 'castad-instagram', script: 'server/instagram/instagram_service.py', cwd: '${APP_DIR}', interpreter: 'python3', instances: 1, autorestart: true, watch: false, env: { INSTAGRAM_SERVICE_PORT: ${INSTAGRAM_PORT} }, error_file: '${LOG_DIR}/instagram-error.log', out_file: '${LOG_DIR}/instagram-out.log', log_date_format: 'YYYY-MM-DD HH:mm:ss Z' } ] }; ECOSYSTEM log "ecosystem.config.cjs 생성됨" } # ============================================ # nginx 설정 생성 # ============================================ create_nginx_config() { header "nginx 설정 생성" local NGINX_CONF="nginx/castad-server.conf" cat > "$NGINX_CONF" << 'NGINX_CONFIG' # ============================================ # CaStAD Nginx Configuration # Generated by startserver.sh # ============================================ upstream castad_backend { server 127.0.0.1:BACKEND_PORT_PLACEHOLDER; keepalive 64; } # HTTP → HTTPS 리다이렉트 server { listen 80; listen [::]:80; server_name DOMAIN_PRIMARY_PLACEHOLDER DOMAIN_SECONDARY_PLACEHOLDER; location /.well-known/acme-challenge/ { root /var/www/certbot; } location / { return 301 https://$host$request_uri; } } # HTTPS 서버 server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name DOMAIN_PRIMARY_PLACEHOLDER DOMAIN_SECONDARY_PLACEHOLDER; # SSL 인증서 (certbot이 자동 설정) ssl_certificate /etc/letsencrypt/live/DOMAIN_PRIMARY_PLACEHOLDER/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/DOMAIN_PRIMARY_PLACEHOLDER/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; # 보안 헤더 add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; # 로그 access_log /var/log/nginx/castad_access.log; error_log /var/log/nginx/castad_error.log; # 파일 업로드 크기 client_max_body_size 500M; client_body_timeout 300s; # 정적 파일 (프론트엔드) root APP_DIR_PLACEHOLDER/dist; index index.html; # Gzip gzip on; gzip_vary on; gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml; # 정적 자원 캐싱 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control "public, immutable"; try_files $uri =404; } # API 요청 location /api/ { proxy_pass http://castad_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $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_cache_bypass $http_upgrade; proxy_read_timeout 300s; } # 렌더링 요청 (긴 타임아웃) location /render { proxy_pass http://castad_backend; proxy_http_version 1.1; proxy_set_header Host $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_read_timeout 600s; proxy_buffering off; } # 다운로드/업로드/임시 파일 location ~ ^/(downloads|temp|uploads)/ { proxy_pass http://castad_backend; proxy_http_version 1.1; proxy_set_header Host $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; } # SPA 라우팅 location / { try_files $uri $uri/ /index.html; } } NGINX_CONFIG # 플레이스홀더 치환 sed -i "s|BACKEND_PORT_PLACEHOLDER|${BACKEND_PORT}|g" "$NGINX_CONF" sed -i "s|DOMAIN_PRIMARY_PLACEHOLDER|${DOMAIN_PRIMARY}|g" "$NGINX_CONF" sed -i "s|DOMAIN_SECONDARY_PLACEHOLDER|${DOMAIN_SECONDARY}|g" "$NGINX_CONF" sed -i "s|APP_DIR_PLACEHOLDER|${APP_DIR}|g" "$NGINX_CONF" log "nginx 설정 파일 생성: $NGINX_CONF" } # ============================================ # nginx 설정 설치 # ============================================ install_nginx_config() { header "nginx 설정 설치" local NGINX_CONF="nginx/castad-server.conf" local NGINX_NAME="castad" if [ ! -f "$NGINX_CONF" ]; then create_nginx_config fi # root 권한 확인 if [ "$EUID" -ne 0 ]; then warn "nginx 설정 설치에 sudo 권한이 필요합니다" info "수동 설치:" info " sudo cp $NGINX_CONF $NGINX_AVAILABLE/$NGINX_NAME" info " sudo ln -sf $NGINX_AVAILABLE/$NGINX_NAME $NGINX_ENABLED/" info " sudo nginx -t && sudo systemctl reload nginx" return 0 fi # 설정 복사 cp "$NGINX_CONF" "$NGINX_AVAILABLE/$NGINX_NAME" # 심볼릭 링크 ln -sf "$NGINX_AVAILABLE/$NGINX_NAME" "$NGINX_ENABLED/" # 설정 검증 if nginx -t 2>/dev/null; then systemctl reload nginx log "nginx 설정 설치 및 적용 완료" else error "nginx 설정 오류!" nginx -t return 1 fi } # ============================================ # DNS 확인 # ============================================ check_dns() { header "DNS 확인" local SERVER_IP=$(curl -s ifconfig.me 2>/dev/null || curl -s icanhazip.com 2>/dev/null) log "서버 IP: ${SERVER_IP}" local DNS_OK=1 # Primary 도메인 확인 local PRIMARY_IP=$(dig +short "$DOMAIN_PRIMARY" 2>/dev/null | head -1) if [ -z "$PRIMARY_IP" ]; then error "${DOMAIN_PRIMARY}: DNS 레코드 없음" DNS_OK=0 elif [ "$PRIMARY_IP" == "$SERVER_IP" ]; then log "${DOMAIN_PRIMARY} → ${PRIMARY_IP} ✓" else warn "${DOMAIN_PRIMARY} → ${PRIMARY_IP} (서버 IP와 다름: ${SERVER_IP})" DNS_OK=0 fi # Secondary 도메인 확인 local SECONDARY_IP=$(dig +short "$DOMAIN_SECONDARY" 2>/dev/null | head -1) if [ -z "$SECONDARY_IP" ]; then error "${DOMAIN_SECONDARY}: DNS 레코드 없음" DNS_OK=0 elif [ "$SECONDARY_IP" == "$SERVER_IP" ]; then log "${DOMAIN_SECONDARY} → ${SECONDARY_IP} ✓" else warn "${DOMAIN_SECONDARY} → ${SECONDARY_IP} (서버 IP와 다름: ${SERVER_IP})" DNS_OK=0 fi if [ $DNS_OK -eq 0 ]; then warn "DNS 설정을 확인하세요. SSL 발급이 실패할 수 있습니다." return 1 fi log "DNS 확인 완료" return 0 } # ============================================ # HTTP 전용 nginx 설정 생성 (SSL 발급 전용) # ============================================ create_nginx_http_only() { header "HTTP 전용 nginx 설정 생성" local NGINX_CONF="nginx/castad-http.conf" cat > "$NGINX_CONF" << NGINX_HTTP # CaStAD HTTP-only config (for SSL certificate issuance) server { listen 80; listen [::]:80; server_name ${DOMAIN_PRIMARY} ${DOMAIN_SECONDARY}; # Let's Encrypt 인증용 location /.well-known/acme-challenge/ { root /var/www/certbot; } # API 프록시 location /api/ { proxy_pass http://127.0.0.1:${BACKEND_PORT}; proxy_http_version 1.1; proxy_set_header Host \$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; } # 렌더링 location /render { proxy_pass http://127.0.0.1:${BACKEND_PORT}; proxy_read_timeout 600s; } # 다운로드/업로드/임시 location ~ ^/(downloads|temp|uploads)/ { proxy_pass http://127.0.0.1:${BACKEND_PORT}; } # 정적 파일 root ${APP_DIR}/dist; index index.html; location / { try_files \$uri \$uri/ /index.html; } } NGINX_HTTP log "HTTP 전용 nginx 설정 생성: $NGINX_CONF" } # ============================================ # SSL 인증서 설치 (전체 자동화) # ============================================ install_ssl() { header "SSL 인증서 설치" # root 권한 확인 if [ "$EUID" -ne 0 ]; then warn "SSL 인증서 설치에 sudo 권한이 필요합니다" info "실행: sudo ./startserver.sh ssl" return 0 fi # DNS 확인 check_dns || { warn "DNS 문제가 있지만 계속 진행합니다..." } # certbot 설치 확인 및 자동 설치 if ! command -v certbot &> /dev/null; then log "certbot 설치 중..." apt-get update -qq apt-get install -y -qq certbot python3-certbot-nginx log "certbot 설치 완료" else log "certbot: 이미 설치됨" fi # certbot 디렉토리 생성 mkdir -p /var/www/certbot # 기존 SSL 인증서 확인 if [ -f "/etc/letsencrypt/live/${DOMAIN_PRIMARY}/fullchain.pem" ]; then log "기존 SSL 인증서 발견" info "갱신 시도 중..." certbot renew --quiet log "SSL 인증서 갱신 완료" else # HTTP 전용 설정으로 먼저 nginx 구성 log "HTTP 전용 nginx 설정 적용 중..." create_nginx_http_only # nginx에 HTTP 설정 적용 cp nginx/castad-http.conf /etc/nginx/sites-available/castad ln -sf /etc/nginx/sites-available/castad /etc/nginx/sites-enabled/ rm -f /etc/nginx/sites-enabled/default 2>/dev/null || true if nginx -t 2>/dev/null; then systemctl reload nginx log "HTTP nginx 설정 적용 완료" else error "nginx 설정 오류" nginx -t return 1 fi # 잠시 대기 (nginx 재시작 완료 대기) sleep 2 # SSL 인증서 발급 log "SSL 인증서 발급 중..." if certbot --nginx \ -d "$DOMAIN_PRIMARY" \ -d "$DOMAIN_SECONDARY" \ --non-interactive \ --agree-tos \ --email "admin@${DOMAIN_PRIMARY}" \ --redirect; then log "SSL 인증서 발급 성공!" else error "SSL 인증서 발급 실패" info "수동으로 시도하세요: sudo certbot --nginx -d $DOMAIN_PRIMARY -d $DOMAIN_SECONDARY" return 1 fi fi # HTTPS nginx 설정 적용 log "HTTPS nginx 설정 적용 중..." create_nginx_config cp nginx/castad-server.conf /etc/nginx/sites-available/castad if nginx -t 2>/dev/null; then systemctl reload nginx log "HTTPS nginx 설정 적용 완료" else warn "HTTPS 설정 오류 - certbot이 자동 생성한 설정 사용" fi # 자동 갱신 확인 log "자동 갱신 테스트..." certbot renew --dry-run --quiet && log "자동 갱신 테스트 성공" || warn "자동 갱신 테스트 실패" echo "" log "🔒 SSL 설치 완료!" info " https://${DOMAIN_PRIMARY}" info " https://${DOMAIN_SECONDARY}" } # ============================================ # 포트를 사용 중인 프로세스 강제 종료 # ============================================ kill_port_process() { local PORT=$1 local PID=$(lsof -t -i:${PORT} 2>/dev/null || ss -tlnp 2>/dev/null | grep ":${PORT} " | grep -oP 'pid=\K[0-9]+' | head -1) if [ -n "$PID" ]; then info "포트 ${PORT} 사용 중인 프로세스(PID: ${PID}) 종료 중..." kill -9 $PID 2>/dev/null || true sleep 1 return 0 fi return 1 } # ============================================ # PM2로 서비스 시작 # ============================================ start_services() { header "서비스 시작" # 기존 PM2 프로세스 정리 log "기존 PM2 프로세스 정리 중..." pm2 delete castad-backend 2>/dev/null || true pm2 delete castad-instagram 2>/dev/null || true # PM2 데몬 완전 정리 (좀비 프로세스 제거) pm2 kill 2>/dev/null || true sleep 2 # 포트를 점유하고 있는 모든 프로세스 강제 종료 log "포트 점유 프로세스 정리 중..." kill_port_process ${BACKEND_PORT} || true kill_port_process ${INSTAGRAM_PORT} || true # 추가로 3003 포트도 정리 (과거 설정으로 인한 좀비 프로세스) kill_port_process 3003 || true kill_port_process 3001 || true kill_port_process 5003 || true kill_port_process 5001 || true sleep 1 # PM2로 시작 log "PM2 서비스 시작 중..." pm2 start ecosystem.config.cjs # 상태 저장 pm2 save --force # 잠시 대기 (서비스 안정화) sleep 5 # 헬스 체크 local RETRY=0 local MAX_RETRY=3 while [ $RETRY -lt $MAX_RETRY ]; do if curl -s "http://localhost:${BACKEND_PORT}/api/admin/stats" > /dev/null 2>&1; then log "Backend 서비스 정상 동작 (포트: ${BACKEND_PORT})" break fi RETRY=$((RETRY + 1)) if [ $RETRY -lt $MAX_RETRY ]; then info "Backend 응답 대기 중... (${RETRY}/${MAX_RETRY})" sleep 3 fi done if [ $RETRY -eq $MAX_RETRY ]; then warn "Backend 서비스 응답 없음 - 로그를 확인하세요" warn "pm2 logs castad-backend --lines 20" fi # nginx 포트 자동 동기화 sync_nginx_port log "서비스 시작 완료" } # ============================================ # nginx 포트 자동 동기화 # ============================================ sync_nginx_port() { if [ ! -f "/etc/nginx/sites-enabled/castad" ]; then return 0 fi local CURRENT_NGINX_PORT=$(grep -oP 'server 127\.0\.0\.1:\K[0-9]+' /etc/nginx/sites-enabled/castad 2>/dev/null | head -1) if [ -z "$CURRENT_NGINX_PORT" ]; then return 0 fi if [ "$CURRENT_NGINX_PORT" == "$BACKEND_PORT" ]; then log "nginx 포트: ${BACKEND_PORT} (정상)" return 0 fi warn "nginx 포트 불일치 감지: ${CURRENT_NGINX_PORT} → ${BACKEND_PORT}" if [ "$EUID" -eq 0 ]; then sed -i "s/127.0.0.1:${CURRENT_NGINX_PORT}/127.0.0.1:${BACKEND_PORT}/g" /etc/nginx/sites-enabled/castad if nginx -t 2>/dev/null; then systemctl reload nginx log "nginx 포트 자동 수정 완료: ${BACKEND_PORT}" else error "nginx 설정 오류" fi else info "nginx 포트 수정이 필요합니다 (sudo 권한 필요):" echo "" echo " sudo sed -i 's/127.0.0.1:${CURRENT_NGINX_PORT}/127.0.0.1:${BACKEND_PORT}/g' /etc/nginx/sites-enabled/castad" echo " sudo nginx -t && sudo systemctl reload nginx" echo "" fi } # ============================================ # 서비스 중지 # ============================================ stop_services() { header "서비스 중지" pm2 delete castad-backend 2>/dev/null || true pm2 delete castad-instagram 2>/dev/null || true pm2 save --force log "서비스 중지 완료" } # ============================================ # 상태 표시 # ============================================ show_status() { header "서비스 상태" pm2 status echo "" echo -e "${CYAN}─────────────────────────────────────────────────────────${NC}" echo -e " ${BOLD}Backend${NC}: http://localhost:${BACKEND_PORT}" echo -e " ${BOLD}Instagram${NC}: http://localhost:${INSTAGRAM_PORT}" echo -e " ${BOLD}도메인${NC}:" echo -e " - https://${DOMAIN_PRIMARY}" echo -e " - https://${DOMAIN_SECONDARY}" echo -e " ${BOLD}관리자${NC}: admin / admin123" echo -e "${CYAN}─────────────────────────────────────────────────────────${NC}" echo "" } # ============================================ # 전체 설치 및 시작 # ============================================ full_install() { show_banner check_environment || exit 1 find_available_ports || exit 1 setup_directories check_env_file || exit 1 install_dependencies build_frontend || exit 1 create_ecosystem # nginx 설정 (SSL 인증서 존재 여부에 따라 다름) if [ "$EUID" -eq 0 ]; then # SSL 인증서가 이미 있으면 HTTPS 설정, 없으면 HTTP만 if [ -f "/etc/letsencrypt/live/${DOMAIN_PRIMARY}/fullchain.pem" ]; then log "기존 SSL 인증서 발견 - HTTPS 설정 적용" create_nginx_config install_nginx_config else log "SSL 인증서 없음 - HTTP 설정만 적용 (SSL은 별도 실행)" create_nginx_http_only cp nginx/castad-http.conf /etc/nginx/sites-available/castad ln -sf /etc/nginx/sites-available/castad /etc/nginx/sites-enabled/ rm -f /etc/nginx/sites-enabled/default 2>/dev/null || true if nginx -t 2>/dev/null; then systemctl reload nginx log "HTTP nginx 설정 적용 완료" fi info "SSL 발급: sudo ./startserver.sh ssl" fi else warn "nginx/SSL 설정은 sudo로 별도 실행하세요:" info " sudo ./startserver.sh ssl" fi start_services show_status log "🍮 CaStAD 배포 완료!" } # ============================================ # 업데이트 # ============================================ update_server() { header "서버 업데이트" # 로컬 변경사항 체크 if ! git diff --quiet 2>/dev/null; then warn "로컬 변경사항이 있습니다. 백업하거나 커밋하세요." info "강제 업데이트: git checkout -- . && git pull origin main" return 1 fi log "Git pull 중..." git pull origin main # 포트 재탐색 (환경이 바뀌었을 수 있음) find_available_ports || exit 1 # 환경변수 업데이트 check_env_file || exit 1 # 의존성 및 빌드 install_dependencies force build_frontend # ecosystem 재생성 (포트 설정 반영) create_ecosystem # 서비스 재시작 start_services log "업데이트 완료!" show_status } # ============================================ # 도움말 # ============================================ show_help() { show_banner echo "사용법: $0 <명령어>" echo "" echo "명령어:" echo -e " ${GREEN}start${NC} 서버 전체 설치 및 시작 (첫 배포 시 사용)" echo -e " ${GREEN}setup${NC} start + nginx + SSL 전체 설치 (sudo 필요)" echo -e " ${GREEN}stop${NC} 서버 중지" echo -e " ${GREEN}restart${NC} 서버 재시작" echo -e " ${GREEN}status${NC} 서버 상태 확인" echo -e " ${GREEN}logs${NC} 실시간 로그 보기" echo -e " ${GREEN}update${NC} Git pull + 재빌드 + 재시작" echo -e " ${GREEN}build${NC} 프론트엔드만 빌드" echo -e " ${GREEN}nginx${NC} nginx 설정 설치 (sudo 필요)" echo -e " ${GREEN}ssl${NC} SSL 인증서 발급 + 자동 설정 (sudo 필요)" echo -e " ${GREEN}dns${NC} DNS 레코드 확인" echo -e " ${GREEN}check${NC} 환경 및 포트 검사 (자동 탐색)" echo "" echo "예시:" echo " sudo ./startserver.sh setup # 전체 설치 (권장)" echo " ./startserver.sh start # 서버만 시작" echo " ./startserver.sh update # 업데이트" echo "" echo "포트 설정 (기본값 - 충돌 시 자동 탐색):" echo " Backend: ${BACKEND_PORT_DEFAULT} (현재: ${BACKEND_PORT})" echo " Instagram: ${INSTAGRAM_PORT_DEFAULT} (현재: ${INSTAGRAM_PORT})" echo "" echo "※ 포트 충돌 시 자동으로 다음 사용 가능한 포트를 찾습니다" echo "" } # ============================================ # 메인 # ============================================ case "$ACTION" in start) full_install ;; setup) # 전체 설치 (sudo로 실행 권장) if [ "$EUID" -ne 0 ]; then warn "setup 명령어는 sudo로 실행하는 것이 좋습니다" info "sudo ./startserver.sh setup" echo "" read -p "계속하시겠습니까? (y/N) " -n 1 -r echo "" if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 0 fi fi full_install install_ssl show_status echo "" log "🍮 CaStAD 전체 설치 완료!" info " https://${DOMAIN_PRIMARY}" info " https://${DOMAIN_SECONDARY}" ;; dns) show_banner check_dns ;; stop) show_banner stop_services ;; restart) show_banner find_available_ports || exit 1 check_env_file || exit 1 create_ecosystem start_services show_status ;; status) show_banner show_status ;; logs) pm2 logs --lines 100 ;; update) show_banner update_server ;; build) show_banner build_frontend ;; nginx) show_banner create_nginx_config install_nginx_config ;; ssl) show_banner install_ssl ;; check) show_banner check_environment find_available_ports ;; help|--help|-h|"") show_help ;; *) error "알 수 없는 명령어: $ACTION" show_help exit 1 ;; esac