From eebeaf1008b33b3f2ea81669ea2e20d07dcda99a Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Mon, 9 Feb 2026 14:40:38 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20NAS(Synology)=20=EB=B0=B0=ED=8F=AC=20?= =?UTF-8?q?=EB=8F=84=EA=B5=AC=20=EB=B0=8F=20=EB=A7=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- backend/migrate_add_photo_fields.py | 41 +++++++ .../migrations/018_add_deletion_log_table.sql | 28 +++++ deploy/deploy.sh | 106 ++++++++++++++++++ deploy/docker-compose.synology.yml | 80 +++++++++++++ deploy/package.sh | 83 ++++++++++++++ 5 files changed, 338 insertions(+) create mode 100644 backend/migrate_add_photo_fields.py create mode 100644 backend/migrations/018_add_deletion_log_table.sql create mode 100755 deploy/deploy.sh create mode 100644 deploy/docker-compose.synology.yml create mode 100755 deploy/package.sh diff --git a/backend/migrate_add_photo_fields.py b/backend/migrate_add_photo_fields.py new file mode 100644 index 0000000..98ba22c --- /dev/null +++ b/backend/migrate_add_photo_fields.py @@ -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() diff --git a/backend/migrations/018_add_deletion_log_table.sql b/backend/migrations/018_add_deletion_log_table.sql new file mode 100644 index 0000000..12fbdbb --- /dev/null +++ b/backend/migrations/018_add_deletion_log_table.sql @@ -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 '삭제 사유 (선택사항)'; diff --git a/deploy/deploy.sh b/deploy/deploy.sh new file mode 100755 index 0000000..5fa2249 --- /dev/null +++ b/deploy/deploy.sh @@ -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 "" diff --git a/deploy/docker-compose.synology.yml b/deploy/docker-compose.synology.yml new file mode 100644 index 0000000..dc9c5f3 --- /dev/null +++ b/deploy/docker-compose.synology.yml @@ -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 diff --git a/deploy/package.sh b/deploy/package.sh new file mode 100755 index 0000000..16cdf46 --- /dev/null +++ b/deploy/package.sh @@ -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 ""