초기 홈 관리 시스템 API 구현

- Express.js 기반 백엔드 API 서버
- MariaDB, Redis, phpMyAdmin Docker 환경
- Device 관리 기본 CRUD 구현
- Mac Mini M4 Pro 전용 설정 및 배포 스크립트
- 자동화된 설치 및 배포 시스템
- 완전한 문서화 및 실행 가이드
This commit is contained in:
Hyungi Ahn
2025-07-30 14:12:09 +09:00
commit 4b77086bb2
24 changed files with 3867 additions and 0 deletions

91
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

56
package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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 };

View 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
};

View 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
View 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;

View 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
View 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
View 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
View 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
View 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
View 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;