feat: NAS(Synology) 배포 도구 및 마이그레이션 추가
- deploy/ 폴더: docker-compose.synology.yml, deploy.sh, package.sh - NAS 배포 패키지 생성/전송/설치 자동화 스크립트 - 삭제 로그 테이블 마이그레이션 (018_add_deletion_log_table.sql) - 사진 필드 마이그레이션 유틸리티 (migrate_add_photo_fields.py) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
41
backend/migrate_add_photo_fields.py
Normal file
41
backend/migrate_add_photo_fields.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
"""
|
||||||
|
데이터베이스 마이그레이션: 사진 필드 추가
|
||||||
|
- 신고 사진 3, 4, 5 추가
|
||||||
|
- 완료 사진 2, 3, 4, 5 추가
|
||||||
|
"""
|
||||||
|
from sqlalchemy import create_engine, text
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 데이터베이스 URL
|
||||||
|
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://postgres:postgres@db:5432/issue_tracker")
|
||||||
|
|
||||||
|
def run_migration():
|
||||||
|
engine = create_engine(DATABASE_URL)
|
||||||
|
|
||||||
|
with engine.connect() as conn:
|
||||||
|
print("마이그레이션 시작...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 신고 사진 필드 추가
|
||||||
|
print("신고 사진 필드 추가 중...")
|
||||||
|
conn.execute(text("ALTER TABLE issues ADD COLUMN IF NOT EXISTS photo_path3 VARCHAR"))
|
||||||
|
conn.execute(text("ALTER TABLE issues ADD COLUMN IF NOT EXISTS photo_path4 VARCHAR"))
|
||||||
|
conn.execute(text("ALTER TABLE issues ADD COLUMN IF NOT EXISTS photo_path5 VARCHAR"))
|
||||||
|
|
||||||
|
# 완료 사진 필드 추가
|
||||||
|
print("완료 사진 필드 추가 중...")
|
||||||
|
conn.execute(text("ALTER TABLE issues ADD COLUMN IF NOT EXISTS completion_photo_path2 VARCHAR(500)"))
|
||||||
|
conn.execute(text("ALTER TABLE issues ADD COLUMN IF NOT EXISTS completion_photo_path3 VARCHAR(500)"))
|
||||||
|
conn.execute(text("ALTER TABLE issues ADD COLUMN IF NOT EXISTS completion_photo_path4 VARCHAR(500)"))
|
||||||
|
conn.execute(text("ALTER TABLE issues ADD COLUMN IF NOT EXISTS completion_photo_path5 VARCHAR(500)"))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print("✅ 마이그레이션 완료!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
conn.rollback()
|
||||||
|
print(f"❌ 마이그레이션 실패: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run_migration()
|
||||||
28
backend/migrations/018_add_deletion_log_table.sql
Normal file
28
backend/migrations/018_add_deletion_log_table.sql
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-- 삭제 로그 테이블 추가
|
||||||
|
-- 생성일: 2025-11-08
|
||||||
|
-- 설명: 부적합 등 엔티티 삭제 시 로그를 보관하기 위한 테이블
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS deletion_logs (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
entity_type VARCHAR(50) NOT NULL,
|
||||||
|
entity_id INTEGER NOT NULL,
|
||||||
|
entity_data JSONB NOT NULL,
|
||||||
|
deleted_by_id INTEGER NOT NULL REFERENCES users(id),
|
||||||
|
deleted_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT (NOW() AT TIME ZONE 'Asia/Seoul'),
|
||||||
|
reason TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 인덱스 추가
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_deletion_logs_entity_type ON deletion_logs(entity_type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_deletion_logs_entity_id ON deletion_logs(entity_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_deletion_logs_deleted_by ON deletion_logs(deleted_by_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_deletion_logs_deleted_at ON deletion_logs(deleted_at);
|
||||||
|
|
||||||
|
-- 테이블 코멘트
|
||||||
|
COMMENT ON TABLE deletion_logs IS '엔티티 삭제 로그 - 삭제된 데이터의 백업 및 추적';
|
||||||
|
COMMENT ON COLUMN deletion_logs.entity_type IS '삭제된 엔티티 타입 (issue, project, daily_work 등)';
|
||||||
|
COMMENT ON COLUMN deletion_logs.entity_id IS '삭제된 엔티티의 ID';
|
||||||
|
COMMENT ON COLUMN deletion_logs.entity_data IS '삭제 시점의 엔티티 전체 데이터 (JSON)';
|
||||||
|
COMMENT ON COLUMN deletion_logs.deleted_by_id IS '삭제 실행자 ID';
|
||||||
|
COMMENT ON COLUMN deletion_logs.deleted_at IS '삭제 시각 (KST)';
|
||||||
|
COMMENT ON COLUMN deletion_logs.reason IS '삭제 사유 (선택사항)';
|
||||||
106
deploy/deploy.sh
Executable file
106
deploy/deploy.sh
Executable file
@@ -0,0 +1,106 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# =============================================================================
|
||||||
|
# M-Project Synology NAS 배포 스크립트
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo "M-Project (부적합관리) 배포 시작"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
# 1. 환경 변수 파일 확인
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
echo "❌ .env 파일이 없습니다."
|
||||||
|
echo " .env.synology 파일을 복사하고 값을 수정하세요:"
|
||||||
|
echo " cp .env.synology .env"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 비밀번호 미설정 확인
|
||||||
|
if grep -q "변경필수" .env; then
|
||||||
|
echo "❌ .env 파일에 기본값이 남아있습니다."
|
||||||
|
echo " 모든 '변경필수' 항목을 실제 값으로 수정하세요."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Docker 이미지 빌드
|
||||||
|
echo ""
|
||||||
|
echo "🔨 Docker 이미지 빌드 중..."
|
||||||
|
docker-compose -f docker-compose.synology.yml build --no-cache
|
||||||
|
|
||||||
|
# 3. 기존 컨테이너 중지
|
||||||
|
echo ""
|
||||||
|
echo "🛑 기존 컨테이너 중지 중..."
|
||||||
|
docker-compose -f docker-compose.synology.yml down 2>/dev/null || true
|
||||||
|
|
||||||
|
# 4. 컨테이너 시작
|
||||||
|
echo ""
|
||||||
|
echo "🚀 컨테이너 시작 중..."
|
||||||
|
docker-compose -f docker-compose.synology.yml up -d
|
||||||
|
|
||||||
|
# 5. DB 초기화 대기
|
||||||
|
echo ""
|
||||||
|
echo "⏳ 데이터베이스 초기화 대기 중 (15초)..."
|
||||||
|
sleep 15
|
||||||
|
|
||||||
|
# 6. DB 마이그레이션 실행
|
||||||
|
echo ""
|
||||||
|
echo "📦 DB 마이그레이션 실행 중..."
|
||||||
|
for sql_file in ./init-db/*.sql; do
|
||||||
|
if [ -f "$sql_file" ]; then
|
||||||
|
echo " 실행: $(basename "$sql_file")"
|
||||||
|
docker exec -i m-project-db psql -U "${POSTGRES_USER:-mproject}" "${POSTGRES_DB:-mproject}" < "$sql_file" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# 7. 데이터베이스 복원 (백업 파일이 있는 경우)
|
||||||
|
BACKUP_FILE=$(ls -t backup_*.sql 2>/dev/null | head -1)
|
||||||
|
if [ -n "$BACKUP_FILE" ]; then
|
||||||
|
echo ""
|
||||||
|
read -p "📦 DB 백업 발견: $BACKUP_FILE - 복원하시겠습니까? (y/N) " -n 1 -r
|
||||||
|
echo ""
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo "📦 데이터베이스 복원 중..."
|
||||||
|
docker exec -i m-project-db psql -U "${POSTGRES_USER:-mproject}" "${POSTGRES_DB:-mproject}" < "$BACKUP_FILE"
|
||||||
|
echo "✅ 데이터베이스 복원 완료"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 8. 상태 확인
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "📊 컨테이너 상태"
|
||||||
|
echo "=========================================="
|
||||||
|
docker-compose -f docker-compose.synology.yml ps
|
||||||
|
|
||||||
|
# 9. 헬스체크
|
||||||
|
echo ""
|
||||||
|
echo "🔍 서비스 확인 중..."
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
check_service() {
|
||||||
|
local name="$1"
|
||||||
|
local url="$2"
|
||||||
|
printf " %-20s " "$name"
|
||||||
|
status=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 3 "$url" 2>/dev/null || echo "000")
|
||||||
|
if [ "$status" -ge 200 ] && [ "$status" -lt 400 ]; then
|
||||||
|
echo "✅ OK ($status)"
|
||||||
|
else
|
||||||
|
echo "❌ FAIL ($status)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_service "Backend API" "http://localhost:16000/api/health"
|
||||||
|
check_service "Frontend" "http://localhost:16080/"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "✅ 배포 완료!"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
echo "접속 URL:"
|
||||||
|
echo " - 웹 UI: http://NAS_IP:16080"
|
||||||
|
echo " - API: http://NAS_IP:16000"
|
||||||
|
echo " - DB: NAS_IP:16432 (PostgreSQL)"
|
||||||
|
echo ""
|
||||||
80
deploy/docker-compose.synology.yml
Normal file
80
deploy/docker-compose.synology.yml
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# PostgreSQL 데이터베이스
|
||||||
|
db:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
container_name: m-project-db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-mproject}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-mproject}
|
||||||
|
TZ: Asia/Seoul
|
||||||
|
PGTZ: Asia/Seoul
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./init-db:/docker-entrypoint-initdb.d
|
||||||
|
ports:
|
||||||
|
- "16432:5432"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-mproject}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- m-project-network
|
||||||
|
|
||||||
|
# FastAPI 백엔드
|
||||||
|
backend:
|
||||||
|
build: ./backend
|
||||||
|
container_name: m-project-backend
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://${POSTGRES_USER:-mproject}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-mproject}
|
||||||
|
SECRET_KEY: ${SECRET_KEY}
|
||||||
|
ALGORITHM: HS256
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES: 10080
|
||||||
|
ADMIN_USERNAME: ${ADMIN_USERNAME:-hyungi}
|
||||||
|
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
|
||||||
|
TZ: Asia/Seoul
|
||||||
|
volumes:
|
||||||
|
- uploads:/app/uploads
|
||||||
|
ports:
|
||||||
|
- "16000:8000"
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
networks:
|
||||||
|
- m-project-network
|
||||||
|
|
||||||
|
# Nginx 프론트엔드
|
||||||
|
nginx:
|
||||||
|
build: ./nginx
|
||||||
|
container_name: m-project-nginx
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "16080:80"
|
||||||
|
volumes:
|
||||||
|
- ./frontend:/usr/share/nginx/html:ro
|
||||||
|
- uploads:/app/uploads
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
networks:
|
||||||
|
- m-project-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
driver: local
|
||||||
|
uploads:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
m-project-network:
|
||||||
|
driver: bridge
|
||||||
|
name: m-project-network
|
||||||
83
deploy/package.sh
Executable file
83
deploy/package.sh
Executable file
@@ -0,0 +1,83 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# =============================================================================
|
||||||
|
# M-Project 배포 패키지 생성 스크립트
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
|
DEPLOY_DIR="$SCRIPT_DIR"
|
||||||
|
PACKAGE_DIR="$DEPLOY_DIR/mproject-package"
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo "M-Project 배포 패키지 생성"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
# 기존 패키지 삭제
|
||||||
|
rm -rf "$PACKAGE_DIR"
|
||||||
|
mkdir -p "$PACKAGE_DIR"
|
||||||
|
|
||||||
|
# 1. Docker 설정 파일
|
||||||
|
echo "📦 Docker 설정 복사..."
|
||||||
|
cp "$DEPLOY_DIR/docker-compose.synology.yml" "$PACKAGE_DIR/docker-compose.yml"
|
||||||
|
cp "$DEPLOY_DIR/.env.synology" "$PACKAGE_DIR/.env.example"
|
||||||
|
cp "$DEPLOY_DIR/deploy.sh" "$PACKAGE_DIR/"
|
||||||
|
chmod +x "$PACKAGE_DIR/deploy.sh"
|
||||||
|
|
||||||
|
# 2. 데이터베이스 백업 생성
|
||||||
|
echo "📦 DB 백업 시도..."
|
||||||
|
if docker exec m-project-db pg_dump -U mproject mproject > "$PACKAGE_DIR/backup_$(date +%Y%m%d_%H%M%S).sql" 2>/dev/null; then
|
||||||
|
echo " ✅ DB 백업 완료"
|
||||||
|
else
|
||||||
|
echo " ⚠️ DB 백업 건너뜀 (컨테이너 미실행)"
|
||||||
|
rm -f "$PACKAGE_DIR"/backup_*.sql
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. 소스 코드 복사
|
||||||
|
echo "📦 소스 코드 복사..."
|
||||||
|
|
||||||
|
# Backend
|
||||||
|
mkdir -p "$PACKAGE_DIR/backend"
|
||||||
|
rsync -a --exclude='__pycache__' --exclude='.git' --exclude='venv' --exclude='*.pyc' \
|
||||||
|
"$PROJECT_DIR/backend/" "$PACKAGE_DIR/backend/"
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
mkdir -p "$PACKAGE_DIR/frontend"
|
||||||
|
rsync -a --exclude='.git' --exclude='uploads' \
|
||||||
|
"$PROJECT_DIR/frontend/" "$PACKAGE_DIR/frontend/"
|
||||||
|
|
||||||
|
# Nginx
|
||||||
|
mkdir -p "$PACKAGE_DIR/nginx"
|
||||||
|
rsync -a "$PROJECT_DIR/nginx/" "$PACKAGE_DIR/nginx/"
|
||||||
|
|
||||||
|
# 4. init-db 폴더 (마이그레이션 스크립트)
|
||||||
|
mkdir -p "$PACKAGE_DIR/init-db"
|
||||||
|
if [ -d "$PROJECT_DIR/backend/migrations" ]; then
|
||||||
|
cp "$PROJECT_DIR/backend/migrations"/*.sql "$PACKAGE_DIR/init-db/" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5. 압축
|
||||||
|
echo "📦 압축 중..."
|
||||||
|
cd "$DEPLOY_DIR"
|
||||||
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||||
|
tar -czf "mproject-deploy-$TIMESTAMP.tar.gz" -C "$DEPLOY_DIR" mproject-package
|
||||||
|
|
||||||
|
# 크기 확인
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "✅ 패키지 생성 완료!"
|
||||||
|
echo "=========================================="
|
||||||
|
ls -lh "$DEPLOY_DIR/mproject-deploy-$TIMESTAMP.tar.gz"
|
||||||
|
echo ""
|
||||||
|
echo "파일: $DEPLOY_DIR/mproject-deploy-$TIMESTAMP.tar.gz"
|
||||||
|
echo ""
|
||||||
|
echo "Synology NAS로 전송:"
|
||||||
|
echo " scp $DEPLOY_DIR/mproject-deploy-$TIMESTAMP.tar.gz admin@NAS_IP:/volume1/docker/"
|
||||||
|
echo ""
|
||||||
|
echo "NAS에서 설치:"
|
||||||
|
echo " cd /volume1/docker/"
|
||||||
|
echo " tar -xzf mproject-deploy-$TIMESTAMP.tar.gz"
|
||||||
|
echo " cd mproject-package"
|
||||||
|
echo " cp .env.example .env && vi .env"
|
||||||
|
echo " bash deploy.sh"
|
||||||
|
echo ""
|
||||||
Reference in New Issue
Block a user