초기 홈 관리 시스템 API 구현
- Express.js 기반 백엔드 API 서버 - MariaDB, Redis, phpMyAdmin Docker 환경 - Device 관리 기본 CRUD 구현 - Mac Mini M4 Pro 전용 설정 및 배포 스크립트 - 자동화된 설치 및 배포 시스템 - 완전한 문서화 및 실행 가이드
This commit is contained in:
91
.gitignore
vendored
Normal file
91
.gitignore
vendored
Normal file
@@ -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-*
|
||||
39
Dockerfile
Normal file
39
Dockerfile
Normal file
@@ -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"]
|
||||
271
README.mac-mini.md
Normal file
271
README.mac-mini.md
Normal file
@@ -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의 성능을 최대한 활용하여 안정적인 홈 관리 시스템을 운영하세요! 🏠💻**
|
||||
270
README.md
Normal file
270
README.md
Normal file
@@ -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)에 등록해주세요.
|
||||
53
config/mariadb.cnf
Normal file
53
config/mariadb.cnf
Normal file
@@ -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
|
||||
52
config/redis.conf
Normal file
52
config/redis.conf
Normal file
@@ -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
|
||||
116
docker-compose.mac-mini.yml
Normal file
116
docker-compose.mac-mini.yml
Normal file
@@ -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
|
||||
94
docker-compose.yml
Normal file
94
docker-compose.yml
Normal file
@@ -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
|
||||
1665
home_management_db_spec.md
Normal file
1665
home_management_db_spec.md
Normal file
File diff suppressed because it is too large
Load Diff
56
package.json
Normal file
56
package.json
Normal file
@@ -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"
|
||||
}
|
||||
154
scripts/deploy-mac-mini.sh
Normal file
154
scripts/deploy-mac-mini.sh
Normal file
@@ -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"
|
||||
197
scripts/setup-db.sql
Normal file
197
scripts/setup-db.sql
Normal file
@@ -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;
|
||||
99
scripts/setup-mac-mini.sh
Normal file
99
scripts/setup-mac-mini.sh
Normal file
@@ -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"
|
||||
90
src/app.js
Normal file
90
src/app.js
Normal file
@@ -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;
|
||||
48
src/config/database.js
Normal file
48
src/config/database.js
Normal file
@@ -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 };
|
||||
134
src/controllers/deviceController.js
Normal file
134
src/controllers/deviceController.js
Normal file
@@ -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
|
||||
};
|
||||
60
src/middleware/errorHandler.js
Normal file
60
src/middleware/errorHandler.js
Normal file
@@ -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;
|
||||
87
src/models/Device.js
Normal file
87
src/models/Device.js
Normal file
@@ -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;
|
||||
79
src/models/PowerConsumption.js
Normal file
79
src/models/PowerConsumption.js
Normal file
@@ -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;
|
||||
82
src/models/User.js
Normal file
82
src/models/User.js
Normal file
@@ -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;
|
||||
23
src/models/index.js
Normal file
23
src/models/index.js
Normal file
@@ -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;
|
||||
20
src/routes/devices.js
Normal file
20
src/routes/devices.js
Normal file
@@ -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;
|
||||
30
src/routes/index.js
Normal file
30
src/routes/index.js
Normal file
@@ -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;
|
||||
57
src/utils/logger.js
Normal file
57
src/utils/logger.js
Normal file
@@ -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;
|
||||
Reference in New Issue
Block a user