From 4b77086bb2fc869682e48c0ae8985ddad9c5c7ee Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Wed, 30 Jul 2025 14:12:09 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B4=88=EA=B8=B0=20=ED=99=88=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Express.js 기반 백엔드 API 서버 - MariaDB, Redis, phpMyAdmin Docker 환경 - Device 관리 기본 CRUD 구현 - Mac Mini M4 Pro 전용 설정 및 배포 스크립트 - 자동화된 설치 및 배포 시스템 - 완전한 문서화 및 실행 가이드 --- .gitignore | 91 ++ Dockerfile | 39 + README.mac-mini.md | 271 +++++ README.md | 270 +++++ config/mariadb.cnf | 53 + config/redis.conf | 52 + docker-compose.mac-mini.yml | 116 ++ docker-compose.yml | 94 ++ home_management_db_spec.md | 1665 +++++++++++++++++++++++++++ package.json | 56 + scripts/deploy-mac-mini.sh | 154 +++ scripts/setup-db.sql | 197 ++++ scripts/setup-mac-mini.sh | 99 ++ src/app.js | 90 ++ src/config/database.js | 48 + src/controllers/deviceController.js | 134 +++ src/middleware/errorHandler.js | 60 + src/models/Device.js | 87 ++ src/models/PowerConsumption.js | 79 ++ src/models/User.js | 82 ++ src/models/index.js | 23 + src/routes/devices.js | 20 + src/routes/index.js | 30 + src/utils/logger.js | 57 + 24 files changed, 3867 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.mac-mini.md create mode 100644 README.md create mode 100644 config/mariadb.cnf create mode 100644 config/redis.conf create mode 100644 docker-compose.mac-mini.yml create mode 100644 docker-compose.yml create mode 100644 home_management_db_spec.md create mode 100644 package.json create mode 100644 scripts/deploy-mac-mini.sh create mode 100644 scripts/setup-db.sql create mode 100644 scripts/setup-mac-mini.sh create mode 100644 src/app.js create mode 100644 src/config/database.js create mode 100644 src/controllers/deviceController.js create mode 100644 src/middleware/errorHandler.js create mode 100644 src/models/Device.js create mode 100644 src/models/PowerConsumption.js create mode 100644 src/models/User.js create mode 100644 src/models/index.js create mode 100644 src/routes/devices.js create mode 100644 src/routes/index.js create mode 100644 src/utils/logger.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2733d3a --- /dev/null +++ b/.gitignore @@ -0,0 +1,91 @@ +# Node.js +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Dependency directories +node_modules/ +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# Environment variables +.env +.env.local +.env.development +.env.test +.env.production + +# Logs +logs/ +*.log + +# Docker +.docker/ + +# Database +*.sqlite +*.db + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# Uploads +uploads/ + +# Temporary files +tmp/ +temp/ + +# Backup files +backup_* +*.backup + +# Mac Mini specific +/Users/hyungi/home-management-* \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..17171e5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +FROM node:18-alpine + +# 작업 디렉토리 설정 +WORKDIR /app + +# 시스템 패키지 업데이트 및 필요한 패키지 설치 +RUN apk add --no-cache \ + curl \ + bash \ + && rm -rf /var/cache/apk/* + +# 사용자 권한 설정 +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodeuser -u 1001 + +# package.json과 package-lock.json 복사 +COPY package*.json ./ + +# 의존성 설치 +RUN npm ci --only=production && npm cache clean --force + +# 소스 코드 복사 +COPY . . + +# 로그 디렉토리 생성 +RUN mkdir -p logs && chown -R nodeuser:nodejs logs + +# 사용자 변경 +USER nodeuser + +# 포트 노출 +EXPOSE 3000 + +# 헬스체크 +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:3000/health || exit 1 + +# 애플리케이션 실행 +CMD ["npm", "start"] \ No newline at end of file diff --git a/README.mac-mini.md b/README.mac-mini.md new file mode 100644 index 0000000..a89dd1f --- /dev/null +++ b/README.mac-mini.md @@ -0,0 +1,271 @@ +# Mac Mini 홈 관리 시스템 실행 가이드 + +Mac Mini M4 Pro에서 홈 관리 시스템을 운영하기 위한 상세 가이드입니다. + +## 🖥️ Mac Mini 환경 요구사항 + +- **Mac Mini M4 Pro** (64GB RAM 권장) +- **macOS Sonoma 14.5+** +- **Docker Desktop for Mac** +- **Git** +- **최소 50GB 여유 공간** + +## 🚀 초기 설정 + +### 1. Docker Desktop 설치 + +```bash +# Homebrew로 설치 (권장) +brew install --cask docker + +# 또는 Docker 공식 사이트에서 다운로드 +# https://www.docker.com/products/docker-desktop/ +``` + +### 2. 프로젝트 클론 + +```bash +cd /Users/hyungi +git clone https://git.hyungi.net/hyungi/myhome-server.git +cd myhome-server +``` + +### 3. 자동 설정 실행 + +```bash +# 실행 권한 부여 +chmod +x scripts/setup-mac-mini.sh + +# 설정 실행 +./scripts/setup-mac-mini.sh +``` + +## 📁 Mac Mini 데이터 구조 + +``` +/Users/hyungi/ +├── myhome-server/ # 프로젝트 소스 +├── home-management-db/ # MariaDB 데이터 +├── home-management-redis/ # Redis 데이터 +├── home-management-data/ # 업로드 파일 +└── home-management-logs/ # 로그 파일 + └── deploy.log +``` + +## 🌐 네트워크 설정 + +### Mac Mini IP 확인 + +```bash +# 현재 IP 주소 확인 +ifconfig | grep "inet " | grep -v 127.0.0.1 + +# 또는 +ipconfig getifaddr en0 +``` + +### 방화벽 설정 (필요시) + +```bash +# 포트 3000 허용 +sudo pfctl -f /etc/pf.conf + +# 또는 시스템 환경설정 > 보안 및 개인정보보호 > 방화벽 +``` + +## 🔄 배포 및 업데이트 + +### 자동 배포 + +```bash +# 최신 코드로 배포 +./scripts/deploy-mac-mini.sh + +# 특정 커밋으로 배포 +./scripts/deploy-mac-mini.sh [commit-hash] +``` + +### 수동 배포 + +```bash +# 코드 업데이트 +git pull origin main + +# 서비스 재시작 +docker-compose -f docker-compose.mac-mini.yml up -d --build +``` + +## 📊 모니터링 및 관리 + +### 서비스 상태 확인 + +```bash +# 컨테이너 상태 +docker-compose -f docker-compose.mac-mini.yml ps + +# 시스템 리소스 +docker stats + +# 로그 확인 +tail -f /Users/hyungi/home-management-logs/deploy.log +``` + +### 데이터베이스 관리 + +```bash +# phpMyAdmin 접속 +open http://localhost:8080 + +# 직접 MySQL 접속 +docker exec -it home_mariadb mysql -u homeuser -p +``` + +### 백업 관리 + +```bash +# 데이터베이스 백업 +docker exec home_mariadb mysqldump -u homeuser -pmac_mini_home_password home_management > backup_$(date +%Y%m%d).sql + +# 데이터 디렉토리 백업 +tar -czf home_management_backup_$(date +%Y%m%d).tar.gz /Users/hyungi/home-management-* +``` + +## 🔧 성능 최적화 + +### Docker 설정 최적화 + +Docker Desktop > Settings > Resources: +- **CPU**: 6-8 cores +- **Memory**: 16-24GB +- **Disk**: 100GB+ + +### MariaDB 최적화 (이미 적용됨) + +```ini +# config/mariadb.cnf +innodb_buffer_pool_size = 4G +innodb_log_file_size = 512M +max_connections = 200 +``` + +## 🔐 보안 설정 + +### SSH 키 설정 (CI/CD용) + +```bash +# SSH 키 생성 +ssh-keygen -t ed25519 -C "mac-mini-home-server" + +# 공개키를 Gitea에 등록 +cat ~/.ssh/id_ed25519.pub +``` + +### 환경별 비밀키 관리 + +```bash +# 프로덕션 환경 변수 (docker-compose.mac-mini.yml에 설정됨) +JWT_SECRET=mac-mini-production-jwt-secret-key-2025 +DB_PASSWORD=mac_mini_home_password +``` + +## 🚨 트러블슈팅 + +### 메모리 부족 + +```bash +# Docker 메모리 사용량 확인 +docker system df + +# 불필요한 이미지 정리 +docker system prune -a +``` + +### 디스크 공간 부족 + +```bash +# 오래된 백업 이미지 정리 +docker images | grep "backup" | tail -n +8 | awk '{print $1":"$2}' | xargs docker rmi + +# 로그 로테이션 +sudo log config --mode "rotate" +``` + +### 네트워크 접근 문제 + +```bash +# 포트 점유 확인 +lsof -i :3000 + +# Docker 네트워크 재설정 +docker network prune +``` + +## 📱 외부 접근 설정 + +### 홈 네트워크에서 접근 + +다른 기기에서 접근하려면: +- **URL**: `http://[Mac Mini IP]:3000` +- **예시**: `http://192.168.1.100:3000` + +### 포트 포워딩 (라우터 설정) + +외부 인터넷에서 접근하려면 라우터에서: +1. 포트 3000을 Mac Mini IP로 포워딩 +2. DDNS 설정 (선택사항) + +## 📈 성능 모니터링 + +### 시스템 메트릭 + +```bash +# CPU 사용률 +top -l 1 | grep "CPU usage" + +# 메모리 사용률 +vm_stat + +# 디스크 I/O +iostat 1 +``` + +### 애플리케이션 메트릭 + +```bash +# API 응답 시간 +time curl http://localhost:3000/api/devices + +# 데이터베이스 성능 +docker exec home_mariadb mysql -u homeuser -pmac_mini_home_password -e "SHOW PROCESSLIST;" +``` + +## 🔄 정기 유지보수 + +### 일일 작업 + +- [ ] 시스템 상태 확인 +- [ ] 로그 모니터링 +- [ ] 백업 상태 확인 + +### 주간 작업 + +- [ ] 코드 업데이트 (git pull) +- [ ] Docker 이미지 정리 +- [ ] 데이터베이스 백업 + +### 월간 작업 + +- [ ] 전체 시스템 백업 +- [ ] 성능 리포트 생성 +- [ ] 보안 업데이트 적용 + +## 📞 지원 + +문제 발생시: +1. `/Users/hyungi/home-management-logs/deploy.log` 확인 +2. Docker 로그 확인: `docker-compose -f docker-compose.mac-mini.yml logs` +3. GitHub Issues에 로그와 함께 문의 + +--- + +**Mac Mini M4 Pro의 성능을 최대한 활용하여 안정적인 홈 관리 시스템을 운영하세요! 🏠💻** \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6ee4345 --- /dev/null +++ b/README.md @@ -0,0 +1,270 @@ +# 홈 관리 시스템 (Home Management API) + +홈 IoT 기기 모니터링 및 관리를 위한 백엔드 API 시스템입니다. + +> **Mac Mini M4 Pro 전용 버전** - 고성능 홈 서버 환경에 최적화 + +## 🎯 주요 기능 + +- **디바이스 관리**: 홈 IoT 기기 등록 및 모니터링 +- **전력 소비 추적**: 실시간 전력 데이터 수집 및 분석 +- **네트워크 트래픽 모니터링**: 디바이스별 네트워크 사용량 추적 +- **시스템 리소스 모니터링**: CPU, 메모리, 디스크 사용량 추적 +- **알림 시스템**: 임계값 기반 실시간 알림 +- **사용자 관리**: 역할 기반 접근 제어 + +## 🛠 기술 스택 + +- **Backend**: Node.js, Express.js +- **Database**: MariaDB 11.x +- **ORM**: Sequelize +- **Cache**: Redis +- **Logging**: Winston +- **Container**: Docker & Docker Compose + +## 📋 요구사항 + +- Node.js 18+ +- Docker & Docker Compose +- Git + +## 🚀 빠른 시작 (Mac Mini) + +### 1. 저장소 클론 + +```bash +git clone https://git.hyungi.net/hyungi/myhome-server.git +cd myhome-server +``` + +### 2. Mac Mini 자동 설정 + +```bash +# 실행 권한 부여 +chmod +x scripts/setup-mac-mini.sh + +# 자동 설정 실행 +./scripts/setup-mac-mini.sh +``` + +### 3. 수동 설정 (선택사항) + +```bash +# Mac Mini 전용 Docker Compose 사용 +docker-compose -f docker-compose.mac-mini.yml up -d +``` + +### 4. 수동 배포 + +```bash +# 배포 스크립트 사용 +chmod +x scripts/deploy-mac-mini.sh +./scripts/deploy-mac-mini.sh + +# 또는 직접 배포 +git pull origin main +docker-compose -f docker-compose.mac-mini.yml up -d --build +``` + +## 📊 서비스 확인 (Mac Mini) + +- **API 서버**: http://localhost:3000 (Mac Mini 로컬) +- **API 서버**: http://mac-mini-m4.local:3000 (네트워크 접근) +- **phpMyAdmin**: http://localhost:8080 +- **API 문서**: http://localhost:3000/api + +### 헬스체크 + +```bash +# Mac Mini 로컬에서 +curl http://localhost:3000/health + +# 다른 기기에서 (IP 주소는 실제 Mac Mini IP로 변경) +curl http://192.168.1.100:3000/health +``` + +## 📚 API 엔드포인트 + +### 디바이스 관리 + +``` +GET /api/devices # 모든 디바이스 조회 +GET /api/devices/:id # 특정 디바이스 조회 +POST /api/devices # 새 디바이스 생성 +PUT /api/devices/:id # 디바이스 업데이트 +DELETE /api/devices/:id # 디바이스 삭제 +``` + +### 기본 사용 예제 + +```bash +# 디바이스 목록 조회 +curl http://localhost:3000/api/devices + +# 새 디바이스 생성 +curl -X POST http://localhost:3000/api/devices \ + -H "Content-Type: application/json" \ + -d '{ + "device_id": "test_device", + "name": "테스트 디바이스", + "device_type": "server", + "location": "테스트실" + }' +``` + +## 🗄️ 데이터베이스 + +### 접속 정보 (Mac Mini) + +- **Host**: localhost (Mac Mini 로컬) / mac-mini-m4.local (네트워크) +- **Port**: 3306 +- **Database**: home_management +- **Username**: homeuser +- **Password**: mac_mini_home_password + +### 기본 데이터 + +시스템 초기화시 다음 데이터가 자동으로 생성됩니다: + +- **관리자 계정**: admin / admin123 +- **가족 계정**: family / admin123 +- **기본 디바이스**: Mac Mini M4 Pro, Synology DS1525+, RT6600ax + +## 🔧 개발 가이드 + +### 폴더 구조 + +``` +src/ +├── config/ # 설정 파일 +├── controllers/ # 컨트롤러 +├── middleware/ # 미들웨어 +├── models/ # 데이터베이스 모델 +├── routes/ # 라우터 +├── services/ # 비즈니스 로직 +├── utils/ # 유틸리티 +└── app.js # 메인 애플리케이션 +``` + +### 스크립트 + +```bash +npm run dev # 개발 서버 실행 +npm run test # 테스트 실행 +npm run lint # 코드 검사 +npm run db:migrate # 데이터베이스 마이그레이션 +``` + +### 로그 확인 + +```bash +# Docker 컨테이너 로그 +docker-compose logs -f api + +# 로컬 로그 파일 +tail -f logs/combined.log +``` + +## 🐳 Docker 명령어 (Mac Mini) + +```bash +# 모든 서비스 시작 (Mac Mini 전용) +docker-compose -f docker-compose.mac-mini.yml up -d + +# 특정 서비스만 시작 +docker-compose -f docker-compose.mac-mini.yml up -d mariadb redis + +# 로그 확인 +docker-compose -f docker-compose.mac-mini.yml logs -f + +# 컨테이너 상태 확인 +docker-compose -f docker-compose.mac-mini.yml ps + +# 서비스 중지 +docker-compose -f docker-compose.mac-mini.yml down + +# 볼륨까지 삭제 (주의: 데이터 손실) +docker-compose -f docker-compose.mac-mini.yml down -v +``` + +## 🔍 문제 해결 + +### 데이터베이스 연결 오류 + +```bash +# MariaDB 컨테이너 상태 확인 +docker-compose -f docker-compose.mac-mini.yml ps mariadb + +# 데이터베이스 로그 확인 +docker-compose -f docker-compose.mac-mini.yml logs mariadb + +# 컨테이너 재시작 +docker-compose -f docker-compose.mac-mini.yml restart mariadb + +# Mac Mini 재배포 +./scripts/deploy-mac-mini.sh +``` + +### 포트 충돌 + +기본 포트가 사용 중인 경우 `docker-compose.mac-mini.yml`에서 포트를 변경하세요: + +```yaml +ports: + - "3001:3000" # 3001로 변경 +``` + +### Mac Mini 네트워크 접근 문제 + +```bash +# 방화벽 확인 +sudo pfctl -s all + +# 포트 사용 확인 +lsof -i :3000 + +# 네트워크 인터페이스 확인 +ifconfig +``` + +## 📈 모니터링 + +### phpMyAdmin으로 데이터베이스 모니터링 + +1. http://localhost:8080 접속 (Mac Mini에서) +2. Server: `mariadb` +3. Username: `homeuser` +4. Password: `mac_mini_home_password` + +### API 성능 모니터링 + +```bash +# API 응답 시간 테스트 +time curl http://localhost:3000/api/devices + +# 부하 테스트 (Apache Bench) +ab -n 1000 -c 10 http://localhost:3000/api/devices +``` + +## 🔐 보안 + +- JWT 기반 인증 (추후 구현) +- 비밀번호 bcrypt 해싱 +- API 요청 제한 (Rate Limiting) +- 입력 데이터 검증 + +## 📝 라이센스 + +MIT License + +## 🤝 기여하기 + +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +## 📞 지원 + +문제가 있으시면 [Issues](https://git.hyungi.net/hyungi/myhome-server/issues)에 등록해주세요. \ No newline at end of file diff --git a/config/mariadb.cnf b/config/mariadb.cnf new file mode 100644 index 0000000..7c09fc6 --- /dev/null +++ b/config/mariadb.cnf @@ -0,0 +1,53 @@ +[mariadb] +# 기본 설정 +character-set-server = utf8mb4 +collation-server = utf8mb4_unicode_ci +init-connect = 'SET NAMES utf8mb4' + +# 메모리 최적화 (개발 환경용) +innodb_buffer_pool_size = 1G +innodb_log_buffer_size = 32M +innodb_log_file_size = 256M +key_buffer_size = 128M +sort_buffer_size = 2M +read_buffer_size = 1M +read_rnd_buffer_size = 4M +thread_cache_size = 25 +table_open_cache = 2000 + +# 연결 설정 +max_connections = 100 +max_user_connections = 90 +wait_timeout = 600 +interactive_timeout = 600 + +# 쿼리 캐시 +query_cache_type = 1 +query_cache_size = 128M +query_cache_limit = 1M + +# InnoDB 최적화 +innodb_flush_log_at_trx_commit = 2 +innodb_flush_method = O_DIRECT +innodb_file_per_table = 1 +innodb_io_capacity = 1000 +innodb_io_capacity_max = 2000 +innodb_read_io_threads = 2 +innodb_write_io_threads = 2 + +# 시계열 데이터 최적화 +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 \ No newline at end of file diff --git a/config/redis.conf b/config/redis.conf new file mode 100644 index 0000000..d0c2639 --- /dev/null +++ b/config/redis.conf @@ -0,0 +1,52 @@ +# Redis 기본 설정 +bind 0.0.0.0 +port 6379 +timeout 300 +tcp-keepalive 60 + +# 메모리 설정 +maxmemory 512mb +maxmemory-policy allkeys-lru + +# 스냅샷 설정 +save 900 1 +save 300 10 +save 60 10000 + +# 로그 설정 +loglevel notice +logfile "" + +# 데이터베이스 설정 +databases 16 + +# AOF 설정 +appendonly yes +appendfsync everysec +no-appendfsync-on-rewrite no +auto-aof-rewrite-percentage 100 +auto-aof-rewrite-min-size 64mb + +# 슬로우 로그 +slowlog-log-slower-than 10000 +slowlog-max-len 128 + +# 클라이언트 연결 설정 +tcp-backlog 511 +timeout 0 +tcp-keepalive 300 + +# 보안 설정 (개발환경) +# requirepass your-redis-password + +# 기타 최적화 +hash-max-ziplist-entries 512 +hash-max-ziplist-value 64 +list-max-ziplist-size -2 +list-compress-depth 0 +set-max-intset-entries 512 +zset-max-ziplist-entries 128 +zset-max-ziplist-value 64 +hll-sparse-max-bytes 3000 +stream-node-max-bytes 4096 +stream-node-max-entries 100 \ No newline at end of file diff --git a/docker-compose.mac-mini.yml b/docker-compose.mac-mini.yml new file mode 100644 index 0000000..a32f245 --- /dev/null +++ b/docker-compose.mac-mini.yml @@ -0,0 +1,116 @@ +version: '3.8' + +services: + mariadb: + image: mariadb:11-jammy + container_name: home_mariadb + environment: + MYSQL_ROOT_PASSWORD: mac_mini_root_password + MYSQL_DATABASE: home_management + MYSQL_USER: homeuser + MYSQL_PASSWORD: mac_mini_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=4G + --innodb-log-file-size=512M + --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: mac_mini_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: production + DB_HOST: mariadb + DB_PORT: 3306 + DB_NAME: home_management + DB_USER: homeuser + DB_PASSWORD: mac_mini_home_password + REDIS_HOST: redis + REDIS_PORT: 6379 + JWT_SECRET: mac-mini-production-jwt-secret-key-2025 + API_PORT: 3000 + API_HOST: 0.0.0.0 + CORS_ORIGIN: http://mac-mini-m4.local:3001,http://localhost:3001,http://192.168.1.100:3001 + LOG_LEVEL: info + BCRYPT_ROUNDS: 12 + ports: + - "3000:3000" + volumes: + - ./logs:/app/logs + - ./uploads:/app/uploads + - /Users/hyungi/home-management-data:/app/data + depends_on: + - mariadb + - redis + restart: unless-stopped + networks: + - home_network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +volumes: + mariadb_data: + driver: local + driver_opts: + type: none + o: bind + device: /Users/hyungi/home-management-db + redis_data: + driver: local + driver_opts: + type: none + o: bind + device: /Users/hyungi/home-management-redis + +networks: + home_network: + driver: bridge + ipam: + config: + - subnet: 172.20.0.0/16 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c95eee4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,94 @@ +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: production + 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: mac-mini-jwt-secret-key + API_PORT: 3000 + API_HOST: 0.0.0.0 + CORS_ORIGIN: http://mac-mini-m4:3001,http://localhost:3001 + ports: + - "3000:3000" + volumes: + - ./logs:/app/logs + - ./uploads:/app/uploads + depends_on: + - mariadb + - redis + restart: unless-stopped + networks: + - home_network + +volumes: + mariadb_data: + redis_data: + +networks: + home_network: + driver: bridge \ No newline at end of file diff --git a/home_management_db_spec.md b/home_management_db_spec.md new file mode 100644 index 0000000..3ed92a8 --- /dev/null +++ b/home_management_db_spec.md @@ -0,0 +1,1665 @@ +# 홈 관리 시스템 DB 구축 계획서 + +## 🎯 프로젝트 개요 + +### Git 저장소 +- **Repository**: https://git.hyungi.net/hyungi/myhome-server.git +- **Gitea Server**: git.hyungi.net (DS1525+ 호스팅) + +### 목표 +- 홈 IoT 기기 모니터링 및 관리 +- 전력 소비, 네트워크 트래픽 추적 +- 시스템 리소스 모니터링 +- 향후 개인 서비스 확장 기반 마련 + +### 기술 스택 +``` +Backend: Express.js (Node.js) +Database: MariaDB 11.x +Admin Tool: phpMyAdmin +Architecture: MVC + Service Layer +Code Style: 모듈형 구조 (500자 이하 함수) +``` + +--- + +## 🗄️ 데이터베이스 설계 + +### 데이터베이스 생성 +```sql +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) +```sql +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) +```sql +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) +```sql +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) +```sql +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) +```sql +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) +```sql +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) +```sql +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) +```sql +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 구성 + +### 주요 의존성 +```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 +```yaml +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 +```env +# 서버 설정 +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 + +# 데이터 수집 설정 +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 +```ini +[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 +```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) +```yaml +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 설정 +```bash +# 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) +```bash +#!/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 설정 (개발 머신) +```bash +# 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 + + + + + Label + com.gitea.act_runner + ProgramArguments + + /usr/local/bin/act_runner + daemon + --config + /Users/$(whoami)/.runner + + RunAtLoad + + KeepAlive + + StandardOutPath + /usr/local/var/log/act_runner.log + StandardErrorPath + /usr/local/var/log/act_runner.error.log + WorkingDirectory + /Users/$(whoami) + UserName + $(whoami) + + +EOF + +# 서비스 시작 +sudo launchctl load /Library/LaunchDaemons/com.gitea.act_runner.plist +sudo launchctl start com.gitea.act_runner +``` + +#### 4. DS1525+ 배포 스크립트 업데이트 +```bash +#!/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+) +```bash +# /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+ 컨테이너) +```yaml +# 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 +```javascript +// 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. 브랜치별 배포 전략 +```yaml +# .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 캐시 최적화 +```yaml +# 캐시 설정으로 빌드 시간 단축 +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 +# .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 +# .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 +# .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+) +```env +# /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) +```javascript +// 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. 배포 로그 수집 테이블 +```sql +-- 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. 배포 성능 모니터링 +```javascript +// 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. 배포 후 성능 테스트 +```bash +# 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 파이프라인을 구축할 수 있습니다. \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..cccf978 --- /dev/null +++ b/package.json @@ -0,0 +1,56 @@ +{ + "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", + "test:unit": "jest --testPathPattern=tests/unit", + "test:integration": "jest --testPathPattern=tests/integration", + "test:coverage": "jest --coverage", + "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" + }, + "engines": { + "node": ">=18.0.0" + }, + "keywords": [ + "home-management", + "iot", + "monitoring", + "api", + "express" + ], + "author": "hyungi", + "license": "MIT" +} \ No newline at end of file diff --git a/scripts/deploy-mac-mini.sh b/scripts/deploy-mac-mini.sh new file mode 100644 index 0000000..a98d802 --- /dev/null +++ b/scripts/deploy-mac-mini.sh @@ -0,0 +1,154 @@ +#!/bin/bash + +# Mac Mini 홈 관리 시스템 배포 스크립트 + +set -e + +# 설정 +IMAGE_NAME="home-management-api" +CONTAINER_NAME="home_api" +BACKUP_DIR="/Users/hyungi/home-management-backups" +LOG_FILE="/Users/hyungi/home-management-logs/deploy.log" +COMMIT_HASH=${1:-"latest"} +DEPLOYMENT_ID=${2:-$(date +%Y%m%d_%H%M%S)} + +# 로그 디렉토리 생성 +mkdir -p "$(dirname "$LOG_FILE")" +mkdir -p "$BACKUP_DIR" + +echo "$(date): Mac Mini 배포 시작 - $DEPLOYMENT_ID (commit: $COMMIT_HASH)" | tee -a "$LOG_FILE" + +# 배포 상태 보고 함수 +function report_status() { + local status=$1 + local message=$2 + echo "$(date): [$status] $message" | tee -a "$LOG_FILE" + + # 향후 Gitea webhook으로 상태 보고 가능 + # curl -s -X POST http://git.hyungi.net/api/deployments/update \ + # -H "Content-Type: application/json" \ + # -d "{\"deployment_id\": \"$DEPLOYMENT_ID\", \"status\": \"$status\", \"message\": \"$message\"}" || true +} + +# 1. Git 최신 코드 가져오기 +report_status "pulling" "Git 최신 코드 가져오는 중" +git fetch origin +git reset --hard origin/main + +# 2. 현재 컨테이너 상태 확인 및 백업 +if docker ps -q -f name=$CONTAINER_NAME | grep -q .; then + echo "현재 실행 중인 컨테이너 백업 중..." | tee -a "$LOG_FILE" + BACKUP_TAG="backup-$(date +%Y%m%d-%H%M%S)" + docker commit $CONTAINER_NAME $IMAGE_NAME:$BACKUP_TAG + report_status "backing_up" "백업 생성: $BACKUP_TAG" + + # Graceful shutdown + echo "서비스 정리 중..." | tee -a "$LOG_FILE" + docker-compose -f docker-compose.mac-mini.yml down +fi + +# 3. 새 이미지 빌드 +report_status "building" "Docker 이미지 빌드 중" +docker-compose -f docker-compose.mac-mini.yml build --no-cache + +# 4. 데이터베이스 및 Redis 시작 +report_status "starting_db" "데이터베이스 서비스 시작 중" +docker-compose -f docker-compose.mac-mini.yml up -d mariadb redis + +# 데이터베이스 연결 대기 +echo "데이터베이스 연결 대기 중..." | tee -a "$LOG_FILE" +sleep 30 + +# 5. 새 API 컨테이너 시작 +report_status "starting_api" "API 서버 시작 중" +docker-compose -f docker-compose.mac-mini.yml up -d api phpmyadmin + +# 6. 헬스체크 +report_status "health_check" "헬스체크 수행 중" +sleep 15 + +HEALTH_CHECK_PASSED=false +for i in {1..30}; do + if curl -f -s http://localhost:3000/health > /dev/null 2>&1; then + # API 기능 테스트 + if curl -f -s http://localhost:3000/api/devices > /dev/null 2>&1; then + echo "$(date): 배포 성공!" | tee -a "$LOG_FILE" + report_status "success" "배포 완료" + HEALTH_CHECK_PASSED=true + + # 성공 시 시스템 정보 기록 + echo "시스템 정보:" | tee -a "$LOG_FILE" + docker-compose -f docker-compose.mac-mini.yml ps | tee -a "$LOG_FILE" + + # 오래된 백업 이미지 정리 (7개 이상) + docker images | grep "$IMAGE_NAME:backup" | tail -n +8 | \ + awk '{print $1":"$2}' | xargs -r docker rmi 2>/dev/null || true + + break + fi + fi + echo "서비스 시작 대기 중... ($i/30)" | tee -a "$LOG_FILE" + sleep 2 +done + +# 7. 실패시 롤백 +if [ "$HEALTH_CHECK_PASSED" = false ]; then + echo "$(date): 배포 실패! 롤백 진행 중..." | tee -a "$LOG_FILE" + report_status "rolling_back" "배포 실패, 롤백 중" + + docker-compose -f docker-compose.mac-mini.yml down + + # 최신 백업으로 롤백 + LATEST_BACKUP=$(docker images | grep "$IMAGE_NAME:backup" | head -1 | awk '{print $2}') + if [ ! -z "$LATEST_BACKUP" ]; then + echo "백업 이미지로 롤백: $LATEST_BACKUP" | tee -a "$LOG_FILE" + + # 백업 이미지로 임시 컨테이너 실행 + docker run -d \ + --name $CONTAINER_NAME \ + --network host \ + --restart unless-stopped \ + -v /Users/hyungi/home-management-logs:/app/logs \ + -v /Users/hyungi/home-management-data:/app/data \ + -e NODE_ENV=production \ + -e DB_HOST=localhost \ + -e REDIS_HOST=localhost \ + $IMAGE_NAME:backup-$LATEST_BACKUP + + report_status "rolled_back" "롤백 완료: backup-$LATEST_BACKUP" + else + report_status "failed" "롤백 실패 - 백업 이미지 없음" + fi + + exit 1 +fi + +# 8. 배포 후 정리 작업 +echo "배포 후 정리 작업 중..." | tee -a "$LOG_FILE" + +# 시스템 상태 확인 +echo "=== 시스템 상태 ===" | tee -a "$LOG_FILE" +docker-compose -f docker-compose.mac-mini.yml ps | tee -a "$LOG_FILE" + +echo "=== 디스크 사용량 ===" | tee -a "$LOG_FILE" +df -h | tee -a "$LOG_FILE" + +echo "=== 메모리 사용량 ===" | tee -a "$LOG_FILE" +docker stats --no-stream | tee -a "$LOG_FILE" + +# 네트워크 정보 +echo "=== 네트워크 정보 ===" | tee -a "$LOG_FILE" +ifconfig | grep "inet " | grep -v 127.0.0.1 | tee -a "$LOG_FILE" + +echo "🎉 Mac Mini 배포 완료!" +echo "" +echo "📊 접속 정보:" +echo "- API 서버: http://localhost:3000" +echo "- phpMyAdmin: http://localhost:8080" +echo "" +echo "🔍 상태 확인:" +echo "curl http://localhost:3000/health" +echo "" +echo "📝 로그 확인:" +echo "tail -f $LOG_FILE" +echo "docker-compose -f docker-compose.mac-mini.yml logs -f api" \ No newline at end of file diff --git a/scripts/setup-db.sql b/scripts/setup-db.sql new file mode 100644 index 0000000..9f74655 --- /dev/null +++ b/scripts/setup-db.sql @@ -0,0 +1,197 @@ +-- 홈 관리 시스템 데이터베이스 초기 설정 + +-- 데이터베이스 생성 (Docker에서 자동 생성되므로 주석 처리) +-- CREATE DATABASE home_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +-- USE home_management; + +-- 1. 디바이스 관리 테이블 +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. 전력 소비 데이터 테이블 +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. 네트워크 트래픽 테이블 +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. 시스템 리소스 테이블 +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. 서비스 상태 테이블 +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. 사용자 관리 테이블 +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. 알림 규칙 테이블 +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. 알림 로그 테이블 +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; + +-- 기본 관리자 사용자 생성 (패스워드는 'admin123') +INSERT INTO users (username, email, password_hash, full_name, role) VALUES +('admin', 'admin@home.local', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewsF5ckPG6Y4TZsG', '시스템 관리자', 'admin'), +('family', 'family@home.local', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewsF5ckPG6Y4TZsG', '가족 사용자', '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); + +-- 성능 모니터링용 뷰 +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; \ No newline at end of file diff --git a/scripts/setup-mac-mini.sh b/scripts/setup-mac-mini.sh new file mode 100644 index 0000000..40f609b --- /dev/null +++ b/scripts/setup-mac-mini.sh @@ -0,0 +1,99 @@ +#!/bin/bash + +# Mac Mini 홈 관리 시스템 설정 스크립트 + +echo "🏠 Mac Mini 홈 관리 시스템 설정을 시작합니다..." + +# 필요한 디렉토리 생성 +echo "📁 데이터 디렉토리 생성 중..." +mkdir -p /Users/hyungi/home-management-db +mkdir -p /Users/hyungi/home-management-redis +mkdir -p /Users/hyungi/home-management-data +mkdir -p ./logs +mkdir -p ./uploads + +# 권한 설정 +echo "🔐 권한 설정 중..." +chmod 755 /Users/hyungi/home-management-db +chmod 755 /Users/hyungi/home-management-redis +chmod 755 /Users/hyungi/home-management-data +chmod 755 ./logs +chmod 755 ./uploads + +# Docker 이미지 빌드 +echo "🐳 Docker 이미지 빌드 중..." +docker-compose -f docker-compose.mac-mini.yml build + +# 기존 컨테이너 정리 +echo "🧹 기존 컨테이너 정리 중..." +docker-compose -f docker-compose.mac-mini.yml down -v + +# 데이터베이스와 Redis 먼저 시작 +echo "🗄️ 데이터베이스 서비스 시작 중..." +docker-compose -f docker-compose.mac-mini.yml up -d mariadb redis + +# 데이터베이스 초기화 대기 +echo "⏳ 데이터베이스 초기화 대기 중 (60초)..." +sleep 60 + +# 데이터베이스 연결 테스트 +echo "🔍 데이터베이스 연결 테스트 중..." +until docker exec home_mariadb mysql -u homeuser -pmac_mini_home_password -e "SELECT 1" > /dev/null 2>&1; do + echo "데이터베이스 연결 대기 중..." + sleep 5 +done + +echo "✅ 데이터베이스 연결 성공!" + +# phpMyAdmin 시작 +echo "🌐 phpMyAdmin 시작 중..." +docker-compose -f docker-compose.mac-mini.yml up -d phpmyadmin + +# API 서버 시작 +echo "🚀 API 서버 시작 중..." +docker-compose -f docker-compose.mac-mini.yml up -d api + +# 서비스 상태 확인 +echo "📊 서비스 상태 확인 중..." +sleep 10 + +# 헬스체크 +echo "🏥 헬스체크 수행 중..." +for i in {1..30}; do + if curl -f http://localhost:3000/health > /dev/null 2>&1; then + echo "✅ API 서버가 정상적으로 시작되었습니다!" + break + fi + echo "API 서버 시작 대기 중... ($i/30)" + sleep 2 +done + +# 컨테이너 상태 출력 +echo "📋 컨테이너 상태:" +docker-compose -f docker-compose.mac-mini.yml ps + +# 접속 정보 출력 +echo "" +echo "🎉 Mac Mini 홈 관리 시스템 설정 완료!" +echo "" +echo "📊 접속 정보:" +echo "- API 서버: http://localhost:3000" +echo "- API 문서: http://localhost:3000/api" +echo "- phpMyAdmin: http://localhost:8080" +echo " - 사용자: homeuser" +echo " - 비밀번호: mac_mini_home_password" +echo "" +echo "🔍 테스트 명령어:" +echo "curl http://localhost:3000/health" +echo "curl http://localhost:3000/api/devices" +echo "" +echo "📝 로그 확인:" +echo "docker-compose -f docker-compose.mac-mini.yml logs -f api" +echo "" + +# 네트워크 정보 확인 +echo "🌐 네트워크 정보:" +ifconfig | grep "inet " | grep -v 127.0.0.1 | awk '{print "- " $2}' +echo "" +echo "💡 다른 기기에서 접속하려면 위 IP 주소를 사용하세요." +echo "예: http://[IP]:3000" \ No newline at end of file diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..08e1c94 --- /dev/null +++ b/src/app.js @@ -0,0 +1,90 @@ +const express = require('express'); +const cors = require('cors'); +const helmet = require('helmet'); +const compression = require('compression'); +const rateLimit = require('express-rate-limit'); +require('dotenv').config(); + +const logger = require('./utils/logger'); +const errorHandler = require('./middleware/errorHandler'); +const { testConnection } = require('./config/database'); + +const app = express(); +const PORT = process.env.API_PORT || 3000; + +// 보안 및 기본 미들웨어 +app.use(helmet()); +app.use(cors({ + origin: process.env.CORS_ORIGIN?.split(',') || 'http://localhost:3001', + credentials: true +})); +app.use(compression()); + +// 요청 제한 +const limiter = rateLimit({ + windowMs: parseInt(process.env.RATE_LIMIT_WINDOW) || 15 * 60 * 1000, + max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 100, + message: 'Too many requests from this IP' +}); +app.use(limiter); + +// 바디 파서 +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); + +// 로깅 미들웨어 +app.use((req, res, next) => { + logger.info(`${req.method} ${req.path} - ${req.ip}`); + next(); +}); + +// 헬스체크 +app.get('/health', (req, res) => { + res.json({ + status: 'healthy', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + environment: process.env.NODE_ENV + }); +}); + +// API 라우터 +app.use('/api', require('./routes')); + +// 에러 핸들링 +app.use(errorHandler); + +// 404 핸들러 +app.use('*', (req, res) => { + res.status(404).json({ + error: 'Route not found', + path: req.originalUrl + }); +}); + +// 서버 시작 +app.listen(PORT, process.env.API_HOST || '0.0.0.0', async () => { + logger.info(`Home Management API Server running on port ${PORT}`); + logger.info(`Environment: ${process.env.NODE_ENV}`); + + // 데이터베이스 연결 테스트 + try { + await testConnection(); + } catch (error) { + logger.error('Failed to connect to database'); + process.exit(1); + } +}); + +// 프로세스 종료 핸들링 +process.on('SIGINT', () => { + logger.info('Received SIGINT, shutting down gracefully'); + process.exit(0); +}); + +process.on('SIGTERM', () => { + logger.info('Received SIGTERM, shutting down gracefully'); + process.exit(0); +}); + +module.exports = app; \ No newline at end of file diff --git a/src/config/database.js b/src/config/database.js new file mode 100644 index 0000000..c0cbccb --- /dev/null +++ b/src/config/database.js @@ -0,0 +1,48 @@ +const { Sequelize } = require('sequelize'); +const logger = require('../utils/logger'); + +// 데이터베이스 연결 설정 +const sequelize = new Sequelize( + process.env.DB_NAME || 'home_management', + process.env.DB_USER || 'homeuser', + process.env.DB_PASSWORD || 'home_password', + { + host: process.env.DB_HOST || 'localhost', + port: process.env.DB_PORT || 3306, + dialect: 'mysql', + logging: (msg) => { + if (process.env.NODE_ENV === 'development') { + logger.debug(msg); + } + }, + pool: { + max: parseInt(process.env.DB_POOL_MAX) || 20, + min: parseInt(process.env.DB_POOL_MIN) || 5, + acquire: parseInt(process.env.DB_TIMEOUT) || 30000, + idle: 10000 + }, + dialectOptions: { + charset: 'utf8mb4', + timezone: '+09:00' + }, + define: { + charset: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + underscored: true, + timestamps: true + } + } +); + +// 데이터베이스 연결 테스트 +const testConnection = async () => { + try { + await sequelize.authenticate(); + logger.info('Database connection has been established successfully.'); + } catch (error) { + logger.error('Unable to connect to the database:', error); + throw error; + } +}; + +module.exports = { sequelize, testConnection }; \ No newline at end of file diff --git a/src/controllers/deviceController.js b/src/controllers/deviceController.js new file mode 100644 index 0000000..e7773fa --- /dev/null +++ b/src/controllers/deviceController.js @@ -0,0 +1,134 @@ +const { Device } = require('../models'); +const logger = require('../utils/logger'); + +// 모든 디바이스 조회 +const getAllDevices = async (req, res, next) => { + try { + const devices = await Device.findAll({ + order: [['created_at', 'DESC']] + }); + + res.json({ + success: true, + data: devices, + count: devices.length + }); + } catch (error) { + logger.error('Error fetching devices:', error); + next(error); + } +}; + +// 특정 디바이스 조회 +const getDeviceById = async (req, res, next) => { + try { + const { deviceId } = req.params; + + const device = await Device.findOne({ + where: { device_id: deviceId } + }); + + if (!device) { + return res.status(404).json({ + success: false, + error: 'Device not found' + }); + } + + res.json({ + success: true, + data: device + }); + } catch (error) { + logger.error('Error fetching device:', error); + next(error); + } +}; + +// 새 디바이스 생성 +const createDevice = async (req, res, next) => { + try { + const deviceData = req.body; + + const device = await Device.create(deviceData); + + logger.info(`New device created: ${device.device_id}`); + + res.status(201).json({ + success: true, + data: device + }); + } catch (error) { + logger.error('Error creating device:', error); + next(error); + } +}; + +// 디바이스 업데이트 +const updateDevice = async (req, res, next) => { + try { + const { deviceId } = req.params; + const updateData = req.body; + + const [updatedRows] = await Device.update(updateData, { + where: { device_id: deviceId } + }); + + if (updatedRows === 0) { + return res.status(404).json({ + success: false, + error: 'Device not found' + }); + } + + const updatedDevice = await Device.findOne({ + where: { device_id: deviceId } + }); + + logger.info(`Device updated: ${deviceId}`); + + res.json({ + success: true, + data: updatedDevice + }); + } catch (error) { + logger.error('Error updating device:', error); + next(error); + } +}; + +// 디바이스 삭제 +const deleteDevice = async (req, res, next) => { + try { + const { deviceId } = req.params; + + const deletedRows = await Device.destroy({ + where: { device_id: deviceId } + }); + + if (deletedRows === 0) { + return res.status(404).json({ + success: false, + error: 'Device not found' + }); + } + + logger.info(`Device deleted: ${deviceId}`); + + res.json({ + success: true, + message: 'Device deleted successfully' + }); + } catch (error) { + logger.error('Error deleting device:', error); + next(error); + } +}; + +module.exports = { + getAllDevices, + getDeviceById, + createDevice, + updateDevice, + deleteDevice +}; \ No newline at end of file diff --git a/src/middleware/errorHandler.js b/src/middleware/errorHandler.js new file mode 100644 index 0000000..f3bb2f2 --- /dev/null +++ b/src/middleware/errorHandler.js @@ -0,0 +1,60 @@ +const logger = require('../utils/logger'); + +const errorHandler = (err, req, res, next) => { + let error = { ...err }; + error.message = err.message; + + // 에러 로깅 + logger.error(`Error: ${err.message}`, { + stack: err.stack, + url: req.originalUrl, + method: req.method, + ip: req.ip, + userAgent: req.get('User-Agent') + }); + + // Sequelize 검증 오류 + if (err.name === 'SequelizeValidationError') { + const message = err.errors.map(error => error.message).join(', '); + error = { + message, + statusCode: 400 + }; + } + + // Sequelize 고유 제약 조건 오류 + if (err.name === 'SequelizeUniqueConstraintError') { + const message = 'Duplicate field value entered'; + error = { + message, + statusCode: 400 + }; + } + + // JWT 오류 + if (err.name === 'JsonWebTokenError') { + const message = 'Invalid token'; + error = { + message, + statusCode: 401 + }; + } + + // JWT 만료 오류 + if (err.name === 'TokenExpiredError') { + const message = 'Token expired'; + error = { + message, + statusCode: 401 + }; + } + + // 기본 에러 응답 + res.status(error.statusCode || 500).json({ + success: false, + error: error.message || 'Server Error', + ...(process.env.NODE_ENV === 'development' && { stack: err.stack }) + }); +}; + +module.exports = errorHandler; \ No newline at end of file diff --git a/src/models/Device.js b/src/models/Device.js new file mode 100644 index 0000000..d624da5 --- /dev/null +++ b/src/models/Device.js @@ -0,0 +1,87 @@ +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../config/database'); + +const Device = sequelize.define('Device', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + device_id: { + type: DataTypes.STRING(50), + unique: true, + allowNull: false, + validate: { + notEmpty: true, + len: [1, 50] + } + }, + name: { + type: DataTypes.STRING(100), + allowNull: false, + validate: { + notEmpty: true, + len: [1, 100] + } + }, + device_type: { + type: DataTypes.ENUM('server', 'nas', 'router', 'smart_plug', 'other'), + allowNull: false + }, + location: { + type: DataTypes.STRING(50), + allowNull: true + }, + ip_address: { + type: DataTypes.STRING(45), + allowNull: true, + validate: { + isIP: true + } + }, + mac_address: { + type: DataTypes.STRING(17), + allowNull: true, + validate: { + is: /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/ + } + }, + power_rating_watts: { + type: DataTypes.INTEGER, + allowNull: true, + validate: { + min: 0 + } + }, + monitoring_enabled: { + type: DataTypes.BOOLEAN, + defaultValue: true, + allowNull: false + }, + metadata: { + type: DataTypes.JSON, + allowNull: true + } +}, { + tableName: 'devices', + indexes: [ + { + unique: true, + fields: ['device_id'] + }, + { + fields: ['device_type', 'monitoring_enabled'] + } + ] +}); + +// 모델 관계 설정 +Device.associate = (models) => { + Device.hasMany(models.PowerConsumption, { + foreignKey: 'device_id', + sourceKey: 'device_id', + onDelete: 'CASCADE' + }); +}; + +module.exports = Device; \ No newline at end of file diff --git a/src/models/PowerConsumption.js b/src/models/PowerConsumption.js new file mode 100644 index 0000000..ba72901 --- /dev/null +++ b/src/models/PowerConsumption.js @@ -0,0 +1,79 @@ +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../config/database'); + +const PowerConsumption = sequelize.define('PowerConsumption', { + id: { + type: DataTypes.BIGINT, + primaryKey: true, + autoIncrement: true + }, + device_id: { + type: DataTypes.STRING(50), + allowNull: false, + references: { + model: 'devices', + key: 'device_id' + } + }, + timestamp: { + type: DataTypes.DATE(3), + defaultValue: DataTypes.NOW, + allowNull: false + }, + watts: { + type: DataTypes.DECIMAL(8, 2), + allowNull: false, + validate: { + min: 0 + } + }, + voltage: { + type: DataTypes.DECIMAL(6, 2), + allowNull: true, + validate: { + min: 0 + } + }, + current: { + type: DataTypes.DECIMAL(6, 3), + allowNull: true, + validate: { + min: 0 + } + }, + kwh_total: { + type: DataTypes.DECIMAL(10, 4), + allowNull: true, + validate: { + min: 0 + } + }, + metadata: { + type: DataTypes.JSON, + allowNull: true + } +}, { + tableName: 'power_consumption', + timestamps: false, + indexes: [ + { + fields: ['device_id', 'timestamp'] + }, + { + fields: ['timestamp'] + }, + { + fields: ['watts'] + } + ] +}); + +// 모델 관계 설정 +PowerConsumption.associate = (models) => { + PowerConsumption.belongsTo(models.Device, { + foreignKey: 'device_id', + targetKey: 'device_id' + }); +}; + +module.exports = PowerConsumption; \ No newline at end of file diff --git a/src/models/User.js b/src/models/User.js new file mode 100644 index 0000000..1d9e9a4 --- /dev/null +++ b/src/models/User.js @@ -0,0 +1,82 @@ +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../config/database'); + +const User = sequelize.define('User', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + username: { + type: DataTypes.STRING(50), + unique: true, + allowNull: false, + validate: { + notEmpty: true, + len: [3, 50], + isAlphanumeric: true + } + }, + email: { + type: DataTypes.STRING(100), + unique: true, + allowNull: true, + validate: { + isEmail: true + } + }, + password_hash: { + type: DataTypes.STRING(255), + allowNull: true + }, + full_name: { + type: DataTypes.STRING(100), + allowNull: true, + validate: { + len: [0, 100] + } + }, + role: { + type: DataTypes.ENUM('admin', 'family', 'guest'), + defaultValue: 'family', + allowNull: false + }, + preferences: { + type: DataTypes.JSON, + allowNull: true + }, + last_login: { + type: DataTypes.DATE, + allowNull: true + }, + is_active: { + type: DataTypes.BOOLEAN, + defaultValue: true, + allowNull: false + } +}, { + tableName: 'users', + indexes: [ + { + unique: true, + fields: ['username'] + }, + { + unique: true, + fields: ['email'] + }, + { + fields: ['role'] + } + ] +}); + +// 모델 관계 설정 +User.associate = (models) => { + User.hasMany(models.AlertRule, { + foreignKey: 'created_by', + onDelete: 'SET NULL' + }); +}; + +module.exports = User; \ No newline at end of file diff --git a/src/models/index.js b/src/models/index.js new file mode 100644 index 0000000..1ca50e8 --- /dev/null +++ b/src/models/index.js @@ -0,0 +1,23 @@ +const { sequelize } = require('../config/database'); + +// 모델 임포트 +const Device = require('./Device'); +const PowerConsumption = require('./PowerConsumption'); +const User = require('./User'); + +// 모델 객체 +const models = { + Device, + PowerConsumption, + User, + sequelize +}; + +// 관계 설정 +Object.keys(models).forEach(modelName => { + if (models[modelName].associate) { + models[modelName].associate(models); + } +}); + +module.exports = models; \ No newline at end of file diff --git a/src/routes/devices.js b/src/routes/devices.js new file mode 100644 index 0000000..4551073 --- /dev/null +++ b/src/routes/devices.js @@ -0,0 +1,20 @@ +const express = require('express'); +const router = express.Router(); +const deviceController = require('../controllers/deviceController'); + +// GET /api/devices - 모든 디바이스 조회 +router.get('/', deviceController.getAllDevices); + +// GET /api/devices/:deviceId - 특정 디바이스 조회 +router.get('/:deviceId', deviceController.getDeviceById); + +// POST /api/devices - 새 디바이스 생성 +router.post('/', deviceController.createDevice); + +// PUT /api/devices/:deviceId - 디바이스 업데이트 +router.put('/:deviceId', deviceController.updateDevice); + +// DELETE /api/devices/:deviceId - 디바이스 삭제 +router.delete('/:deviceId', deviceController.deleteDevice); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/index.js b/src/routes/index.js new file mode 100644 index 0000000..5b27f9a --- /dev/null +++ b/src/routes/index.js @@ -0,0 +1,30 @@ +const express = require('express'); +const router = express.Router(); + +// API 정보 +router.get('/', (req, res) => { + res.json({ + name: 'Home Management API', + version: '1.0.0', + description: '홈 관리 시스템 백엔드 API', + endpoints: { + health: '/health', + devices: '/api/devices', + power: '/api/power', + network: '/api/network', + system: '/api/system', + users: '/api/users', + alerts: '/api/alerts' + } + }); +}); + +// 라우터 연결 +router.use('/devices', require('./devices')); +// router.use('/power', require('./power')); +// router.use('/network', require('./network')); +// router.use('/system', require('./system')); +// router.use('/users', require('./users')); +// router.use('/alerts', require('./alerts')); + +module.exports = router; \ No newline at end of file diff --git a/src/utils/logger.js b/src/utils/logger.js new file mode 100644 index 0000000..7d5605e --- /dev/null +++ b/src/utils/logger.js @@ -0,0 +1,57 @@ +const winston = require('winston'); +const path = require('path'); + +// 로그 레벨 설정 +const logLevel = process.env.LOG_LEVEL || 'info'; + +// 로그 포맷 설정 +const logFormat = winston.format.combine( + winston.format.timestamp({ + format: 'YYYY-MM-DD HH:mm:ss' + }), + winston.format.errors({ stack: true }), + winston.format.json() +); + +// 콘솔 출력용 포맷 +const consoleFormat = winston.format.combine( + winston.format.colorize(), + winston.format.timestamp({ + format: 'HH:mm:ss' + }), + winston.format.printf(({ timestamp, level, message, stack }) => { + return stack + ? `${timestamp} [${level}]: ${message}\n${stack}` + : `${timestamp} [${level}]: ${message}`; + }) +); + +// 로거 생성 +const logger = winston.createLogger({ + level: logLevel, + format: logFormat, + defaultMeta: { service: 'home-management-api' }, + transports: [ + // 파일 로그 + new winston.transports.File({ + filename: path.join(process.cwd(), 'logs', 'error.log'), + level: 'error', + maxsize: 5242880, // 5MB + maxFiles: 5 + }), + new winston.transports.File({ + filename: path.join(process.cwd(), 'logs', 'combined.log'), + maxsize: 5242880, // 5MB + maxFiles: 5 + }) + ] +}); + +// 개발 환경에서는 콘솔 출력 추가 +if (process.env.NODE_ENV !== 'production') { + logger.add(new winston.transports.Console({ + format: consoleFormat + })); +} + +module.exports = logger; \ No newline at end of file