#!/usr/bin/env bash # # PharmQ PVE 원클릭 통합 설치 스크립트 # 사용법: curl -fsSL https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/pharmq-setup.sh | bash # # Phase 1: PVE Repository Fix (구독 제한 해제) # Phase 2: PVE Host Tailscale → Headscale 등록 # Phase 3: 약국 정보 수집 # Phase 4: PBS 등록 + Windows VM 복원 # Phase 5: Ubuntu CT 생성 # Phase 6: CT 내부 환경 구축 # Phase 7: 약국 + 장비 + 계정 등록 # Phase 8: 검증 + 결과 출력 # set -euo pipefail # ============================================================ # 설정 # ============================================================ HEADSCALE_SERVER="http://head.pharmq.kr" PREAUTH_KEY="b46923995afeaec90e588168f2e1bf99801775e8657ce003" # PBS 설정 PBS_SERVER="100.64.0.10" PBS_PORT="8007" PBS_USERNAME="0bin@pbs" PBS_PASSWORD="@Trajet6640" PBS_DATASTORE="PBS-DVA" PBS_FINGERPRINT="24:42:c6:0f:a8:1b:93:32:32:44:84:be:6a:c5:71:97:e4:4d:61:fc:a4:48:12:0c:97:3b:9f:1f:cc:b2:54:e8" PBS_STORAGE_NAME="PBS-Auto" # API 서버 FARMQ_API="https://demo.pharmq.kr" GATEWAY_API="https://gateway.pharmq.kr" # CT 기본값 CT_CORES=4 CT_MEMORY=8192 CT_DISK=30 CT_PASSWORD="trajet6640" CT_TEMPLATE="ubuntu-24.04-standard_24.04-2_amd64.tar.zst" # 결과 변수 (Phase 간 공유) PVE_VPN_IP="" PVE_HOSTNAME="" CT_VMID="" CT_LAN_IP="" CT_VPN_IP="" VM_VMID="" MSSQL_SERVER="" PHARMACY_NAME="" PHARMACY_CODE="" HIRA_CODE="" PHARMACY_ADDRESS="" OWNER_NAME="" PHARMACY_PHONE="" # ============================================================ # 색상 + 유틸 # ============================================================ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' WHITE='\033[1;37m' NC='\033[0m' print_phase() { echo "" echo -e "${PURPLE}════════════════════════════════════════════${NC}" echo -e "${WHITE} $1${NC}" echo -e "${PURPLE}════════════════════════════════════════════${NC}" echo "" } print_step() { echo -e "${BLUE}▶ $1${NC}"; } print_ok() { echo -e "${GREEN}✅ $1${NC}"; } print_warn() { echo -e "${YELLOW}⚠️ $1${NC}"; } print_err() { echo -e "${RED}❌ $1${NC}"; } print_info() { echo -e "${CYAN} $1${NC}"; } # ============================================================ # Phase 1: PVE Repository Fix # ============================================================ phase1_repo_fix() { print_phase "Phase 1/8: PVE Repository Fix" # Proxmox 환경 확인 if [ ! -f /etc/pve/storage.cfg ]; then print_err "Proxmox VE가 설치되어 있지 않습니다. 이 스크립트는 PVE host에서 실행해야 합니다." exit 1 fi # Root 확인 if [ "$EUID" -ne 0 ]; then print_err "root 권한으로 실행해야 합니다." exit 1 fi print_ok "Proxmox VE 환경 확인" # Keyring 확인 install -d -m 0755 /usr/share/keyrings if [ ! -f /usr/share/keyrings/proxmox-archive-keyring.gpg ]; then print_step "proxmox-archive-keyring 설치 중..." apt-get update -qq apt-get install -y proxmox-archive-keyring || true fi CODENAME="trixie" # PVE 9 = Debian 13 # No-subscription 리포 설정 print_step "No-subscription 리포 설정 중..." cat >/etc/apt/sources.list.d/proxmox.sources </etc/apt/sources.list.d/ceph.sources < "$f.new" && mv "$f.new" "$f" fi done for f in /etc/apt/sources.list /etc/apt/sources.list.d/*.list; do [ -f "$f" ] || continue if grep -q "enterprise\.proxmox\.com" "$f" 2>/dev/null; then sed -i -E 's|^(deb .*enterprise\.proxmox\.com.*)$|# \1|' "$f" fi done rm -f /etc/apt/sources.list.d/pve-no-subscription.list 2>/dev/null || true print_step "apt-get update 실행 중..." apt-get update -qq print_ok "Phase 1 완료: PVE 리포지토리 수정됨" } # ============================================================ # Phase 2: PVE Host Tailscale → Headscale 등록 # ============================================================ phase2_tailscale_pve() { print_phase "Phase 2/8: PVE Host Tailscale 등록" # Tailscale 설치 if ! command -v tailscale >/dev/null 2>&1; then print_step "Tailscale 설치 중..." curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/jammy.noarmor.gpg | tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/jammy.tailscale-keyring.list | tee /etc/apt/sources.list.d/tailscale.list >/dev/null apt-get update -qq apt-get install -y tailscale else print_ok "Tailscale 이미 설치됨: $(tailscale version | head -n1)" fi # 서비스 시작 systemctl enable tailscaled >/dev/null 2>&1 || true systemctl start tailscaled >/dev/null 2>&1 || true sleep 3 # 이미 연결돼있는지 확인 if tailscale status >/dev/null 2>&1; then PVE_VPN_IP=$(tailscale ip -4 2>/dev/null || echo "") if [ -n "$PVE_VPN_IP" ]; then print_ok "이미 Headscale에 연결됨: $PVE_VPN_IP" PVE_HOSTNAME=$(hostname) return 0 fi fi # Headscale 등록 PVE_HOSTNAME=$(hostname) print_step "Headscale 서버에 등록 중... (hostname: ${PVE_HOSTNAME})" if tailscale up \ --login-server="$HEADSCALE_SERVER" \ --authkey="$PREAUTH_KEY" \ --accept-routes \ --accept-dns=false 2>/dev/null; then sleep 5 PVE_VPN_IP=$(tailscale ip -4 2>/dev/null || echo "") if [ -n "$PVE_VPN_IP" ]; then print_ok "Headscale 등록 성공!" print_info "VPN IP: $PVE_VPN_IP" else print_err "VPN IP를 가져올 수 없습니다." exit 1 fi else print_err "Headscale 등록 실패" print_info "수동 등록: tailscale up --login-server=$HEADSCALE_SERVER --authkey=$PREAUTH_KEY --accept-routes --accept-dns=false" exit 1 fi print_ok "Phase 2 완료: PVE Host VPN 연결됨 ($PVE_VPN_IP)" } # ============================================================ # Phase 3: 약국 정보 수집 # ============================================================ phase3_collect_info() { print_phase "Phase 3/8: 약국 정보 수집" # 약국명 (필수) PHARMACY_NAME="" while [ -z "$PHARMACY_NAME" ]; do echo -ne "${CYAN}약국명 (필수): ${NC}" read -r PHARMACY_NAME /dev/null | grep -q "^${PBS_STORAGE_NAME} "; then print_ok "PBS 스토리지 이미 등록됨" else pvesm add pbs "${PBS_STORAGE_NAME}" \ --server "${PBS_SERVER}" \ --port "${PBS_PORT}" \ --datastore "${PBS_DATASTORE}" \ --username "${PBS_USERNAME}" \ --password "${PBS_PASSWORD}" \ --fingerprint "${PBS_FINGERPRINT}" \ --namespace "PQ" 2>&1 if [ $? -eq 0 ]; then print_ok "PBS 스토리지 등록 완료" else print_err "PBS 스토리지 등록 실패" exit 1 fi fi # PBS API 인증 print_step "PBS API 인증 중..." curl -k -s -X POST "https://${PBS_SERVER}:${PBS_PORT}/api2/json/access/ticket" \ -d "username=${PBS_USERNAME}" \ -d "password=${PBS_PASSWORD}" > /tmp/pbs_auth.json local PBS_TICKET PBS_CSRF PBS_TICKET=$(python3 -c 'import json; data=json.load(open("/tmp/pbs_auth.json")); print(data["data"]["ticket"])' 2>/dev/null) PBS_CSRF=$(python3 -c 'import json; data=json.load(open("/tmp/pbs_auth.json")); print(data["data"]["CSRFPreventionToken"])' 2>/dev/null) if [ -z "$PBS_TICKET" ]; then print_err "PBS API 인증 실패" exit 1 fi print_ok "PBS API 인증 완료" # 백업 목록 조회 print_step "VM 백업 목록 조회 중..." curl -k -s -X GET "https://${PBS_SERVER}:${PBS_PORT}/api2/json/admin/datastore/${PBS_DATASTORE}/groups?ns=PQ" \ -H "Cookie: PBSAuthCookie=${PBS_TICKET}" \ -H "CSRFPreventionToken: ${PBS_CSRF}" > /tmp/pbs_groups.json export PBS_TICKET PBS_CSRF python3 << 'PYEOF' import json, subprocess, os from datetime import datetime PBS_TICKET = os.environ.get('PBS_TICKET', '') PBS_CSRF = os.environ.get('PBS_CSRF', '') with open('/tmp/pbs_groups.json', 'r') as f: data = json.load(f) groups = data.get("data", []) vm_groups = [g for g in groups if g.get("backup-type") == "vm"] if vm_groups: print("\033[1;36m=== VM 백업 목록 ===\033[0m") for i, group in enumerate(vm_groups, 1): bid = group.get("backup-id", "N/A") count = group.get("backup-count", 0) last = group.get("last-backup", 0) comment = group.get("comment", "") if not comment: try: cmd = ['curl', '-k', '-s', '-X', 'GET', f'https://100.64.0.10:8007/api2/json/admin/datastore/PBS-DVA/snapshots?backup-type=vm&backup-id={bid}&ns=PQ', '-H', f'Cookie: PBSAuthCookie={PBS_TICKET}', '-H', f'CSRFPreventionToken: {PBS_CSRF}'] result = subprocess.run(cmd, capture_output=True, text=True) sdata = json.loads(result.stdout) snaps = sdata.get("data", []) if snaps: latest = max(snaps, key=lambda x: x.get("backup-time", 0)) comment = latest.get("comment", "") except: pass last_str = datetime.fromtimestamp(last).strftime("%Y-%m-%d %H:%M") if last > 0 else "N/A" print(f" {i}. VM {bid:<6} - {count}개 백업 (최근: {last_str})") if comment: print(f" \033[0;90m└─ {comment}\033[0m") else: print("VM 백업이 없습니다.") PYEOF echo "" echo -ne "${CYAN}복원할 VM 백업 ID (숫자, Enter로 건너뛰기): ${NC}" read -r TEMPLATE_VMID /dev/null | tail -n +2 | while read -r line; do sname=$(echo "$line" | awk '{print $1}') stype=$(echo "$line" | awk '{print $2}') echo -e " ${GREEN}●${NC} ${sname} (${stype})" done echo "" echo -ne "${CYAN}저장 스토리지 [기본: local-lvm]: ${NC}" read -r TARGET_STORAGE /tmp/pbs_snapshots.json local LATEST_SNAPSHOT LATEST_SNAPSHOT=$(python3 << 'PYEOF' import json, sys from datetime import datetime with open('/tmp/pbs_snapshots.json', 'r') as f: data = json.load(f) snapshots = data.get("data", []) if not snapshots: sys.exit(1) latest = max(snapshots, key=lambda x: x.get("backup-time", 0)) backup_time = latest.get("backup-time", 0) print(datetime.utcfromtimestamp(backup_time).strftime("%Y-%m-%dT%H:%M:%SZ")) PYEOF ) if [ -z "$LATEST_SNAPSHOT" ]; then print_err "백업을 찾을 수 없습니다." exit 1 fi print_ok "최신 백업: $LATEST_SNAPSHOT" # 기존 VM 확인 if qm status "$VM_VMID" 2>/dev/null; then print_warn "VMID $VM_VMID가 이미 존재합니다." echo -ne "${YELLOW}삭제하고 복원하시겠습니까? (y/N): ${NC}" read -r confirm /dev/null || true sleep 2 qm destroy "$VM_VMID" --purge --skiplock 2>/dev/null || true sleep 2 print_ok "기존 VM 삭제 완료" else print_err "복원 취소됨" exit 1 fi fi # 복원 실행 local BACKUP_PATH="${PBS_STORAGE_NAME}:backup/vm/${TEMPLATE_VMID}/${LATEST_SNAPSHOT}" print_step "VM 복원 중... (시간이 걸릴 수 있습니다)" print_info "경로: $BACKUP_PATH → VMID $VM_VMID" if qmrestore "$BACKUP_PATH" "$VM_VMID" --storage "$TARGET_STORAGE" --unique 1 2>&1; then print_ok "VM 복원 완료!" else print_err "VM 복원 실패" exit 1 fi # VM 시작 print_step "Windows VM 시작 중..." qm start "$VM_VMID" print_info "Windows 부팅 대기 (30초)..." sleep 30 print_ok "Phase 4 완료: PBS 등록 + Windows VM($VM_VMID) 복원됨" } # ============================================================ # Phase 5: Ubuntu CT 생성 # ============================================================ phase5_create_ct() { print_phase "Phase 5/8: Ubuntu CT 생성" # 템플릿 준비 print_step "Ubuntu 템플릿 확인 중..." if pveam list local 2>/dev/null | grep -q "$CT_TEMPLATE"; then print_ok "템플릿 이미 다운로드됨" else print_step "템플릿 다운로드 중..." pveam update >/dev/null 2>&1 pveam download local "$CT_TEMPLATE" fi # VMID 자동 선택 (200~299, VM/CT 모두 확인) print_step "사용 가능한 VMID 탐색 중..." CT_VMID="" for vmid in $(seq 200 299); do if ! qm status "$vmid" >/dev/null 2>&1 && ! pct status "$vmid" >/dev/null 2>&1; then CT_VMID=$vmid break fi done if [ -z "$CT_VMID" ]; then print_err "200~299 범위에서 사용 가능한 VMID가 없습니다." exit 1 fi print_ok "CT VMID: $CT_VMID" # LAN IP 자동 선택 (192.168.0.100~199) print_step "사용 가능한 LAN IP 탐색 중..." CT_LAN_IP="" for octet in $(seq 100 199); do local test_ip="192.168.0.${octet}" if ! ping -c1 -W1 "$test_ip" >/dev/null 2>&1; then CT_LAN_IP="$test_ip" break fi done if [ -z "$CT_LAN_IP" ]; then print_err "192.168.0.100~199 범위에서 사용 가능한 IP가 없습니다." exit 1 fi print_ok "CT LAN IP: $CT_LAN_IP" # CT 생성 print_step "CT 생성 중... (VMID: $CT_VMID, IP: $CT_LAN_IP)" pct create "$CT_VMID" "local:vztmpl/${CT_TEMPLATE}" \ --hostname ubuntu-api \ --cores "$CT_CORES" --memory "$CT_MEMORY" \ --rootfs "local-lvm:${CT_DISK}" \ --net0 "name=eth0,bridge=vmbr0,ip=${CT_LAN_IP}/24,gw=192.168.0.1" \ --nameserver "8.8.8.8" \ --password "$CT_PASSWORD" \ --unprivileged 1 \ --features nesting=1 print_ok "CT $CT_VMID 생성 완료" # TUN 디바이스 설정 print_step "TUN 디바이스 설정 중... (Tailscale 필수)" echo 'lxc.cgroup2.devices.allow: c 10:200 rwm' >> "/etc/pve/lxc/${CT_VMID}.conf" echo 'lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file' >> "/etc/pve/lxc/${CT_VMID}.conf" print_ok "TUN 디바이스 설정 완료" # CT 시작 print_step "CT 시작 중..." pct start "$CT_VMID" sleep 5 print_ok "CT $CT_VMID 시작됨" print_ok "Phase 5 완료: Ubuntu CT($CT_VMID) 생성됨 ($CT_LAN_IP)" } # ============================================================ # Phase 6: CT 내부 환경 구축 # ============================================================ phase6_setup_ct() { print_phase "Phase 6/8: CT 내부 환경 구축" # 헬퍼: CT 내부에서 명령 실행 ct_exec() { pct exec "$CT_VMID" -- bash -c "$1" } # 6-1. 기본 패키지 print_step "기본 패키지 설치 중..." ct_exec "apt-get update -qq && apt-get install -y curl git python3-pip python3-dev python3-venv pkg-config unixodbc unixodbc-dev build-essential" >/dev/null 2>&1 print_ok "기본 패키지 설치 완료" # 6-2. Microsoft ODBC Driver 18 print_step "ODBC Driver 18 설치 중... (FreeTDS 아님!)" ct_exec "curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg" ct_exec 'echo "deb [arch=amd64 signed-by=/usr/share/keyrings/microsoft-prod.gpg] https://packages.microsoft.com/ubuntu/24.04/prod noble main" > /etc/apt/sources.list.d/mssql-release.list' ct_exec "apt-get update -qq && ACCEPT_EULA=Y apt-get install -y msodbcsql18" >/dev/null 2>&1 # FreeTDS/tdsodbc 제거 (혹시 설치돼있으면) ct_exec "apt-get remove -y tdsodbc 2>/dev/null || true" print_ok "ODBC Driver 18 설치 완료" # 6-3. Tailscale 설치 + Headscale 등록 print_step "CT에 Tailscale 설치 중..." ct_exec "curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/jammy.noarmor.gpg | tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null" ct_exec "curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/jammy.tailscale-keyring.list | tee /etc/apt/sources.list.d/tailscale.list >/dev/null" ct_exec "apt-get update -qq && apt-get install -y tailscale" >/dev/null 2>&1 ct_exec "systemctl enable --now tailscaled" >/dev/null 2>&1 sleep 3 # hostname은 임시로 설정 (pharmacy_code는 Phase 7에서 확정) print_step "CT를 Headscale에 등록 중..." ct_exec "tailscale up --login-server=${HEADSCALE_SERVER} --authkey=${PREAUTH_KEY} --accept-routes --accept-dns=false" 2>/dev/null sleep 5 CT_VPN_IP=$(ct_exec "tailscale ip -4 2>/dev/null" | tr -d '[:space:]') if [ -n "$CT_VPN_IP" ]; then print_ok "CT Headscale 등록 성공: $CT_VPN_IP" else print_err "CT VPN IP를 가져올 수 없습니다." exit 1 fi # 6-4. API 서버 코드 클론 + 설치 print_step "API 서버 코드 설치 중..." ct_exec "git clone https://git.0bin.in/thug0bin/person-lookup-web.git /srv/person-lookup-web-local 2>/dev/null || (cd /srv/person-lookup-web-local && git pull)" ct_exec "cd /srv/person-lookup-web-local && pip3 install -r requirements.txt --break-system-packages -q" 2>/dev/null ct_exec "pip3 install brother_ql pytz qrcode netifaces pydantic anthropic httpx PyYAML rich --break-system-packages -q" 2>/dev/null print_ok "API 서버 코드 설치 완료" # 6-5. .env 파일 생성 print_step ".env 파일 생성 중..." ct_exec "cat > /srv/person-lookup-web-local/.env << 'ENVEOF' MSSQL_SERVER=${MSSQL_SERVER} MSSQL_USER=sa MSSQL_PASSWORD=tmddls214!%( ENVEOF" print_ok ".env 파일 생성 완료" # 6-6. systemd 서비스 등록 print_step "API 서비스 등록 중..." ct_exec "cat > /etc/systemd/system/pharmq-api.service << 'SVCEOF' [Unit] Description=PharmQ API Server After=network.target [Service] Type=simple WorkingDirectory=/srv/person-lookup-web-local ExecStart=/usr/bin/python3 app.py Restart=always RestartSec=5 Environment=FLASK_ENV=production [Install] WantedBy=multi-user.target SVCEOF" ct_exec "systemctl daemon-reload && systemctl enable --now pharmq-api" >/dev/null 2>&1 print_ok "API 서비스 등록 완료" print_ok "Phase 6 완료: CT 환경 구축됨 (VPN: $CT_VPN_IP)" } # ============================================================ # Phase 7: 약국 + 장비 + 계정 등록 # ============================================================ phase7_register() { print_phase "Phase 7/8: 약국 + 장비 + 계정 등록" # 7-1. 약국 생성 (CT를 primary 장비로) print_step "약국 생성 중... (farmq.db)" local PHARMACY_RESPONSE PHARMACY_RESPONSE=$(curl -s -X POST "${FARMQ_API}/api/pharmacy" \ -H "Content-Type: application/json" \ -d "$(cat </dev/null || true # CT hostname도 변경 ct_exec() { pct exec "$CT_VMID" -- bash -c "$1"; } ct_exec "tailscale set --hostname=${PHARMACY_CODE}-ubuntu-api" 2>/dev/null || true # CT .env에 PHARMACY_CODE 추가 ct_exec "echo 'PHARMACY_CODE=${PHARMACY_CODE}' >> /srv/person-lookup-web-local/.env" # 7-3. Gateway 계정 생성 print_step "관리자 계정 생성 중... (gateway.db)" local USERNAME USERNAME=$(echo "$PHARMACY_CODE" | tr '[:upper:]' '[:lower:]') local REGISTER_RESPONSE REGISTER_RESPONSE=$(curl -s -X POST "${GATEWAY_API}/api/auth/register" \ -H "Content-Type: application/json" \ -d "$(cat </dev/null || echo "연결 실패") if echo "$API_STATUS" | grep -qi "connected\|ok\|status"; then print_ok "API 서버 정상" else print_warn "API 서버 응답 확인 필요" print_info "수동 확인: curl http://${CT_VPN_IP}:8082/api/status" fi # Windows VM 상태 if [ -n "$VM_VMID" ]; then print_step "Windows VM 상태 확인 중..." qm status "$VM_VMID" 2>/dev/null || print_warn "VM $VM_VMID 상태 확인 실패" fi # 네트워크 테스트 print_step "VPN 네트워크 테스트 중..." if ping -c2 -W3 "$CT_VPN_IP" >/dev/null 2>&1; then print_ok "PVE → CT 네트워크 정상" else print_warn "PVE → CT ping 실패 (방화벽 확인)" fi # 최종 결과 출력 local USERNAME USERNAME=$(echo "$PHARMACY_CODE" | tr '[:upper:]' '[:lower:]') echo "" echo -e "${GREEN}════════════════════════════════════════════${NC}" echo -e "${WHITE} PharmQ 원클릭 설치 완료!${NC}" echo -e "${GREEN}════════════════════════════════════════════${NC}" echo "" echo -e "${WHITE}약국 코드: ${CYAN}${PHARMACY_CODE}${NC}" echo -e "${WHITE}약국명: ${CYAN}${PHARMACY_NAME}${NC}" echo "" if [ -n "$VM_VMID" ]; then echo -e "${WHITE}[Windows VM - 팜IT3000]${NC}" echo -e " VMID: ${VM_VMID}" echo -e " MSSQL: ${MSSQL_SERVER}" echo "" fi echo -e "${WHITE}[PVE Host]${NC}" echo -e " VPN IP: ${PVE_VPN_IP}" echo -e " hostname: ${PHARMACY_CODE}-pve-pharmq" echo "" echo -e "${WHITE}[Ubuntu API CT]${NC}" echo -e " VPN IP: ${CT_VPN_IP}" echo -e " hostname: ${PHARMACY_CODE}-ubuntu-api" echo -e " CT VMID: ${CT_VMID}" echo -e " LAN IP: ${CT_LAN_IP}" echo -e " API: http://${CT_VPN_IP}:8082" echo "" echo -e "${WHITE}[로그인 정보]${NC}" echo -e " URL: ${CYAN}https://pharmq.kr${NC}" echo -e " ID: ${CYAN}${USERNAME}${NC}" echo -e " PW: ${CYAN}12341234${NC}" echo "" echo -e "${YELLOW}⚠ 최초 로그인 후 비밀번호를 변경하세요!${NC}" echo "" echo -e "${GREEN}════════════════════════════════════════════${NC}" echo "" # 유용한 명령어 echo -e "${WHITE}유용한 명령어:${NC}" echo " tailscale status # VPN 상태" echo " pct enter ${CT_VMID} # CT 접속" echo " pct exec ${CT_VMID} -- systemctl status pharmq-api # API 상태" if [ -n "$VM_VMID" ]; then echo " qm status ${VM_VMID} # Windows VM 상태" fi echo "" # 임시 파일 정리 rm -f /tmp/pbs_auth.json /tmp/pbs_groups.json /tmp/pbs_snapshots.json 2>/dev/null || true } # ============================================================ # 에러 핸들링 # ============================================================ trap 'echo -e "\n${RED}❌ 설치 중 오류가 발생했습니다 (line $LINENO). 로그를 확인해주세요.${NC}"; exit 1' ERR # ============================================================ # 메인 실행 # ============================================================ main() { echo "" echo -e "${PURPLE}╔════════════════════════════════════════════╗${NC}" echo -e "${PURPLE}║ ║${NC}" echo -e "${PURPLE}║${NC} ${WHITE}PharmQ PVE 원클릭 통합 설치${NC} ${PURPLE}║${NC}" echo -e "${PURPLE}║${NC} ${CYAN}Repo Fix → VPN → PBS → CT → 등록${NC} ${PURPLE}║${NC}" echo -e "${PURPLE}║ ║${NC}" echo -e "${PURPLE}╚════════════════════════════════════════════╝${NC}" echo "" phase1_repo_fix phase2_tailscale_pve phase3_collect_info phase4_pbs_restore phase5_create_ct phase6_setup_ct phase7_register phase8_verify } main "$@"