Files
myhome-server/home_management_db_spec.md
Hyungi Ahn 4b77086bb2 초기 홈 관리 시스템 API 구현
- Express.js 기반 백엔드 API 서버
- MariaDB, Redis, phpMyAdmin Docker 환경
- Device 관리 기본 CRUD 구현
- Mac Mini M4 Pro 전용 설정 및 배포 스크립트
- 자동화된 설치 및 배포 시스템
- 완전한 문서화 및 실행 가이드
2025-07-30 14:12:09 +09:00

50 KiB

홈 관리 시스템 DB 구축 계획서

🎯 프로젝트 개요

Git 저장소

목표

  • 홈 IoT 기기 모니터링 및 관리
  • 전력 소비, 네트워크 트래픽 추적
  • 시스템 리소스 모니터링
  • 향후 개인 서비스 확장 기반 마련

기술 스택

Backend: Express.js (Node.js)
Database: MariaDB 11.x
Admin Tool: phpMyAdmin
Architecture: MVC + Service Layer
Code Style: 모듈형 구조 (500자 이하 함수)

🗄️ 데이터베이스 설계

데이터베이스 생성

CREATE DATABASE home_management 
CHARACTER SET utf8mb4 
COLLATE utf8mb4_unicode_ci;

CREATE USER 'homeuser'@'localhost' IDENTIFIED BY 'secure_password';
GRANT ALL PRIVILEGES ON home_management.* TO 'homeuser'@'localhost';
FLUSH PRIVILEGES;

테이블 설계

1. 디바이스 관리 (devices)

CREATE TABLE devices (
    id INT AUTO_INCREMENT PRIMARY KEY,
    device_id VARCHAR(50) UNIQUE NOT NULL,
    name VARCHAR(100) NOT NULL,
    device_type ENUM('server', 'nas', 'router', 'smart_plug', 'other') NOT NULL,
    location VARCHAR(50),
    ip_address VARCHAR(45),
    mac_address VARCHAR(17),
    power_rating_watts INT,
    monitoring_enabled BOOLEAN DEFAULT TRUE,
    metadata JSON,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_device_id (device_id),
    INDEX idx_type_enabled (device_type, monitoring_enabled)
) ENGINE=InnoDB;

2. 전력 소비 데이터 (power_consumption)

CREATE TABLE power_consumption (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    device_id VARCHAR(50) NOT NULL,
    timestamp TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3),
    watts DECIMAL(8,2) NOT NULL,
    voltage DECIMAL(6,2),
    current DECIMAL(6,3),
    kwh_total DECIMAL(10,4),
    metadata JSON,
    INDEX idx_device_time (device_id, timestamp),
    INDEX idx_timestamp (timestamp),
    INDEX idx_watts (watts),
    FOREIGN KEY (device_id) REFERENCES devices(device_id) ON DELETE CASCADE
) ENGINE=InnoDB
PARTITION BY RANGE (UNIX_TIMESTAMP(timestamp)) (
    PARTITION p_2025_q1 VALUES LESS THAN (UNIX_TIMESTAMP('2025-04-01')),
    PARTITION p_2025_q2 VALUES LESS THAN (UNIX_TIMESTAMP('2025-07-01')),
    PARTITION p_2025_q3 VALUES LESS THAN (UNIX_TIMESTAMP('2025-10-01')),
    PARTITION p_2025_q4 VALUES LESS THAN (UNIX_TIMESTAMP('2026-01-01')),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);

3. 네트워크 트래픽 (network_traffic)

CREATE TABLE network_traffic (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    device_mac VARCHAR(17) NOT NULL,
    device_name VARCHAR(100),
    timestamp TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3),
    bytes_in BIGINT UNSIGNED NOT NULL,
    bytes_out BIGINT UNSIGNED NOT NULL,
    packets_in INT UNSIGNED,
    packets_out INT UNSIGNED,
    connection_count INT UNSIGNED,
    metadata JSON,
    INDEX idx_mac_time (device_mac, timestamp),
    INDEX idx_timestamp (timestamp)
) ENGINE=InnoDB;

4. 시스템 리소스 (system_resources)

CREATE TABLE system_resources (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    server_name VARCHAR(50) NOT NULL,
    timestamp TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3),
    cpu_percent DECIMAL(5,2),
    memory_used_gb DECIMAL(6,2),
    memory_total_gb DECIMAL(6,2),
    disk_used_gb DECIMAL(8,2),
    disk_total_gb DECIMAL(8,2),
    network_io JSON,
    temperature DECIMAL(4,1),
    load_average JSON,
    processes_count INT,
    uptime_seconds BIGINT,
    INDEX idx_server_time (server_name, timestamp)
) ENGINE=InnoDB;

5. 서비스 상태 (service_status)

CREATE TABLE service_status (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    service_name VARCHAR(50) NOT NULL,
    server_name VARCHAR(50) NOT NULL,
    timestamp TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3),
    status ENUM('online', 'offline', 'degraded', 'maintenance') NOT NULL,
    response_time_ms INT UNSIGNED,
    cpu_usage DECIMAL(5,2),
    memory_usage_mb INT,
    error_message TEXT,
    metadata JSON,
    INDEX idx_service_time (service_name, timestamp),
    INDEX idx_server_service (server_name, service_name),
    INDEX idx_status (status, timestamp)
) ENGINE=InnoDB;

6. 사용자 관리 (users)

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(100) UNIQUE,
    password_hash VARCHAR(255),
    full_name VARCHAR(100),
    role ENUM('admin', 'family', 'guest') DEFAULT 'family',
    preferences JSON,
    last_login TIMESTAMP NULL,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_username (username),
    INDEX idx_email (email),
    INDEX idx_role (role)
) ENGINE=InnoDB;

7. 알림 규칙 (alert_rules)

CREATE TABLE alert_rules (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    description TEXT,
    condition_type ENUM('threshold', 'change', 'pattern', 'custom') NOT NULL,
    target_table VARCHAR(50) NOT NULL,
    target_field VARCHAR(50) NOT NULL,
    operator ENUM('>', '<', '>=', '<=', '=', '!=', 'contains') NOT NULL,
    threshold_value DECIMAL(15,4),
    time_window_minutes INT DEFAULT 5,
    severity ENUM('info', 'warning', 'critical') DEFAULT 'warning',
    notification_methods JSON,
    is_active BOOLEAN DEFAULT TRUE,
    cooldown_minutes INT DEFAULT 60,
    last_triggered TIMESTAMP NULL,
    created_by INT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (created_by) REFERENCES users(id),
    INDEX idx_active (is_active),
    INDEX idx_target (target_table, target_field)
) ENGINE=InnoDB;

8. 알림 로그 (alert_logs)

CREATE TABLE alert_logs (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    alert_rule_id INT NOT NULL,
    triggered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    trigger_value DECIMAL(15,4),
    message TEXT,
    severity ENUM('info', 'warning', 'critical') NOT NULL,
    notification_sent BOOLEAN DEFAULT FALSE,
    resolved_at TIMESTAMP NULL,
    metadata JSON,
    FOREIGN KEY (alert_rule_id) REFERENCES alert_rules(id) ON DELETE CASCADE,
    INDEX idx_rule_time (alert_rule_id, triggered_at),
    INDEX idx_severity (severity, triggered_at)
) ENGINE=InnoDB;

🚀 Express.js 프로젝트 구조

폴더 구조

home-management-api/
├── src/
│   ├── config/
│   │   ├── database.js          # DB 연결 설정
│   │   ├── redis.js             # Redis 캐시 설정
│   │   └── environment.js       # 환경변수 관리
│   ├── models/
│   │   ├── index.js             # 모델 인덱스
│   │   ├── Device.js            # 디바이스 모델
│   │   ├── PowerConsumption.js  # 전력 소비 모델
│   │   ├── NetworkTraffic.js    # 네트워크 트래픽 모델
│   │   ├── SystemResource.js    # 시스템 리소스 모델
│   │   ├── ServiceStatus.js     # 서비스 상태 모델
│   │   ├── User.js              # 사용자 모델
│   │   ├── AlertRule.js         # 알림 규칙 모델
│   │   └── AlertLog.js          # 알림 로그 모델
│   ├── controllers/
│   │   ├── deviceController.js      # 디바이스 CRUD
│   │   ├── powerController.js       # 전력 데이터 처리
│   │   ├── networkController.js     # 네트워크 데이터 처리
│   │   ├── systemController.js      # 시스템 모니터링
│   │   ├── dashboardController.js   # 대시보드 데이터
│   │   ├── authController.js        # 인증/권한
│   │   └── alertController.js       # 알림 관리
│   ├── services/
│   │   ├── deviceService.js         # 디바이스 비즈니스 로직
│   │   ├── powerService.js          # 전력 분석 서비스
│   │   ├── networkService.js        # 네트워크 분석 서비스
│   │   ├── statisticsService.js     # 통계 계산 서비스
│   │   ├── alertService.js          # 알림 처리 서비스
│   │   ├── cacheService.js          # 캐시 관리 서비스
│   │   └── schedulerService.js      # 스케줄 작업 서비스
│   ├── routes/
│   │   ├── index.js             # 라우터 인덱스
│   │   ├── devices.js           # 디바이스 라우터
│   │   ├── power.js             # 전력 라우터
│   │   ├── network.js           # 네트워크 라우터
│   │   ├── system.js            # 시스템 라우터
│   │   ├── dashboard.js         # 대시보드 라우터
│   │   ├── auth.js              # 인증 라우터
│   │   └── alerts.js            # 알림 라우터
│   ├── middleware/
│   │   ├── auth.js              # 인증 미들웨어
│   │   ├── validation.js        # 데이터 검증
│   │   ├── rateLimit.js         # 요청 제한
│   │   ├── errorHandler.js      # 에러 처리
│   │   ├── logger.js            # 로깅 미들웨어
│   │   └── cors.js              # CORS 설정
│   ├── utils/
│   │   ├── logger.js            # 로깅 유틸
│   │   ├── validation.js        # 검증 함수들
│   │   ├── dateUtils.js         # 날짜 처리 유틸
│   │   ├── mathUtils.js         # 수학 계산 유틸
│   │   ├── encryption.js        # 암호화 유틸
│   │   └── responseUtils.js     # 응답 포매팅
│   ├── collectors/
│   │   ├── powerCollector.js    # 전력 데이터 수집
│   │   ├── networkCollector.js  # 네트워크 데이터 수집
│   │   ├── systemCollector.js   # 시스템 데이터 수집
│   │   └── serviceCollector.js  # 서비스 상태 수집
│   └── app.js                   # Express 앱 설정
├── tests/
│   ├── unit/                    # 단위 테스트
│   ├── integration/             # 통합 테스트
│   └── fixtures/                # 테스트 데이터
├── docs/
│   ├── api/                     # API 문서
│   └── database/                # DB 스키마 문서
├── scripts/
│   ├── setup-db.sql             # 초기 DB 설정
│   ├── seed-data.sql            # 샘플 데이터
│   └── migrate.js               # 마이그레이션 스크립트
├── docker-compose.yml           # Docker 구성
├── package.json
├── .env.example
└── README.md

📦 Package.json 구성

주요 의존성

{
  "name": "home-management-api",
  "version": "1.0.0",
  "description": "홈 관리 시스템 백엔드 API",
  "main": "src/app.js",
  "scripts": {
    "start": "node src/app.js",
    "dev": "nodemon src/app.js",
    "test": "jest",
    "test:watch": "jest --watch",
    "db:migrate": "node scripts/migrate.js",
    "db:seed": "node scripts/seed.js",
    "lint": "eslint src/",
    "lint:fix": "eslint src/ --fix"
  },
  "dependencies": {
    "express": "^4.18.2",
    "mysql2": "^3.6.0",
    "sequelize": "^6.32.1",
    "redis": "^4.6.7",
    "bcryptjs": "^2.4.3",
    "jsonwebtoken": "^9.0.2",
    "joi": "^17.9.2",
    "helmet": "^7.0.0",
    "cors": "^2.8.5",
    "compression": "^1.7.4",
    "express-rate-limit": "^6.8.1",
    "winston": "^3.10.0",
    "node-cron": "^3.0.2",
    "dotenv": "^16.3.1",
    "express-validator": "^7.0.1",
    "moment": "^2.29.4"
  },
  "devDependencies": {
    "nodemon": "^3.0.1",
    "jest": "^29.6.1",
    "supertest": "^6.3.3",
    "eslint": "^8.45.0",
    "prettier": "^3.0.0"
  }
}

🐳 Docker Compose 구성

docker-compose.yml

version: '3.8'

services:
  mariadb:
    image: mariadb:11-jammy
    container_name: home_mariadb
    environment:
      MYSQL_ROOT_PASSWORD: root_password
      MYSQL_DATABASE: home_management
      MYSQL_USER: homeuser
      MYSQL_PASSWORD: home_password
    ports:
      - "3306:3306"
    volumes:
      - mariadb_data:/var/lib/mysql
      - ./scripts/setup-db.sql:/docker-entrypoint-initdb.d/setup.sql
      - ./config/mariadb.cnf:/etc/mysql/conf.d/custom.cnf
    command: >
      --character-set-server=utf8mb4
      --collation-server=utf8mb4_unicode_ci
      --innodb-buffer-pool-size=2G
      --innodb-log-file-size=256M
      --max-connections=200
      --query-cache-size=256M
    restart: unless-stopped
    networks:
      - home_network

  phpmyadmin:
    image: phpmyadmin/phpmyadmin:latest
    container_name: home_phpmyadmin
    environment:
      PMA_HOST: mariadb
      PMA_PORT: 3306
      PMA_USER: homeuser
      PMA_PASSWORD: home_password
      UPLOAD_LIMIT: 2G
      MEMORY_LIMIT: 512M
    ports:
      - "8080:80"
    depends_on:
      - mariadb
    restart: unless-stopped
    networks:
      - home_network

  redis:
    image: redis:7-alpine
    container_name: home_redis
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
      - ./config/redis.conf:/usr/local/etc/redis/redis.conf
    command: redis-server /usr/local/etc/redis/redis.conf
    restart: unless-stopped
    networks:
      - home_network

  api:
    build: .
    container_name: home_api
    environment:
      NODE_ENV: development
      DB_HOST: mariadb
      DB_PORT: 3306
      DB_NAME: home_management
      DB_USER: homeuser
      DB_PASSWORD: home_password
      REDIS_HOST: redis
      REDIS_PORT: 6379
      JWT_SECRET: your-jwt-secret-key
      API_PORT: 3000
    ports:
      - "3000:3000"
    volumes:
      - ./src:/app/src
      - ./logs:/app/logs
    depends_on:
      - mariadb
      - redis
    restart: unless-stopped
    networks:
      - home_network

volumes:
  mariadb_data:
  redis_data:

networks:
  home_network:
    driver: bridge

🔧 환경 설정

.env.example

# 서버 설정
NODE_ENV=development
API_PORT=3000
API_HOST=0.0.0.0

# 데이터베이스 설정
DB_HOST=localhost
DB_PORT=3306
DB_NAME=home_management
DB_USER=homeuser
DB_PASSWORD=home_password
DB_POOL_MIN=5
DB_POOL_MAX=20
DB_TIMEOUT=30000

# Redis 설정
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
REDIS_CACHE_TTL=3600

# JWT 설정
JWT_SECRET=your-super-secret-jwt-key
JWT_EXPIRES_IN=24h
JWT_REFRESH_EXPIRES_IN=7d

# 로깅 설정
LOG_LEVEL=info
LOG_FILE_PATH=./logs/app.log
LOG_MAX_SIZE=10m
LOG_MAX_FILES=5

# 알림 설정
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASSWORD=your-app-password
EMAIL_FROM=Home Management System <noreply@home.local>

# 데이터 수집 설정
POWER_COLLECTION_INTERVAL=300000  # 5분
NETWORK_COLLECTION_INTERVAL=60000 # 1분
SYSTEM_COLLECTION_INTERVAL=30000  # 30초

# API 제한 설정
RATE_LIMIT_WINDOW=900000  # 15분
RATE_LIMIT_MAX_REQUESTS=100

# 보안 설정
BCRYPT_ROUNDS=12
CORS_ORIGIN=http://localhost:3001,https://yourdomain.com

📊 MariaDB 최적화 설정

config/mariadb.cnf

[mariadb]
# 기본 설정
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
init-connect = 'SET NAMES utf8mb4'

# 메모리 최적화 (Mac Mini M4 Pro 64GB 기준)
innodb_buffer_pool_size = 4G
innodb_log_buffer_size = 64M
innodb_log_file_size = 512M
key_buffer_size = 256M
sort_buffer_size = 4M
read_buffer_size = 2M
read_rnd_buffer_size = 8M
thread_cache_size = 50
table_open_cache = 4000

# 연결 설정
max_connections = 200
max_user_connections = 180
wait_timeout = 600
interactive_timeout = 600

# 쿼리 캐시
query_cache_type = 1
query_cache_size = 256M
query_cache_limit = 2M

# InnoDB 최적화
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
innodb_file_per_table = 1
innodb_io_capacity = 2000
innodb_io_capacity_max = 4000
innodb_read_io_threads = 4
innodb_write_io_threads = 4

# 시계열 데이터 최적화
innodb_compression_default = ON
innodb_page_compression = ON
innodb_adaptive_hash_index = ON

# 로깅
general_log = OFF
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2

# 바이너리 로그 (백업/복제용)
log_bin = mysql-bin
binlog_format = ROW
expire_logs_days = 7
max_binlog_size = 100M

🔍 초기 데이터 및 인덱스

scripts/setup-db.sql

-- 기본 관리자 사용자 생성
INSERT INTO users (username, email, password_hash, full_name, role) VALUES
('admin', 'admin@home.local', '$2b$12$encrypted_password_hash', '시스템 관리자', 'admin'),
('family', 'family@home.local', '$2b$12$encrypted_password_hash', '가족 사용자', 'family');

-- 기본 디바이스 등록
INSERT INTO devices (device_id, name, device_type, location, monitoring_enabled) VALUES
('mac_mini_m4', 'Mac Mini M4 Pro', 'server', '서재', TRUE),
('ds1525plus', 'Synology DS1525+', 'nas', '서재', TRUE),
('rt6600ax', 'Synology RT6600ax', 'router', '거실', TRUE);

-- 기본 알림 규칙
INSERT INTO alert_rules (name, description, condition_type, target_table, target_field, operator, threshold_value, severity, notification_methods, created_by) VALUES
('높은 CPU 사용률', 'CPU 사용률이 80% 이상일 때 알림', 'threshold', 'system_resources', 'cpu_percent', '>', 80.0, 'warning', '["email"]', 1),
('높은 전력 소비', '전력 소비가 평소보다 50% 이상 증가했을 때', 'change', 'power_consumption', 'watts', '>', 50.0, 'warning', '["email"]', 1),
('서비스 다운', '서비스가 오프라인 상태일 때', 'threshold', 'service_status', 'status', '=', 'offline', 'critical', '["email", "push"]', 1);

-- 파티션 관리 프로시저
DELIMITER //
CREATE PROCEDURE CreateMonthlyPartitions()
BEGIN
    DECLARE partition_date DATE;
    DECLARE partition_name VARCHAR(20);
    DECLARE partition_value BIGINT;
    
    SET partition_date = DATE_ADD(NOW(), INTERVAL 3 MONTH);
    SET partition_name = CONCAT('p_', DATE_FORMAT(partition_date, '%Y_%m'));
    SET partition_value = UNIX_TIMESTAMP(DATE_ADD(partition_date, INTERVAL 1 MONTH));
    
    SET @sql = CONCAT(
        'ALTER TABLE power_consumption ADD PARTITION (',
        'PARTITION ', partition_name, 
        ' VALUES LESS THAN (', partition_value, '))'
    );
    
    PREPARE stmt FROM @sql;
    EXECUTE stmt;
    DEALLOCATE PREPARE stmt;
END //
DELIMITER ;

-- 월별 파티션 자동 생성 이벤트
CREATE EVENT monthly_partition_maintenance
ON SCHEDULE EVERY 1 MONTH
STARTS CURRENT_TIMESTAMP
DO CALL CreateMonthlyPartitions();

-- 성능 모니터링용 뷰
CREATE VIEW device_power_summary AS
SELECT 
    d.device_id,
    d.name,
    d.device_type,
    AVG(pc.watts) as avg_watts,
    MAX(pc.watts) as max_watts,
    COUNT(*) as reading_count,
    MAX(pc.timestamp) as last_reading
FROM devices d
LEFT JOIN power_consumption pc ON d.device_id = pc.device_id
WHERE d.monitoring_enabled = TRUE
GROUP BY d.device_id, d.name, d.device_type;

CREATE VIEW system_health_summary AS
SELECT 
    server_name,
    AVG(cpu_percent) as avg_cpu,
    AVG(memory_used_gb / memory_total_gb * 100) as avg_memory_pct,
    AVG(disk_used_gb / disk_total_gb * 100) as avg_disk_pct,
    MAX(timestamp) as last_update
FROM system_resources
WHERE timestamp >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
GROUP BY server_name;

🔄 CI/CD 파이프라인 구성

개발 환경 구성

MacBook Pro M3 Pro (개발/빌드)
Mac Mini M4 Pro (개발/빌드)
    ↓ git push
Gitea Server (git.hyungi.net - DS1525+ Container)
    ↓ webhook
개발 머신 (MacBook Pro / Mac Mini)
    ↓ docker build & test
Synology DS1525+ (프로덕션 배포)

개발 환경 특징

  • MacBook Pro M3 Pro: 이동성이 필요한 개발 작업
  • Mac Mini M4 Pro: 고정된 개발 환경, 장시간 빌드/테스트
  • 공통 환경: 동일한 ARM64 아키텍처로 일관된 빌드 환경
  • 유연한 작업: 두 머신 모두에서 완전한 개발 사이클 가능

CI/CD 워크플로우

1. Gitea Actions 설정 (.gitea/workflows/deploy.yml)

name: Home Management CI/CD

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: self-hosted
    container:
      image: node:18-alpine
      options: --network host
    
    services:
      mariadb:
        image: mariadb:11-jammy
        env:
          MARIADB_ROOT_PASSWORD: test_password
          MARIADB_DATABASE: home_management_test
          MARIADB_USER: testuser
          MARIADB_PASSWORD: test_password
        ports:
          - 3307:3306
        options: >-
          --health-cmd="mariadb-admin ping -h localhost"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=5

    steps:
    - name: Checkout code
      uses: actions/checkout@v4
    
    - name: Cache Node modules
      uses: actions/cache@v3
      with:
        path: ~/.npm
        key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
        restore-keys: |
          ${{ runner.os }}-node-
    
    - name: Install dependencies
      run: npm ci
    
    - name: Wait for MariaDB
      run: |
        for i in {1..30}; do
          if nc -z localhost 3307; then break; fi
          echo "Waiting for MariaDB... ($i/30)"
          sleep 2
        done
    
    - name: Run database migrations
      run: npm run db:migrate
      env:
        NODE_ENV: test
        DB_HOST: localhost
        DB_PORT: 3307
        DB_NAME: home_management_test
        DB_USER: testuser
        DB_PASSWORD: test_password
    
    - name: Run unit tests
      run: npm run test:unit
      env:
        NODE_ENV: test
        DB_HOST: localhost
        DB_PORT: 3307
    
    - name: Run integration tests
      run: npm run test:integration
      env:
        NODE_ENV: test
        DB_HOST: localhost
        DB_PORT: 3307
    
    - name: Generate coverage report
      run: npm run test:coverage
    
    - name: Lint check
      run: npm run lint
    
    - name: Security audit
      run: npm audit --audit-level high

  build:
    needs: test
    runs-on: self-hosted
    if: gitea.ref == 'refs/heads/main'
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
    
    - name: Set build info
      run: |
        echo "BUILD_TIME=$(date -Iseconds)" >> $GITHUB_ENV
        echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
        echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
    
    - name: Build Docker image
      run: |
        docker build \
          --build-arg BUILD_TIME=$BUILD_TIME \
          --build-arg COMMIT_HASH=$COMMIT_HASH \
          --build-arg BRANCH_NAME=$BRANCH_NAME \
          -t home-management-api:$COMMIT_HASH \
          -t home-management-api:latest .
    
    - name: Run container security scan
      run: |
        docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
          aquasec/trivy image home-management-api:latest
    
    - name: Save Docker image
      run: |
        docker save home-management-api:latest | gzip > home-management-api.tar.gz
    
    - name: Record deployment start
      run: |
        curl -X POST http://localhost:3000/api/deployments/start \
             -H "Content-Type: application/json" \
             -d '{
                 "deployment_id": "${{ gitea.run_id }}",
                 "commit_hash": "${{ env.COMMIT_HASH }}",
                 "branch": "${{ env.BRANCH_NAME }}",
                 "stage": "build"
             }'
    
    - name: Deploy to NAS
      run: |
        scp -o StrictHostKeyChecking=no \
            home-management-api.tar.gz \
            admin@ds1525plus:/volume1/docker/images/
        
        ssh -o StrictHostKeyChecking=no admin@ds1525plus \
            "cd /volume1/docker && ./deploy-api.sh $COMMIT_HASH ${{ gitea.run_id }}"
    
    - name: Record deployment result
      if: always()
      run: |
        STATUS=${{ job.status == 'success' && 'success' || 'failed' }}
        curl -X POST http://localhost:3000/api/deployments/complete \
             -H "Content-Type: application/json" \
             -d '{
                 "deployment_id": "${{ gitea.run_id }}",
                 "status": "'$STATUS'",
                 "stage": "deploy"
             }'

2. Gitea Webhook 설정

# Gitea 서버에서 webhook 설정
# git.hyungi.net > myhome-server > Settings > Webhooks > Add Webhook

Payload URL: http://developer-machine:9000/webhook  # MacBook Pro 또는 Mac Mini IP
Content Type: application/json
Secret: your-webhook-secret
Events: Push events, Pull request events

# 개발 머신에서 webhook 리스너 설정 (webhook.js)
const express = require('express');
const crypto = require('crypto');
const { execSync } = require('child_process');

const app = express();
app.use(express.json());

const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
const GITEA_ACTIONS_PATH = '/Users/admin/gitea-actions';

function verifySignature(payload, signature) {
    const computedSignature = crypto
        .createHmac('sha256', WEBHOOK_SECRET)
        .update(payload)
        .digest('hex');
    return crypto.timingSafeEqual(
        Buffer.from(`sha256=${computedSignature}`),
        Buffer.from(signature)
    );
}

app.post('/webhook', (req, res) => {
    const signature = req.headers['x-gitea-signature'];
    const payload = JSON.stringify(req.body);
    
    if (!verifySignature(payload, signature)) {
        return res.status(401).send('Unauthorized');
    }
    
    const { repository, ref, commits } = req.body;
    
    if (ref === 'refs/heads/main' || ref === 'refs/heads/develop') {
        console.log(`Received push to ${ref} in ${repository.full_name}`);
        
        try {
            // Gitea Actions Runner 트리거
            execSync(`cd ${GITEA_ACTIONS_PATH} && ./act_runner exec`, 
                { stdio: 'inherit' });
            res.status(200).send('Workflow triggered');
        } catch (error) {
            console.error('Failed to trigger workflow:', error);
            res.status(500).send('Workflow trigger failed');
        }
    } else {
        res.status(200).send('No action needed');
    }
});

app.listen(9000, () => {
    console.log('Webhook server listening on port 9000');
});

2. DS1525+ 배포 스크립트 (/volume1/docker/deploy-api.sh)

#!/bin/bash

# 배포 설정
IMAGE_NAME="home-management-api"
CONTAINER_NAME="home-api"
BACKUP_DIR="/volume1/backups/deployments"
LOG_FILE="/volume1/logs/deploy.log"

echo "$(date): Starting deployment..." >> $LOG_FILE

# 1. 기존 컨테이너 백업
if [ "$(docker ps -q -f name=$CONTAINER_NAME)" ]; then
    echo "Creating backup of current deployment..." >> $LOG_FILE
    docker commit $CONTAINER_NAME $IMAGE_NAME:backup-$(date +%Y%m%d-%H%M%S)
    docker stop $CONTAINER_NAME
    docker rm $CONTAINER_NAME
fi

# 2. 새 이미지 로드
echo "Loading new Docker image..." >> $LOG_FILE
gunzip -c /volume1/docker/images/home-management-api.tar.gz | docker load

# 3. 데이터베이스 마이그레이션 체크
echo "Checking database migrations..." >> $LOG_FILE
docker run --rm --network=host -e NODE_ENV=production $IMAGE_NAME:latest npm run db:migrate:check

# 4. 새 컨테이너 시작
echo "Starting new container..." >> $LOG_FILE
docker run -d \
  --name $CONTAINER_NAME \
  --network=host \
  --restart=unless-stopped \
  -v /volume1/docker/config/api.env:/app/.env \
  -v /volume1/logs/api:/app/logs \
  -v /volume1/data/uploads:/app/uploads \
  $IMAGE_NAME:latest

# 5. 헬스체크
echo "Performing health check..." >> $LOG_FILE
sleep 10
for i in {1..30}; do
    if curl -f http://localhost:3000/health > /dev/null 2>&1; then
        echo "$(date): Deployment successful!" >> $LOG_FILE
        # 이전 백업 이미지 정리 (7일 이상 된 것)
        docker images | grep "$IMAGE_NAME:backup" | awk '{print $2}' | tail -n +8 | xargs -I {} docker rmi $IMAGE_NAME:{}
        exit 0
    fi
    echo "Waiting for service to start... ($i/30)" >> $LOG_FILE
    sleep 2
done

echo "$(date): Deployment failed! Rolling back..." >> $LOG_FILE
# 롤백 로직
LATEST_BACKUP=$(docker images | grep "$IMAGE_NAME:backup" | head -1 | awk '{print $2}')
if [ ! -z "$LATEST_BACKUP" ]; then
    docker stop $CONTAINER_NAME 2>/dev/null
    docker rm $CONTAINER_NAME 2>/dev/null
    docker run -d --name $CONTAINER_NAME --network=host --restart=unless-stopped $IMAGE_NAME:backup-$LATEST_BACKUP
fi
exit 1

3. Gitea Actions Runner 설정 (개발 머신)

# Gitea Actions Runner 설치 및 설정 (MacBook Pro / Mac Mini 공통)
cd /Users/$(whoami)
wget https://gitea.com/gitea/act_runner/releases/download/v0.2.6/act_runner-0.2.6-darwin-arm64
chmod +x act_runner-0.2.6-darwin-arm64
sudo mv act_runner-0.2.6-darwin-arm64 /usr/local/bin/act_runner

# Runner 등록 (Gitea에서 토큰 생성 후)
act_runner register \
    --instance https://git.hyungi.net \
    --token YOUR_RUNNER_TOKEN \
    --name $(hostname)-runner \
    --labels self-hosted,macOS,ARM64

# 설정 파일 생성 (.runner 파일이 생성됨)
# 데몬으로 실행하기 위한 LaunchDaemon 설정
sudo tee /Library/LaunchDaemons/com.gitea.act_runner.plist << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.gitea.act_runner</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/act_runner</string>
        <string>daemon</string>
        <string>--config</string>
        <string>/Users/$(whoami)/.runner</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>StandardOutPath</key>
    <string>/usr/local/var/log/act_runner.log</string>
    <key>StandardErrorPath</key>
    <string>/usr/local/var/log/act_runner.error.log</string>
    <key>WorkingDirectory</key>
    <string>/Users/$(whoami)</string>
    <key>UserName</key>
    <string>$(whoami)</string>
</dict>
</plist>
EOF

# 서비스 시작
sudo launchctl load /Library/LaunchDaemons/com.gitea.act_runner.plist
sudo launchctl start com.gitea.act_runner

4. DS1525+ 배포 스크립트 업데이트

#!/bin/bash
# /volume1/docker/deploy-api.sh

# 배포 설정
IMAGE_NAME="home-management-api"
CONTAINER_NAME="home-api"
BACKUP_DIR="/volume1/backups/deployments"
LOG_FILE="/volume1/logs/deploy.log"
COMMIT_HASH=$1
DEPLOYMENT_ID=$2

echo "$(date): Starting deployment $DEPLOYMENT_ID (commit: $COMMIT_HASH)..." >> $LOG_FILE

# 배포 상태를 API에 보고
function report_status() {
    local status=$1
    local message=$2
    curl -s -X POST http://localhost:3000/api/deployments/update \
         -H "Content-Type: application/json" \
         -d "{
             \"deployment_id\": \"$DEPLOYMENT_ID\",
             \"status\": \"$status\",
             \"message\": \"$message\",
             \"timestamp\": \"$(date -Iseconds)\"
         }" || true
}

# 1. 현재 컨테이너 상태 확인 및 백업
if [ "$(docker ps -q -f name=$CONTAINER_NAME)" ]; then
    echo "Creating backup of current deployment..." >> $LOG_FILE
    BACKUP_TAG="backup-$(date +%Y%m%d-%H%M%S)"
    docker commit $CONTAINER_NAME $IMAGE_NAME:$BACKUP_TAG
    report_status "backing_up" "Creating backup: $BACKUP_TAG"
    
    # Graceful shutdown
    docker exec $CONTAINER_NAME npm run graceful-shutdown 2>/dev/null || true
    sleep 5
    docker stop $CONTAINER_NAME
    docker rm $CONTAINER_NAME
fi

# 2. 새 이미지 로드
echo "Loading new Docker image..." >> $LOG_FILE
report_status "loading_image" "Loading Docker image"
if ! gunzip -c /volume1/docker/images/home-management-api.tar.gz | docker load; then
    echo "Failed to load Docker image" >> $LOG_FILE
    report_status "failed" "Failed to load Docker image"
    exit 1
fi

# 3. 데이터베이스 마이그레이션 체크
echo "Checking database migrations..." >> $LOG_FILE
report_status "migrating" "Running database migrations"
if ! docker run --rm --network=host \
    -v /volume1/docker/config/api.env:/app/.env \
    $IMAGE_NAME:latest npm run db:migrate; then
    echo "Database migration failed" >> $LOG_FILE
    report_status "failed" "Database migration failed"
    exit 1
fi

# 4. 새 컨테이너 시작
echo "Starting new container..." >> $LOG_FILE
report_status "starting" "Starting new container"
docker run -d \
  --name $CONTAINER_NAME \
  --network=host \
  --restart=unless-stopped \
  -v /volume1/docker/config/api.env:/app/.env \
  -v /volume1/logs/api:/app/logs \
  -v /volume1/data/uploads:/app/uploads \
  -e COMMIT_HASH=$COMMIT_HASH \
  -e DEPLOYMENT_ID=$DEPLOYMENT_ID \
  $IMAGE_NAME:latest

if [ $? -ne 0 ]; then
    echo "Failed to start container" >> $LOG_FILE
    report_status "failed" "Failed to start container"
    exit 1
fi

# 5. 헬스체크 및 성능 테스트
echo "Performing health check..." >> $LOG_FILE
report_status "health_check" "Performing health check"
sleep 15

for i in {1..30}; do
    if curl -f -s http://localhost:3000/health > /dev/null 2>&1; then
        # 기본 기능 테스트
        if curl -f -s http://localhost:3000/api/devices > /dev/null 2>&1; then
            echo "$(date): Deployment successful!" >> $LOG_FILE
            report_status "success" "Deployment completed successfully"
            
            # 성능 벤치마크 실행
            /volume1/docker/scripts/performance-test.sh $DEPLOYMENT_ID &
            
            # 오래된 백업 이미지 정리 (7개 이상)
            docker images | grep "$IMAGE_NAME:backup" | tail -n +8 | \
                awk '{print $1":"$2}' | xargs -r docker rmi
            
            exit 0
        fi
    fi
    echo "Waiting for service to start... ($i/30)" >> $LOG_FILE
    sleep 2
done

# 6. 실패시 롤백
echo "$(date): Deployment failed! Rolling back..." >> $LOG_FILE
report_status "rolling_back" "Deployment failed, rolling back"

docker stop $CONTAINER_NAME 2>/dev/null || true
docker rm $CONTAINER_NAME 2>/dev/null || true

# 최신 백업으로 롤백
LATEST_BACKUP=$(docker images | grep "$IMAGE_NAME:backup" | head -1 | awk '{print $2}')
if [ ! -z "$LATEST_BACKUP" ]; then
    docker run -d \
        --name $CONTAINER_NAME \
        --network=host \
        --restart=unless-stopped \
        -v /volume1/docker/config/api.env:/app/.env \
        -v /volume1/logs/api:/app/logs \
        -v /volume1/data/uploads:/app/uploads \
        $IMAGE_NAME:backup-$LATEST_BACKUP
    
    report_status "rolled_back" "Rolled back to: backup-$LATEST_BACKUP"
else
    report_status "failed" "Rollback failed - no backup available"
fi

exit 1

4. Docker 컨테이너 모니터링 (DS1525+)

# /volume1/docker/monitor-containers.sh
#!/bin/bash

SERVICES=("home-api" "home-mariadb" "home-redis" "home-phpmyadmin")
WEBHOOK_URL="http://developer-machine:3000/api/alerts/webhook"

for service in "${SERVICES[@]}"; do
    if ! docker ps --format "table {{.Names}}" | grep -q "^${service}$"; then
        # 서비스 다운 알림
        curl -X POST $WEBHOOK_URL \
             -H "Content-Type: application/json" \
             -d "{
                 \"service\": \"$service\",
                 \"status\": \"down\",
                 \"timestamp\": \"$(date -Iseconds)\",
                 \"server\": \"ds1525plus\"
             }"
        
        # 자동 재시작 시도
        echo "$(date): Attempting to restart $service..." >> /volume1/logs/monitor.log
        docker start $service
    fi
done

Gitea 연동 최적화

1. Gitea 서버 설정 (DS1525+ 컨테이너)

# docker-compose.yml에 추가
  gitea:
    image: gitea/gitea:1.21
    container_name: home_gitea
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - GITEA__database__DB_TYPE=mysql
      - GITEA__database__HOST=mariadb:3306
      - GITEA__database__NAME=gitea
      - GITEA__database__USER=gitea
      - GITEA__database__PASSWD=gitea_password
      - GITEA__server__DOMAIN=git.hyungi.net
      - GITEA__server__HTTP_PORT=3000
      - GITEA__server__ROOT_URL=https://git.hyungi.net
      - GITEA__actions__ENABLED=true
      - GITEA__actions__DEFAULT_ACTIONS_URL=https://github.com
    restart: unless-stopped
    networks:
      - home_network
    volumes:
      - gitea_data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "3000:3000"
      - "222:22"
    depends_on:
      - mariadb

2. 배포 상태 추적 API

// routes/deployments.js
const express = require('express');
const router = express.Router();
const DeploymentService = require('../services/deploymentService');

// 배포 시작 기록
router.post('/start', async (req, res) => {
    try {
        const { deployment_id, commit_hash, branch, stage } = req.body;
        
        const logId = await DeploymentService.recordDeployment({
            id: deployment_id,
            stage: stage || 'build',
            commit: commit_hash,
            branch: branch,
            user: req.user?.username || 'system'
        });
        
        res.json({ log_id: logId, status: 'recorded' });
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

// 배포 상태 업데이트
router.post('/update', async (req, res) => {
    try {
        const { deployment_id, status, message } = req.body;
        
        await DeploymentService.updateDeploymentStatus(
            deployment_id, status, message
        );
        
        // 실시간 알림 (WebSocket)
        req.app.get('io').emit('deployment_update', {
            deployment_id,
            status,
            message,
            timestamp: new Date()
        });
        
        res.json({ status: 'updated' });
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

// 배포 완료 처리
router.post('/complete', async (req, res) => {
    try {
        const { deployment_id, status, stage } = req.body;
        
        await DeploymentService.completeDeployment(
            deployment_id, status, stage
        );
        
        if (status === 'success') {
            // 성공시 현재 버전 업데이트
            await DeploymentService.updateActiveVersion(deployment_id);
        }
        
        res.json({ status: 'completed' });
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

// 배포 히스토리 조회
router.get('/history', async (req, res) => {
    try {
        const { limit = 20, offset = 0 } = req.query;
        const history = await DeploymentService.getDeploymentHistory(
            parseInt(limit), parseInt(offset)
        );
        res.json(history);
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

module.exports = router;

3. 브랜치별 배포 전략

# .gitea/workflows/deploy.yml 추가 구성
on:
  push:
    branches: [ main, develop, feature/* ]
  pull_request:
    branches: [ main, develop ]

jobs:
  test:
    runs-on: self-hosted
    # 모든 브랜치에서 테스트 실행
    
  build-develop:
    needs: test
    runs-on: self-hosted
    if: gitea.ref == 'refs/heads/develop'
    steps:
    - name: Deploy to Staging
      run: |
        docker build -t home-management-api:develop .
        # 스테이징 환경 배포 (포트 3001)
        ssh admin@ds1525plus \
          "cd /volume1/docker && ./deploy-staging.sh develop"

  build-main:
    needs: test
    runs-on: self-hosted
    if: gitea.ref == 'refs/heads/main'
    steps:
    - name: Deploy to Production
      run: |
        docker build -t home-management-api:latest .
        # 프로덕션 배포 (포트 3000)
        ssh admin@ds1525plus \
          "cd /volume1/docker && ./deploy-api.sh latest ${{ gitea.run_id }}"

  feature-test:
    needs: test
    runs-on: self-hosted
    if: startsWith(gitea.ref, 'refs/heads/feature/')
    steps:
    - name: Feature Branch Tests
      run: |
        # 추가 테스트만 수행, 배포 안함
        npm run test:e2e
        npm run test:security

4. Gitea Actions 캐시 최적화

# 캐시 설정으로 빌드 시간 단축
steps:
- name: Cache Node modules
  uses: actions/cache@v3
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

- name: Cache Docker layers
  uses: actions/cache@v3
  with:
    path: /tmp/.buildx-cache
    key: ${{ runner.os }}-buildx-${{ gitea.sha }}
    restore-keys: |
      ${{ runner.os }}-buildx-

🔧 환경 설정

1. 개발 환경 (MacBook Pro / Mac Mini)

# .env.development
NODE_ENV=development
DB_HOST=ds1525plus  # NAS의 개발용 DB 연결
DB_PORT=3306
API_PORT=3000
CORS_ORIGIN=http://localhost:3001
LOG_LEVEL=debug
REDIS_HOST=ds1525plus

2. 로컬 개발 환경 (개발 머신)

# .env.local
NODE_ENV=development
DB_HOST=localhost  # 로컬 Docker DB
DB_PORT=3306
API_PORT=3000
CORS_ORIGIN=http://localhost:3001
LOG_LEVEL=debug
REDIS_HOST=localhost

3. 테스트 환경 (Mac Mini CI)

# .env.test  
NODE_ENV=test
DB_HOST=localhost
DB_PORT=3307
DB_NAME=home_management_test
API_PORT=3001
REDIS_HOST=localhost
REDIS_DB=1

4. 프로덕션 환경 (DS1525+)

# /volume1/docker/config/api.env
NODE_ENV=production
DB_HOST=localhost
DB_PORT=3306
DB_NAME=home_management
API_PORT=3000
CORS_ORIGIN=https://home.yourdomain.com
LOG_LEVEL=info
REDIS_HOST=localhost

# 보안 설정
JWT_SECRET=production-jwt-secret-key
BCRYPT_ROUNDS=14

# 프로덕션 최적화
NODE_OPTIONS=--max-old-space-size=2048
PM2_INSTANCES=2

데이터베이스 마이그레이션 관리

1. 마이그레이션 스크립트 구조

migrations/
├── 001_initial_schema.sql
├── 002_add_alert_system.sql  
├── 003_add_user_preferences.sql
├── 004_optimize_indexes.sql
└── 005_add_ci_cd_logs.sql

2. 마이그레이션 실행기 (scripts/migrate.js)

// 500자 이하로 간단하게 구성
const mysql = require('mysql2/promise');
const fs = require('fs').promises;
const path = require('path');

async function runMigrations() {
    const connection = await mysql.createConnection({
        host: process.env.DB_HOST,
        user: process.env.DB_USER,
        password: process.env.DB_PASSWORD,
        database: process.env.DB_NAME,
        multipleStatements: true
    });

    // 마이그레이션 테이블 생성
    await connection.execute(`
        CREATE TABLE IF NOT EXISTS migrations (
            id INT AUTO_INCREMENT PRIMARY KEY,
            filename VARCHAR(255) UNIQUE NOT NULL,
            executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    `);

    const migrationsDir = path.join(__dirname, '../migrations');
    const files = await fs.readdir(migrationsDir);
    
    for (const file of files.sort()) {
        if (!file.endsWith('.sql')) continue;
        
        const [rows] = await connection.execute(
            'SELECT id FROM migrations WHERE filename = ?', [file]
        );
        
        if (rows.length === 0) {
            console.log(`Running migration: ${file}`);
            const sql = await fs.readFile(path.join(migrationsDir, file), 'utf8');
            await connection.execute(sql);
            await connection.execute(
                'INSERT INTO migrations (filename) VALUES (?)', [file]
            );
        }
    }
    
    await connection.end();
}

module.exports = { runMigrations };

로그 및 모니터링 통합

1. 배포 로그 수집 테이블

-- 005_add_ci_cd_logs.sql
CREATE TABLE deployment_logs (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    deployment_id VARCHAR(100) NOT NULL,
    stage ENUM('test', 'build', 'deploy', 'rollback') NOT NULL,
    status ENUM('started', 'success', 'failed') NOT NULL,
    commit_hash VARCHAR(40),
    branch_name VARCHAR(100),
    deployed_by VARCHAR(100), 
    start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    end_time TIMESTAMP NULL,
    duration_seconds INT GENERATED ALWAYS AS (
        CASE WHEN end_time IS NOT NULL 
        THEN TIMESTAMPDIFF(SECOND, start_time, end_time)
        ELSE NULL END
    ) STORED,
    error_message TEXT,
    metadata JSON,
    INDEX idx_deployment_stage (deployment_id, stage),
    INDEX idx_status_time (status, start_time)
) ENGINE=InnoDB;

CREATE TABLE service_deployments (
    id INT AUTO_INCREMENT PRIMARY KEY,
    service_name VARCHAR(50) NOT NULL,
    version VARCHAR(50) NOT NULL,
    commit_hash VARCHAR(40) NOT NULL,
    deployed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    deployed_by VARCHAR(100),
    rollback_version VARCHAR(50) NULL,
    is_active BOOLEAN DEFAULT TRUE,
    performance_baseline JSON, -- 배포 후 성능 지표
    INDEX idx_service_version (service_name, version),
    INDEX idx_active (is_active, deployed_at)
) ENGINE=InnoDB;

2. 배포 성능 모니터링

// services/deploymentService.js
class DeploymentService {
    async recordDeployment(deploymentData) {
        // 배포 기록 저장 (500자 이하)
        const deployment = await this.db.query(`
            INSERT INTO deployment_logs 
            (deployment_id, stage, status, commit_hash, branch_name, deployed_by) 
            VALUES (?, ?, ?, ?, ?, ?)
        `, [deploymentData.id, deploymentData.stage, 'started', 
            deploymentData.commit, deploymentData.branch, deploymentData.user]);
        
        return deployment.insertId;
    }

    async updateDeploymentStatus(logId, status, errorMessage = null) {
        // 배포 상태 업데이트
        await this.db.query(`
            UPDATE deployment_logs 
            SET status = ?, end_time = CURRENT_TIMESTAMP, error_message = ?
            WHERE id = ?
        `, [status, errorMessage, logId]);
    }

    async getDeploymentHistory(limit = 50) {
        // 최근 배포 이력 조회
        const [rows] = await this.db.query(`
            SELECT * FROM deployment_logs 
            ORDER BY start_time DESC LIMIT ?
        `, [limit]);
        
        return rows;
    }
}

성능 벤치마크 자동화

1. 배포 후 성능 테스트

# scripts/performance-test.sh
#!/bin/bash

API_URL="http://localhost:3000"
RESULTS_FILE="/tmp/perf-results.json"

echo "Running performance tests after deployment..."

# API 응답 시간 테스트
ab -n 1000 -c 10 -g /tmp/ab-results.tsv $API_URL/api/devices/ > /tmp/ab-output.txt

# 메모리 사용량 체크
MEMORY_USAGE=$(docker stats --no-stream --format "table {{.Container}}\t{{.MemUsage}}" | grep home-api)

# 결과를 JSON으로 저장
echo "{
    \"timestamp\": \"$(date -Iseconds)\",
    \"memory_usage\": \"$MEMORY_USAGE\",
    \"ab_results\": \"$(tail -5 /tmp/ab-output.txt)\"
}" > $RESULTS_FILE

# 성능 저하 감지시 알림
if [ $(awk '/Time per request/ {print $4}' /tmp/ab-output.txt | head -1 | cut -d. -f1) -gt 100 ]; then
    curl -X POST http://developer-machine:3000/api/alerts/webhook \
         -d '{"type":"performance_degradation","details":"'$(cat $RESULTS_FILE)'"}'
fi

🚀 개발 시작 체크리스트

1. 환경 준비

  • Node.js 18+ 설치
  • Docker & Docker Compose 설치
  • Git 저장소 클론: git clone https://git.hyungi.net/hyungi/myhome-server.git
  • 프로젝트 폴더 구조 생성

2. 데이터베이스 설정

  • docker-compose up -d mariadb 실행
  • phpMyAdmin 접속 확인 (localhost:8080)
  • 테이블 생성 및 초기 데이터 삽입
  • 인덱스 및 파티션 설정 확인

3. 백엔드 개발 순서

  1. 기본 설정: Express 앱, DB 연결, 미들웨어 설정
  2. 모델 개발: Sequelize 모델 정의 (devices → power_consumption → network_traffic)
  3. 컨트롤러 개발: 기본 CRUD 작업 (GET, POST, PUT, DELETE)
  4. 서비스 계층: 비즈니스 로직 분리 (통계, 분석, 집계)
  5. 라우터 연결: API 엔드포인트 구성
  6. 데이터 수집기: 실제 하드웨어 연동 모듈
  7. 알림 시스템: 실시간 모니터링 및 알림
  8. 테스트 작성: 단위 테스트 및 통합 테스트

4. 성능 최적화

  • Redis 캐싱 구현
  • DB 쿼리 최적화
  • API 응답 시간 모니터링
  • 메모리 사용량 추적

5. CI/CD 파이프라인 구축

  • Gitea Actions 활성화 및 설정
  • 개발 머신(MacBook Pro/Mac Mini)에 Gitea Actions Runner 설치
  • DS1525+에 Docker 환경 및 Gitea 서버 구성
  • 배포 스크립트 작성 및 테스트 (프로덕션/스테이징)
  • 데이터베이스 마이그레이션 자동화
  • 브랜치별 배포 전략 설정 (main→production, develop→staging)
  • 성능 모니터링 및 배포 알림 설정
  • Webhook 서버 구성 (개발 머신)

6. 보안 설정

  • JWT 인증 구현
  • API 속도 제한 설정
  • HTTPS 인증서 구성 (Let's Encrypt)
  • 입력 데이터 검증 강화
  • SSH Key 기반 서버 간 통신 설정
  • Gitea 보안 설정 (2FA, 브랜치 보호)

7. 모니터링 및 알림

  • 배포 성공/실패 실시간 알림 (WebSocket)
  • 서비스 상태 모니터링 대시보드
  • 성능 저하 감지 및 자동 알림
  • 로그 중앙화 (Mac Mini → DS1525+)
  • 배포 메트릭 수집 및 분석

🎯 Gitea 기반 CI/CD 장점

완전 프라이빗 환경

  • 내부 네트워크: 모든 CI/CD 프로세스가 홈 네트워크 내에서 실행
  • 데이터 보안: 소스코드와 빌드 아티팩트가 외부로 유출되지 않음
  • 비용 효율: 외부 CI/CD 서비스 비용 절약

하드웨어 최적화

  • MacBook Pro: 모바일 개발 환경, 외부 작업 가능
  • Mac Mini: 고정 개발 환경, 안정적인 빌드 서버 역할
  • 공통 ARM64: 두 머신 모두 네이티브 빌드 환경
  • DS1525+: 안정적인 Git 서버 및 프로덕션 환경
  • 통합 관리: 홈 관리 시스템과 개발 도구 통합

확장성

  • 멀티 브랜치: feature/develop/main 브랜치별 배포 전략
  • 스테이징: develop 브랜치로 스테이징 환경 자동 배포
  • 롤백: 실패시 자동 롤백 및 알림

이제 완전히 프라이빗한 환경에서 기업급 CI/CD 파이프라인을 구축할 수 있습니다. Gitea 서버로 완전한 DevOps 환경을 홈에서 운영하는 것이 가능하겠네요!

이 계획서를 따라 단계별로 구현하면 확장성과 유지보수성을 갖춘 견고한 홈 관리 시스템과 함께 전문적인 CI/CD 파이프라인을 구축할 수 있습니다.