diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
index ec48126e..e5c8d001 100644
--- a/.gitea/workflows/deploy.yml
+++ b/.gitea/workflows/deploy.yml
@@ -1,19 +1,16 @@
-# Gitea Actions Workflow - vexplor 자동 배포
+# Gitea Actions Workflow - vexplor 이미지 빌드 & Harbor Push
#
-# 환경 변수:
-# - GITEA_DOMAIN: g.wace.me
-# - HARBOR_REGISTRY: harbor.wace.me
-# - K8S_NAMESPACE: vexplor
+# 동작 방식:
+# 1. main 브랜치에 push 시 자동 실행
+# 2. Docker 이미지 빌드 (Backend, Frontend)
+# 3. Harbor 레지스트리에 Push
+# 4. 공장 서버의 Watchtower가 새 이미지 감지 후 자동 업데이트
#
# 필수 Secrets (Repository Settings > Secrets):
# - HARBOR_USERNAME: Harbor 사용자명
# - HARBOR_PASSWORD: Harbor 비밀번호
-# - K8S_SSH_KEY: base64로 인코딩된 SSH 비밀키 (쿠버네티스 서버 접속용)
-#
-# Application Secrets:
-# - k8s/vexplor-secret.yaml 파일에서 관리
-name: Deploy vexplor
+name: Build and Push Images
on:
push:
@@ -24,44 +21,35 @@ on:
- "backend-node/**"
- "frontend/**"
- "docker/**"
- - "k8s/**"
- ".gitea/workflows/deploy.yml"
- workflow_dispatch:
+ paths-ignore:
+ - "**.md"
+ - "deploy/**"
+ - "k8s/**"
+ workflow_dispatch: # 수동 실행도 가능
env:
GITEA_DOMAIN: g.wace.me
HARBOR_REGISTRY: localhost:5001
- HARBOR_REGISTRY_K8S: harbor.wace.me
HARBOR_REGISTRY_EXTERNAL: harbor.wace.me
HARBOR_PROJECT: speefox_vexplor
- K8S_NAMESPACE: vexplor
-
- # 쿠버네티스 서버 SSH 접속 정보
- K8S_SSH_HOST: 112.168.212.142
- K8S_SSH_PORT: 22
- K8S_SSH_USER: wace
# Frontend 빌드 환경 변수
NEXT_PUBLIC_API_URL: "https://api.vexplor.com/api"
NEXT_PUBLIC_ENV: "production"
- INTERNAL_API_URL: "http://vexplor-backend-service:3001"
# Frontend 설정
FRONTEND_IMAGE_NAME: vexplor-frontend
- FRONTEND_DEPLOYMENT_NAME: vexplor-frontend
- FRONTEND_CONTAINER_NAME: vexplor-frontend
FRONTEND_BUILD_CONTEXT: frontend
FRONTEND_DOCKERFILE_PATH: docker/deploy/frontend.Dockerfile
# Backend 설정
BACKEND_IMAGE_NAME: vexplor-backend
- BACKEND_DEPLOYMENT_NAME: vexplor-backend
- BACKEND_CONTAINER_NAME: vexplor-backend
BACKEND_BUILD_CONTEXT: backend-node
BACKEND_DOCKERFILE_PATH: docker/deploy/backend.Dockerfile
jobs:
- build-and-deploy:
+ build-and-push:
runs-on: ubuntu-24.04
steps:
@@ -79,7 +67,7 @@ jobs:
run: |
echo "필수 도구 설치 중..."
apt-get update -qq
- apt-get install -y git curl ca-certificates gnupg openssh-client
+ apt-get install -y git curl ca-certificates gnupg
# Docker 클라이언트 설치
install -m 0755 -d /etc/apt/keyrings
@@ -94,7 +82,6 @@ jobs:
echo "설치 완료:"
git --version
- ssh -V
docker --version
export DOCKER_HOST=unix:///var/run/docker.sock
@@ -120,13 +107,13 @@ jobs:
# Frontend 이미지
echo "FRONTEND_FULL_IMAGE=${HARBOR_REGISTRY}/${HARBOR_PROJECT}/${FRONTEND_IMAGE_NAME}" >> $GITHUB_ENV
- echo "FRONTEND_FULL_IMAGE_K8S=${HARBOR_REGISTRY_K8S}/${HARBOR_PROJECT}/${FRONTEND_IMAGE_NAME}" >> $GITHUB_ENV
# Backend 이미지
echo "BACKEND_FULL_IMAGE=${HARBOR_REGISTRY}/${HARBOR_PROJECT}/${BACKEND_IMAGE_NAME}" >> $GITHUB_ENV
- echo "BACKEND_FULL_IMAGE_K8S=${HARBOR_REGISTRY_K8S}/${HARBOR_PROJECT}/${BACKEND_IMAGE_NAME}" >> $GITHUB_ENV
+ echo "=========================================="
echo "빌드 태그: ${IMAGE_TAG}"
+ echo "=========================================="
# Harbor 로그인
- name: Login to Harbor
@@ -144,7 +131,9 @@ jobs:
# Backend 빌드 및 푸시
- name: Build and Push Backend image
run: |
- echo "Backend 이미지 빌드 및 푸시..."
+ echo "=========================================="
+ echo "Backend 이미지 빌드 시작..."
+ echo "=========================================="
export DOCKER_HOST=unix:///var/run/docker.sock
cd /workspace/source
@@ -154,14 +143,22 @@ jobs:
-f ${BACKEND_DOCKERFILE_PATH} \
${BACKEND_BUILD_CONTEXT}
+ echo "Backend 이미지 푸시..."
docker push ${BACKEND_FULL_IMAGE}:${IMAGE_TAG}
docker push ${BACKEND_FULL_IMAGE}:latest
- echo "Backend 푸시 완료"
+
+ echo "=========================================="
+ echo "Backend 푸시 완료!"
+ echo " - ${BACKEND_FULL_IMAGE}:${IMAGE_TAG}"
+ echo " - ${BACKEND_FULL_IMAGE}:latest"
+ echo "=========================================="
# Frontend 빌드 및 푸시
- name: Build and Push Frontend image
run: |
- echo "Frontend 이미지 빌드 및 푸시..."
+ echo "=========================================="
+ echo "Frontend 이미지 빌드 시작..."
+ echo "=========================================="
export DOCKER_HOST=unix:///var/run/docker.sock
cd /workspace/source
@@ -176,175 +173,40 @@ jobs:
--build-arg NEXT_PUBLIC_API_URL="${NEXT_PUBLIC_API_URL}" \
${FRONTEND_BUILD_CONTEXT}
+ echo "Frontend 이미지 푸시..."
docker push ${FRONTEND_FULL_IMAGE}:${IMAGE_TAG}
docker push ${FRONTEND_FULL_IMAGE}:latest
- echo "Frontend 푸시 완료"
+
+ echo "=========================================="
+ echo "Frontend 푸시 완료!"
+ echo " - ${FRONTEND_FULL_IMAGE}:${IMAGE_TAG}"
+ echo " - ${FRONTEND_FULL_IMAGE}:latest"
+ echo "=========================================="
- # SSH 키 설정 (쿠버네티스 서버 접속용)
- - name: Setup SSH Key
- env:
- SSH_KEY_CONTENT: ${{ secrets.K8S_SSH_KEY }}
- run: |
- echo "SSH 키 설정..."
-
- if [ -z "${SSH_KEY_CONTENT}" ]; then
- echo "K8S_SSH_KEY secret이 설정되지 않았습니다!"
- exit 1
- fi
-
- mkdir -p ~/.ssh
- echo "${SSH_KEY_CONTENT}" | base64 -d > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
-
- # known_hosts에 쿠버네티스 서버 추가
- ssh-keyscan -p ${K8S_SSH_PORT} ${K8S_SSH_HOST} >> ~/.ssh/known_hosts 2>/dev/null
-
- # SSH 연결 테스트
- echo "SSH 연결 테스트..."
- ssh -o StrictHostKeyChecking=no -p ${K8S_SSH_PORT} ${K8S_SSH_USER}@${K8S_SSH_HOST} "echo 'SSH 연결 성공'"
- echo "SSH 키 설정 완료"
-
- # k8s 매니페스트 파일을 쿠버네티스 서버로 전송
- - name: Transfer k8s manifests
- run: |
- echo "k8s 매니페스트 파일 전송..."
- cd /workspace/source
-
- # 쿠버네티스 서버에 디렉토리 생성
- ssh -p ${K8S_SSH_PORT} ${K8S_SSH_USER}@${K8S_SSH_HOST} "mkdir -p ~/vexplor-deploy/k8s"
-
- # k8s 파일 전송
- scp -P ${K8S_SSH_PORT} -r k8s/* ${K8S_SSH_USER}@${K8S_SSH_HOST}:~/vexplor-deploy/k8s/
-
- echo "매니페스트 파일 전송 완료"
-
- # Kubernetes 배포 (SSH를 통해 원격 실행)
- - name: Deploy to Kubernetes
- env:
- HARBOR_USER: ${{ secrets.HARBOR_USERNAME }}
- HARBOR_PASS: ${{ secrets.HARBOR_PASSWORD }}
- run: |
- echo "Kubernetes 배포 시작 (SSH 원격 실행)..."
-
- # SSH를 통해 쿠버네티스 서버에서 kubectl 명령 실행
- ssh -p ${K8S_SSH_PORT} ${K8S_SSH_USER}@${K8S_SSH_HOST} << 'DEPLOY_SCRIPT'
- set -e
- cd ~/vexplor-deploy
-
- echo "네임스페이스 확인..."
- kubectl apply -f k8s/namespace.yaml
-
- echo "ConfigMap 적용..."
- kubectl apply -f k8s/vexplor-config.yaml -n vexplor
-
- # Secret 적용 (존재하는 경우에만)
- if [ -f k8s/vexplor-secret.yaml ]; then
- echo "Secret 적용..."
- kubectl apply -f k8s/vexplor-secret.yaml -n vexplor
- fi
-
- echo "네임스페이스 및 ConfigMap 적용 완료"
- DEPLOY_SCRIPT
-
- # Harbor Registry Secret 생성 (별도로 처리 - 환경변수 사용)
- echo "Harbor Registry Secret 확인..."
- ssh -p ${K8S_SSH_PORT} ${K8S_SSH_USER}@${K8S_SSH_HOST} "kubectl get secret harbor-registry -n vexplor" > /dev/null 2>&1 || \
- ssh -p ${K8S_SSH_PORT} ${K8S_SSH_USER}@${K8S_SSH_HOST} "kubectl create secret docker-registry harbor-registry \
- --docker-server=${HARBOR_REGISTRY_K8S} \
- --docker-username=${HARBOR_USER} \
- --docker-password='${HARBOR_PASS}' \
- -n vexplor"
- echo "Harbor Registry Secret 확인 완료"
-
- # Backend 배포
- echo "Backend 배포..."
- ssh -p ${K8S_SSH_PORT} ${K8S_SSH_USER}@${K8S_SSH_HOST} << BACKEND_DEPLOY
- set -e
- cd ~/vexplor-deploy
- kubectl apply -f k8s/vexplor-backend-deployment.yaml -n vexplor
-
- echo "Backend 이미지 업데이트..."
- kubectl set image deployment/${BACKEND_DEPLOYMENT_NAME} \
- ${BACKEND_CONTAINER_NAME}=${HARBOR_REGISTRY_K8S}/${HARBOR_PROJECT}/${BACKEND_IMAGE_NAME}:latest \
- -n vexplor || true
- kubectl rollout restart deployment/${BACKEND_DEPLOYMENT_NAME} -n vexplor
-
- echo "Backend Rolling Update 진행 중..."
- kubectl rollout status deployment/${BACKEND_DEPLOYMENT_NAME} -n vexplor --timeout=5m
- echo "Backend 배포 완료"
- BACKEND_DEPLOY
-
- # Frontend 배포
- echo "Frontend 배포..."
- ssh -p ${K8S_SSH_PORT} ${K8S_SSH_USER}@${K8S_SSH_HOST} << FRONTEND_DEPLOY
- set -e
- cd ~/vexplor-deploy
- kubectl apply -f k8s/vexplor-frontend-deployment.yaml -n vexplor
-
- echo "Frontend 이미지 업데이트..."
- kubectl set image deployment/${FRONTEND_DEPLOYMENT_NAME} \
- ${FRONTEND_CONTAINER_NAME}=${HARBOR_REGISTRY_K8S}/${HARBOR_PROJECT}/${FRONTEND_IMAGE_NAME}:latest \
- -n vexplor || true
- kubectl rollout restart deployment/${FRONTEND_DEPLOYMENT_NAME} -n vexplor
-
- echo "Frontend Rolling Update 진행 중..."
- kubectl rollout status deployment/${FRONTEND_DEPLOYMENT_NAME} -n vexplor --timeout=5m
- echo "Frontend 배포 완료"
- FRONTEND_DEPLOY
-
- # Ingress 배포
- echo "Ingress 배포..."
- ssh -p ${K8S_SSH_PORT} ${K8S_SSH_USER}@${K8S_SSH_HOST} "cd ~/vexplor-deploy && kubectl apply -f k8s/vexplor-ingress.yaml -n vexplor"
-
- echo "전체 배포 완료!"
-
- # 배포 검증
- - name: Verify deployment
- run: |
- echo "배포 검증..."
- ssh -p ${K8S_SSH_PORT} ${K8S_SSH_USER}@${K8S_SSH_HOST} << 'VERIFY_SCRIPT'
- echo ""
- echo "Backend 상태:"
- kubectl get deployment vexplor-backend -n vexplor
- kubectl get pods -l app=vexplor-backend -n vexplor
- echo ""
- echo "Frontend 상태:"
- kubectl get deployment vexplor-frontend -n vexplor
- kubectl get pods -l app=vexplor-frontend -n vexplor
- echo ""
- echo "Services:"
- kubectl get svc -n vexplor
- echo ""
- echo "Ingress:"
- kubectl get ingress -n vexplor
- echo ""
- echo "검증 완료"
- VERIFY_SCRIPT
-
- # 배포 요약
- - name: Deployment summary
+ # 빌드 완료 요약
+ - name: Build summary
if: success()
run: |
+ echo ""
echo "=========================================="
- echo "배포가 성공적으로 완료되었습니다!"
+ echo " 이미지 빌드 & Push 완료!"
echo "=========================================="
+ echo ""
echo "빌드 버전: ${IMAGE_TAG}"
- echo "Frontend: https://v1.vexplor.com"
- echo "Backend API: https://api.vexplor.com"
+ echo ""
+ echo "푸시된 이미지:"
+ echo " - Backend: ${HARBOR_REGISTRY_EXTERNAL}/${HARBOR_PROJECT}/${BACKEND_IMAGE_NAME}:latest"
+ echo " - Frontend: ${HARBOR_REGISTRY_EXTERNAL}/${HARBOR_PROJECT}/${FRONTEND_IMAGE_NAME}:latest"
+ echo ""
+ echo "다음 단계:"
+ echo " - 공장 서버의 Watchtower가 자동으로 새 이미지를 감지합니다"
+ echo " - 또는 수동 업데이트: docker compose pull && docker compose up -d"
+ echo ""
echo "=========================================="
- # 실패 시 롤백
- - name: Rollback on failure
- if: failure()
- run: |
- echo "배포 실패! 이전 버전으로 롤백..."
- ssh -p ${K8S_SSH_PORT} ${K8S_SSH_USER}@${K8S_SSH_HOST} "kubectl rollout undo deployment/vexplor-backend -n vexplor" || true
- ssh -p ${K8S_SSH_PORT} ${K8S_SSH_USER}@${K8S_SSH_HOST} "kubectl rollout undo deployment/vexplor-frontend -n vexplor" || true
-
# Harbor 로그아웃
- name: Logout from Harbor
if: always()
run: |
export DOCKER_HOST=unix:///var/run/docker.sock
docker logout ${HARBOR_REGISTRY} || true
-
diff --git a/.gitignore b/.gitignore
index 5b2b1f56..24814953 100644
--- a/.gitignore
+++ b/.gitignore
@@ -291,4 +291,6 @@ uploads/
*.hwp
*.hwpx
-claude.md
\ No newline at end of file
+claude.md
+
+.cursor/mcp.json
\ No newline at end of file
diff --git a/deploy/customers/README.md b/deploy/customers/README.md
new file mode 100644
index 00000000..8b55214b
--- /dev/null
+++ b/deploy/customers/README.md
@@ -0,0 +1,115 @@
+# 고객사별 환경 변수 관리
+
+## 개요
+
+이 폴더는 각 고객사(업체)별 환경 변수 설정을 **참고용**으로 관리합니다.
+
+**중요:** 실제 비밀번호는 이 파일에 저장하지 마세요. 템플릿으로만 사용합니다.
+
+---
+
+## 고객사 목록
+
+| 고객사 | 파일 | 배포 형태 | 상태 |
+| :--- | :--- | :--- | :--- |
+| 스피폭스 | `spifox.env` | 온프레미스 (공장 서버) | 진행 중 |
+| 엔키드 | `enkid.env` | 온프레미스 (공장 서버) | 예정 |
+
+---
+
+## 신규 고객사 추가 절차
+
+### 1단계: 환경 변수 파일 생성
+
+```bash
+# 기존 파일 복사
+cp spifox.env newcustomer.env
+
+# 수정
+nano newcustomer.env
+```
+
+필수 수정 항목:
+- `COMPANY_CODE`: 고유한 회사 코드 (예: NEWCO)
+- `SERVER_IP`: 고객사 서버 IP
+- `DB_PASSWORD`: 고유한 비밀번호
+- `JWT_SECRET`: 고유한 시크릿 키
+
+### 2단계: 데이터베이스에 회사 등록
+
+```sql
+-- company_info 테이블에 추가
+INSERT INTO company_info (company_code, company_name, status)
+VALUES ('NEWCO', '신규고객사', 'ACTIVE');
+```
+
+### 3단계: 관리자 계정 생성
+
+```sql
+-- user_info 테이블에 관리자 추가
+INSERT INTO user_info (user_id, user_name, company_code, role)
+VALUES ('newco_admin', '신규고객사 관리자', 'NEWCO', 'COMPANY_ADMIN');
+```
+
+### 4단계: 고객사 서버에 배포
+
+```bash
+# 고객사 서버에 SSH 접속
+ssh user@customer-server
+
+# 설치 폴더 생성
+sudo mkdir -p /opt/vexplor
+cd /opt/vexplor
+
+# docker-compose.yml 복사 (deploy/onpremise/에서)
+# .env 파일 복사 및 수정
+
+# 서비스 시작
+docker compose up -d
+```
+
+---
+
+## 환경 변수 설명
+
+| 변수 | 설명 | 예시 |
+| :--- | :--- | :--- |
+| `COMPANY_CODE` | 회사 고유 코드 (멀티테넌시) | `SPIFOX`, `ENKID` |
+| `SERVER_IP` | 서버의 실제 IP | `192.168.0.100` |
+| `DB_PASSWORD` | DB 비밀번호 | (고객사별 고유) |
+| `JWT_SECRET` | JWT 토큰 시크릿 | (고객사별 고유) |
+| `IMAGE_TAG` | Docker 이미지 버전 | `latest`, `v1.0.0` |
+
+---
+
+## 보안 주의사항
+
+1. **비밀번호**: 이 폴더의 파일에는 실제 비밀번호를 저장하지 마세요
+2. **Git**: `.env` 파일이 커밋되지 않도록 `.gitignore` 확인
+3. **고객사별 격리**: 각 고객사는 별도 서버, 별도 DB로 완전 격리
+4. **키 관리**: JWT_SECRET은 고객사별로 반드시 다르게 설정
+
+---
+
+## 구조 다이어그램
+
+```
+[Harbor (이미지 저장소)]
+ │
+ │ docker pull
+ ↓
+┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
+│ 스피폭스 공장 │ │ 엔키드 공장 │ │ 신규 고객사 │
+│ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │
+│ │ Vexplor │ │ │ │ Vexplor │ │ │ │ Vexplor │ │
+│ │ SPIFOX │ │ │ │ ENKID │ │ │ │ NEWCO │ │
+│ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │
+│ │ │ │ │ │
+│ [독립 DB] │ │ [독립 DB] │ │ [독립 DB] │
+└─────────────────┘ └─────────────────┘ └─────────────────┘
+
+* 각 공장은 완전히 독립적으로 운영
+* 같은 Docker 이미지 사용, .env만 다름
+* 데이터는 절대 섞이지 않음 (물리적 격리)
+```
+
diff --git a/deploy/customers/enkid.env b/deploy/customers/enkid.env
new file mode 100644
index 00000000..3a9e84df
--- /dev/null
+++ b/deploy/customers/enkid.env
@@ -0,0 +1,36 @@
+# ============================================
+# 엔키드(ENKID) 공장 서버 환경 변수
+# ============================================
+# 이 파일을 엔키드 공장 서버의 /opt/vexplor/.env로 복사
+
+# 회사 정보
+COMPANY_CODE=ENKID
+
+# 서버 정보 (실제 서버 IP로 변경 필요)
+SERVER_IP=10.0.0.50
+
+# 데이터베이스
+DB_USER=vexplor
+DB_PASSWORD=enkid_secure_password_here
+DB_NAME=vexplor
+DB_PORT=5432
+
+# 백엔드
+BACKEND_PORT=3001
+JWT_SECRET=enkid_jwt_secret_minimum_32_characters
+JWT_EXPIRES_IN=24h
+LOG_LEVEL=info
+
+# 프론트엔드
+FRONTEND_PORT=80
+
+# Harbor 레지스트리
+HARBOR_USER=enkid_harbor_user
+HARBOR_PASSWORD=enkid_harbor_password
+
+# 이미지 태그
+IMAGE_TAG=latest
+
+# Watchtower (1시간마다 업데이트 확인)
+UPDATE_INTERVAL=3600
+
diff --git a/deploy/customers/spifox.env b/deploy/customers/spifox.env
new file mode 100644
index 00000000..ab7d6004
--- /dev/null
+++ b/deploy/customers/spifox.env
@@ -0,0 +1,36 @@
+# ============================================
+# 스피폭스(SPIFOX) 공장 서버 환경 변수
+# ============================================
+# 이 파일을 스피폭스 공장 서버의 /opt/vexplor/.env로 복사
+
+# 회사 정보
+COMPANY_CODE=SPIFOX
+
+# 서버 정보 (실제 서버 IP로 변경 필요)
+SERVER_IP=192.168.0.100
+
+# 데이터베이스
+DB_USER=vexplor
+DB_PASSWORD=spifox_secure_password_here
+DB_NAME=vexplor
+DB_PORT=5432
+
+# 백엔드
+BACKEND_PORT=3001
+JWT_SECRET=spifox_jwt_secret_minimum_32_characters
+JWT_EXPIRES_IN=24h
+LOG_LEVEL=info
+
+# 프론트엔드
+FRONTEND_PORT=80
+
+# Harbor 레지스트리
+HARBOR_USER=spifox_harbor_user
+HARBOR_PASSWORD=spifox_harbor_password
+
+# 이미지 태그
+IMAGE_TAG=latest
+
+# Watchtower (1시간마다 업데이트 확인)
+UPDATE_INTERVAL=3600
+
diff --git a/deploy/onpremise/README.md b/deploy/onpremise/README.md
new file mode 100644
index 00000000..76cad490
--- /dev/null
+++ b/deploy/onpremise/README.md
@@ -0,0 +1,321 @@
+# Vexplor 온프레미스(공장) 배포 가이드
+
+## 개요
+
+이 가이드는 Vexplor를 **공장 내부 서버(온프레미스)**에 배포하는 방법을 설명합니다.
+
+**Watchtower**를 사용하여 Harbor에 새 이미지가 푸시되면 **자동으로 업데이트**됩니다.
+
+---
+
+## 사전 요구사항
+
+### 서버 요구사항
+
+| 항목 | 최소 사양 | 권장 사양 |
+| :--- | :--- | :--- |
+| OS | Ubuntu 20.04+ | Ubuntu 22.04 LTS |
+| CPU | 4 Core | 8 Core |
+| RAM | 8 GB | 16 GB |
+| Disk | 50 GB | 100 GB SSD |
+| Network | Harbor 접근 가능 | - |
+
+### 필수 소프트웨어
+
+```bash
+# Docker 설치 확인
+docker --version # 20.10 이상
+
+# Docker Compose 설치 확인
+docker compose version # v2.0 이상
+```
+
+---
+
+## 1단계: 초기 설정
+
+### 1.1 배포 폴더 생성
+
+```bash
+# 배포 폴더 생성
+sudo mkdir -p /opt/vexplor
+cd /opt/vexplor
+
+# 파일 복사 (또는 git clone)
+# deploy/onpremise/ 폴더의 내용을 복사
+```
+
+### 1.2 환경 변수 설정
+
+```bash
+# 예제 파일 복사
+cp env.example .env
+
+# 편집
+nano .env
+```
+
+**필수 수정 항목:**
+
+```bash
+# 서버 IP (이 서버의 실제 IP)
+SERVER_IP=192.168.0.100
+
+# 회사 코드
+COMPANY_CODE=SPIFOX
+
+# DB 비밀번호 (강력한 비밀번호 설정)
+DB_PASSWORD=MySecurePassword123!
+
+# JWT 시크릿 (32자 이상)
+JWT_SECRET=your-super-secret-jwt-key-minimum-32-chars
+
+# Harbor 인증 정보
+HARBOR_USER=your_username
+HARBOR_PASSWORD=your_password
+```
+
+### 1.3 Harbor 레지스트리 로그인
+
+Watchtower가 이미지를 당겨올 수 있도록 Docker 로그인이 필요합니다.
+
+```bash
+# Harbor 로그인
+docker login harbor.wace.me
+
+# Username: (입력)
+# Password: (입력)
+
+# 로그인 성공 확인
+cat ~/.docker/config.json
+```
+
+---
+
+## 2단계: 서비스 실행
+
+### 2.1 서비스 시작
+
+```bash
+cd /opt/vexplor
+
+# 이미지 다운로드 & 실행
+docker compose up -d
+
+# 상태 확인
+docker compose ps
+```
+
+### 2.2 정상 동작 확인
+
+```bash
+# 모든 컨테이너 Running 상태 확인
+docker compose ps
+
+# 로그 확인
+docker compose logs -f
+
+# 개별 서비스 로그
+docker compose logs -f backend
+docker compose logs -f frontend
+docker compose logs -f watchtower
+```
+
+### 2.3 웹 접속 테스트
+
+```
+프론트엔드: http://SERVER_IP:80
+백엔드 API: http://SERVER_IP:3001/health
+```
+
+---
+
+## 3단계: 자동 업데이트 확인
+
+### Watchtower 동작 확인
+
+```bash
+# Watchtower 로그 확인
+docker compose logs -f watchtower
+```
+
+**정상 로그 예시:**
+
+```
+watchtower | time="2024-12-28T10:00:00+09:00" level=info msg="Checking for updates..."
+watchtower | time="2024-12-28T10:00:05+09:00" level=info msg="Found new image harbor.wace.me/vexplor/vexplor-backend:latest"
+watchtower | time="2024-12-28T10:00:10+09:00" level=info msg="Stopping container vexplor-backend"
+watchtower | time="2024-12-28T10:00:15+09:00" level=info msg="Starting container vexplor-backend"
+```
+
+### 업데이트 주기 변경
+
+```bash
+# .env 파일에서 변경
+UPDATE_INTERVAL=3600 # 1시간마다 확인
+
+# 변경 후 watchtower 재시작
+docker compose restart watchtower
+```
+
+---
+
+## 운영 가이드
+
+### 서비스 관리 명령어
+
+```bash
+# 모든 서비스 상태 확인
+docker compose ps
+
+# 모든 서비스 중지
+docker compose stop
+
+# 모든 서비스 시작
+docker compose start
+
+# 모든 서비스 재시작
+docker compose restart
+
+# 모든 서비스 삭제 (데이터 유지)
+docker compose down
+
+# 모든 서비스 삭제 + 볼륨 삭제 (주의: 데이터 삭제됨!)
+docker compose down -v
+```
+
+### 로그 확인
+
+```bash
+# 전체 로그 (실시간)
+docker compose logs -f
+
+# 특정 서비스 로그
+docker compose logs -f backend
+docker compose logs -f frontend
+docker compose logs -f database
+
+# 최근 100줄만
+docker compose logs --tail=100 backend
+```
+
+### 수동 업데이트 (긴급 시)
+
+자동 업데이트를 기다리지 않고 즉시 업데이트하려면:
+
+```bash
+# 최신 이미지 다운로드
+docker compose pull
+
+# 재시작
+docker compose up -d
+```
+
+### 특정 버전으로 롤백
+
+```bash
+# .env 파일에서 버전 지정
+IMAGE_TAG=v1.0.0
+
+# 재시작
+docker compose up -d
+```
+
+---
+
+## 백업 가이드
+
+### DB 백업
+
+```bash
+# 백업 디렉토리 생성
+mkdir -p /opt/vexplor/backups
+
+# PostgreSQL 백업
+docker compose exec database pg_dump -U vexplor vexplor > /opt/vexplor/backups/backup_$(date +%Y%m%d_%H%M%S).sql
+```
+
+### 업로드 파일 백업
+
+```bash
+# 볼륨 위치 확인
+docker volume inspect vexplor_backend_uploads
+
+# 또는 직접 복사
+docker cp vexplor-backend:/app/uploads /opt/vexplor/backups/uploads_$(date +%Y%m%d)
+```
+
+### 자동 백업 스크립트 (Cron)
+
+```bash
+# crontab 편집
+crontab -e
+
+# 매일 새벽 3시 DB 백업
+0 3 * * * docker compose -f /opt/vexplor/docker-compose.yml exec -T database pg_dump -U vexplor vexplor > /opt/vexplor/backups/backup_$(date +\%Y\%m\%d).sql
+```
+
+---
+
+## 문제 해결
+
+### 컨테이너가 시작되지 않음
+
+```bash
+# 로그 확인
+docker compose logs backend
+
+# 일반적인 원인:
+# 1. 환경 변수 누락 → .env 파일 확인
+# 2. 포트 충돌 → netstat -tlnp | grep 3001
+# 3. 메모리 부족 → free -h
+```
+
+### DB 연결 실패
+
+```bash
+# DB 컨테이너 상태 확인
+docker compose logs database
+
+# DB 직접 접속 테스트
+docker compose exec database psql -U vexplor -d vexplor -c "SELECT 1"
+```
+
+### Watchtower가 업데이트하지 않음
+
+```bash
+# Watchtower 로그 확인
+docker compose logs watchtower
+
+# Harbor 인증 확인
+docker pull harbor.wace.me/vexplor/vexplor-backend:latest
+
+# 라벨 확인 (라벨이 있는 컨테이너만 업데이트)
+docker inspect vexplor-backend | grep watchtower
+```
+
+### 디스크 공간 부족
+
+```bash
+# 사용하지 않는 이미지/컨테이너 정리
+docker system prune -a
+
+# 오래된 로그 정리
+docker compose logs --tail=0 backend # 로그 초기화
+```
+
+---
+
+## 보안 권장사항
+
+1. **방화벽 설정**: 필요한 포트(80, 3001)만 개방
+2. **SSL/TLS**: Nginx 리버스 프록시 + Let's Encrypt 적용 권장
+3. **정기 백업**: 최소 주 1회 DB 백업
+4. **로그 모니터링**: 비정상 접근 감시
+
+---
+
+## 연락처
+
+배포 관련 문의: [담당자 이메일]
+
diff --git a/deploy/onpremise/docker-compose.yml b/deploy/onpremise/docker-compose.yml
new file mode 100644
index 00000000..a779cad7
--- /dev/null
+++ b/deploy/onpremise/docker-compose.yml
@@ -0,0 +1,155 @@
+# Vexplor 온프레미스(공장) 배포용 Docker Compose
+# 사용법: docker compose up -d
+
+services:
+ # ============================================
+ # 1. 데이터베이스 (PostgreSQL)
+ # ============================================
+ database:
+ image: postgres:15-alpine
+ container_name: vexplor-db
+ environment:
+ POSTGRES_USER: ${DB_USER:-vexplor}
+ POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
+ POSTGRES_DB: ${DB_NAME:-vexplor}
+ TZ: Asia/Seoul
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ - ./init-db:/docker-entrypoint-initdb.d # 초기화 스크립트 (선택)
+ ports:
+ - "${DB_PORT:-5432}:5432"
+ restart: always
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-vexplor}"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ networks:
+ - vexplor-network
+
+ # ============================================
+ # 2. 백엔드 API (Node.js)
+ # ============================================
+ backend:
+ image: harbor.wace.me/speefox_vexplor/vexplor-backend:${IMAGE_TAG:-latest}
+ container_name: vexplor-backend
+ environment:
+ NODE_ENV: production
+ PORT: 3001
+ HOST: 0.0.0.0
+ TZ: Asia/Seoul
+ # DB 연결
+ DB_HOST: database
+ DB_PORT: 5432
+ DB_USER: ${DB_USER:-vexplor}
+ DB_PASSWORD: ${DB_PASSWORD}
+ DB_NAME: ${DB_NAME:-vexplor}
+ # JWT
+ JWT_SECRET: ${JWT_SECRET:?JWT_SECRET is required}
+ JWT_EXPIRES_IN: ${JWT_EXPIRES_IN:-24h}
+ # 암호화 키 (메일 등 민감정보 암호화용)
+ ENCRYPTION_KEY: ${ENCRYPTION_KEY:-vexplor-encryption-key-32characters-secure}
+ # 회사 코드 (온프레미스는 단일 회사)
+ DEFAULT_COMPANY_CODE: ${COMPANY_CODE:-SPIFOX}
+ # 로깅
+ LOG_LEVEL: ${LOG_LEVEL:-info}
+ volumes:
+ - backend_uploads:/app/uploads
+ - backend_data:/app/data
+ - backend_logs:/app/logs
+ ports:
+ - "${BACKEND_PORT:-3001}:3001"
+ depends_on:
+ database:
+ condition: service_healthy
+ restart: always
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:3001/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 40s
+ networks:
+ - vexplor-network
+ labels:
+ - "com.centurylinklabs.watchtower.enable=true"
+
+ # ============================================
+ # 3. 프론트엔드 (Next.js)
+ # ============================================
+ frontend:
+ image: harbor.wace.me/speefox_vexplor/vexplor-frontend:${IMAGE_TAG:-latest}
+ container_name: vexplor-frontend
+ environment:
+ NODE_ENV: production
+ PORT: 3000
+ HOSTNAME: 0.0.0.0
+ TZ: Asia/Seoul
+ # 백엔드 API URL (내부 통신)
+ NEXT_PUBLIC_API_URL: http://${SERVER_IP:-localhost}:${BACKEND_PORT:-3001}/api
+ ports:
+ - "${FRONTEND_PORT:-80}:3000"
+ depends_on:
+ - backend
+ restart: always
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:3000"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 30s
+ networks:
+ - vexplor-network
+ labels:
+ - "com.centurylinklabs.watchtower.enable=true"
+
+ # ============================================
+ # 4. Watchtower (자동 업데이트)
+ # ============================================
+ watchtower:
+ image: containrrr/watchtower:latest
+ container_name: vexplor-watchtower
+ environment:
+ TZ: Asia/Seoul
+ DOCKER_API_VERSION: "1.44"
+ # Harbor 레지스트리 인증
+ REPO_USER: ${HARBOR_USER}
+ REPO_PASS: ${HARBOR_PASSWORD}
+ # 업데이트 설정
+ # WATCHTOWER_POLL_INTERVAL: ${UPDATE_INTERVAL:-300} # 간격 기반 (비활성화)
+ WATCHTOWER_SCHEDULE: "0 0 * * * *" # 매시 정각에 실행 (cron 형식)
+ WATCHTOWER_CLEANUP: "true" # 이전 이미지 자동 삭제
+ WATCHTOWER_INCLUDE_STOPPED: "true" # 중지된 컨테이너도 업데이트
+ WATCHTOWER_ROLLING_RESTART: "true" # 순차 재시작 (다운타임 최소화)
+ WATCHTOWER_LABEL_ENABLE: "true" # 라벨이 있는 컨테이너만 업데이트
+ # 알림 설정 (선택)
+ # WATCHTOWER_NOTIFICATIONS: slack
+ # WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL: ${SLACK_WEBHOOK_URL}
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock
+ # Harbor 인증 정보 (docker login 후 생성됨)
+ - ~/.docker/config.json:/config.json:ro
+ restart: always
+ networks:
+ - vexplor-network
+
+# ============================================
+# 볼륨 정의
+# ============================================
+volumes:
+ postgres_data:
+ driver: local
+ backend_uploads:
+ driver: local
+ backend_data:
+ driver: local
+ backend_logs:
+ driver: local
+
+# ============================================
+# 네트워크 정의
+# ============================================
+networks:
+ vexplor-network:
+ driver: bridge
+
diff --git a/deploy/onpremise/env.example b/deploy/onpremise/env.example
new file mode 100644
index 00000000..7ffc0d5b
--- /dev/null
+++ b/deploy/onpremise/env.example
@@ -0,0 +1,65 @@
+# ============================================
+# Vexplor 온프레미스(공장) 환경 변수
+# ============================================
+# 사용법: 이 파일을 .env로 복사 후 값 수정
+# cp env.example .env
+
+# ============================================
+# 서버 정보
+# ============================================
+# 이 서버의 IP 주소 (프론트엔드가 백엔드 API 호출할 때 사용)
+SERVER_IP=192.168.0.100
+
+# ============================================
+# 회사 정보
+# ============================================
+# 이 공장의 회사 코드 (멀티테넌시용)
+COMPANY_CODE=SPIFOX
+
+# ============================================
+# 데이터베이스 설정
+# ============================================
+DB_USER=vexplor
+DB_PASSWORD=your_secure_password_here
+DB_NAME=vexplor
+DB_PORT=5432
+
+# ============================================
+# 백엔드 설정
+# ============================================
+BACKEND_PORT=3001
+JWT_SECRET=your_jwt_secret_key_minimum_32_characters
+JWT_EXPIRES_IN=24h
+LOG_LEVEL=info
+
+# ============================================
+# 프론트엔드 설정
+# ============================================
+FRONTEND_PORT=80
+
+# ============================================
+# Harbor 레지스트리 인증
+# ============================================
+# Watchtower가 이미지를 당겨올 때 사용
+HARBOR_USER=your_harbor_username
+HARBOR_PASSWORD=your_harbor_password
+
+# ============================================
+# 이미지 태그
+# ============================================
+# latest 또는 특정 버전 (v1.0.0 등)
+IMAGE_TAG=latest
+
+# ============================================
+# Watchtower 설정
+# ============================================
+# 업데이트 확인 주기 (초 단위)
+# 300 = 5분, 3600 = 1시간, 86400 = 24시간
+UPDATE_INTERVAL=3600
+
+# ============================================
+# 알림 설정 (선택)
+# ============================================
+# Slack 웹훅 URL (업데이트 알림 받기)
+# SLACK_WEBHOOK_URL=https://hooks.slack.com/services/xxx/xxx/xxx
+
diff --git a/deploy/onpremise/scripts/backup.sh b/deploy/onpremise/scripts/backup.sh
new file mode 100644
index 00000000..1e3a65fd
--- /dev/null
+++ b/deploy/onpremise/scripts/backup.sh
@@ -0,0 +1,57 @@
+#!/bin/bash
+# ============================================
+# Vexplor 백업 스크립트
+# Cron에 등록하여 정기 백업 가능
+# ============================================
+
+set -e
+
+INSTALL_DIR="/opt/vexplor"
+BACKUP_DIR="/opt/vexplor/backups"
+DATE=$(date +%Y%m%d_%H%M%S)
+
+# 백업 디렉토리 생성
+mkdir -p $BACKUP_DIR
+
+echo "=========================================="
+echo " Vexplor 백업 시작 - $DATE"
+echo "=========================================="
+
+cd $INSTALL_DIR
+
+# 1. PostgreSQL 데이터베이스 백업
+echo "[1/3] 데이터베이스 백업..."
+docker compose exec -T database pg_dump -U vexplor vexplor > "$BACKUP_DIR/db_$DATE.sql"
+gzip "$BACKUP_DIR/db_$DATE.sql"
+echo " → $BACKUP_DIR/db_$DATE.sql.gz"
+
+# 2. 업로드 파일 백업
+echo "[2/3] 업로드 파일 백업..."
+docker cp vexplor-backend:/app/uploads "$BACKUP_DIR/uploads_$DATE" 2>/dev/null || echo " → 업로드 폴더 없음 (스킵)"
+if [ -d "$BACKUP_DIR/uploads_$DATE" ]; then
+ tar -czf "$BACKUP_DIR/uploads_$DATE.tar.gz" -C "$BACKUP_DIR" "uploads_$DATE"
+ rm -rf "$BACKUP_DIR/uploads_$DATE"
+ echo " → $BACKUP_DIR/uploads_$DATE.tar.gz"
+fi
+
+# 3. 환경 설정 백업
+echo "[3/3] 환경 설정 백업..."
+cp "$INSTALL_DIR/.env" "$BACKUP_DIR/env_$DATE"
+cp "$INSTALL_DIR/docker-compose.yml" "$BACKUP_DIR/docker-compose_$DATE.yml"
+echo " → $BACKUP_DIR/env_$DATE"
+echo " → $BACKUP_DIR/docker-compose_$DATE.yml"
+
+# 4. 오래된 백업 정리 (30일 이상)
+echo ""
+echo "[정리] 30일 이상 된 백업 삭제..."
+find $BACKUP_DIR -type f -mtime +30 -delete 2>/dev/null || true
+
+# 완료
+echo ""
+echo "=========================================="
+echo " 백업 완료!"
+echo "=========================================="
+echo ""
+echo "백업 위치: $BACKUP_DIR"
+ls -lh $BACKUP_DIR | tail -10
+
diff --git a/deploy/onpremise/scripts/install.sh b/deploy/onpremise/scripts/install.sh
new file mode 100644
index 00000000..880dcbcc
--- /dev/null
+++ b/deploy/onpremise/scripts/install.sh
@@ -0,0 +1,79 @@
+#!/bin/bash
+# ============================================
+# Vexplor 온프레미스 초기 설치 스크립트
+# ============================================
+
+set -e
+
+echo "=========================================="
+echo " Vexplor 온프레미스 설치 스크립트"
+echo "=========================================="
+
+# 색상 정의
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m' # No Color
+
+# 설치 경로
+INSTALL_DIR="/opt/vexplor"
+
+# 1. Docker 설치 확인
+echo -e "\n${YELLOW}[1/5] Docker 설치 확인...${NC}"
+if ! command -v docker &> /dev/null; then
+ echo -e "${RED}Docker가 설치되어 있지 않습니다.${NC}"
+ echo "다음 명령어로 설치하세요:"
+ echo " curl -fsSL https://get.docker.com | sh"
+ echo " sudo usermod -aG docker \$USER"
+ exit 1
+fi
+echo -e "${GREEN}Docker $(docker --version | cut -d' ' -f3)${NC}"
+
+# 2. Docker Compose 확인
+echo -e "\n${YELLOW}[2/5] Docker Compose 확인...${NC}"
+if ! docker compose version &> /dev/null; then
+ echo -e "${RED}Docker Compose v2가 설치되어 있지 않습니다.${NC}"
+ exit 1
+fi
+echo -e "${GREEN}$(docker compose version)${NC}"
+
+# 3. 설치 디렉토리 생성
+echo -e "\n${YELLOW}[3/5] 설치 디렉토리 생성...${NC}"
+sudo mkdir -p $INSTALL_DIR
+sudo chown $USER:$USER $INSTALL_DIR
+echo -e "${GREEN}$INSTALL_DIR 생성 완료${NC}"
+
+# 4. 파일 복사
+echo -e "\n${YELLOW}[4/5] 설정 파일 복사...${NC}"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+
+cp "$SCRIPT_DIR/docker-compose.yml" "$INSTALL_DIR/"
+cp "$SCRIPT_DIR/env.example" "$INSTALL_DIR/"
+
+if [ ! -f "$INSTALL_DIR/.env" ]; then
+ cp "$SCRIPT_DIR/env.example" "$INSTALL_DIR/.env"
+ echo -e "${YELLOW}[주의] .env 파일을 생성했습니다. 반드시 수정하세요!${NC}"
+fi
+
+echo -e "${GREEN}파일 복사 완료${NC}"
+
+# 5. Harbor 로그인 안내
+echo -e "\n${YELLOW}[5/5] Harbor 레지스트리 로그인...${NC}"
+if [ ! -f ~/.docker/config.json ] || ! grep -q "harbor.wace.me" ~/.docker/config.json 2>/dev/null; then
+ echo -e "${YELLOW}Harbor 로그인이 필요합니다:${NC}"
+ echo " docker login harbor.wace.me"
+else
+ echo -e "${GREEN}Harbor 로그인 확인됨${NC}"
+fi
+
+# 완료 메시지
+echo -e "\n=========================================="
+echo -e "${GREEN} 설치 준비 완료!${NC}"
+echo "=========================================="
+echo ""
+echo "다음 단계:"
+echo " 1. 환경 변수 설정: nano $INSTALL_DIR/.env"
+echo " 2. Harbor 로그인: docker login harbor.wace.me"
+echo " 3. 서비스 시작: cd $INSTALL_DIR && docker compose up -d"
+echo ""
+
diff --git a/deploy/onpremise/scripts/server-setup.sh b/deploy/onpremise/scripts/server-setup.sh
new file mode 100644
index 00000000..fa20a85f
--- /dev/null
+++ b/deploy/onpremise/scripts/server-setup.sh
@@ -0,0 +1,130 @@
+#!/bin/bash
+# ============================================
+# Vexplor 온프레미스 서버 초기 설정 스크립트
+# 스피폭스 공장 서버용
+# ============================================
+# 사용법: sudo bash server-setup.sh
+
+set -e
+
+# 색상 정의
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m'
+
+echo ""
+echo "=========================================="
+echo " Vexplor 서버 초기 설정"
+echo "=========================================="
+echo ""
+
+# root 권한 확인
+if [ "$EUID" -ne 0 ]; then
+ echo -e "${RED}이 스크립트는 root 권한이 필요합니다.${NC}"
+ echo "다음 명령어로 실행하세요: sudo bash server-setup.sh"
+ exit 1
+fi
+
+# ============================================
+# 1. Docker 설치
+# ============================================
+echo -e "${YELLOW}[1/5] Docker 설치 중...${NC}"
+
+# 기존 Docker 제거
+apt-get remove -y docker docker-engine docker.io containerd runc 2>/dev/null || true
+
+# 필수 패키지 설치
+apt-get update
+apt-get install -y ca-certificates curl gnupg
+
+# Docker GPG 키 추가
+install -m 0755 -d /etc/apt/keyrings
+curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
+chmod a+r /etc/apt/keyrings/docker.gpg
+
+# Docker 저장소 추가
+echo \
+ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
+ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
+ tee /etc/apt/sources.list.d/docker.list > /dev/null
+
+# Docker 설치
+apt-get update
+apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
+
+echo -e "${GREEN}Docker 설치 완료!${NC}"
+docker --version
+docker compose version
+
+# ============================================
+# 2. 사용자를 docker 그룹에 추가
+# ============================================
+echo ""
+echo -e "${YELLOW}[2/5] 사용자 권한 설정...${NC}"
+
+# wace 사용자를 docker 그룹에 추가
+usermod -aG docker wace
+
+echo -e "${GREEN}wace 사용자를 docker 그룹에 추가했습니다.${NC}"
+
+# ============================================
+# 3. Vexplor 디렉토리 생성
+# ============================================
+echo ""
+echo -e "${YELLOW}[3/5] Vexplor 디렉토리 생성...${NC}"
+
+mkdir -p /opt/vexplor
+chown wace:wace /opt/vexplor
+
+echo -e "${GREEN}/opt/vexplor 디렉토리 생성 완료!${NC}"
+
+# ============================================
+# 4. Docker 서비스 시작 및 자동 시작 설정
+# ============================================
+echo ""
+echo -e "${YELLOW}[4/5] Docker 서비스 설정...${NC}"
+
+systemctl start docker
+systemctl enable docker
+
+echo -e "${GREEN}Docker 서비스 활성화 완료!${NC}"
+
+# ============================================
+# 5. 방화벽 설정 (필요시)
+# ============================================
+echo ""
+echo -e "${YELLOW}[5/5] 방화벽 설정 확인...${NC}"
+
+if command -v ufw &> /dev/null; then
+ ufw status
+ echo ""
+ echo "필요시 다음 포트를 개방하세요:"
+ echo " sudo ufw allow 80/tcp # 웹 서비스"
+ echo " sudo ufw allow 3001/tcp # 백엔드 API"
+else
+ echo "ufw가 설치되어 있지 않습니다. (방화벽 설정 스킵)"
+fi
+
+# ============================================
+# 완료
+# ============================================
+echo ""
+echo "=========================================="
+echo -e "${GREEN} 서버 초기 설정 완료!${NC}"
+echo "=========================================="
+echo ""
+echo "다음 단계:"
+echo " 1. 로그아웃 후 다시 로그인 (docker 그룹 적용)"
+echo " exit"
+echo " ssh -p 22 wace@112.168.212.142"
+echo ""
+echo " 2. Docker 동작 확인"
+echo " docker ps"
+echo ""
+echo " 3. Vexplor 배포 진행"
+echo " cd /opt/vexplor"
+echo " # docker-compose.yml 및 .env 파일 복사 후"
+echo " docker compose up -d"
+echo ""
+
diff --git a/deploy/onpremise/scripts/update.sh b/deploy/onpremise/scripts/update.sh
new file mode 100644
index 00000000..77e7678b
--- /dev/null
+++ b/deploy/onpremise/scripts/update.sh
@@ -0,0 +1,41 @@
+#!/bin/bash
+# ============================================
+# Vexplor 수동 업데이트 스크립트
+# Watchtower를 기다리지 않고 즉시 업데이트할 때 사용
+# ============================================
+
+set -e
+
+INSTALL_DIR="/opt/vexplor"
+cd $INSTALL_DIR
+
+echo "=========================================="
+echo " Vexplor 수동 업데이트"
+echo "=========================================="
+
+# 1. 현재 상태 백업
+echo "[1/4] 현재 설정 백업..."
+docker compose config > "backup-config-$(date +%Y%m%d-%H%M%S).yml"
+
+# 2. 최신 이미지 다운로드
+echo "[2/4] 최신 이미지 다운로드..."
+docker compose pull backend frontend
+
+# 3. 서비스 재시작 (롤링 업데이트)
+echo "[3/4] 서비스 재시작..."
+docker compose up -d --no-deps backend
+sleep 10 # 백엔드가 완전히 뜰 때까지 대기
+docker compose up -d --no-deps frontend
+
+# 4. 상태 확인
+echo "[4/4] 상태 확인..."
+sleep 5
+docker compose ps
+
+echo ""
+echo "=========================================="
+echo " 업데이트 완료!"
+echo "=========================================="
+echo ""
+echo "로그 확인: docker compose logs -f"
+
diff --git a/digitalTwin/architecture-v4.md b/digitalTwin/architecture-v4.md
new file mode 100644
index 00000000..96e32ef1
--- /dev/null
+++ b/digitalTwin/architecture-v4.md
@@ -0,0 +1,209 @@
+# 디지털트윈 아키텍처 v4
+
+## 변경사항 (v3 → v4)
+
+| 구분 | v3 | v4 |
+| :--- | :--- | :--- |
+| OTA 업데이트 | 개념만 존재 | Fleet Manager + MQTT 구현 |
+| 디바이스 관리 | 없음 | Device Registry 추가 |
+| 상태 모니터링 | 없음 | Heartbeat + Metrics 추가 |
+| 원격 제어 | 없음 | MQTT 기반 명령 추가 |
+| Agent | 없음 | Fleet Agent 추가 |
+
+---
+
+## Mermaid 다이어그램
+
+```mermaid
+---
+config:
+ layout: dagre
+---
+flowchart BT
+ subgraph Global_Platform["☁️ Vexplor 글로벌 플랫폼"]
+ direction TB
+ AAS_Dashboard["💻 AAS 통합 대시보드
(React/Next.js)
• 중앙 모니터링
• Fleet 관리 UI"]
+ Global_API["🌐 글로벌 API 게이트웨이
• 사용자 인증 (Auth)
• 고객사 라우팅
• Fleet API"]
+
+ subgraph Fleet_System["🎛️ Fleet Management"]
+ Fleet_Manager["📊 Fleet Manager
• Device Registry
• 배포 오케스트레이션
• 상태 모니터링"]
+ MQTT_Broker["📡 MQTT Broker
(Mosquitto/EMQX)
• 실시간 통신
• 10,000+ 연결"]
+ Monitoring["📈 Monitoring
(Prometheus/Grafana)
• 메트릭 수집
• 알림"]
+ end
+
+ Update_Server["🚀 배포/업데이트 매니저
• Docker 이미지 레지스트리 (Harbor)
• 버전 관리
• Canary 배포"]
+ end
+
+ subgraph Local_Server["스피폭스 사내 서버 (Local Server)"]
+ Fleet_Agent_A["🤖 Fleet Agent
• MQTT 연결
• Heartbeat (30초)
• 원격 명령 실행
• Docker 관리"]
+ VEX_Engine["VEX Flow 엔진
데이터 수집/처리"]
+ Customer_DB[("사내 통합 DB
(모든 데이터 보유)")]
+ Watchtower_A["🐋 Watchtower
이미지 자동 업데이트"]
+ end
+
+ subgraph Edge_Internals["🖥️ 엣지 디바이스 (Store & Forward)"]
+ Edge_Collector["수집/가공
(Python)"]
+ Edge_Buffer[("💾 로컬 버퍼
(TimescaleDB)
단절 시 임시 저장")]
+ Edge_Sender["📤 전송 매니저
(Priority Queue)"]
+ Edge_Retry_Queue[("🕒 재전송 큐
(SQLite/File)")]
+ end
+
+ subgraph Factory_A["🏭 스피폭스 공장 현장 (Factory Floor)"]
+ Edge_Internals
+ PLC_A["PLC / 센서"]
+ end
+
+ subgraph Customer_A["🏢 고객사 A: 스피폭스 (사내망)"]
+ Local_Server
+ Factory_A
+ end
+
+ subgraph Local_Server_B["고객사 B 사내 서버"]
+ Fleet_Agent_B["🤖 Fleet Agent"]
+ Watchtower_B["🐋 Watchtower"]
+ end
+
+ subgraph Customer_B["🏭 고객사 B (확장 예정)"]
+ Local_Server_B
+ end
+
+ subgraph Local_Server_N["고객사 N 사내 서버"]
+ Fleet_Agent_N["🤖 Fleet Agent"]
+ end
+
+ subgraph Customer_N["🏭 고객사 N (10,000개)"]
+ Local_Server_N
+ end
+
+ %% 대시보드 연결
+ AAS_Dashboard <--> Global_API
+ AAS_Dashboard <--> Fleet_Manager
+
+ %% Fleet System 내부 연결
+ Fleet_Manager <--> MQTT_Broker
+ Fleet_Manager <--> Monitoring
+ Fleet_Manager <--> Update_Server
+
+ %% 공장 내부 연결
+ PLC_A <--> Edge_Collector
+ Edge_Collector --> Edge_Buffer
+ Edge_Buffer --> Edge_Sender
+ Edge_Sender -- ① 정상 전송 --> VEX_Engine
+ Edge_Sender -- ② 전송 실패 시 --> Edge_Retry_Queue
+ Edge_Retry_Queue -. ③ 네트워크 복구 시 재전송 .-> Edge_Sender
+ VEX_Engine <--> Customer_DB
+
+ %% Fleet Agent 연결 (MQTT - Outbound Only)
+ Fleet_Agent_A == 📡 MQTT (Heartbeat/명령) ==> MQTT_Broker
+ Fleet_Agent_B == 📡 MQTT ==> MQTT_Broker
+ Fleet_Agent_N == 📡 MQTT ==> MQTT_Broker
+
+ %% Agent ↔ 로컬 컴포넌트
+ Fleet_Agent_A <--> VEX_Engine
+ Fleet_Agent_A <--> Watchtower_A
+ Fleet_Agent_A <--> Customer_DB
+
+ %% OTA 업데이트 (Pull 방식)
+ Update_Server -. 이미지 배포 .-> Watchtower_A
+ Update_Server -. 이미지 배포 .-> Watchtower_B
+ Watchtower_A -. 컨테이너 업데이트 .-> VEX_Engine
+
+ %% 엣지 업데이트
+ VEX_Engine -. 엣지 업데이트 .-> Edge_Collector
+
+ %% 스타일
+ AAS_Dashboard:::user
+ Global_API:::global
+ Update_Server:::global
+ Fleet_Manager:::fleet
+ MQTT_Broker:::fleet
+ Monitoring:::fleet
+ VEX_Engine:::localServer
+ Customer_DB:::localServer
+ Fleet_Agent_A:::agent
+ Fleet_Agent_B:::agent
+ Fleet_Agent_N:::agent
+ Watchtower_A:::agent
+ Watchtower_B:::agent
+ Edge_Collector:::edge
+ Edge_Buffer:::edgedb
+ Edge_Sender:::edge
+ Edge_Retry_Queue:::fail
+ PLC_A:::factory
+
+ classDef factory fill:#e1f5fe,stroke:#01579b,stroke-width:2px
+ classDef edge fill:#fff9c4,stroke:#fbc02d,stroke-width:2px
+ classDef edgedb fill:#fff9c4,stroke:#fbc02d,stroke-width:2px,stroke-dasharray: 5 5
+ classDef localServer fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
+ classDef global fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
+ classDef user fill:#ffebee,stroke:#c62828,stroke-width:2px
+ classDef fleet fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
+ classDef agent fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
+ classDef fail fill:#ffebee,stroke:#c62828,stroke-width:2px,stroke-dasharray: 5 5
+
+ linkStyle 8 stroke:#2e7d32,stroke-width:2px,fill:none
+ linkStyle 9 stroke:#c62828,stroke-width:2px,fill:none
+ linkStyle 10 stroke:#fbc02d,stroke-width:2px,stroke-dasharray: 5 5,fill:none
+```
+
+---
+
+## 추가된 컴포넌트 설명
+
+### 1. Fleet Management (신규)
+
+| 컴포넌트 | 역할 |
+| :--- | :--- |
+| **Fleet Manager** | 10,000개 디바이스 등록/관리, 배포 오케스트레이션 |
+| **MQTT Broker** | 실시간 양방향 통신 (Outbound Only 유지) |
+| **Monitoring** | Prometheus + Grafana, 메트릭 수집 & 알림 |
+
+### 2. Fleet Agent (각 공장 서버에 설치)
+
+| 기능 | 설명 |
+| :--- | :--- |
+| **MQTT 연결** | 글로벌 플랫폼과 상시 연결 (Outbound) |
+| **Heartbeat** | 30초마다 상태 보고 |
+| **원격 명령** | 업데이트, 재시작, 설정 변경 수신 |
+| **Docker 관리** | 컨테이너 상태 모니터링 & 제어 |
+
+### 3. Watchtower (기존 유지)
+
+- Harbor에서 새 이미지 자동 Pull
+- Fleet Agent의 명령으로 즉시 업데이트 가능
+
+---
+
+## 통신 흐름 비교
+
+### v3 (기존)
+```
+보안 커넥터 ←→ 글로벌 API (양방향 터널)
+```
+
+### v4 (신규)
+```
+Fleet Agent ──→ MQTT Broker (Outbound Only)
+ ←── 명령 수신 (Subscribe)
+ ──→ 상태 보고 (Publish)
+
+Watchtower ──→ Harbor (Pull Only)
+```
+
+**장점:**
+- 방화벽 Inbound 규칙 불필요
+- 10,000개 동시 연결 가능
+- 실시간 명령 전달
+
+---
+
+## 데이터 흐름
+
+```
+[공장 → 글로벌]
+PLC → 엣지 → VEX Flow → Fleet Agent → MQTT → Fleet Manager → Dashboard
+
+[글로벌 → 공장]
+Dashboard → Fleet Manager → MQTT → Fleet Agent → Docker/VEX Flow
+```
+
diff --git a/digitalTwin/fleet-management-plan.md b/digitalTwin/fleet-management-plan.md
new file mode 100644
index 00000000..e80aaab9
--- /dev/null
+++ b/digitalTwin/fleet-management-plan.md
@@ -0,0 +1,725 @@
+# Fleet Management 시스템 구축 계획서
+
+## 개요
+
+**목표:** 10,000개 이상의 온프레미스 공장 서버를 중앙에서 효율적으로 관리
+
+**현재 상태:** 1개 업체 (스피폭스), Watchtower 기반 자동 업데이트
+
+**목표 상태:** 10,000개 업체, 실시간 모니터링 & 원격 제어 가능
+
+---
+
+## 목차
+
+1. [아키텍처 설계](#1-아키텍처-설계)
+2. [Phase별 구현 계획](#2-phase별-구현-계획)
+3. [핵심 컴포넌트 상세](#3-핵심-컴포넌트-상세)
+4. [데이터베이스 스키마](#4-데이터베이스-스키마)
+5. [API 설계](#5-api-설계)
+6. [기술 스택](#6-기술-스택)
+7. [일정 및 마일스톤](#7-일정-및-마일스톤)
+8. [리스크 및 대응](#8-리스크-및-대응)
+
+---
+
+## 1. 아키텍처 설계
+
+### 1.1 전체 아키텍처
+
+```
+┌─────────────────────────────────────────────────────────────────────────┐
+│ Vexplor 글로벌 플랫폼 │
+├─────────────────────────────────────────────────────────────────────────┤
+│ │
+│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
+│ │ Web UI │ │ Fleet API │ │ Config │ │ Monitoring │ │
+│ │ (Dashboard) │ │ Gateway │ │ Server │ │ & Alerts │ │
+│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
+│ │ │ │ │ │
+│ └────────────────┼────────────────┼────────────────┘ │
+│ │ │ │
+│ ▼ ▼ │
+│ ┌─────────────┐ ┌─────────────┐ │
+│ │ Message │ │ Device │ │
+│ │ Broker │ │ Registry │ │
+│ │ (MQTT) │ │ (Redis) │ │
+│ └──────┬──────┘ └─────────────┘ │
+│ │ │
+└──────────────────────────┼────────────────────────────────────────────┘
+ │
+ │ MQTT (TLS)
+ │
+ ┌──────────────────┼──────────────────┐
+ │ │ │
+ ▼ ▼ ▼
+ ┌─────────┐ ┌─────────┐ ┌─────────┐
+ │ Agent │ │ Agent │ │ Agent │
+ │ 스피폭스 │ │ 엔키드 │ │ 고객 N │
+ └─────────┘ └─────────┘ └─────────┘
+ │ │ │
+ ┌─────────┐ ┌─────────┐ ┌─────────┐
+ │ Vexplor │ │ Vexplor │ │ Vexplor │
+ │ Backend │ │ Backend │ │ Backend │
+ │Frontend │ │Frontend │ │Frontend │
+ │ DB │ │ DB │ │ DB │
+ └─────────┘ └─────────┘ └─────────┘
+```
+
+### 1.2 통신 흐름
+
+```
+[공장 서버 → 글로벌]
+1. Agent 시작 시 MQTT 연결 (Outbound Only)
+2. 주기적 Heartbeat 전송 (30초)
+3. 상태/메트릭 보고 (5분)
+4. 로그 전송 (선택적)
+
+[글로벌 → 공장 서버]
+1. 업데이트 명령
+2. 설정 변경
+3. 재시작 명령
+4. 데이터 요청
+```
+
+---
+
+## 2. Phase별 구현 계획
+
+### Phase 1: 기반 구축 (1~10개 업체)
+**기간:** 2주
+
+| 구현 항목 | 설명 | 우선순위 |
+| :--- | :--- | :--- |
+| Device Registry API | 디바이스 등록/조회 | P0 |
+| Heartbeat API | 상태 보고 수신 | P0 |
+| 기본 대시보드 | 디바이스 목록/상태 표시 | P1 |
+| Agent 기본 버전 | Heartbeat 전송 기능 | P0 |
+
+**산출물:**
+- `POST /api/fleet/devices/register`
+- `POST /api/fleet/devices/heartbeat`
+- `GET /api/fleet/devices`
+- Agent Docker 이미지
+
+---
+
+### Phase 2: 실시간 통신 (10~100개 업체)
+**기간:** 4주
+
+| 구현 항목 | 설명 | 우선순위 |
+| :--- | :--- | :--- |
+| MQTT 브로커 설치 | Eclipse Mosquitto | P0 |
+| Agent MQTT 연결 | 상시 연결 유지 | P0 |
+| 원격 명령 기능 | 업데이트/재시작 명령 | P1 |
+| 실시간 상태 업데이트 | WebSocket → 대시보드 | P1 |
+
+**산출물:**
+- MQTT 브로커 (Docker)
+- Agent v2 (MQTT 지원)
+- 원격 명령 UI
+
+---
+
+### Phase 3: 배포 관리 (100~500개 업체)
+**기간:** 6주
+
+| 구현 항목 | 설명 | 우선순위 |
+| :--- | :--- | :--- |
+| 버전 관리 시스템 | 릴리즈 버전 관리 | P0 |
+| 단계적 롤아웃 | Canary 배포 | P0 |
+| 롤백 기능 | 이전 버전 복구 | P0 |
+| 그룹 관리 | 지역/업종별 그룹핑 | P1 |
+| 배포 스케줄링 | 시간대별 배포 | P2 |
+
+**산출물:**
+- Release Management UI
+- Deployment Pipeline
+- Rollback 자동화
+
+---
+
+### Phase 4: 모니터링 강화 (500~2,000개 업체)
+**기간:** 6주
+
+| 구현 항목 | 설명 | 우선순위 |
+| :--- | :--- | :--- |
+| 메트릭 수집 | CPU/Memory/Disk | P0 |
+| 알림 시스템 | Slack/Email/SMS | P0 |
+| 로그 중앙화 | 원격 로그 수집 | P1 |
+| 이상 탐지 | 자동 장애 감지 | P1 |
+| SLA 대시보드 | 가용성 리포트 | P2 |
+
+**산출물:**
+- Prometheus + Grafana
+- Alert Manager
+- Log Aggregator (Loki)
+
+---
+
+### Phase 5: 대규모 확장 (2,000~10,000개 업체)
+**기간:** 8주
+
+| 구현 항목 | 설명 | 우선순위 |
+| :--- | :--- | :--- |
+| MQTT 클러스터링 | 고가용성 브로커 | P0 |
+| 샤딩 | 지역별 분산 | P0 |
+| 자동 프로비저닝 | 신규 업체 자동 설정 | P1 |
+| API Rate Limiting | 과부하 방지 | P1 |
+| 멀티 리전 | 글로벌 분산 | P2 |
+
+**산출물:**
+- MQTT Cluster (EMQX)
+- Regional Gateway
+- Auto-provisioning System
+
+---
+
+## 3. 핵심 컴포넌트 상세
+
+### 3.1 Fleet Agent (공장 서버에 설치)
+
+```
+┌─────────────────────────────────────────┐
+│ Fleet Agent │
+├─────────────────────────────────────────┤
+│ ┌─────────────┐ ┌─────────────┐ │
+│ │ MQTT │ │ Command │ │
+│ │ Client │ │ Executor │ │
+│ └──────┬──────┘ └──────┬──────┘ │
+│ │ │ │
+│ ▼ ▼ │
+│ ┌─────────────────────────────┐ │
+│ │ Core Controller │ │
+│ └─────────────────────────────┘ │
+│ │ │ │
+│ ▼ ▼ │
+│ ┌─────────────┐ ┌─────────────┐ │
+│ │ Metrics │ │ Docker │ │
+│ │ Collector │ │ Manager │ │
+│ └─────────────┘ └─────────────┘ │
+└─────────────────────────────────────────┘
+```
+
+**주요 기능:**
+- MQTT 연결 유지 (자동 재연결)
+- Heartbeat 전송 (30초)
+- 시스템 메트릭 수집
+- Docker 컨테이너 관리
+- 원격 명령 실행
+
+### 3.2 Fleet Manager (글로벌 서버)
+
+**주요 기능:**
+- 디바이스 등록/인증
+- 상태 모니터링
+- 배포 오케스트레이션
+- 설정 관리
+- 알림 발송
+
+### 3.3 Message Broker (MQTT)
+
+**선택지:**
+| 옵션 | 장점 | 단점 | 추천 규모 |
+| :--- | :--- | :--- | :--- |
+| Mosquitto | 가볍고 간단 | 클러스터링 어려움 | ~1,000 |
+| EMQX | 클러스터링, 고성능 | 복잡함 | 1,000~100,000 |
+| HiveMQ | 엔터프라이즈급 | 비용 | 100,000+ |
+
+**권장:** Phase 1~3은 Mosquitto, Phase 4~5는 EMQX
+
+---
+
+## 4. 데이터베이스 스키마
+
+### 4.1 디바이스 테이블
+
+```sql
+-- 디바이스 (공장 서버) 정보
+CREATE TABLE fleet_devices (
+ id SERIAL PRIMARY KEY,
+ device_id VARCHAR(50) UNIQUE NOT NULL, -- 고유 식별자
+ company_code VARCHAR(20) NOT NULL, -- 회사 코드
+ device_name VARCHAR(100), -- 표시 이름
+
+ -- 연결 정보
+ ip_address VARCHAR(45),
+ last_seen_at TIMESTAMPTZ,
+ is_online BOOLEAN DEFAULT false,
+
+ -- 버전 정보
+ agent_version VARCHAR(20),
+ app_version VARCHAR(20),
+
+ -- 시스템 정보
+ os_info JSONB,
+ hardware_info JSONB,
+
+ -- 그룹/태그
+ device_group VARCHAR(50),
+ tags JSONB DEFAULT '[]',
+
+ -- 메타
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
+
+ FOREIGN KEY (company_code) REFERENCES company_info(company_code)
+);
+
+CREATE INDEX idx_fleet_devices_company ON fleet_devices(company_code);
+CREATE INDEX idx_fleet_devices_online ON fleet_devices(is_online);
+CREATE INDEX idx_fleet_devices_group ON fleet_devices(device_group);
+```
+
+### 4.2 Heartbeat 로그 테이블
+
+```sql
+-- Heartbeat 기록 (TimescaleDB 권장)
+CREATE TABLE fleet_heartbeats (
+ id BIGSERIAL,
+ device_id VARCHAR(50) NOT NULL,
+ received_at TIMESTAMPTZ DEFAULT NOW(),
+
+ -- 상태
+ status VARCHAR(20), -- OK, WARNING, ERROR
+ uptime_seconds BIGINT,
+
+ -- 메트릭
+ cpu_percent DECIMAL(5,2),
+ memory_percent DECIMAL(5,2),
+ disk_percent DECIMAL(5,2),
+
+ -- 컨테이너 상태
+ containers JSONB,
+
+ PRIMARY KEY (device_id, received_at)
+);
+
+-- TimescaleDB 하이퍼테이블 변환 (선택)
+-- SELECT create_hypertable('fleet_heartbeats', 'received_at');
+```
+
+### 4.3 배포 테이블
+
+```sql
+-- 릴리즈 버전 관리
+CREATE TABLE fleet_releases (
+ id SERIAL PRIMARY KEY,
+ version VARCHAR(20) NOT NULL,
+ release_type VARCHAR(20), -- stable, beta, hotfix
+
+ -- 이미지 정보
+ backend_image VARCHAR(200),
+ frontend_image VARCHAR(200),
+ agent_image VARCHAR(200),
+
+ -- 변경사항
+ changelog TEXT,
+
+ -- 상태
+ status VARCHAR(20) DEFAULT 'draft', -- draft, testing, released, deprecated
+ released_at TIMESTAMPTZ,
+
+ created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- 배포 작업
+CREATE TABLE fleet_deployments (
+ id SERIAL PRIMARY KEY,
+ release_id INTEGER REFERENCES fleet_releases(id),
+
+ -- 배포 대상
+ target_type VARCHAR(20), -- all, group, specific
+ target_value VARCHAR(100), -- 그룹명 또는 device_id
+
+ -- 롤아웃 설정
+ rollout_strategy VARCHAR(20), -- immediate, canary, scheduled
+ rollout_percentage INTEGER,
+ scheduled_at TIMESTAMPTZ,
+
+ -- 상태
+ status VARCHAR(20) DEFAULT 'pending',
+ started_at TIMESTAMPTZ,
+ completed_at TIMESTAMPTZ,
+
+ -- 결과
+ total_devices INTEGER,
+ success_count INTEGER DEFAULT 0,
+ failed_count INTEGER DEFAULT 0,
+
+ created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- 개별 디바이스 배포 상태
+CREATE TABLE fleet_deployment_status (
+ id SERIAL PRIMARY KEY,
+ deployment_id INTEGER REFERENCES fleet_deployments(id),
+ device_id VARCHAR(50),
+
+ status VARCHAR(20) DEFAULT 'pending', -- pending, downloading, installing, completed, failed
+ started_at TIMESTAMPTZ,
+ completed_at TIMESTAMPTZ,
+ error_message TEXT,
+
+ UNIQUE(deployment_id, device_id)
+);
+```
+
+### 4.4 알림 규칙 테이블
+
+```sql
+-- 알림 규칙
+CREATE TABLE fleet_alert_rules (
+ id SERIAL PRIMARY KEY,
+ name VARCHAR(100) NOT NULL,
+
+ -- 조건
+ condition_type VARCHAR(50), -- offline, version_mismatch, high_cpu, etc.
+ condition_value JSONB,
+ threshold_minutes INTEGER, -- 조건 지속 시간
+
+ -- 알림 채널
+ notify_channels JSONB, -- ["slack", "email"]
+ notify_targets JSONB, -- 수신자 목록
+
+ -- 상태
+ is_enabled BOOLEAN DEFAULT true,
+
+ created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- 알림 기록
+CREATE TABLE fleet_alerts (
+ id SERIAL PRIMARY KEY,
+ rule_id INTEGER REFERENCES fleet_alert_rules(id),
+ device_id VARCHAR(50),
+
+ alert_type VARCHAR(50),
+ message TEXT,
+ severity VARCHAR(20), -- info, warning, critical
+
+ -- 해결 상태
+ status VARCHAR(20) DEFAULT 'open', -- open, acknowledged, resolved
+ resolved_at TIMESTAMPTZ,
+
+ created_at TIMESTAMPTZ DEFAULT NOW()
+);
+```
+
+---
+
+## 5. API 설계
+
+### 5.1 Device Management API
+
+```yaml
+# 디바이스 등록
+POST /api/fleet/devices/register
+Request:
+ device_id: string (required)
+ company_code: string (required)
+ device_name: string
+ agent_version: string
+ os_info: object
+Response:
+ success: boolean
+ data:
+ device_id: string
+ mqtt_credentials:
+ broker_url: string
+ username: string
+ password: string
+
+# 디바이스 목록 조회
+GET /api/fleet/devices
+Query:
+ company_code: string
+ is_online: boolean
+ device_group: string
+ page: number
+ limit: number
+Response:
+ success: boolean
+ data: Device[]
+ pagination: { total, page, limit }
+
+# 디바이스 상세 조회
+GET /api/fleet/devices/:deviceId
+Response:
+ success: boolean
+ data:
+ device: Device
+ recent_heartbeats: Heartbeat[]
+ recent_alerts: Alert[]
+```
+
+### 5.2 Heartbeat API
+
+```yaml
+# Heartbeat 전송
+POST /api/fleet/devices/:deviceId/heartbeat
+Request:
+ status: string
+ uptime_seconds: number
+ metrics:
+ cpu_percent: number
+ memory_percent: number
+ disk_percent: number
+ containers:
+ - name: string
+ status: string
+ version: string
+Response:
+ success: boolean
+ data:
+ commands: Command[] # 대기 중인 명령 반환
+```
+
+### 5.3 Deployment API
+
+```yaml
+# 배포 생성
+POST /api/fleet/deployments
+Request:
+ release_id: number
+ target_type: "all" | "group" | "specific"
+ target_value: string
+ rollout_strategy: "immediate" | "canary" | "scheduled"
+ rollout_percentage: number
+ scheduled_at: datetime
+Response:
+ success: boolean
+ data:
+ deployment_id: number
+ estimated_devices: number
+
+# 배포 상태 조회
+GET /api/fleet/deployments/:deploymentId
+Response:
+ success: boolean
+ data:
+ deployment: Deployment
+ status_summary:
+ pending: number
+ in_progress: number
+ completed: number
+ failed: number
+ device_statuses: DeploymentStatus[]
+
+# 배포 롤백
+POST /api/fleet/deployments/:deploymentId/rollback
+Response:
+ success: boolean
+ data:
+ rollback_deployment_id: number
+```
+
+### 5.4 Command API
+
+```yaml
+# 원격 명령 전송
+POST /api/fleet/devices/:deviceId/commands
+Request:
+ command_type: "update" | "restart" | "config" | "logs"
+ payload: object
+Response:
+ success: boolean
+ data:
+ command_id: string
+ status: "queued"
+
+# 명령 결과 조회
+GET /api/fleet/commands/:commandId
+Response:
+ success: boolean
+ data:
+ command_id: string
+ status: "queued" | "sent" | "executing" | "completed" | "failed"
+ result: object
+```
+
+---
+
+## 6. 기술 스택
+
+### 6.1 글로벌 플랫폼
+
+| 컴포넌트 | 기술 | 비고 |
+| :--- | :--- | :--- |
+| Fleet API | Node.js (기존 backend-node 확장) | 기존 코드 재사용 |
+| Message Broker | Mosquitto → EMQX | 단계적 전환 |
+| Device Registry | Redis | 빠른 조회 |
+| Database | PostgreSQL | 기존 DB 확장 |
+| Time-series DB | TimescaleDB | Heartbeat 저장 |
+| Monitoring | Prometheus + Grafana | 메트릭 시각화 |
+| Log | Loki | 로그 중앙화 |
+| Alert | AlertManager | 알림 관리 |
+
+### 6.2 Fleet Agent
+
+| 컴포넌트 | 기술 | 비고 |
+| :--- | :--- | :--- |
+| Runtime | Go 또는 Node.js | 가볍고 안정적 |
+| MQTT Client | Paho MQTT | 표준 라이브러리 |
+| Docker SDK | Docker API | 컨테이너 관리 |
+| Metrics | gopsutil | 시스템 메트릭 |
+
+### 6.3 대시보드
+
+| 컴포넌트 | 기술 | 비고 |
+| :--- | :--- | :--- |
+| UI Framework | Next.js (기존) | 기존 코드 확장 |
+| Real-time | Socket.io | 실시간 상태 |
+| Charts | Recharts | 메트릭 시각화 |
+| Map | Leaflet | 지역별 표시 |
+
+---
+
+## 7. 일정 및 마일스톤
+
+### 7.1 전체 일정
+
+```
+2025 Q1 2025 Q2 2025 Q3
+│ │ │
+├── Phase 1 (2주) ─────────┤ │
+│ Device Registry │ │
+│ Heartbeat API │ │
+│ 기본 대시보드 │ │
+│ │ │
+│ ├── Phase 2 (4주) ──────────┤ │
+│ │ MQTT 브로커 │ │
+│ │ Agent v2 │ │
+│ │ 원격 명령 │ │
+│ │ │ │
+│ │ ├── Phase 3 (6주) ──────┤
+│ │ │ 버전 관리 │
+│ │ │ Canary 배포 │
+│ │ │ 롤백 │
+│ │ │ │
+```
+
+### 7.2 상세 마일스톤
+
+| 마일스톤 | 목표 | 완료 기준 | 예상 일정 |
+| :--- | :--- | :--- | :--- |
+| M1 | Device Registry | 디바이스 등록/조회 API 완료 | 1주차 |
+| M2 | Heartbeat | 상태 보고 & 저장 완료 | 2주차 |
+| M3 | Basic Dashboard | 디바이스 목록 UI 완료 | 2주차 |
+| M4 | MQTT Setup | 브로커 설치 & 연결 테스트 | 4주차 |
+| M5 | Agent v2 | MQTT 기반 Agent 완료 | 6주차 |
+| M6 | Remote Command | 업데이트/재시작 명령 완료 | 8주차 |
+| M7 | Release Mgmt | 버전 관리 UI 완료 | 10주차 |
+| M8 | Canary Deploy | 단계적 배포 완료 | 14주차 |
+
+---
+
+## 8. 리스크 및 대응
+
+### 8.1 기술적 리스크
+
+| 리스크 | 영향 | 확률 | 대응 |
+| :--- | :--- | :--- | :--- |
+| MQTT 연결 불안정 | 높음 | 중간 | 자동 재연결, 오프라인 큐 |
+| 대량 동시 접속 | 높음 | 높음 | 클러스터링, 로드밸런싱 |
+| 보안 취약점 | 높음 | 낮음 | TLS 필수, 인증 강화 |
+| 네트워크 단절 | 중간 | 높음 | 로컬 캐시, 재전송 로직 |
+
+### 8.2 운영 리스크
+
+| 리스크 | 영향 | 확률 | 대응 |
+| :--- | :--- | :--- | :--- |
+| 잘못된 배포 | 높음 | 중간 | Canary 배포, 자동 롤백 |
+| 모니터링 누락 | 중간 | 중간 | 다중 알림 채널 |
+| 버전 파편화 | 중간 | 높음 | 강제 업데이트 정책 |
+
+---
+
+## 9. 다음 단계
+
+### 즉시 시작할 작업 (Phase 1)
+
+1. **Device Registry 테이블 생성**
+ - `fleet_devices` 테이블 마이그레이션
+
+2. **Fleet API 엔드포인트 개발**
+ - `POST /api/fleet/devices/register`
+ - `POST /api/fleet/devices/:deviceId/heartbeat`
+ - `GET /api/fleet/devices`
+
+3. **Agent 기본 버전 개발**
+ - Docker 이미지로 배포
+ - 주기적 Heartbeat 전송
+
+4. **대시보드 기본 화면**
+ - 디바이스 목록
+ - 온라인/오프라인 상태 표시
+
+---
+
+## 부록
+
+### A. MQTT 토픽 설계
+
+```
+vexplor/
+├── devices/
+│ ├── {device_id}/
+│ │ ├── status # 상태 보고 (Agent → Server)
+│ │ ├── metrics # 메트릭 보고 (Agent → Server)
+│ │ ├── commands # 명령 수신 (Server → Agent)
+│ │ └── responses # 명령 응답 (Agent → Server)
+│ │
+├── broadcasts/
+│ ├── all # 전체 공지
+│ └── groups/{group} # 그룹별 공지
+│
+└── system/
+ ├── announcements # 시스템 공지
+ └── maintenance # 점검 알림
+```
+
+### B. Agent 설정 파일
+
+```yaml
+# /opt/vexplor/agent/config.yaml
+device:
+ id: "SPIFOX-001"
+ company_code: "SPIFOX"
+ name: "스피폭스 메인 서버"
+
+mqtt:
+ broker: "mqtts://mqtt.vexplor.com:8883"
+ username: "${MQTT_USERNAME}"
+ password: "${MQTT_PASSWORD}"
+ keepalive: 60
+ reconnect_interval: 5
+
+heartbeat:
+ interval: 30 # seconds
+
+metrics:
+ enabled: true
+ interval: 300 # 5 minutes
+ collect:
+ - cpu
+ - memory
+ - disk
+ - network
+
+docker:
+ socket: "/var/run/docker.sock"
+ managed_containers:
+ - vexplor-backend
+ - vexplor-frontend
+ - vexplor-db
+```
+
+### C. 참고 자료
+
+- [EMQX Documentation](https://docs.emqx.com/)
+- [Eclipse Mosquitto](https://mosquitto.org/)
+- [AWS IoT Device Management](https://aws.amazon.com/iot-device-management/)
+- [Google Cloud IoT Core](https://cloud.google.com/iot-core)
+- [HashiCorp Nomad](https://www.nomadproject.io/)
+
diff --git a/digitalTwin/디지털트윈 아키텍쳐_v3.png b/digitalTwin/디지털트윈 아키텍쳐_v3.png
new file mode 100644
index 00000000..b72e7549
Binary files /dev/null and b/digitalTwin/디지털트윈 아키텍쳐_v3.png differ
diff --git a/digitalTwin/디지털트윈 아키텍쳐_v4.png b/digitalTwin/디지털트윈 아키텍쳐_v4.png
new file mode 100644
index 00000000..62d72b47
Binary files /dev/null and b/digitalTwin/디지털트윈 아키텍쳐_v4.png differ
diff --git a/k8s/ingress-nginx.yaml b/k8s/ingress-nginx.yaml
deleted file mode 100644
index dfb551cd..00000000
--- a/k8s/ingress-nginx.yaml
+++ /dev/null
@@ -1,41 +0,0 @@
-# Nginx Ingress Controller 설치
-# 단일 노드 클러스터용 설정 (NodePort 사용)
-#
-# 설치 명령어:
-# kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.9.5/deploy/static/provider/baremetal/deploy.yaml
-#
-# 또는 이 파일로 커스텀 설치:
-# kubectl apply -f k8s/ingress-nginx.yaml
-
-# NodePort를 80, 443으로 고정하는 패치용 설정
-apiVersion: v1
-kind: Service
-metadata:
- name: ingress-nginx-controller
- namespace: ingress-nginx
- labels:
- app.kubernetes.io/name: ingress-nginx
- app.kubernetes.io/instance: ingress-nginx
- app.kubernetes.io/component: controller
-spec:
- type: NodePort
- externalTrafficPolicy: Local
- ipFamilyPolicy: SingleStack
- ipFamilies:
- - IPv4
- ports:
- - name: http
- port: 80
- protocol: TCP
- targetPort: http
- nodePort: 30080
- - name: https
- port: 443
- protocol: TCP
- targetPort: https
- nodePort: 30443
- selector:
- app.kubernetes.io/name: ingress-nginx
- app.kubernetes.io/instance: ingress-nginx
- app.kubernetes.io/component: controller
-
diff --git a/kubernetes-setup-guide.md b/kubernetes-setup-guide.md
index 3d27b04c..8f785b46 100644
--- a/kubernetes-setup-guide.md
+++ b/kubernetes-setup-guide.md
@@ -12,29 +12,29 @@
### 기존 서버 (참조용)
-| 항목 | 값 |
-|------|-----|
-| IP | 211.115.91.170 |
-| SSH 포트 | 12991 |
-| 사용자 | geonhee |
-| OS | Ubuntu 24.04.3 LTS |
-| K8s 버전 | v1.28.0 |
-| 컨테이너 런타임 | containerd 1.7.28 |
+| 항목 | 값 |
+| --------------- | ------------------ |
+| IP | 211.115.91.170 |
+| SSH 포트 | 12991 |
+| 사용자 | geonhee |
+| OS | Ubuntu 24.04.3 LTS |
+| K8s 버전 | v1.28.0 |
+| 컨테이너 런타임 | containerd 1.7.28 |
### 새 서버 (구축 완료)
-| 항목 | 값 |
-|------|-----|
-| IP | 112.168.212.142 |
-| SSH 포트 | 22 |
-| 사용자 | wace |
-| 호스트명 | waceserver |
-| OS | Ubuntu 24.04.3 LTS |
-| K8s 버전 | v1.28.15 |
-| 컨테이너 런타임 | containerd 1.7.28 |
-| 내부 IP | 10.10.0.74 |
-| CPU | 20코어 |
-| 메모리 | 31GB |
+| 항목 | 값 |
+| --------------- | ------------------ |
+| IP | 112.168.212.142 |
+| SSH 포트 | 22 |
+| 사용자 | wace |
+| 호스트명 | waceserver |
+| OS | Ubuntu 24.04.3 LTS |
+| K8s 버전 | v1.28.15 |
+| 컨테이너 런타임 | containerd 1.7.28 |
+| 내부 IP | 10.10.0.74 |
+| CPU | 20코어 |
+| 메모리 | 31GB |
---
@@ -112,6 +112,7 @@ sudo kubeadm init --pod-network-cidr=10.244.0.0/16
```
**출력 결과 (중요 정보)**:
+
- 클러스터 초기화 성공
- API 서버: https://10.10.0.74:6443
- 워커 노드 조인 토큰 생성됨
@@ -155,9 +156,9 @@ kubectl taint nodes --all node-role.kubernetes.io/control-plane-
kubectl get nodes -o wide
```
-| NAME | STATUS | ROLES | VERSION | INTERNAL-IP | OS-IMAGE | CONTAINER-RUNTIME |
-|------|--------|-------|---------|-------------|----------|-------------------|
-| waceserver | Ready | control-plane | v1.28.15 | 10.10.0.74 | Ubuntu 24.04.3 LTS | containerd://1.7.28 |
+| NAME | STATUS | ROLES | VERSION | INTERNAL-IP | OS-IMAGE | CONTAINER-RUNTIME |
+| ---------- | ------ | ------------- | -------- | ----------- | ------------------ | ------------------- |
+| waceserver | Ready | control-plane | v1.28.15 | 10.10.0.74 | Ubuntu 24.04.3 LTS | containerd://1.7.28 |
### 시스템 Pod 상태
@@ -166,15 +167,15 @@ kubectl get pods -n kube-system
kubectl get pods -n kube-flannel
```
-| 컴포넌트 | 상태 |
-|---------|------|
-| etcd | ✅ Running |
-| kube-apiserver | ✅ Running |
+| 컴포넌트 | 상태 |
+| ----------------------- | ---------- |
+| etcd | ✅ Running |
+| kube-apiserver | ✅ Running |
| kube-controller-manager | ✅ Running |
-| kube-scheduler | ✅ Running |
-| kube-proxy | ✅ Running |
-| coredns (x2) | ✅ Running |
-| kube-flannel | ✅ Running |
+| kube-scheduler | ✅ Running |
+| kube-proxy | ✅ Running |
+| coredns (x2) | ✅ Running |
+| kube-flannel | ✅ Running |
---
@@ -188,6 +189,7 @@ kubeadm join 10.10.0.74:6443 --token 4lfga6.luad9f367uxh0rlq \
```
**토큰 만료 시 새 토큰 생성**:
+
```bash
kubeadm token create --print-join-command
```
@@ -271,11 +273,11 @@ k8s/
#### Gitea Repository Secrets 설정 필요
-| Secret 이름 | 설명 |
-|------------|------|
-| `HARBOR_USERNAME` | Harbor 사용자명 |
-| `HARBOR_PASSWORD` | Harbor 비밀번호 |
-| `KUBECONFIG` | base64 인코딩된 Kubernetes config |
+| Secret 이름 | 설명 |
+| ------------------- | --------------------------------- |
+| `HARBOR_USERNAME` | Harbor 사용자명 |
+| `HARBOR_PASSWORD` | Harbor 비밀번호 |
+| `KUBECONFIG` | base64 인코딩된 Kubernetes config |
```bash
# KUBECONFIG 생성 방법 (K8s 서버에서 실행)
@@ -301,4 +303,3 @@ ssh -p 12991 geonhee@211.115.91.170
- [Kubernetes 공식 문서](https://kubernetes.io/docs/)
- [kubeadm 설치 가이드](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/)
- [Flannel 네트워크 플러그인](https://github.com/flannel-io/flannel)
-