초기 커밋
This commit is contained in:
564
docs/API_REFERENCE.md
Normal file
564
docs/API_REFERENCE.md
Normal file
@@ -0,0 +1,564 @@
|
||||
# API 레퍼런스
|
||||
|
||||
> Base URL: `http://localhost:8000/api/v1`
|
||||
> 인증: `Authorization: Bearer <access_token>`
|
||||
|
||||
---
|
||||
|
||||
## 인증 (Auth)
|
||||
|
||||
### POST `/auth/register`
|
||||
|
||||
회원가입 후 토큰을 발급한다.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"password": "securepassword",
|
||||
"full_name": "홍길동"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (201):**
|
||||
```json
|
||||
{
|
||||
"access_token": "eyJ...",
|
||||
"refresh_token": "eyJ...",
|
||||
"token_type": "bearer"
|
||||
}
|
||||
```
|
||||
|
||||
**에러:**
|
||||
- `409` — 이미 등록된 이메일
|
||||
|
||||
---
|
||||
|
||||
### POST `/auth/login`
|
||||
|
||||
이메일/비밀번호로 로그인한다.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"password": "securepassword"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"access_token": "eyJ...",
|
||||
"refresh_token": "eyJ...",
|
||||
"token_type": "bearer"
|
||||
}
|
||||
```
|
||||
|
||||
**에러:**
|
||||
- `401` — 잘못된 이메일/비밀번호 또는 비활성 계정
|
||||
|
||||
---
|
||||
|
||||
### POST `/auth/refresh`
|
||||
|
||||
리프레시 토큰으로 새 토큰 쌍을 발급한다. 기존 리프레시 토큰은 폐기된다.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"refresh_token": "eyJ..."
|
||||
}
|
||||
```
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"access_token": "eyJ...(new)",
|
||||
"refresh_token": "eyJ...(new)",
|
||||
"token_type": "bearer"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST `/auth/logout`
|
||||
|
||||
현재 사용자의 모든 리프레시 토큰을 폐기한다.
|
||||
|
||||
**Headers:** `Authorization: Bearer <access_token>`
|
||||
|
||||
**Response:** `204 No Content`
|
||||
|
||||
---
|
||||
|
||||
## 사용자 (Users)
|
||||
|
||||
### GET `/users/me`
|
||||
|
||||
현재 로그인한 사용자의 정보를 조회한다.
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"email": "user@example.com",
|
||||
"role": "user",
|
||||
"is_active": true,
|
||||
"is_verified": false,
|
||||
"full_name": "홍길동",
|
||||
"phone": "010-1234-5678",
|
||||
"organization": "ACME Corp",
|
||||
"avatar_url": "",
|
||||
"created_at": "2025-01-01T00:00:00"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### PATCH `/users/me`
|
||||
|
||||
자신의 프로필 정보를 수정한다. `role`과 `is_active`는 변경할 수 없다.
|
||||
|
||||
**Request Body (partial):**
|
||||
```json
|
||||
{
|
||||
"full_name": "김철수",
|
||||
"phone": "010-9999-8888"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GET `/users`
|
||||
|
||||
사용자 목록을 페이징 조회한다.
|
||||
|
||||
**권한:** SUPERADMIN, ADMIN
|
||||
|
||||
**Query Parameters:**
|
||||
| 파라미터 | 타입 | 기본값 | 설명 |
|
||||
|---------|------|--------|------|
|
||||
| page | int | 1 | 페이지 번호 |
|
||||
| size | int | 20 | 페이지 크기 (max 100) |
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"items": [{ ... }],
|
||||
"total": 150,
|
||||
"page": 1,
|
||||
"size": 20,
|
||||
"pages": 8
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GET `/users/{user_id}`
|
||||
|
||||
특정 사용자를 조회한다.
|
||||
|
||||
**권한:** SUPERADMIN, ADMIN
|
||||
|
||||
---
|
||||
|
||||
### POST `/users`
|
||||
|
||||
사용자를 생성한다.
|
||||
|
||||
**권한:** SUPERADMIN, ADMIN
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"email": "new@example.com",
|
||||
"password": "password123",
|
||||
"role": "manager",
|
||||
"full_name": "박매니저",
|
||||
"phone": "010-1111-2222",
|
||||
"organization": "Dev Team"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### PATCH `/users/{user_id}`
|
||||
|
||||
사용자 정보를 수정한다. 관리자는 역할과 활성 상태도 변경할 수 있다.
|
||||
|
||||
**권한:** SUPERADMIN, ADMIN
|
||||
|
||||
---
|
||||
|
||||
### DELETE `/users/{user_id}`
|
||||
|
||||
사용자를 소프트 삭제한다.
|
||||
|
||||
**권한:** SUPERADMIN, ADMIN
|
||||
|
||||
**Response:** `204 No Content`
|
||||
|
||||
---
|
||||
|
||||
## 디바이스 (Devices)
|
||||
|
||||
### GET `/devices`
|
||||
|
||||
디바이스 목록을 페이징 조회한다.
|
||||
|
||||
**Query Parameters:**
|
||||
| 파라미터 | 타입 | 기본값 | 설명 |
|
||||
|---------|------|--------|------|
|
||||
| page | int | 1 | 페이지 번호 |
|
||||
| size | int | 20 | 페이지 크기 (max 100) |
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"device_uid": "sensor-temp-001",
|
||||
"name": "1층 온도센서",
|
||||
"device_type": "temperature",
|
||||
"status": "online",
|
||||
"firmware_version": "1.2.0",
|
||||
"ip_address": "192.168.1.100",
|
||||
"group_id": 1,
|
||||
"owner_id": 2,
|
||||
"last_seen_at": "2025-01-15T12:00:00",
|
||||
"created_at": "2025-01-01T00:00:00"
|
||||
}
|
||||
],
|
||||
"total": 50,
|
||||
"page": 1,
|
||||
"size": 20,
|
||||
"pages": 3
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GET `/devices/{device_id}`
|
||||
|
||||
특정 디바이스 상세를 조회한다.
|
||||
|
||||
---
|
||||
|
||||
### POST `/devices`
|
||||
|
||||
디바이스를 등록한다.
|
||||
|
||||
**권한:** SUPERADMIN, ADMIN, MANAGER
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"device_uid": "sensor-temp-001",
|
||||
"name": "1층 온도센서",
|
||||
"device_type": "temperature",
|
||||
"group_id": 1,
|
||||
"owner_id": 2,
|
||||
"firmware_version": "1.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
**에러:**
|
||||
- `409` — 이미 등록된 device_uid
|
||||
|
||||
---
|
||||
|
||||
### PATCH `/devices/{device_id}`
|
||||
|
||||
디바이스 정보를 수정한다.
|
||||
|
||||
**권한:** SUPERADMIN, ADMIN, MANAGER
|
||||
|
||||
**Request Body (partial):**
|
||||
```json
|
||||
{
|
||||
"name": "수정된 센서명",
|
||||
"status": "maintenance"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### DELETE `/devices/{device_id}`
|
||||
|
||||
디바이스를 소프트 삭제한다.
|
||||
|
||||
**권한:** SUPERADMIN, ADMIN
|
||||
|
||||
**Response:** `204 No Content`
|
||||
|
||||
---
|
||||
|
||||
## 모니터링 (Monitoring)
|
||||
|
||||
### GET `/monitoring/health`
|
||||
|
||||
시스템 상태를 상세 조회한다.
|
||||
|
||||
**권한:** SUPERADMIN, ADMIN, MANAGER
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"mariadb": "connected",
|
||||
"mongodb": "connected",
|
||||
"redis": "connected",
|
||||
"mqtt": "connected",
|
||||
"active_devices": 42,
|
||||
"active_alerts": 3
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GET `/monitoring/alerts`
|
||||
|
||||
미확인 알림 목록을 조회한다.
|
||||
|
||||
**권한:** SUPERADMIN, ADMIN, MANAGER
|
||||
|
||||
**Query Parameters:**
|
||||
| 파라미터 | 타입 | 기본값 | 설명 |
|
||||
|---------|------|--------|------|
|
||||
| skip | int | 0 | 건너뛸 수 |
|
||||
| limit | int | 50 | 최대 개수 (max 200) |
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"rule_id": 3,
|
||||
"device_id": 5,
|
||||
"severity": "warning",
|
||||
"message": "Temperature exceeded threshold",
|
||||
"is_acknowledged": false,
|
||||
"acknowledged_by": null,
|
||||
"acknowledged_at": null,
|
||||
"created_at": "2025-01-15T10:30:00"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST `/monitoring/alerts/{alert_id}/acknowledge`
|
||||
|
||||
알림을 확인 처리한다.
|
||||
|
||||
---
|
||||
|
||||
### GET `/monitoring/alert-rules`
|
||||
|
||||
알림 규칙 목록을 조회한다.
|
||||
|
||||
---
|
||||
|
||||
### POST `/monitoring/alert-rules`
|
||||
|
||||
알림 규칙을 생성한다.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "고온 경고",
|
||||
"description": "온도가 40도를 초과하면 알림",
|
||||
"metric": "temperature",
|
||||
"condition": "gt",
|
||||
"threshold": 40.0,
|
||||
"severity": "warning",
|
||||
"device_group_id": 1
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 분석 (Analytics)
|
||||
|
||||
### GET `/analytics/telemetry/{device_id}`
|
||||
|
||||
특정 디바이스의 텔레메트리 데이터를 시간 간격별로 집계한다.
|
||||
|
||||
**권한:** SUPERADMIN, ADMIN, MANAGER
|
||||
|
||||
**Query Parameters:**
|
||||
| 파라미터 | 타입 | 필수 | 설명 |
|
||||
|---------|------|------|------|
|
||||
| start | datetime | O | 시작 시각 (ISO 8601) |
|
||||
| end | datetime | O | 종료 시각 (ISO 8601) |
|
||||
| interval | string | X | 집계 간격 (기본값: `1h`) |
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"device_id": "sensor-temp-001",
|
||||
"records": [
|
||||
{"timestamp": "2025-01-15T10:00:00", "temperature": 23.5, "humidity": 45.2},
|
||||
{"timestamp": "2025-01-15T11:00:00", "temperature": 24.1, "humidity": 44.8}
|
||||
],
|
||||
"count": 2
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST `/analytics/reports/{device_id}`
|
||||
|
||||
디바이스 종합 리포트를 생성한다 (상태 분석 + 추세 분석).
|
||||
|
||||
**Query Parameters:** `start`, `end` (ISO 8601)
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"report_id": "65a1b2c3...",
|
||||
"device_id": "sensor-temp-001",
|
||||
"status": {
|
||||
"total_events": 150,
|
||||
"status_counts": {"online": 140, "offline": 10},
|
||||
"uptime_ratio": 0.9333
|
||||
},
|
||||
"trends": {
|
||||
"count": 720,
|
||||
"mean": 23.5,
|
||||
"std": 1.2,
|
||||
"min": 20.1,
|
||||
"max": 28.3,
|
||||
"slope": 0.0023,
|
||||
"trend": "stable"
|
||||
},
|
||||
"created_at": "2025-01-15T12:00:00"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GET `/analytics/status/{device_id}`
|
||||
|
||||
디바이스 상태 변경 이력을 분석한다.
|
||||
|
||||
---
|
||||
|
||||
### GET `/analytics/trends/{device_id}`
|
||||
|
||||
텔레메트리 데이터의 추세를 분석한다 (선형 회귀).
|
||||
|
||||
---
|
||||
|
||||
### GET `/analytics/results`
|
||||
|
||||
저장된 분석 결과를 조회한다.
|
||||
|
||||
**Query Parameters:**
|
||||
| 파라미터 | 타입 | 필수 | 설명 |
|
||||
|---------|------|------|------|
|
||||
| analysis_type | string | O | 분석 유형 |
|
||||
| device_id | string | X | 디바이스 ID |
|
||||
| skip | int | X | 건너뛸 수 (기본 0) |
|
||||
| limit | int | X | 최대 개수 (기본 20) |
|
||||
|
||||
---
|
||||
|
||||
## 시스템 (System)
|
||||
|
||||
### GET `/system/health`
|
||||
|
||||
서비스 헬스체크. 인증 불필요.
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"service": "core-api",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GET `/system/info`
|
||||
|
||||
시스템 설정 정보를 조회한다.
|
||||
|
||||
**권한:** SUPERADMIN
|
||||
|
||||
---
|
||||
|
||||
## 공통 에러 응답
|
||||
|
||||
모든 에러는 다음 형식으로 반환된다:
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "에러 메시지"
|
||||
}
|
||||
```
|
||||
|
||||
| 상태 코드 | 설명 |
|
||||
|----------|------|
|
||||
| 401 | 인증 실패 (토큰 없음/만료/유효하지 않음) |
|
||||
| 403 | 권한 부족 |
|
||||
| 404 | 리소스를 찾을 수 없음 |
|
||||
| 409 | 리소스 충돌 (중복) |
|
||||
| 422 | 요청 데이터 검증 실패 |
|
||||
| 429 | 요청 속도 제한 초과 |
|
||||
| 500 | 서버 내부 오류 |
|
||||
|
||||
---
|
||||
|
||||
## Socket.IO 이벤트
|
||||
|
||||
> 연결: `io("http://localhost:8000", { path: "/socket.io/" })`
|
||||
|
||||
### 네임스페이스: `/monitoring`
|
||||
|
||||
| 이벤트 | 방향 | 데이터 | 설명 |
|
||||
|--------|------|--------|------|
|
||||
| `subscribe_device` | Client → Server | `{ device_uid }` | 디바이스 모니터링 구독 |
|
||||
| `unsubscribe_device` | Client → Server | `{ device_uid }` | 구독 해제 |
|
||||
| `telemetry` | Server → Client | `{ device_uid, data }` | 실시간 텔레메트리 |
|
||||
| `device_status` | Server → Client | `{ device_uid, status }` | 상태 변경 |
|
||||
|
||||
### 네임스페이스: `/device`
|
||||
|
||||
| 이벤트 | 방향 | 데이터 | 설명 |
|
||||
|--------|------|--------|------|
|
||||
| `send_command` | Client → Server | `{ device_uid, command }` | 디바이스 명령 전송 |
|
||||
| `device_response` | Server → Client | `{ device_uid, data }` | 명령 응답 |
|
||||
|
||||
### 네임스페이스: `/notification`
|
||||
|
||||
| 이벤트 | 방향 | 데이터 | 설명 |
|
||||
|--------|------|--------|------|
|
||||
| `join_user_room` | Client → Server | `{ user_id }` | 알림 수신 등록 |
|
||||
| `notification` | Server → Client | `{ title, message, type }` | 알림 푸시 |
|
||||
|
||||
---
|
||||
|
||||
## MQTT 토픽
|
||||
|
||||
> 브로커: `mqtt://localhost:1883`
|
||||
|
||||
### Device → Server
|
||||
|
||||
| 토픽 | 페이로드 | 처리 |
|
||||
|------|---------|------|
|
||||
| `devices/{uid}/telemetry` | `{ temperature: 23.5, ... }` | MongoDB 저장 + Socket.IO 브로드캐스트 |
|
||||
| `devices/{uid}/status` | `{ status: "online" }` | MongoDB 로그 + Socket.IO 전송 |
|
||||
| `devices/{uid}/log` | `{ event_type: "...", ... }` | MongoDB 저장 |
|
||||
| `devices/{uid}/response` | `{ command_id: "...", ... }` | Socket.IO 전송 |
|
||||
|
||||
### Server → Device
|
||||
|
||||
| 토픽 | 페이로드 | 용도 |
|
||||
|------|---------|------|
|
||||
| `devices/{uid}/command` | `{ action: "restart" }` | 원격 명령 |
|
||||
| `devices/{uid}/config` | `{ interval: 30 }` | 설정 변경 |
|
||||
| `devices/{uid}/ota` | `{ url: "...", action: "update" }` | 펌웨어 업데이트 |
|
||||
595
docs/ARCHITECTURE.md
Normal file
595
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,595 @@
|
||||
# core-api 아키텍처 문서
|
||||
|
||||
## 1. 개요
|
||||
|
||||
IoT 임베디드 디바이스 통신, 사용자/관리자 관리, 모니터링, 데이터 분석을 지원하는 FastAPI 기반 통합 백엔드 서버이다. 향후 MSA 전환을 고려한 논리적 계층 분리를 핵심 설계 원칙으로 한다.
|
||||
|
||||
**주요 특징:**
|
||||
- Flutter 웹/앱 프론트엔드 연동
|
||||
- MariaDB + MongoDB 듀얼 DB
|
||||
- MQTT + Socket.IO 실시간 통신
|
||||
- Celery 비동기 태스크 처리
|
||||
- RBAC 기반 접근 제어
|
||||
|
||||
---
|
||||
|
||||
## 2. 계층 아키텍처
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ASGI Entry Point │
|
||||
│ Socket.IO ASGIApp (최외곽 래퍼) │
|
||||
│ app/asgi.py │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Middleware Stack │
|
||||
│ RequestIDMiddleware → RequestLoggingMiddleware → CORS │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ API Layer │
|
||||
│ app/api/v1/endpoints/*.py │
|
||||
│ auth │ users │ devices │ monitoring │ analytics │ system│
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Service Layer │
|
||||
│ app/services/*.py │
|
||||
│ AuthService │ UserService │ DeviceService │ ... │
|
||||
├───────────────────────┬─────────────────────────────────────┤
|
||||
│ Repository Layer │ Communication Layer │
|
||||
│ app/repositories/*.py │ app/communication/ │
|
||||
│ BaseRepository[T] │ mqtt/ │ socketio/ │ external/ │
|
||||
├───────────────────────┴─────────────────────────────────────┤
|
||||
│ DB Layer │
|
||||
│ MariaDB │ MongoDB │ Redis │
|
||||
│ app/db/mariadb.py │ mongodb.py │ redis.py │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│ │
|
||||
┌───────┴───────┐ ┌────────┴────────┐
|
||||
│ Celery Tasks │ │ Data Processing │
|
||||
│ app/tasks/*.py │ │ app/processing/ │
|
||||
└────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
### 의존성 규칙
|
||||
|
||||
```
|
||||
API Layer → Service Layer → Repository Layer → DB Layer
|
||||
│
|
||||
Communication Layer (MQTT, Socket.IO, External API)
|
||||
│
|
||||
Task Layer (Celery)
|
||||
```
|
||||
|
||||
- 각 계층은 **바로 아래 계층만** 의존한다.
|
||||
- 도메인 간 직접 Repository 호출을 금지하고, 반드시 Service 인터페이스를 통해 통신한다.
|
||||
- 이 규칙을 지키면 향후 MSA 전환 시 Service 인터페이스를 gRPC/HTTP 호출로 교체하는 것만으로 분리가 가능하다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 디렉토리 구조
|
||||
|
||||
```
|
||||
python-api/
|
||||
├── pyproject.toml # 의존성 + 빌드 설정
|
||||
├── .env.example # 환경변수 템플릿
|
||||
├── alembic.ini # Alembic 마이그레이션 설정
|
||||
├── Dockerfile # 앱 이미지
|
||||
├── Dockerfile.worker # Celery 워커 이미지
|
||||
├── docker-compose.yml # 개발 환경
|
||||
├── docker-compose.prod.yml # 프로덕션 오버라이드
|
||||
│
|
||||
├── alembic/ # DB 마이그레이션
|
||||
│ ├── env.py
|
||||
│ ├── script.py.mako
|
||||
│ └── versions/
|
||||
│
|
||||
├── scripts/ # 운영 스크립트
|
||||
│ ├── init_db.py # DB 시드 데이터
|
||||
│ ├── create_superuser.py # 관리자 계정 생성
|
||||
│ ├── run_dev.sh
|
||||
│ ├── run_worker.sh
|
||||
│ └── run_beat.sh
|
||||
│
|
||||
├── docker/
|
||||
│ └── mosquitto/mosquitto.conf
|
||||
│
|
||||
├── tests/ # 테스트
|
||||
│ ├── conftest.py
|
||||
│ ├── unit/
|
||||
│ ├── integration/
|
||||
│ └── e2e/
|
||||
│
|
||||
├── docs/ # 문서
|
||||
│
|
||||
└── app/ # 애플리케이션 루트
|
||||
├── main.py # FastAPI 앱 팩토리 + 라이프사이클
|
||||
├── asgi.py # ASGI 엔트리포인트
|
||||
├── core/ # 횡단 관심사
|
||||
├── middleware/ # HTTP 미들웨어
|
||||
├── db/ # DB 연결/세션
|
||||
├── models/ # DB 모델 (mariadb/ + mongodb/)
|
||||
├── schemas/ # Pydantic DTO
|
||||
├── repositories/ # 데이터 접근 계층
|
||||
├── services/ # 비즈니스 로직
|
||||
├── api/ # API 라우터
|
||||
├── admin/ # SQLAdmin 관리자 패널
|
||||
├── communication/ # MQTT, Socket.IO, 외부 API
|
||||
├── tasks/ # Celery 비동기 태스크
|
||||
├── processing/ # 데이터 분석 파이프라인
|
||||
└── utils/ # 범용 유틸리티
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 기술 스택
|
||||
|
||||
| 카테고리 | 기술 | 용도 |
|
||||
|---------|------|------|
|
||||
| **Web Framework** | FastAPI + Uvicorn | ASGI 비동기 웹 서버 |
|
||||
| **MariaDB ORM** | SQLModel + SQLAlchemy(async) + aiomysql | 관계형 DB 비동기 ORM |
|
||||
| **Migration** | Alembic + pymysql | DB 스키마 마이그레이션 |
|
||||
| **MongoDB ODM** | Beanie + Motor | 문서 DB 비동기 ODM |
|
||||
| **Cache/Queue** | Redis (hiredis) | 캐싱 + Celery 브로커/백엔드 |
|
||||
| **Auth** | python-jose + passlib[bcrypt] | JWT 토큰 + 비밀번호 해싱 |
|
||||
| **MQTT** | fastapi-mqtt | IoT 디바이스 양방향 통신 |
|
||||
| **WebSocket** | python-socketio | 실시간 프론트엔드 푸시 |
|
||||
| **Background** | Celery + Flower | 비동기 태스크 + 모니터링 |
|
||||
| **Admin** | SQLAdmin | 관리자 대시보드 |
|
||||
| **Data** | Polars + Pandas + NumPy | 데이터 분석/집계/통계 |
|
||||
| **Logging** | structlog | 구조화 로깅 (JSON/Console) |
|
||||
| **Test** | pytest + pytest-asyncio + factory-boy | 테스트 프레임워크 |
|
||||
| **Lint** | Ruff + mypy + pre-commit | 코드 품질 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 데이터 모델
|
||||
|
||||
### 5.1 MariaDB (관계형 — SQLModel)
|
||||
|
||||
```
|
||||
┌──────────────┐ 1:1 ┌──────────────┐
|
||||
│ User │────────────▶│ UserProfile │
|
||||
│──────────────│ │──────────────│
|
||||
│ id (PK) │ │ user_id (FK) │
|
||||
│ email │ │ full_name │
|
||||
│ hashed_pwd │ │ phone │
|
||||
│ role │ │ organization │
|
||||
│ is_active │ │ avatar_url │
|
||||
│ is_verified │ └───────────────┘
|
||||
│ last_login_at│
|
||||
└──────┬───────┘
|
||||
│ 1:N
|
||||
▼
|
||||
┌──────────────┐ ┌───────────────┐
|
||||
│ RefreshToken │ │ OAuthAccount │
|
||||
│──────────────│ │───────────────│
|
||||
│ user_id (FK) │ │ user_id (FK) │
|
||||
│ token │ │ provider │
|
||||
│ expires_at │ │ provider_uid │
|
||||
│ is_revoked │ │ access_token │
|
||||
└──────────────┘ └───────────────┘
|
||||
|
||||
┌──────────────┐ N:1 ┌──────────────┐
|
||||
│ Device │────────────▶│ DeviceGroup │
|
||||
│──────────────│ │──────────────│
|
||||
│ device_uid │ │ name │
|
||||
│ name │ │ description │
|
||||
│ device_type │ └──────────────┘
|
||||
│ status │
|
||||
│ firmware_ver │
|
||||
│ ip_address │
|
||||
│ owner_id(FK) │
|
||||
│ last_seen_at │
|
||||
└──────────────┘
|
||||
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ AlertRule │ │ Alert │
|
||||
│──────────────│ │──────────────│
|
||||
│ metric │◀────────────│ rule_id (FK) │
|
||||
│ condition │ │ device_id(FK)│
|
||||
│ threshold │ │ severity │
|
||||
│ severity │ │ message │
|
||||
│ is_enabled │ │ is_ack │
|
||||
└──────────────┘ └──────────────┘
|
||||
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ SystemConfig │ │ AuditLog │
|
||||
│──────────────│ │──────────────│
|
||||
│ key (unique) │ │ user_id (FK) │
|
||||
│ value │ │ action │
|
||||
│ is_secret │ │ resource_type│
|
||||
└──────────────┘ │ details │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
**공통 Mixin:**
|
||||
- `TimestampMixin` — `created_at`, `updated_at` (서버 기본값 + 자동 갱신)
|
||||
- `SoftDeleteMixin` — `is_deleted`, `deleted_at` (논리 삭제)
|
||||
|
||||
### 5.2 MongoDB (문서형 — Beanie)
|
||||
|
||||
| Collection | 주요 필드 | 인덱스 | 비고 |
|
||||
|-----------|----------|--------|------|
|
||||
| `device_logs` | device_id, event_type, payload, timestamp | device_id, event_type, timestamp(desc) | TTL: 90일 |
|
||||
| `telemetry_data` | device_id, metrics(dict), timestamp | device_id, timestamp(desc), 복합(device_id+timestamp) | 시계열 |
|
||||
| `analytics_results` | analysis_type, parameters, result, device_id, period | analysis_type, device_id, created_at(desc) | 분석 결과 |
|
||||
| `notifications` | user_id, title, message, type, is_read | user_id, 복합(user_id+is_read), created_at(desc) | 사용자 알림 |
|
||||
|
||||
---
|
||||
|
||||
## 6. API 엔드포인트
|
||||
|
||||
### 6.1 인증 (`/api/v1/auth`)
|
||||
|
||||
| Method | Path | 설명 | 인증 |
|
||||
|--------|------|------|------|
|
||||
| POST | `/register` | 회원가입 → 토큰 반환 | - |
|
||||
| POST | `/login` | 로그인 → 토큰 반환 | - |
|
||||
| POST | `/refresh` | 리프레시 토큰으로 갱신 | - |
|
||||
| POST | `/logout` | 모든 리프레시 토큰 폐기 | Bearer |
|
||||
|
||||
### 6.2 사용자 (`/api/v1/users`)
|
||||
|
||||
| Method | Path | 설명 | 권한 |
|
||||
|--------|------|------|------|
|
||||
| GET | `/me` | 내 정보 조회 | 인증됨 |
|
||||
| PATCH | `/me` | 내 프로필 수정 | 인증됨 |
|
||||
| GET | `/` | 사용자 목록 (페이징) | SUPERADMIN, ADMIN |
|
||||
| GET | `/{user_id}` | 사용자 상세 | SUPERADMIN, ADMIN |
|
||||
| POST | `/` | 사용자 생성 | SUPERADMIN, ADMIN |
|
||||
| PATCH | `/{user_id}` | 사용자 수정 | SUPERADMIN, ADMIN |
|
||||
| DELETE | `/{user_id}` | 사용자 삭제 (소프트) | SUPERADMIN, ADMIN |
|
||||
|
||||
### 6.3 디바이스 (`/api/v1/devices`)
|
||||
|
||||
| Method | Path | 설명 | 권한 |
|
||||
|--------|------|------|------|
|
||||
| GET | `/` | 디바이스 목록 (페이징) | 인증됨 |
|
||||
| GET | `/{device_id}` | 디바이스 상세 | 인증됨 |
|
||||
| POST | `/` | 디바이스 등록 | SUPERADMIN, ADMIN, MANAGER |
|
||||
| PATCH | `/{device_id}` | 디바이스 수정 | SUPERADMIN, ADMIN, MANAGER |
|
||||
| DELETE | `/{device_id}` | 디바이스 삭제 (소프트) | SUPERADMIN, ADMIN |
|
||||
|
||||
### 6.4 모니터링 (`/api/v1/monitoring`)
|
||||
|
||||
| Method | Path | 설명 | 권한 |
|
||||
|--------|------|------|------|
|
||||
| GET | `/health` | 시스템 상태 상세 | MANAGEMENT |
|
||||
| GET | `/alerts` | 미확인 알림 목록 | MANAGEMENT |
|
||||
| POST | `/alerts/{id}/acknowledge` | 알림 확인 처리 | 인증됨 |
|
||||
| GET | `/alert-rules` | 알림 규칙 목록 | MANAGEMENT |
|
||||
| POST | `/alert-rules` | 알림 규칙 생성 | 인증됨 |
|
||||
|
||||
### 6.5 분석 (`/api/v1/analytics`)
|
||||
|
||||
| Method | Path | 설명 | 권한 |
|
||||
|--------|------|------|------|
|
||||
| GET | `/telemetry/{device_id}` | 텔레메트리 집계 | MANAGEMENT |
|
||||
| POST | `/reports/{device_id}` | 종합 리포트 생성 | MANAGEMENT |
|
||||
| GET | `/status/{device_id}` | 디바이스 상태 분석 | MANAGEMENT |
|
||||
| GET | `/trends/{device_id}` | 추세 분석 | MANAGEMENT |
|
||||
| GET | `/results` | 분석 결과 조회 | MANAGEMENT |
|
||||
|
||||
### 6.6 시스템 (`/api/v1/system`)
|
||||
|
||||
| Method | Path | 설명 | 권한 |
|
||||
|--------|------|------|------|
|
||||
| GET | `/health` | 헬스체크 | - |
|
||||
| GET | `/info` | 시스템 정보 | SUPERADMIN |
|
||||
|
||||
---
|
||||
|
||||
## 7. 인증/인가 체계
|
||||
|
||||
### 7.1 JWT 토큰 흐름
|
||||
|
||||
```
|
||||
클라이언트 서버
|
||||
│ │
|
||||
│─── POST /auth/login ──────────▶│
|
||||
│ {email, password} │
|
||||
│ │── 비밀번호 검증
|
||||
│ │── Access Token 생성 (30분)
|
||||
│ │── Refresh Token 생성 (7일)
|
||||
│ │── Refresh Token DB 저장
|
||||
│◀── {access_token, │
|
||||
│ refresh_token} ────────────│
|
||||
│ │
|
||||
│─── GET /api/v1/users/me ──────▶│
|
||||
│ Authorization: Bearer {AT} │── decode_token()
|
||||
│ │── get_current_user_payload()
|
||||
│◀── {user data} ───────────────│
|
||||
│ │
|
||||
│─── POST /auth/refresh ────────▶│
|
||||
│ {refresh_token} │── 기존 RT 폐기
|
||||
│ │── 새 AT + RT 발급
|
||||
│◀── {new tokens} ──────────────│
|
||||
```
|
||||
|
||||
### 7.2 역할 기반 접근 제어 (RBAC)
|
||||
|
||||
```
|
||||
SUPERADMIN (4) ─── 전체 시스템 관리, 시스템 설정
|
||||
│
|
||||
ADMIN (3) ─── 사용자/디바이스 관리, 삭제 권한
|
||||
│
|
||||
MANAGER (2) ─── 디바이스 등록/수정, 모니터링, 분석
|
||||
│
|
||||
USER (1) ─── 자기 프로필, 디바이스 조회
|
||||
│
|
||||
DEVICE (0) ─── 디바이스 전용 (MQTT 인증)
|
||||
```
|
||||
|
||||
- `require_role(*roles)` — 허용된 역할만 접근 가능한 Dependency
|
||||
- `can_manage_user(actor_role, target_role)` — 상위 역할만 하위 역할 관리 가능
|
||||
|
||||
---
|
||||
|
||||
## 8. 실시간 통신
|
||||
|
||||
### 8.1 MQTT (IoT 디바이스 ↔ 서버)
|
||||
|
||||
```
|
||||
┌──────────┐ ┌──────────────┐ ┌──────────┐
|
||||
│ Device │──MQTT───▶│ Mosquitto │──────────▶│ Server │
|
||||
│ │◀──MQTT───│ (Broker) │◀──────────│ │
|
||||
└──────────┘ └──────────────┘ └──────────┘
|
||||
```
|
||||
|
||||
**토픽 구조:**
|
||||
|
||||
| 방향 | 토픽 패턴 | 용도 |
|
||||
|------|----------|------|
|
||||
| Device → Server | `devices/{uid}/telemetry` | 센서 데이터 전송 |
|
||||
| Device → Server | `devices/{uid}/status` | 상태 변경 알림 |
|
||||
| Device → Server | `devices/{uid}/log` | 이벤트 로그 |
|
||||
| Device → Server | `devices/{uid}/response` | 명령 응답 |
|
||||
| Server → Device | `devices/{uid}/command` | 원격 명령 |
|
||||
| Server → Device | `devices/{uid}/config` | 설정 변경 |
|
||||
| Server → Device | `devices/{uid}/ota` | 펌웨어 업데이트 |
|
||||
|
||||
**데이터 파이프라인:**
|
||||
```
|
||||
MQTT 수신 → JSON 파싱 → MongoDB 저장 → Socket.IO 브로드캐스트
|
||||
```
|
||||
|
||||
### 8.2 Socket.IO (서버 → 프론트엔드)
|
||||
|
||||
| 네임스페이스 | 이벤트 | 설명 |
|
||||
|------------|--------|------|
|
||||
| `/monitoring` | `telemetry` | 실시간 텔레메트리 데이터 |
|
||||
| `/monitoring` | `device_status` | 디바이스 상태 변경 |
|
||||
| `/device` | `device_response` | 디바이스 명령 응답 |
|
||||
| `/device` | `send_command` | 클라이언트→디바이스 명령 전달 |
|
||||
| `/notification` | `notification` | 사용자 알림 푸시 |
|
||||
|
||||
**Room 기반 구독:**
|
||||
- `device:{device_uid}` — 특정 디바이스 모니터링
|
||||
- `user:{user_id}` — 특정 사용자 알림
|
||||
|
||||
---
|
||||
|
||||
## 9. 비동기 태스크 (Celery)
|
||||
|
||||
### 9.1 큐 구성
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ Redis │ ← Broker (redis://...6379/1)
|
||||
│ (Broker) │ ← Result Backend (redis://...6379/2)
|
||||
└──────┬──────┘
|
||||
│
|
||||
┌────┴────────────────────────────────┐
|
||||
│ Celery Workers │
|
||||
├─────────┬───────────┬───────────────┤
|
||||
│ default │ analytics │ notifications │ devices │
|
||||
│ queue │ queue │ queue │ queue │
|
||||
├─────────┼───────────┼───────────────┤
|
||||
│ 토큰정리 │ 일간분석 │ 푸시알림 │ 헬스체크 │
|
||||
│ 이메일 │ 디바이스분석 │ 대량알림 │ OTA배치 │
|
||||
└─────────┴───────────┴───────────────┘
|
||||
```
|
||||
|
||||
### 9.2 Beat 스케줄 (정기 실행)
|
||||
|
||||
| 태스크 | 스케줄 | 큐 | 설명 |
|
||||
|--------|--------|-----|------|
|
||||
| `cleanup_expired_tokens` | 매일 03:00 | default | 만료/폐기된 리프레시 토큰 삭제 |
|
||||
| `check_device_health` | 5분마다 | devices | 10분 이상 미응답 디바이스를 OFFLINE 처리 |
|
||||
| `run_daily_analytics` | 매일 01:00 | analytics | 전일 텔레메트리 데이터 집계 및 저장 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 미들웨어 파이프라인
|
||||
|
||||
요청은 아래 순서로 미들웨어를 통과한다:
|
||||
|
||||
```
|
||||
요청 ──▶ RequestIDMiddleware ──▶ RequestLoggingMiddleware ──▶ CORS ──▶ FastAPI Router
|
||||
│ │
|
||||
│ X-Request-ID 생성/전파 │ method, path, status, elapsed_ms 로깅
|
||||
│ structlog 컨텍스트 바인딩 │
|
||||
▼ ▼
|
||||
응답 ◀── X-Request-ID 헤더 추가 ◀── 로그 기록 ◀────────────────────── 핸들러 응답
|
||||
```
|
||||
|
||||
**RateLimitMiddleware** (선택 사용):
|
||||
- Redis 기반 IP별 속도 제한
|
||||
- 기본값: 100 요청/60초
|
||||
- `/docs`, `/redoc` 경로 제외
|
||||
|
||||
---
|
||||
|
||||
## 11. 데이터 분석 파이프라인
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌──────────────────┐ ┌────────────────┐
|
||||
│ TelemetryData │────▶│ Polars/Pandas │────▶│ AnalyticsResult│
|
||||
│ (MongoDB) │ │ Processing │ │ (MongoDB) │
|
||||
└─────────────────┘ └──────────────────┘ └────────────────┘
|
||||
│
|
||||
┌─────────┴──────────┐
|
||||
│ │
|
||||
┌──────┴──────┐ ┌──────┴──────┐
|
||||
│ Aggregation │ │ Analysis │
|
||||
│ (Polars) │ │ (NumPy) │
|
||||
│ │ │ │
|
||||
│ - resample │ │ - trend │
|
||||
│ - group_by │ │ - anomaly │
|
||||
│ - mean/sum │ │ - percentile│
|
||||
└─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
| 모듈 | 기능 |
|
||||
|------|------|
|
||||
| `telemetry_pipeline` | 시간 간격별 텔레메트리 집계 (Polars group_by_dynamic) |
|
||||
| `report_pipeline` | 디바이스 종합 리포트 생성 (상태 + 추세) |
|
||||
| `device_analyzer` | 디바이스 상태 변경 분석, 가동률 계산 |
|
||||
| `trend_analyzer` | 선형 회귀 기반 추세 분석 (NumPy polyfit) |
|
||||
| `statistics` | 이동평균, Z-score 이상치 탐지, 백분위 통계 |
|
||||
|
||||
---
|
||||
|
||||
## 12. MSA 전환 전략
|
||||
|
||||
현재 모놀리식 구조에서 각 도메인을 독립 서비스로 분리할 수 있는 논리적 경계가 이미 설정되어 있다.
|
||||
|
||||
| 도메인 | 현재 패키지 | 향후 MSA 서비스 | 주 DB |
|
||||
|--------|------------|----------------|-------|
|
||||
| auth | auth_service + auth_repo | Auth Service | MariaDB + Redis |
|
||||
| users | user_service + user_repo | User Service | MariaDB |
|
||||
| devices | device_service + device_repo | Device Service | MariaDB + MongoDB |
|
||||
| monitoring | monitoring_service + monitoring_repo | Monitoring Service | MongoDB + Redis |
|
||||
| analytics | analytics_service + analytics_repo | Analytics Service | MongoDB |
|
||||
| notification | notification_service + socketio | Notification Service | Redis + MongoDB |
|
||||
|
||||
**전환 시 변경점:**
|
||||
1. Service 간 호출을 HTTP/gRPC 클라이언트로 교체
|
||||
2. 각 서비스별 독립 DB 인스턴스 할당
|
||||
3. 이벤트 버스(Kafka/RabbitMQ)로 서비스 간 비동기 통신
|
||||
4. API Gateway 도입 (인증/라우팅 통합)
|
||||
|
||||
---
|
||||
|
||||
## 13. 배포 구성
|
||||
|
||||
### 13.1 Docker 서비스
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ Mosquitto │:1883 (MQTT)
|
||||
└──────┬──────┘
|
||||
│
|
||||
┌──────────┐ ┌──────┴──────┐ ┌──────────┐
|
||||
│ Redis │:6379 ◀─│ App │:8000 ──▶│ MariaDB │ (호스트)
|
||||
└────┬─────┘ │ (FastAPI) │ └──────────┘
|
||||
│ └──────┬──────┘ ┌──────────┐
|
||||
│ │ ────▶│ MongoDB │ (호스트)
|
||||
│ ┌──────┴──────┐ └──────────┘
|
||||
├──────────────│ Worker │
|
||||
│ │ (Celery) │
|
||||
│ └─────────────┘
|
||||
│ ┌─────────────┐
|
||||
├──────────────│ Beat │
|
||||
│ │ (Scheduler) │
|
||||
│ └─────────────┘
|
||||
│ ┌─────────────┐
|
||||
└──────────────│ Flower │:5555 (모니터링)
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
- MariaDB/MongoDB는 **호스트에 직접 설치** (Docker 외부)
|
||||
- App, Worker, Beat, Flower, Redis, Mosquitto는 **Docker 컨테이너**로 관리
|
||||
|
||||
### 13.2 환경별 차이
|
||||
|
||||
| 항목 | 개발 (docker-compose.yml) | 프로덕션 (docker-compose.prod.yml) |
|
||||
|------|--------------------------|-----------------------------------|
|
||||
| App 워커 수 | 1 (reload 모드) | 4 |
|
||||
| Celery 동시성 | 2 | 4 |
|
||||
| Worker 레플리카 | 1 | 2 |
|
||||
| 로그 레벨 | info | warning |
|
||||
| Flower 인증 | 없음 | basic_auth |
|
||||
| 볼륨 마운트 | 소스 코드 (.:/app) | 이미지 내장 |
|
||||
|
||||
---
|
||||
|
||||
## 14. 실행 방법
|
||||
|
||||
### 개발 환경
|
||||
|
||||
```bash
|
||||
# 1. 환경변수 설정
|
||||
cp .env.example .env
|
||||
# .env 파일에서 DB 접속 정보 등 수정
|
||||
|
||||
# 2. 의존성 설치
|
||||
pip install -e ".[dev]"
|
||||
|
||||
# 3. DB 마이그레이션
|
||||
alembic upgrade head
|
||||
|
||||
# 4. 시드 데이터
|
||||
python -m scripts.init_db
|
||||
|
||||
# 5. 슈퍼관리자 생성
|
||||
python -m scripts.create_superuser admin@example.com password123
|
||||
|
||||
# 6. 인프라 (Redis + Mosquitto)
|
||||
docker-compose up -d redis mosquitto
|
||||
|
||||
# 7. 앱 서버 실행
|
||||
uvicorn app.asgi:app --reload
|
||||
|
||||
# 8. Celery 워커 (별도 터미널)
|
||||
celery -A app.tasks.celery_app worker --loglevel=info -Q default,analytics,notifications,devices
|
||||
|
||||
# 9. Celery 스케줄러 (별도 터미널)
|
||||
celery -A app.tasks.celery_app beat --loglevel=info
|
||||
```
|
||||
|
||||
### Docker 전체 스택
|
||||
|
||||
```bash
|
||||
# 개발
|
||||
docker-compose up -d
|
||||
|
||||
# 프로덕션
|
||||
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### 검증
|
||||
|
||||
```bash
|
||||
# 헬스체크
|
||||
curl http://localhost:8000/api/v1/system/health
|
||||
|
||||
# Swagger UI
|
||||
open http://localhost:8000/docs
|
||||
|
||||
# Flower (Celery 모니터링)
|
||||
open http://localhost:5555
|
||||
|
||||
# SQLAdmin (관리자)
|
||||
open http://localhost:8000/admin
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15. 테스트
|
||||
|
||||
```bash
|
||||
# 전체 테스트
|
||||
pytest tests/ -v
|
||||
|
||||
# 커버리지
|
||||
pytest tests/ -v --cov=app --cov-report=html
|
||||
|
||||
# 단위 테스트만
|
||||
pytest tests/unit/ -v
|
||||
|
||||
# 통합 테스트만
|
||||
pytest tests/integration/ -v
|
||||
```
|
||||
|
||||
**테스트 구조:**
|
||||
- `tests/unit/` — 순수 함수 테스트 (보안, 검증, 통계, 권한)
|
||||
- `tests/integration/` — API 엔드포인트 + DB 연동 테스트
|
||||
- `tests/e2e/` — 전체 CRUD 플로우 테스트
|
||||
338
docs/DATABASE.md
Normal file
338
docs/DATABASE.md
Normal file
@@ -0,0 +1,338 @@
|
||||
# 데이터베이스 설계 문서
|
||||
|
||||
## 1. 개요
|
||||
|
||||
| DB | 용도 | 연결 방식 |
|
||||
|----|------|----------|
|
||||
| **MariaDB** | 사용자, 인증, 디바이스, 알림 규칙, 시스템 설정 | SQLModel + SQLAlchemy (async, aiomysql) |
|
||||
| **MongoDB** | 디바이스 로그, 텔레메트리, 분석 결과, 알림 | Beanie + Motor (async) |
|
||||
| **Redis** | 캐싱, 세션, 속도 제한, Celery 브로커 | redis-py (async, hiredis) |
|
||||
|
||||
---
|
||||
|
||||
## 2. MariaDB 스키마
|
||||
|
||||
### 2.1 users
|
||||
|
||||
사용자 계정 정보.
|
||||
|
||||
| 컬럼 | 타입 | 제약 | 설명 |
|
||||
|------|------|------|------|
|
||||
| id | INT | PK, AUTO_INCREMENT | |
|
||||
| email | VARCHAR(255) | UNIQUE, INDEX | 로그인 ID |
|
||||
| hashed_password | VARCHAR(255) | NOT NULL | bcrypt 해시 |
|
||||
| role | VARCHAR(20) | DEFAULT 'user' | superadmin/admin/manager/user/device |
|
||||
| is_active | BOOLEAN | DEFAULT TRUE | 계정 활성 여부 |
|
||||
| is_verified | BOOLEAN | DEFAULT FALSE | 이메일 인증 여부 |
|
||||
| last_login_at | DATETIME | NULLABLE | 마지막 로그인 |
|
||||
| is_deleted | BOOLEAN | DEFAULT FALSE | 소프트 삭제 |
|
||||
| deleted_at | DATETIME | NULLABLE | 삭제 시각 |
|
||||
| created_at | DATETIME | server_default=NOW() | |
|
||||
| updated_at | DATETIME | onupdate=NOW() | |
|
||||
|
||||
### 2.2 user_profiles
|
||||
|
||||
사용자 프로필 (1:1).
|
||||
|
||||
| 컬럼 | 타입 | 제약 | 설명 |
|
||||
|------|------|------|------|
|
||||
| id | INT | PK | |
|
||||
| user_id | INT | FK→users.id, UNIQUE | |
|
||||
| full_name | VARCHAR(100) | | 이름 |
|
||||
| phone | VARCHAR(20) | | 전화번호 |
|
||||
| organization | VARCHAR(100) | | 소속 |
|
||||
| avatar_url | VARCHAR(500) | | 프로필 이미지 |
|
||||
| created_at | DATETIME | | |
|
||||
| updated_at | DATETIME | | |
|
||||
|
||||
### 2.3 refresh_tokens
|
||||
|
||||
리프레시 토큰 저장소. 토큰 순환(rotation) 방식 사용.
|
||||
|
||||
| 컬럼 | 타입 | 제약 | 설명 |
|
||||
|------|------|------|------|
|
||||
| id | INT | PK | |
|
||||
| user_id | INT | FK→users.id, INDEX | |
|
||||
| token | VARCHAR(500) | UNIQUE, INDEX | JWT 리프레시 토큰 |
|
||||
| expires_at | DATETIME | NOT NULL | 만료 시각 |
|
||||
| is_revoked | BOOLEAN | DEFAULT FALSE | 폐기 여부 |
|
||||
| device_info | VARCHAR(255) | | 접속 디바이스 정보 |
|
||||
| created_at | DATETIME | | |
|
||||
| updated_at | DATETIME | | |
|
||||
|
||||
### 2.4 oauth_accounts
|
||||
|
||||
소셜 로그인 연동 계정.
|
||||
|
||||
| 컬럼 | 타입 | 제약 | 설명 |
|
||||
|------|------|------|------|
|
||||
| id | INT | PK | |
|
||||
| user_id | INT | FK→users.id, INDEX | |
|
||||
| provider | VARCHAR(50) | | google/kakao/naver |
|
||||
| provider_user_id | VARCHAR(255) | | 제공자 사용자 ID |
|
||||
| access_token | VARCHAR(500) | | OAuth 액세스 토큰 |
|
||||
| refresh_token | VARCHAR(500) | | OAuth 리프레시 토큰 |
|
||||
| expires_at | DATETIME | NULLABLE | 토큰 만료 |
|
||||
| created_at | DATETIME | | |
|
||||
| updated_at | DATETIME | | |
|
||||
|
||||
### 2.5 devices
|
||||
|
||||
IoT 디바이스 정보.
|
||||
|
||||
| 컬럼 | 타입 | 제약 | 설명 |
|
||||
|------|------|------|------|
|
||||
| id | INT | PK | |
|
||||
| device_uid | VARCHAR(100) | UNIQUE, INDEX | 디바이스 고유 식별자 |
|
||||
| name | VARCHAR(100) | | 디바이스 이름 |
|
||||
| device_type | VARCHAR(50) | | 센서 유형 (temperature, humidity 등) |
|
||||
| status | VARCHAR(20) | DEFAULT 'offline' | online/offline/error/maintenance |
|
||||
| firmware_version | VARCHAR(50) | | 펌웨어 버전 |
|
||||
| ip_address | VARCHAR(45) | | IPv4/IPv6 |
|
||||
| group_id | INT | FK→device_groups.id, NULLABLE | |
|
||||
| owner_id | INT | FK→users.id, NULLABLE | |
|
||||
| last_seen_at | DATETIME | NULLABLE | 마지막 통신 시각 |
|
||||
| metadata_json | VARCHAR(2000) | DEFAULT '{}' | 추가 메타데이터 |
|
||||
| is_deleted | BOOLEAN | DEFAULT FALSE | |
|
||||
| deleted_at | DATETIME | NULLABLE | |
|
||||
| created_at | DATETIME | | |
|
||||
| updated_at | DATETIME | | |
|
||||
|
||||
### 2.6 device_groups
|
||||
|
||||
디바이스 그룹 (논리적 분류).
|
||||
|
||||
| 컬럼 | 타입 | 제약 | 설명 |
|
||||
|------|------|------|------|
|
||||
| id | INT | PK | |
|
||||
| name | VARCHAR(100) | UNIQUE | 그룹명 |
|
||||
| description | VARCHAR(500) | | 설명 |
|
||||
| created_at | DATETIME | | |
|
||||
| updated_at | DATETIME | | |
|
||||
|
||||
### 2.7 alert_rules
|
||||
|
||||
알림 발생 조건 규칙.
|
||||
|
||||
| 컬럼 | 타입 | 제약 | 설명 |
|
||||
|------|------|------|------|
|
||||
| id | INT | PK | |
|
||||
| name | VARCHAR(100) | | 규칙명 |
|
||||
| description | VARCHAR(500) | | 설명 |
|
||||
| metric | VARCHAR(100) | | 감시 메트릭 (temperature, humidity 등) |
|
||||
| condition | VARCHAR(50) | | 조건 (gt, lt, eq, gte, lte) |
|
||||
| threshold | FLOAT | | 임계값 |
|
||||
| severity | VARCHAR(20) | DEFAULT 'warning' | critical/warning/info |
|
||||
| is_enabled | BOOLEAN | DEFAULT TRUE | |
|
||||
| device_group_id | INT | FK→device_groups.id, NULLABLE | 대상 그룹 |
|
||||
| created_by | INT | FK→users.id, NULLABLE | 생성자 |
|
||||
| created_at | DATETIME | | |
|
||||
| updated_at | DATETIME | | |
|
||||
|
||||
### 2.8 alerts
|
||||
|
||||
발생한 알림 이력.
|
||||
|
||||
| 컬럼 | 타입 | 제약 | 설명 |
|
||||
|------|------|------|------|
|
||||
| id | INT | PK | |
|
||||
| rule_id | INT | FK→alert_rules.id, NULLABLE | 원인 규칙 |
|
||||
| device_id | INT | FK→devices.id, NULLABLE | 대상 디바이스 |
|
||||
| severity | VARCHAR(20) | | critical/warning/info |
|
||||
| message | VARCHAR(500) | | 알림 메시지 |
|
||||
| is_acknowledged | BOOLEAN | DEFAULT FALSE | 확인 여부 |
|
||||
| acknowledged_by | INT | FK→users.id, NULLABLE | 확인한 사용자 |
|
||||
| acknowledged_at | DATETIME | NULLABLE | 확인 시각 |
|
||||
| created_at | DATETIME | | |
|
||||
| updated_at | DATETIME | | |
|
||||
|
||||
### 2.9 system_configs
|
||||
|
||||
시스템 설정 키-값 저장소.
|
||||
|
||||
| 컬럼 | 타입 | 제약 | 설명 |
|
||||
|------|------|------|------|
|
||||
| id | INT | PK | |
|
||||
| key | VARCHAR(100) | UNIQUE, INDEX | 설정 키 |
|
||||
| value | VARCHAR(2000) | | 설정 값 |
|
||||
| description | VARCHAR(500) | | 설명 |
|
||||
| is_secret | BOOLEAN | DEFAULT FALSE | 비밀 값 여부 |
|
||||
| created_at | DATETIME | | |
|
||||
| updated_at | DATETIME | | |
|
||||
|
||||
### 2.10 audit_logs
|
||||
|
||||
감사 로그 (변경 이력 추적).
|
||||
|
||||
| 컬럼 | 타입 | 제약 | 설명 |
|
||||
|------|------|------|------|
|
||||
| id | INT | PK | |
|
||||
| user_id | INT | FK→users.id, NULLABLE | 행위자 |
|
||||
| action | VARCHAR(100) | | 액션 (create, update, delete, login 등) |
|
||||
| resource_type | VARCHAR(50) | | 대상 리소스 타입 |
|
||||
| resource_id | VARCHAR(50) | | 대상 리소스 ID |
|
||||
| details | VARCHAR(2000) | DEFAULT '{}' | 변경 상세 (JSON) |
|
||||
| ip_address | VARCHAR(45) | | 요청 IP |
|
||||
| created_at | DATETIME | | |
|
||||
| updated_at | DATETIME | | |
|
||||
|
||||
---
|
||||
|
||||
## 3. MariaDB ER 다이어그램
|
||||
|
||||
```
|
||||
users ──────────┬──── 1:1 ──── user_profiles
|
||||
│
|
||||
├──── 1:N ──── refresh_tokens
|
||||
│
|
||||
├──── 1:N ──── oauth_accounts
|
||||
│
|
||||
├──── 1:N ──── devices (owner_id)
|
||||
│
|
||||
├──── 1:N ──── alert_rules (created_by)
|
||||
│
|
||||
├──── 1:N ──── alerts (acknowledged_by)
|
||||
│
|
||||
└──── 1:N ──── audit_logs (user_id)
|
||||
|
||||
device_groups ──┬──── 1:N ──── devices (group_id)
|
||||
│
|
||||
└──── 1:N ──── alert_rules (device_group_id)
|
||||
|
||||
alert_rules ────┬──── 1:N ──── alerts (rule_id)
|
||||
|
||||
devices ────────┴──── 1:N ──── alerts (device_id)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. MongoDB 컬렉션
|
||||
|
||||
### 4.1 device_logs
|
||||
|
||||
디바이스 이벤트 로그. TTL 인덱스로 90일 후 자동 삭제.
|
||||
|
||||
```json
|
||||
{
|
||||
"_id": "ObjectId",
|
||||
"device_id": "sensor-temp-001",
|
||||
"event_type": "status_change",
|
||||
"payload": {
|
||||
"status": "online",
|
||||
"reason": "boot"
|
||||
},
|
||||
"ip_address": "192.168.1.100",
|
||||
"timestamp": "2025-01-15T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**인덱스:**
|
||||
- `device_id` (단일)
|
||||
- `event_type` (단일)
|
||||
- `timestamp` (내림차순)
|
||||
|
||||
### 4.2 telemetry_data
|
||||
|
||||
디바이스 센서 측정 데이터 (시계열).
|
||||
|
||||
```json
|
||||
{
|
||||
"_id": "ObjectId",
|
||||
"device_id": "sensor-temp-001",
|
||||
"metrics": {
|
||||
"temperature": 23.5,
|
||||
"humidity": 45.2,
|
||||
"pressure": 1013.25
|
||||
},
|
||||
"timestamp": "2025-01-15T12:05:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**인덱스:**
|
||||
- `device_id` (단일)
|
||||
- `timestamp` (내림차순)
|
||||
- `(device_id, timestamp)` (복합, 범위 쿼리 최적화)
|
||||
|
||||
### 4.3 analytics_results
|
||||
|
||||
분석 수행 결과 저장.
|
||||
|
||||
```json
|
||||
{
|
||||
"_id": "ObjectId",
|
||||
"analysis_type": "daily_telemetry",
|
||||
"parameters": {
|
||||
"date": "2025-01-14"
|
||||
},
|
||||
"result": {
|
||||
"count": 1440,
|
||||
"avg_value": 23.8
|
||||
},
|
||||
"device_id": "sensor-temp-001",
|
||||
"period_start": "2025-01-14T00:00:00Z",
|
||||
"period_end": "2025-01-15T00:00:00Z",
|
||||
"created_at": "2025-01-15T01:05:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**인덱스:**
|
||||
- `analysis_type` (단일)
|
||||
- `device_id` (단일)
|
||||
- `created_at` (내림차순)
|
||||
|
||||
### 4.4 notifications
|
||||
|
||||
사용자별 알림 메시지.
|
||||
|
||||
```json
|
||||
{
|
||||
"_id": "ObjectId",
|
||||
"user_id": 1,
|
||||
"title": "디바이스 오프라인 알림",
|
||||
"message": "sensor-temp-001이 오프라인 상태입니다.",
|
||||
"notification_type": "warning",
|
||||
"is_read": false,
|
||||
"read_at": null,
|
||||
"created_at": "2025-01-15T12:10:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**인덱스:**
|
||||
- `user_id` (단일)
|
||||
- `(user_id, is_read)` (복합, 읽지 않은 알림 조회)
|
||||
- `created_at` (내림차순)
|
||||
|
||||
---
|
||||
|
||||
## 5. Redis 사용 패턴
|
||||
|
||||
| 용도 | 키 패턴 | TTL | 설명 |
|
||||
|------|---------|-----|------|
|
||||
| 속도 제한 | `rate_limit:{ip}` | 60초 | IP별 요청 카운터 |
|
||||
| Celery 브로커 | redis://...6379/1 | - | 태스크 큐 |
|
||||
| Celery 결과 | redis://...6379/2 | - | 태스크 결과 저장 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 마이그레이션
|
||||
|
||||
Alembic으로 MariaDB 스키마를 관리한다.
|
||||
|
||||
```bash
|
||||
# 마이그레이션 생성
|
||||
alembic revision --autogenerate -m "description"
|
||||
|
||||
# 마이그레이션 적용
|
||||
alembic upgrade head
|
||||
|
||||
# 롤백
|
||||
alembic downgrade -1
|
||||
|
||||
# 현재 리비전 확인
|
||||
alembic current
|
||||
|
||||
# 히스토리 확인
|
||||
alembic history
|
||||
```
|
||||
|
||||
MongoDB는 스키마리스이므로 별도 마이그레이션이 불필요하다. 인덱스는 Beanie 모델의 `Settings.indexes`로 앱 시작 시 자동 생성된다.
|
||||
260
docs/DEPLOYMENT.md
Normal file
260
docs/DEPLOYMENT.md
Normal file
@@ -0,0 +1,260 @@
|
||||
# 배포 가이드
|
||||
|
||||
## 1. 전제 조건
|
||||
|
||||
### 호스트에 직접 설치 (Docker 외부)
|
||||
- **MariaDB** 10.6+ — 관계형 데이터 저장
|
||||
- **MongoDB** 7.0+ — 문서 데이터 저장
|
||||
|
||||
### Docker로 관리
|
||||
- **Docker** 24.0+
|
||||
- **Docker Compose** v2
|
||||
|
||||
---
|
||||
|
||||
## 2. 최초 설정
|
||||
|
||||
### 2.1 환경변수
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
`.env` 파일에서 반드시 변경해야 할 항목:
|
||||
|
||||
```bash
|
||||
# 보안 키 (반드시 변경)
|
||||
SECRET_KEY=<랜덤 문자열>
|
||||
JWT_SECRET_KEY=<랜덤 문자열>
|
||||
|
||||
# MariaDB 접속 정보
|
||||
MARIADB_HOST=127.0.0.1
|
||||
MARIADB_PORT=3306
|
||||
MARIADB_USER=core_api_user
|
||||
MARIADB_PASSWORD=<강력한 비밀번호>
|
||||
MARIADB_DATABASE=core_api
|
||||
|
||||
# MongoDB 접속 정보
|
||||
MONGODB_URL=mongodb://127.0.0.1:27017
|
||||
MONGODB_DATABASE=core_api
|
||||
```
|
||||
|
||||
### 2.2 MariaDB 준비
|
||||
|
||||
```sql
|
||||
CREATE DATABASE core_api CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
CREATE USER 'core_api_user'@'%' IDENTIFIED BY 'your_password';
|
||||
GRANT ALL PRIVILEGES ON core_api.* TO 'core_api_user'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
```
|
||||
|
||||
### 2.3 MongoDB 준비
|
||||
|
||||
```javascript
|
||||
use core_api
|
||||
db.createUser({
|
||||
user: "core_api_user",
|
||||
pwd: "your_password",
|
||||
roles: [{ role: "readWrite", db: "core_api" }]
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 개발 환경
|
||||
|
||||
### 3.1 로컬 실행
|
||||
|
||||
```bash
|
||||
# 의존성 설치
|
||||
pip install -e ".[dev]"
|
||||
|
||||
# DB 마이그레이션
|
||||
alembic upgrade head
|
||||
|
||||
# 시드 데이터
|
||||
python -m scripts.init_db
|
||||
|
||||
# 관리자 계정 생성
|
||||
python -m scripts.create_superuser admin@example.com password123
|
||||
|
||||
# 인프라 시작 (Redis + Mosquitto)
|
||||
docker-compose up -d redis mosquitto
|
||||
|
||||
# 앱 서버
|
||||
uvicorn app.asgi:app --reload --host 0.0.0.0 --port 8000
|
||||
|
||||
# Celery 워커 (별도 터미널)
|
||||
celery -A app.tasks.celery_app worker --loglevel=info \
|
||||
-Q default,analytics,notifications,devices
|
||||
|
||||
# Celery 스케줄러 (별도 터미널)
|
||||
celery -A app.tasks.celery_app beat --loglevel=info
|
||||
```
|
||||
|
||||
### 3.2 Docker 전체 스택
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
서비스 확인:
|
||||
- API: http://localhost:8000/docs
|
||||
- Flower: http://localhost:5555
|
||||
|
||||
---
|
||||
|
||||
## 4. 프로덕션 배포
|
||||
|
||||
### 4.1 Docker Compose
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
프로덕션 차이점:
|
||||
- Uvicorn 워커 4개 (`--workers 4`)
|
||||
- Celery 동시성 4, Worker 레플리카 2
|
||||
- Flower 기본 인증 활성화
|
||||
- 소스 코드 볼륨 마운트 없음 (이미지 내장)
|
||||
- 로그 레벨 `warning`
|
||||
|
||||
### 4.2 환경변수 (프로덕션 추가)
|
||||
|
||||
```bash
|
||||
APP_ENV=production
|
||||
DEBUG=false
|
||||
LOG_LEVEL=WARNING
|
||||
|
||||
# Flower 인증
|
||||
FLOWER_USER=admin
|
||||
FLOWER_PASSWORD=<강력한 비밀번호>
|
||||
|
||||
# CORS (실제 도메인)
|
||||
CORS_ORIGINS=["https://your-domain.com"]
|
||||
```
|
||||
|
||||
### 4.3 리버스 프록시 (Nginx 예시)
|
||||
|
||||
```nginx
|
||||
upstream core_api {
|
||||
server 127.0.0.1:8000;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name api.your-domain.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://core_api;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /socket.io/ {
|
||||
proxy_pass http://core_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 서비스 구성
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────┐
|
||||
│ Docker Compose │
|
||||
│ │
|
||||
│ ┌─────────┐ ┌─────────┐ ┌────────┐ ┌───────┐ │
|
||||
│ │ App │ │ Worker │ │ Beat │ │Flower │ │
|
||||
│ │ :8000 │ │ (x2) │ │ │ │ :5555 │ │
|
||||
│ └────┬────┘ └────┬────┘ └───┬────┘ └───┬───┘ │
|
||||
│ │ │ │ │ │
|
||||
│ ┌────┴────┐ ┌────┴──────────┴────────────┘ │
|
||||
│ │ Redis │ │ │
|
||||
│ │ :6379 │ │ │
|
||||
│ └─────────┘ │ │
|
||||
│ ┌────────────┘ │
|
||||
│ │ Mosquitto │ │
|
||||
│ │ :1883 │ │
|
||||
│ └───────────┘ │
|
||||
└────────────────────────────────────────────────────┘
|
||||
│ │
|
||||
┌────┴────┐ ┌───┴────┐
|
||||
│ MariaDB │ │MongoDB │
|
||||
│ (호스트) │ │(호스트) │
|
||||
└─────────┘ └────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 헬스체크
|
||||
|
||||
```bash
|
||||
# 기본 헬스체크
|
||||
curl http://localhost:8000/api/v1/system/health
|
||||
# → {"status": "ok", "service": "core-api", "version": "0.1.0"}
|
||||
|
||||
# 상세 시스템 상태 (인증 필요)
|
||||
curl -H "Authorization: Bearer <token>" \
|
||||
http://localhost:8000/api/v1/monitoring/health
|
||||
|
||||
# Docker 서비스 상태
|
||||
docker-compose ps
|
||||
|
||||
# Celery 워커 상태
|
||||
docker-compose logs worker
|
||||
|
||||
# Celery 활성 태스크
|
||||
docker-compose exec worker celery -A app.tasks.celery_app inspect active
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 로그
|
||||
|
||||
### 개발 환경
|
||||
- structlog 콘솔 렌더러 (사람이 읽기 좋은 형식)
|
||||
- 로그 레벨: DEBUG
|
||||
|
||||
### 프로덕션 환경
|
||||
- structlog JSON 렌더러 (ELK/Grafana 연동)
|
||||
- 로그 레벨: WARNING
|
||||
|
||||
```bash
|
||||
# 실시간 로그 확인
|
||||
docker-compose logs -f app
|
||||
docker-compose logs -f worker
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 백업
|
||||
|
||||
### MariaDB
|
||||
```bash
|
||||
mysqldump -u root -p core_api > backup_$(date +%Y%m%d).sql
|
||||
```
|
||||
|
||||
### MongoDB
|
||||
```bash
|
||||
mongodump --db core_api --out backup_$(date +%Y%m%d)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 트러블슈팅
|
||||
|
||||
| 증상 | 원인 | 해결 |
|
||||
|------|------|------|
|
||||
| `Cannot connect to MySQL` | MariaDB 미실행 또는 접속 정보 오류 | `.env` 확인, MariaDB 상태 확인 |
|
||||
| `MongoDB not initialized` | MongoDB 미실행 | MongoDB 서비스 시작 |
|
||||
| `Redis connection refused` | Redis 미실행 | `docker-compose up -d redis` |
|
||||
| Celery 태스크 실행 안 됨 | Worker 미실행 또는 큐 불일치 | Worker 로그 확인, 큐 이름 확인 |
|
||||
| Socket.IO 연결 실패 | CORS 설정 누락 | `CORS_ORIGINS`에 클라이언트 URL 추가 |
|
||||
| MQTT 연결 실패 | Mosquitto 미실행 | `docker-compose up -d mosquitto` |
|
||||
Reference in New Issue
Block a user