Skip to content

AIX-01/aegis-backend

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

270 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

AEGIS Backend

Agent ๊ธฐ๋ฐ˜ ์•ˆ์ „ ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ - ๋ฐฑ์—”๋“œ

๊ฐœ์š”

AEGIS Backend๋Š” CCTV ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ์˜ REST API, ์ธ์ฆ/์ธ๊ฐ€, ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ, ๋ฏธ๋””์–ด ์„œ๋ฒ„ ์—ฐ๋™์„ ๋‹ด๋‹นํ•˜๋Š” Spring Boot ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ž…๋‹ˆ๋‹ค.

๊ธฐ์ˆ  ์Šคํƒ

๋ถ„๋ฅ˜ ๊ธฐ์ˆ 
Framework Spring Boot 3.5.9
Language Java 21
Database PostgreSQL, Spring Data JPA
Cache Redis, Spring Data Redis
Security Spring Security, JWT (jjwt 0.12)
Storage AWS S3 SDK v2 (MinIO ํ˜ธํ™˜)
Docs SpringDoc OpenAPI 2.8
Build Gradle 8.x

ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

src/main/java/com/aegis/aegisbackend/
โ”œโ”€โ”€ AegisBackendApplication.java    # ๋ฉ”์ธ ํด๋ž˜์Šค
โ”œโ”€โ”€ domain/                         # ๋„๋ฉ”์ธ ๊ณ„์ธต
โ”‚   โ”œโ”€โ”€ auth/                       # ์ธ์ฆ
โ”‚   โ”‚   โ”œโ”€โ”€ controller/AuthController.java
โ”‚   โ”‚   โ”œโ”€โ”€ dto/AuthDto.java
โ”‚   โ”‚   โ””โ”€โ”€ service/AuthService.java
โ”‚   โ”œโ”€โ”€ camera/                     # ์นด๋ฉ”๋ผ
โ”‚   โ”‚   โ”œโ”€โ”€ controller/CameraController.java
โ”‚   โ”‚   โ”œโ”€โ”€ dto/CameraDto.java
โ”‚   โ”‚   โ”œโ”€โ”€ entity/Camera.java
โ”‚   โ”‚   โ”œโ”€โ”€ entity/UserCamera.java
โ”‚   โ”‚   โ”œโ”€โ”€ repository/CameraRepository.java
โ”‚   โ”‚   โ”œโ”€โ”€ repository/UserCameraRepository.java
โ”‚   โ”‚   โ””โ”€โ”€ service/CameraService.java
โ”‚   โ”œโ”€โ”€ event/                      # ์ด๋ฒคํŠธ
โ”‚   โ”‚   โ”œโ”€โ”€ controller/EventController.java
โ”‚   โ”‚   โ”œโ”€โ”€ controller/ActionController.java  # ์•ก์…˜ ์Šน์ธ/๊ฑฐ๋ถ€ API
โ”‚   โ”‚   โ”œโ”€โ”€ dto/EventDto.java
โ”‚   โ”‚   โ”œโ”€โ”€ entity/Event.java
โ”‚   โ”‚   โ”œโ”€โ”€ entity/EventAction.java     # ์ด๋ฒคํŠธ ์•ก์…˜ ๋กœ๊ทธ
โ”‚   โ”‚   โ”œโ”€โ”€ repository/EventRepository.java
โ”‚   โ”‚   โ”œโ”€โ”€ repository/EventActionRepository.java
โ”‚   โ”‚   โ”œโ”€โ”€ repository/EventSpecification.java  # ๋™์  ํ•„ํ„ฐ๋ง ์ฟผ๋ฆฌ
โ”‚   โ”‚   โ””โ”€โ”€ service/EventService.java
โ”‚   โ”œโ”€โ”€ notification/               # ์•Œ๋ฆผ
โ”‚   โ”‚   โ”œโ”€โ”€ controller/NotificationController.java
โ”‚   โ”‚   โ”œโ”€โ”€ dto/NotificationDto.java
โ”‚   โ”‚   โ”œโ”€โ”€ entity/Notification.java
โ”‚   โ”‚   โ”œโ”€โ”€ repository/NotificationRepository.java
โ”‚   โ”‚   โ”œโ”€โ”€ service/NotificationService.java
โ”‚   โ”‚   โ””โ”€โ”€ service/SseEmitterService.java
โ”‚   โ”œโ”€โ”€ stats/                      # ํ†ต๊ณ„
โ”‚   โ”‚   โ”œโ”€โ”€ controller/StatsController.java
โ”‚   โ”‚   โ”œโ”€โ”€ dto/StatsDto.java
โ”‚   โ”‚   โ””โ”€โ”€ service/StatsService.java
โ”‚   โ”œโ”€โ”€ stream/                     # ์ŠคํŠธ๋ฆผ (DTO๋งŒ)
โ”‚   โ”‚   โ””โ”€โ”€ dto/StreamDto.java
โ”‚   โ””โ”€โ”€ user/                       # ์‚ฌ์šฉ์ž
โ”‚       โ”œโ”€โ”€ controller/UserController.java
โ”‚       โ”œโ”€โ”€ dto/UserDto.java
โ”‚       โ”œโ”€โ”€ entity/User.java
โ”‚       โ”œโ”€โ”€ repository/UserRepository.java
โ”‚       โ””โ”€โ”€ service/UserService.java
โ”œโ”€โ”€ global/                         # ์ „์—ญ ์„ค์ •
โ”‚   โ”œโ”€โ”€ common/
โ”‚   โ”‚   โ”œโ”€โ”€ dto/PageResponse.java
โ”‚   โ”‚   โ””โ”€โ”€ enums/
โ”‚   โ”‚       โ”œโ”€โ”€ EventRisk.java      # NORMAL, SUSPICIOUS, ABNORMAL
โ”‚   โ”‚       โ”œโ”€โ”€ EventStatus.java    # PROCESSING, ANALYZED
โ”‚   โ”‚       โ”œโ”€โ”€ EventType.java      # ASSAULT, BURGLARY, DUMP, SWOON, VANDALISM
โ”‚   โ”‚       โ”œโ”€โ”€ NotificationType.java # ALERT, WARNING, INFO, SUCCESS
โ”‚   โ”‚       โ””โ”€โ”€ UserRole.java       # USER, ADMIN
โ”‚   โ”œโ”€โ”€ config/
โ”‚   โ”‚   โ”œโ”€โ”€ AsyncConfig.java        # ๋น„๋™๊ธฐ ์„ค์ •
โ”‚   โ”‚   โ”œโ”€โ”€ DataInitializer.java    # ์ดˆ๊ธฐ Admin ์ƒ์„ฑ
โ”‚   โ”‚   โ”œโ”€โ”€ OpenApiConfig.java      # Swagger ์„ค์ •
โ”‚   โ”‚   โ”œโ”€โ”€ RedisConfig.java        # Redis ์„ค์ •
โ”‚   โ”‚   โ”œโ”€โ”€ S3Config.java           # S3 ์„ค์ •
โ”‚   โ”‚   โ””โ”€โ”€ SecurityConfig.java     # Spring Security ์„ค์ •
โ”‚   โ”œโ”€โ”€ exception/
โ”‚   โ”‚   โ”œโ”€โ”€ BusinessException.java
โ”‚   โ”‚   โ”œโ”€โ”€ ErrorCode.java
โ”‚   โ”‚   โ””โ”€โ”€ GlobalExceptionHandler.java
โ”‚   โ””โ”€โ”€ security/
โ”‚       โ”œโ”€โ”€ CustomUserDetailsService.java
โ”‚       โ”œโ”€โ”€ JwtAuthenticationFilter.java
โ”‚       โ””โ”€โ”€ JwtTokenProvider.java
โ””โ”€โ”€ infra/                          # ์ธํ”„๋ผ ๊ณ„์ธต
    โ”œโ”€โ”€ agent/                      # AI Agent ์—ฐ๋™
    โ”‚   โ”œโ”€โ”€ AgentWebhookController.java
    โ”‚   โ”œโ”€โ”€ dto/
    โ”‚   โ”‚   โ”œโ”€โ”€ CreateEventRequest.java
    โ”‚   โ”‚   โ”œโ”€โ”€ EventActionRequest.java
    โ”‚   โ”‚   โ”œโ”€โ”€ EventActionUpdateRequest.java
    โ”‚   โ”‚   โ”œโ”€โ”€ EventUpdateRequest.java
    โ”‚   โ”‚   โ””โ”€โ”€ PendingActionResponse.java
    โ”‚   โ””โ”€โ”€ service/
    โ”‚       โ””โ”€โ”€ PendingActionService.java
    โ”œโ”€โ”€ mediamtx/                   # MediaMTX ์—ฐ๋™
    โ”‚   โ”œโ”€โ”€ MediaMTXSyncService.java
    โ”‚   โ””โ”€โ”€ MediaMTXWebhookController.java
    โ”œโ”€โ”€ redis/
    โ”‚   โ””โ”€โ”€ RedisTokenService.java
    โ””โ”€โ”€ s3/
        โ”œโ”€โ”€ S3Service.java
        โ””โ”€โ”€ TempClipCleanupScheduler.java

ํ•ต์‹ฌ ์›Œํฌํ”Œ๋กœ์šฐ

1. ์นด๋ฉ”๋ผ ๋™๊ธฐํ™” ํ๋ฆ„

1. MediaMTX์—์„œ ์ŠคํŠธ๋ฆผ ์‹œ์ž‘/์ข…๋ฃŒ ์‹œ runOnReady/runOnNotReady ํ›… ์‹คํ–‰
2. MediaMTX โ†’ POST /internal/mediamtx/sync ํ˜ธ์ถœ
3. MediaMTXSyncService:
   - GET /v3/paths/list๋กœ MediaMTX API์—์„œ ์ŠคํŠธ๋ฆผ ๋ชฉ๋ก ์กฐํšŒ
   - DB์˜ ์นด๋ฉ”๋ผ ๋ชฉ๋ก๊ณผ ๋น„๊ต
   - ์ƒˆ ์ŠคํŠธ๋ฆผ: INSERT (connected=true, enabled=false)
   - ๊ธฐ์กด ์ŠคํŠธ๋ฆผ: UPDATE connected ์ƒํƒœ
   - ์‚ฌ๋ผ์ง„ ์ŠคํŠธ๋ฆผ: UPDATE connected=false
4. analysisEnabled=true์ธ ์นด๋ฉ”๋ผ ๋ชฉ๋ก์„ Redis์— ์ €์žฅ (analysis:cameras)
5. Redis Pub/Sub์œผ๋กœ "camera:analysis:update" ์ฑ„๋„์— "sync" ๋ฐœํ–‰
6. SSE๋กœ ํ”„๋ก ํŠธ์—”๋“œ์— "camera" ์ด๋ฒคํŠธ ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ

2. AI Agent ์ด๋ฒคํŠธ ์ƒ์„ฑ ํ๋ฆ„

1. AI Agent โ†’ POST /internal/agent/events (1์ฐจ ๋ถ„์„ ๊ฒฐ๊ณผ)
   - Request: { cameraId, risk, type, occurredAt }
   - ์ด๋ฒคํŠธ ์ƒ์„ฑ (status=PROCESSING)
   - ์•Œ๋ฆผ ์ƒ์„ฑ (risk ๊ธฐ๋ฐ˜ ํƒ€์ž…: abnormalโ†’ALERT, suspiciousโ†’WARNING, normalโ†’INFO)
   - SSE ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ (event)
   - Response: { eventId }

2. AI Agent โ†’ GET /internal/agent/events/{id}/clip/upload-url
   - S3 presigned PUT URL ์ƒ์„ฑ (clips/{eventId}.mp4, 10๋ถ„ ๋งŒ๋ฃŒ)
   - Response: { uploadUrl }

3. AI Agent โ†’ presigned URL๋กœ MinIO์— ์ง์ ‘ ์—…๋กœ๋“œ

4. AI Agent โ†’ POST /internal/agent/events/{id}/clip/confirm
   - S3์—์„œ ํด๋ฆฝ ์กด์žฌ ํ™•์ธ (clips/{eventId}.mp4)
   - Event.clipUrl ์ €์žฅ
   - SSE ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ (event)

5. AI Agent โ†’ PATCH /internal/agent/events/{id} (2์ฐจ ๋ถ„์„ ๊ฒฐ๊ณผ)
   - Request: { risk, type, summary, report, status }
   - null์ด ์•„๋‹Œ ํ•„๋“œ๋งŒ ์—…๋ฐ์ดํŠธ
   - ์•Œ๋ฆผ ์ƒ์„ฑ (์ˆ˜์ •๋œ risk ๊ธฐ๋ฐ˜ ํƒ€์ž…)
   - SSE ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ (event)

6. AI Agent โ†’ POST /internal/agent/events/{id}/actions (์•ก์…˜ ๊ธฐ๋ก)
   - Request: { action, description }
   - ์•ก์…˜ ๋กœ๊ทธ ์ €์žฅ
   - ์•Œ๋ฆผ ์ƒ์„ฑ (์ด๋ฒคํŠธ risk ๊ธฐ๋ฐ˜ ํƒ€์ž…)
   - Response: { actionId }

7. AI Agent โ†’ PATCH /internal/agent/events/{id}/actions/{actionId} (์•ก์…˜ ์ˆ˜์ •)
   - Request: { userId (์„ ํƒ), action, description }
   - userId๊ฐ€ ์žˆ์œผ๋ฉด ์Šน์ธ/๊ฑฐ์ ˆ ์‚ฌ์šฉ์ž ์—…๋ฐ์ดํŠธ
   - ์•Œ๋ฆผ ์ƒ์„ฑ (์ด๋ฒคํŠธ risk ๊ธฐ๋ฐ˜ ํƒ€์ž…)
   - Response: { actionId }

8. AI Agent โ†’ POST /internal/agent/events/{id}/actions/{actionId}/pending (Human-in-the-Loop)
   - Request: (์—†์Œ)
   - DeferredResult๋กœ ์‘๋‹ต ํ™€๋”ฉ
   - ์•Œ๋ฆผ ์ƒ์„ฑ (์ด๋ฒคํŠธ risk ๊ธฐ๋ฐ˜ ํƒ€์ž…)
   - SSE "action-pending" ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ
   - ์‚ฌ์šฉ์ž ์Šน์ธ/๊ฑฐ๋ถ€ ์‹œ ์‘๋‹ต ๋ฐ˜ํ™˜
   - Response: { userId, userName, userEmail, result }

2-1. Human-in-the-Loop ์Šน์ธ ํ๋ฆ„

AI Agent                         Spring                          Frontend
    |                               |                                |
    |-- POST /actions ------------>|                                |
    |<-- { actionId } -------------|                                |
    |                               |                                |
    |-- POST /actions/{id}/pending->|                                |
    |   (์‘๋‹ต ํ™€๋”ฉ)                  |-- SSE "action-pending" ------->|
    |                               |                                |
    |                               |   [์Šน์ธ/๊ฑฐ๋ถ€ UI ํ‘œ์‹œ]            |
    |                               |                                |
    |                               |<-- POST /actions/{id}/resolve -|
    |                               |    { approved: true/false }    |
    |                               |                                |
    |<-- { userId, userName, ------|-- SSE "action-resolved" ------>|
    |      userEmail, result }     |                                |

3. WebRTC ์ŠคํŠธ๋ฆผ ์ธ์ฆ ํ๋ฆ„

1. ๋ธŒ๋ผ์šฐ์ € โ†’ POST /stream/{camera}/whep (via Caddy)
   - Authorization: Basic base64("_:" + accessToken)

2. MediaMTX โ†’ POST /internal/mediamtx/auth (์ธ์ฆ ์œ„์ž„)
   - Request: { user, password, action, path, protocol, ip }

3. MediaMTXWebhookController:
   - protocol๋ณ„ ๋ถ„๊ธฐ:
     - SRT publish: ID/PW ๊ฒ€์ฆ
     - WebRTC read: password ํ•„๋“œ์˜ JWT ๊ฒ€์ฆ + ์นด๋ฉ”๋ผ ๊ถŒํ•œ ํ™•์ธ
     - RTSP/HLS read: ์ธ์ฆ ์—†์Œ (๋‚ด๋ถ€๋ง)
   - ์„ฑ๊ณต: 200 OK
   - ์‹คํŒจ: 401 Unauthorized

4. ์ธ์ฆ ์„ฑ๊ณต ์‹œ ๋ธŒ๋ผ์šฐ์ € โ†” MediaMTX WebRTC ์—ฐ๊ฒฐ ์ˆ˜๋ฆฝ

4. ์ธ์ฆ/ํ† ํฐ ๊ด€๋ฆฌ ํ๋ฆ„

1. ๋กœ๊ทธ์ธ (POST /api/auth/login):
   - ์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ
   - Access Token ์ƒ์„ฑ (15๋ถ„)
   - Refresh Token ์ƒ์„ฑ (7์ผ)
   - Redis์— Refresh Token ์ €์žฅ (key: refresh_token:{token}, value: userId)
   - Cookie์— Refresh Token ์„ค์ • (HttpOnly, Secure)
   - Response: { accessToken, user }

2. API ์š”์ฒญ:
   - Authorization: Bearer {accessToken}
   - JwtAuthenticationFilter์—์„œ JWT ๊ฒ€์ฆ
   - SecurityContext์— ์ธ์ฆ ์ •๋ณด ์„ค์ •

3. ํ† ํฐ ๊ฐฑ์‹  (POST /api/auth/refresh):
   - Cookie์—์„œ Refresh Token ์ถ”์ถœ
   - Redis์—์„œ userId ์กฐํšŒ
   - ์ƒˆ Access Token ๋ฐœ๊ธ‰
   - Response: { accessToken }

4. ๋กœ๊ทธ์•„์›ƒ (POST /api/auth/logout):
   - Redis์—์„œ Refresh Token ์‚ญ์ œ
   - Cookie ์‚ญ์ œ

5. SSE ์•Œ๋ฆผ ์‹œ์Šคํ…œ

1. ํด๋ผ์ด์–ธํŠธ ์—ฐ๊ฒฐ (GET /api/notifications/stream):
   - SseEmitter ์ƒ์„ฑ (ํƒ€์ž„์•„์›ƒ: 30๋ถ„)
   - ์‚ฌ์šฉ์ž๋ณ„ Map์— ์ €์žฅ
   - "connect" ์ด๋ฒคํŠธ ์ „์†ก
   - ํ˜„์žฌ pending ์•ก์…˜ ๋ชฉ๋ก ์ „์†ก

2. ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ:
   - NotificationService: DB์— ์•Œ๋ฆผ ์ €์žฅ
   - SseEmitterService.broadcast*(): ๋ชจ๋“  ์—ฐ๊ฒฐ๋œ ํด๋ผ์ด์–ธํŠธ์— ์ „์†ก

3. ์ด๋ฒคํŠธ ํƒ€์ž…:
   - notification: ์ƒˆ ์•Œ๋ฆผ (ํ† ์ŠคํŠธ ํ‘œ์‹œ)
   - camera: ์นด๋ฉ”๋ผ ์ƒํƒœ ๋ณ€๊ฒฝ
   - event: ์ด๋ฒคํŠธ ์ƒ์„ฑ/์ˆ˜์ •
   - event-deleted: ์ด๋ฒคํŠธ ์‚ญ์ œ
   - member: ๋ฉค๋ฒ„ ๋ณ€๊ฒฝ
   - action-update: ์•ก์…˜ ์ƒ์„ฑ/์ˆ˜์ • (๋ชจ๋‹ฌ ๊ฐฑ์‹ )
   - action-pending: ์•ก์…˜ ์Šน์ธ ๋Œ€๊ธฐ (Human-in-the-Loop)
   - action-resolved: ์•ก์…˜ ์Šน์ธ/๊ฑฐ๋ถ€ ์™„๋ฃŒ

4. ์—ฐ๊ฒฐ ์ข…๋ฃŒ/์˜ค๋ฅ˜ ์‹œ:
   - Map์—์„œ Emitter ์ œ๊ฑฐ
   - ํด๋ผ์ด์–ธํŠธ ์žฌ์—ฐ๊ฒฐ ํ•„์š”

ํ•ต์‹ฌ ์„œ๋น„์Šค ์ƒ์„ธ

AuthService

๋ฉ”์„œ๋“œ ๊ธฐ๋Šฅ ํŠน์ด์‚ฌํ•ญ
signup() ํšŒ์›๊ฐ€์ž… approved=false๋กœ ์ƒ์„ฑ, ๊ด€๋ฆฌ์ž ์Šน์ธ ํ•„์š”
login() ๋กœ๊ทธ์ธ approved/deleted ๊ฒ€์ฆ, Refresh Token Redis ์ €์žฅ
logout() ๋กœ๊ทธ์•„์›ƒ Redis์—์„œ Refresh Token ์‚ญ์ œ
refresh() ํ† ํฐ ๊ฐฑ์‹  Cookie์˜ Refresh Token์œผ๋กœ Access Token ์žฌ๋ฐœ๊ธ‰
changePassword() ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ ํ›„ ๋ณ€๊ฒฝ
deleteAccount() ํšŒ์› ํƒˆํ‡ด ์†Œํ”„ํŠธ ์‚ญ์ œ (deleted=true, deletedAt ๊ธฐ๋ก)

CameraService

๋ฉ”์„œ๋“œ ๊ธฐ๋Šฅ ํŠน์ด์‚ฌํ•ญ
getCamerasPaged() ์นด๋ฉ”๋ผ ๋ชฉ๋ก ๊ถŒํ•œ๋ณ„ ํ•„ํ„ฐ๋ง, ์ •๋ ฌ: connectedโ†’enabledโ†’location
updateCamera() ์นด๋ฉ”๋ผ ์ˆ˜์ • location, enabled, analysisEnabled ์ˆ˜์ • ๊ฐ€๋Šฅ
syncAnalysisCamerasToRedis() Redis ๋™๊ธฐํ™” analysisEnabled=true ์นด๋ฉ”๋ผ๋ฅผ Redis์— ์ €์žฅ

EventService

๋ฉ”์„œ๋“œ ๊ธฐ๋Šฅ ํŠน์ด์‚ฌํ•ญ
getEventsPaged() ์ด๋ฒคํŠธ ๋ชฉ๋ก ๊ถŒํ•œ๋ณ„ ํ•„ํ„ฐ๋ง (Admin: ์ „์ฒด, User: ํ• ๋‹น ์นด๋ฉ”๋ผ๋งŒ)
deleteEvent() ์ด๋ฒคํŠธ ์‚ญ์ œ S3 ํด๋ฆฝ ์‚ญ์ œ โ†’ ์•Œ๋ฆผ ์‚ญ์ œ โ†’ ์ด๋ฒคํŠธ ์‚ญ์ œ โ†’ SSE ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ

MediaMTXSyncService

๋ฉ”์„œ๋“œ ๊ธฐ๋Šฅ ํŠน์ด์‚ฌํ•ญ
syncCameras() ์นด๋ฉ”๋ผ ๋™๊ธฐํ™” Redis Lock์œผ๋กœ ์ค‘๋ณต ์‹คํ–‰ ๋ฐฉ์ง€ (1์ดˆ TTL)

S3Service

๋ฉ”์„œ๋“œ ๊ธฐ๋Šฅ ํŠน์ด์‚ฌํ•ญ
generateUploadUrl() ์—…๋กœ๋“œ URL ์ƒ์„ฑ clips/{eventId}.mp4, 10๋ถ„ ๋งŒ๋ฃŒ
generateDownloadUrl() ๋‹ค์šด๋กœ๋“œ URL ์ƒ์„ฑ Caddy ๋„๋ฉ”์ธ์œผ๋กœ ์„œ๋ช…
clipExists() ํด๋ฆฝ ์กด์žฌ ํ™•์ธ clips/{eventId}.mp4 ํ™•์ธ
downloadClip() ํด๋ฆฝ ๋‹ค์šด๋กœ๋“œ byte[] ๋ฐ˜ํ™˜
deleteClip() ํด๋ฆฝ ์‚ญ์ œ ์ด๋ฒคํŠธ ์‚ญ์ œ ์‹œ ํ˜ธ์ถœ
cleanupTempClips() ์ž„์‹œ ํด๋ฆฝ ์ •๋ฆฌ ์Šค์ผ€์ค„๋Ÿฌ์—์„œ ํ˜ธ์ถœ (ํ˜„์žฌ temp/clips ๋ฏธ์‚ฌ์šฉ)

์„ค์น˜ ๋ฐ ์‹คํ–‰

# ๋นŒ๋“œ
./gradlew build

# ์‹คํ–‰
./gradlew bootRun

# ํ…Œ์ŠคํŠธ
./gradlew test

ํ™˜๊ฒฝ ๋ณ€์ˆ˜

application.properties ๋˜๋Š” ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ์„ค์ •:

๋ณ€์ˆ˜ ์„ค๋ช… ๊ธฐ๋ณธ๊ฐ’
DB_URL PostgreSQL URL jdbc:postgresql://localhost:5432/aegis
DB_USERNAME DB ์‚ฌ์šฉ์ž aegis
DB_PASSWORD DB ๋น„๋ฐ€๋ฒˆํ˜ธ trillion
REDIS_HOST Redis ํ˜ธ์ŠคํŠธ localhost
REDIS_PORT Redis ํฌํŠธ 6379
REDIS_PASSWORD Redis ๋น„๋ฐ€๋ฒˆํ˜ธ (๋นˆ ๋ฌธ์ž์—ด)
AWS_S3_ACCESS_KEY S3 Access Key aegis
AWS_S3_SECRET_KEY S3 Secret Key trillion
AWS_S3_REGION S3 ๋ฆฌ์ „ us-east-1
AWS_S3_BUCKET S3 ๋ฒ„ํ‚ท aegis
AWS_S3_ENDPOINT S3 ์—”๋“œํฌ์ธํŠธ (MinIO์šฉ) http://localhost:9000
JWT_SECRET JWT ์„œ๋ช… ํ‚ค (256bit ์ด์ƒ) (๊ฐœ๋ฐœ์šฉ ๊ธฐ๋ณธ๊ฐ’)
JWT_ACCESS_EXPIRATION Access Token ๋งŒ๋ฃŒ (ms) 900000 (15๋ถ„)
JWT_REFRESH_EXPIRATION Refresh Token ๋งŒ๋ฃŒ (ms) 604800000 (7์ผ)
MEDIAMTX_API_URL MediaMTX API URL http://localhost:9997
MEDIAMTX_WEBRTC_URL WebRTC WHEP ๊ธฐ๋ณธ ๊ฒฝ๋กœ /stream
MEDIAMTX_SRT_USER SRT ์ธ์ฆ ์‚ฌ์šฉ์ž aegis
MEDIAMTX_SRT_PASSWORD SRT ์ธ์ฆ ๋น„๋ฐ€๋ฒˆํ˜ธ trillion
ADMIN_EMAIL ์ดˆ๊ธฐ Admin ์ด๋ฉ”์ผ admin@aegis.local
ADMIN_PASSWORD ์ดˆ๊ธฐ Admin ๋น„๋ฐ€๋ฒˆํ˜ธ changeyourpassword
ADMIN_NAME ์ดˆ๊ธฐ Admin ์ด๋ฆ„ Admin

API ๋ช…์„ธ

Auth API (/api/auth)

Method Path ์„ค๋ช…
POST /signup ํšŒ์›๊ฐ€์ž…
POST /login ๋กœ๊ทธ์ธ
POST /logout ๋กœ๊ทธ์•„์›ƒ
POST /refresh ํ† ํฐ ๊ฐฑ์‹ 
GET /me ๋‚ด ์ •๋ณด ์กฐํšŒ
PATCH /me ํ”„๋กœํ•„ ์ˆ˜์ •
PATCH /password ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ
DELETE /me ํšŒ์› ํƒˆํ‡ด

POST /api/auth/signup

Request:

{
  "email": "string (ํ•„์ˆ˜, ์ด๋ฉ”์ผ ํ˜•์‹)",
  "password": "string (ํ•„์ˆ˜, 6์ž ์ด์ƒ)",
  "name": "string (ํ•„์ˆ˜, 100์ž ์ดํ•˜)"
}

Response: 200 OK

{
  "success": true,
  "message": "ํšŒ์›๊ฐ€์ž…์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž ์Šน์ธ ํ›„ ๋กœ๊ทธ์ธ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."
}

POST /api/auth/login

Request:

{
  "email": "string (ํ•„์ˆ˜)",
  "password": "string (ํ•„์ˆ˜)"
}

Response: 200 OK

{
  "accessToken": "JWT ํ† ํฐ",
  "user": {
    "id",
    "email",
    "name",
    "role",
    "assignedCameras",
    "createdAt",
    "approved"
  }
}

Cookie: refreshToken (HttpOnly, Secure, 7์ผ)

POST /api/auth/refresh

Cookie: refreshToken ํ•„์š”

Response: 200 OK

{
  "accessToken": "์ƒˆ JWT ํ† ํฐ"
}

PATCH /api/auth/password

Request:

{
  "currentPassword": "string (ํ•„์ˆ˜)",
  "newPassword": "string (ํ•„์ˆ˜, 6์ž ์ด์ƒ)"
}

Camera API (/api/cameras)

Method Path ์„ค๋ช…
GET / ์นด๋ฉ”๋ผ ๋ชฉ๋ก (ํŽ˜์ด์ง€๋„ค์ด์…˜, ๊ธฐ๋ณธ size=6)
GET /all ์นด๋ฉ”๋ผ ์ „์ฒด ๋ชฉ๋ก (๋ฉค๋ฒ„ ๊ด€๋ฆฌ - ์นด๋ฉ”๋ผ ํ• ๋‹น์šฉ)
GET /{id} ์นด๋ฉ”๋ผ ์ƒ์„ธ
PATCH /{id} ์นด๋ฉ”๋ผ ์ˆ˜์ •

์ •๋ ฌ ์ˆœ์„œ: connected DESC โ†’ enabled DESC โ†’ location ASC

GET /api/cameras

Response: 200 OK (PageResponse)

{
  "content": [
    {
      "id": "UUID",
      "name": "์นด๋ฉ”๋ผ๋ช… (MediaMTX ์›๋ณธ)",
      "location": "์žฅ์†Œ",
      "connected": true,
      "enabled": true,
      "analysisEnabled": true,
      "streamUrl": "/stream/{name}/whep"
    }
  ],
  "page": 0,
  "size": 6,
  "totalElements": 10,
  "totalPages": 2,
  "first": true,
  "last": false
}

GET /api/cameras/all

Response: 200 OK

[
  {
    "id": "UUID",
    "name": "์นด๋ฉ”๋ผ๋ช…",
    "location": "์žฅ์†Œ",
    "connected": true,
    "enabled": true,
    "analysisEnabled": true,
    "streamUrl": "/stream/{name}/whep"
  }
]

GET /api/cameras/{id}

Response: 200 OK

{
  "id": "UUID",
  "name": "์นด๋ฉ”๋ผ๋ช…",
  "location": "์žฅ์†Œ",
  "connected": true,
  "enabled": true,
  "analysisEnabled": true,
  "streamUrl": "/stream/{name}/whep"
}

PATCH /api/cameras/{id}

Request:

{
  "location": "string (์„ ํƒ)",
  "enabled": "boolean (์„ ํƒ)",
  "analysisEnabled": "boolean (์„ ํƒ)"
}

Response: 200 OK

{
  "id": "UUID",
  "name": "์นด๋ฉ”๋ผ๋ช…",
  "location": "์žฅ์†Œ",
  "connected": true,
  "enabled": true,
  "analysisEnabled": true,
  "streamUrl": "/stream/{name}/whep"
}

Event API (/api/events)

Method Path ์„ค๋ช…
GET / ์ด๋ฒคํŠธ ๋ชฉ๋ก (ํŽ˜์ด์ง€๋„ค์ด์…˜, ๊ธฐ๋ณธ size=20)
GET /{id} ์ด๋ฒคํŠธ ์ƒ์„ธ
GET /{id}/report ๋ณด๊ณ ์„œ HTML ์กฐํšŒ
DELETE /{id} ์ด๋ฒคํŠธ ์‚ญ์ œ (Admin)
GET /{id}/clip-url ํด๋ฆฝ ์žฌ์ƒ์šฉ presigned URL
GET /{id}/clip/download-url ํด๋ฆฝ ๋‹ค์šด๋กœ๋“œ์šฉ presigned URL
POST /{id}/actions/{actionId}/resolve ์•ก์…˜ ์Šน์ธ/๊ฑฐ๋ถ€ (Human-in-the-Loop)

GET /api/events

Query Parameters:

ํŒŒ๋ผ๋ฏธํ„ฐ ํƒ€์ž… ์„ค๋ช…
page int ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (๊ธฐ๋ณธ: 0)
size int ํŽ˜์ด์ง€ ํฌ๊ธฐ (๊ธฐ๋ณธ: 20)
risks string[] ์œ„ํ—˜๋„ ํ•„ํ„ฐ (suspicious, abnormal)
types string[] ์ด์ƒํ–‰๋™ ์œ ํ˜• (assault, burglary, dump, swoon, vandalism)
statuses string[] ๋ถ„์„ ์ƒํƒœ (processing, analyzed)
cameraIds string[] ์นด๋ฉ”๋ผ ID ๋ชฉ๋ก
startDate datetime ์‹œ์ž‘ ๋‚ ์งœ (ISO 8601)
endDate datetime ์ข…๋ฃŒ ๋‚ ์งœ (ISO 8601)

_empty ๋งˆ์ปค๋ฅผ ์ „์†กํ•˜๋ฉด ํ•ด๋‹น ํ•„ํ„ฐ์—์„œ ๊ฒฐ๊ณผ ์—†์Œ ๋ฐ˜ํ™˜ (์ „์ฒด ํ•ด์ œ)

Response: 200 OK (PageResponse)

{
  "content": [
    {
      "id": "UUID",
      "cameraId": "UUID",
      "cameraName": "์นด๋ฉ”๋ผ๋ช…",
      "cameraLocation": "์„ค์น˜ ์žฅ์†Œ",
      "risk": "normal | suspicious | abnormal",
      "type": "assault | burglary | dump | swoon | vandalism",
      "occurredAt": "2026-01-31T12:00:00",
      "status": "processing | analyzed",
      "clipUrl": "S3 URL (nullable)",
      "summary": "AI ์š”์•ฝ (nullable)",
      "actions": "[{...}] (nullable)",
      "report": "์ƒ์„ธ ๋ณด๊ณ ์„œ (nullable)"
    }
  ],
  "page": 0,
  "size": 20,
  "totalElements": 100,
  "totalPages": 5,
  "first": true,
  "last": false
}

GET /api/events/{id}/report

Response: 200 OK (Content-Type: text/html)

<!DOCTYPE html>
<html>
<head>...</head>
<body>
<!-- AI Agent๊ฐ€ ์ƒ์„ฑํ•œ ๋ถ„์„ ๋ณด๊ณ ์„œ HTML -->
</body>
</html>

Error: 404 Not Found (๋ณด๊ณ ์„œ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ)

GET /api/events/{id}/clip-url

ํด๋ฆฝ ์žฌ์ƒ์šฉ presigned URL ๋ฐ˜ํ™˜

Response: 200 OK

{
  "url": "https://... (presigned URL)"
}

Error: 404 Not Found (ํด๋ฆฝ์ด ์—†๋Š” ๊ฒฝ์šฐ)

GET /api/events/{id}/clip/download-url

ํด๋ฆฝ ๋‹ค์šด๋กœ๋“œ์šฉ presigned URL ๋ฐ˜ํ™˜

Response: 200 OK

{
  "url": "https://... (presigned URL)",
  "filename": "event_{id}.mp4"
}

Error: 404 Not Found (ํด๋ฆฝ์ด ์—†๋Š” ๊ฒฝ์šฐ)

POST /api/events/{id}/actions/{actionId}/resolve

์•ก์…˜ ์Šน์ธ/๊ฑฐ๋ถ€ (Human-in-the-Loop)

Request:

{
  "approved": boolean
}

Response: 200 OK

{
  "success": true
}

Error: 400 Bad Request (์•ก์…˜์„ ์ฐพ์„ ์ˆ˜ ์—†๊ฑฐ๋‚˜ ์ด๋ฏธ ์ฒ˜๋ฆฌ๋จ)

Notification API (/api/notifications)

Method Path ์„ค๋ช…
GET /stream SSE ์—ฐ๊ฒฐ
GET / ์•Œ๋ฆผ ๋ชฉ๋ก
DELETE / ์ „์ฒด ์‚ญ์ œ

GET /api/notifications

Response: 200 OK

[
  {
    "id": "UUID",
    "type": "alert | warning | info | success",
    "title": "์•Œ๋ฆผ ์ œ๋ชฉ",
    "message": "์•Œ๋ฆผ ๋ฉ”์‹œ์ง€",
    "timestamp": "2026-01-31T12:00:00",
    "eventId": "UUID (nullable)"
  }
]

DELETE /api/notifications

Response: 200 OK

{
  "success": true
}

Stats API (/api/stats)

Method Path ์„ค๋ช…
GET / ์ „์ฒด ํ†ต๊ณ„ (type ๋ฏธ์ง€์ • ์‹œ)
GET /?type=daily ์ผ๋ณ„ ํ†ต๊ณ„ (์ฃผ๊ฐ„)
GET /?type=event-types ์œ ํ˜•๋ณ„ ํ†ต๊ณ„
GET /?type=monthly ์›”๋ณ„ ํ†ต๊ณ„

GET /api/stats?type=daily

Response: 200 OK

[
  {
    "day": "์ผ",
    "events": 5,
    "resolved": 3
  },
  {
    "day": "์›”",
    "events": 8,
    "resolved": 6
  }
]

GET /api/stats?type=event-types

Response: 200 OK

[
  {
    "type": "assault",
    "count": 10,
    "color": "#ef4444"
  },
  {
    "type": "burglary",
    "count": 5,
    "color": "#f97316"
  }
]

GET /api/stats?type=monthly

Response: 200 OK

{
  "2026-01-15": {
    "events": 5,
    "alerts": 2
  },
  "2026-01-16": {
    "events": 3,
    "alerts": 1
  }
}

User API (/api/users) - Admin ์ „์šฉ

Method Path ์„ค๋ช…
GET / ์Šน์ธ๋œ ์‚ฌ์šฉ์ž ๋ชฉ๋ก (์ตœ์‹  ๊ฐ€์ž…์ˆœ)
GET /pending ๋ฏธ์Šน์ธ ์‚ฌ์šฉ์ž ๋ชฉ๋ก (์ตœ์‹  ๊ฐ€์ž…์ˆœ)
GET /pending/count ๋ฏธ์Šน์ธ ์‚ฌ์šฉ์ž ์ˆ˜
GET /{id} ์‚ฌ์šฉ์ž ์ƒ์„ธ
PATCH /{id} ์‚ฌ์šฉ์ž ์ˆ˜์ • (์–ด๋“œ๋ฏผ์€ ์นด๋ฉ”๋ผ ๊ถŒํ•œ ์ˆ˜์ • ๋ถˆ๊ฐ€)
DELETE /{id} ์‚ฌ์šฉ์ž ์‚ญ์ œ
PATCH /{id}/approve ์‚ฌ์šฉ์ž ์Šน์ธ

GET /api/users

์Šน์ธ๋œ ์‚ฌ์šฉ์ž ๋ชฉ๋ก ์กฐํšŒ (์ตœ์‹  ๊ฐ€์ž…์ˆœ ์ •๋ ฌ)

Response: 200 OK (PageResponse)

{
  "content": [
    {
      "id": "UUID",
      "email": "user@example.com",
      "name": "์‚ฌ์šฉ์ž๋ช…",
      "role": "user | admin",
      "approved": true,
      "assignedCameras": [
        "UUID ๋ฐฐ์—ด"
      ],
      "createdAt": "2026-01-31T12:00:00"
    }
  ],
  "page": 0,
  "size": 20,
  "totalElements": 10,
  "totalPages": 1,
  "first": true,
  "last": true
}

GET /api/users/{id}

Response: 200 OK

{
  "id": "UUID",
  "email": "user@example.com",
  "name": "์‚ฌ์šฉ์ž๋ช…",
  "role": "user | admin",
  "approved": true,
  "assignedCameras": [
    "UUID ๋ฐฐ์—ด"
  ],
  "createdAt": "2026-01-31T12:00:00"
}

PATCH /api/users/{id}

์–ด๋“œ๋ฏผ ์‚ฌ์šฉ์ž๋Š” ์นด๋ฉ”๋ผ ๊ถŒํ•œ(assignedCameras) ์ˆ˜์ • ๋ถˆ๊ฐ€

Request:

{
  "name": "string (์„ ํƒ)",
  "role": "user | admin (์„ ํƒ)",
  "assignedCameras": [
    "์นด๋ฉ”๋ผ UUID ๋ฐฐ์—ด (์„ ํƒ)"
  ]
}

Response: 200 OK

{
  "id": "UUID",
  "email": "user@example.com",
  "name": "์‚ฌ์šฉ์ž๋ช…",
  "role": "user | admin",
  "approved": true,
  "assignedCameras": [
    "UUID ๋ฐฐ์—ด"
  ],
  "createdAt": "2026-01-31T12:00:00"
}

DELETE /api/users/{id}

Response: 200 OK

{
  "success": true
}

PATCH /api/users/{id}/approve

Response: 200 OK

{
  "id": "UUID",
  "email": "user@example.com",
  "name": "์‚ฌ์šฉ์ž๋ช…",
  "role": "user | admin",
  "approved": true,
  "assignedCameras": [
    "UUID ๋ฐฐ์—ด"
  ],
  "createdAt": "2026-01-31T12:00:00"
}

Internal API (๋‚ด๋ถ€๋ง ์ „์šฉ)

Agent Webhook (/internal/agent)

Method Path ์„ค๋ช…
POST /events ์ด๋ฒคํŠธ ์ƒ์„ฑ
PATCH /events/{id} ์ด๋ฒคํŠธ ์ˆ˜์ •
GET /events/{id}/clip/upload-url ํด๋ฆฝ ์—…๋กœ๋“œ URL ๋ฐœ๊ธ‰
POST /events/{id}/clip/confirm ํด๋ฆฝ ์—…๋กœ๋“œ ์™„๋ฃŒ ํ™•์ธ
POST /events/{id}/actions ์ด๋ฒคํŠธ ์•ก์…˜ ์ƒ์„ฑ
PATCH /events/{id}/actions/{actionId} ์ด๋ฒคํŠธ ์•ก์…˜ ์ˆ˜์ •
POST /events/{id}/actions/{actionId}/pending ์•ก์…˜ ์Šน์ธ ๋Œ€๊ธฐ (Human-in-the-Loop)
POST /internal/agent/events

Request:

{
  "cameraId": "UUID (ํ•„์ˆ˜)",
  "risk": "normal | suspicious | abnormal (ํ•„์ˆ˜)",
  "type": "assault | burglary | dump | swoon | vandalism (ํ•„์ˆ˜)",
  "occurredAt": "ISO8601 (์„ ํƒ, ๊ธฐ๋ณธ now)"
}

Response: 201 Created

{
  "eventId": "UUID"
}
POST /internal/agent/events/{id}/clip/confirm

clips/{eventId}.mp4 ์กด์žฌ ํ™•์ธ ํ›„ Event.clipUrl ์ €์žฅ

Request: Body ์—†์Œ

Response: 200 OK

{
  "clipUrl": "clips/{eventId}.mp4"
}
PATCH /internal/agent/events/{id}

Request:

{
  "risk": "normal | suspicious | abnormal (์„ ํƒ)",
  "type": "assault | burglary | dump | swoon | vandalism (์„ ํƒ)",
  "summary": "string (์„ ํƒ)",
  "report": "string (์„ ํƒ)",
  "status": "processing | analyzed (์„ ํƒ)"
}

Response: 200 OK

{
  "eventId": "UUID"
}
GET /internal/agent/events/{id}/clip/upload-url

ํด๋ฆฝ ์—…๋กœ๋“œ๋ฅผ ์œ„ํ•œ S3 presigned URL ๋ฐœ๊ธ‰

Request: Body ์—†์Œ

Response: 200 OK

{
  "uploadUrl": "https://..."
}
POST /internal/agent/events/{id}/actions

์ด๋ฒคํŠธ ์•ก์…˜ ์ƒ์„ฑ

Request:

{
  "action": "string (ํ•„์ˆ˜) - ์•ก์…˜ ์ข…๋ฅ˜",
  "description": "string (ํ•„์ˆ˜) - ์•ก์…˜ ์„ค๋ช…"
}

Response: 201 Created

{
  "actionId": "UUID"
}
PATCH /internal/agent/events/{id}/actions/{actionId}

์ด๋ฒคํŠธ ์•ก์…˜ ์ˆ˜์ •

Request:

{
  "userId": "UUID (์„ ํƒ) - ์Šน์ธ/๊ฑฐ์ ˆํ•œ ์‚ฌ์šฉ์ž",
  "action": "string (ํ•„์ˆ˜)",
  "description": "string (ํ•„์ˆ˜)"
}

Response: 200 OK

{
  "actionId": "UUID"
}
POST /internal/agent/events/{id}/actions/{actionId}/pending

์•ก์…˜ ์Šน์ธ ๋Œ€๊ธฐ (Human-in-the-Loop)

DeferredResult๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž ์‘๋‹ต๊นŒ์ง€ HTTP ์—ฐ๊ฒฐ ์œ ์ง€

Request: Body ์—†์Œ

Response: 200 OK (์‚ฌ์šฉ์ž ์Šน์ธ/๊ฑฐ๋ถ€ ํ›„ ๋ฐ˜ํ™˜)

{
  "userId": "UUID",
  "userName": "string",
  "userEmail": "string",
  "result": true/false
}

MediaMTX Webhook (/internal/mediamtx)

Method Path ์„ค๋ช…
POST /sync ์นด๋ฉ”๋ผ ๋™๊ธฐํ™” ํŠธ๋ฆฌ๊ฑฐ
POST /auth ์ŠคํŠธ๋ฆผ ์ธ์ฆ (MediaMTX ํ˜ธ์ถœ)
POST /internal/mediamtx/auth

MediaMTX์—์„œ ํ˜ธ์ถœํ•˜๋Š” ์ธ์ฆ ์š”์ฒญ:

Request:

{
  "user": "์‚ฌ์šฉ์ž๋ช…",
  "password": "๋น„๋ฐ€๋ฒˆํ˜ธ ๋˜๋Š” JWT",
  "action": "publish | read",
  "path": "์นด๋ฉ”๋ผ ๊ฒฝ๋กœ",
  "protocol": "srt | rtsp | hls | webrtc",
  "ip": "ํด๋ผ์ด์–ธํŠธ IP"
}

์ธ์ฆ ๊ทœ์น™:

  • SRT publish: ID/PW ๊ฒ€์ฆ
  • WebRTC read: JWT ๊ฒ€์ฆ + ์นด๋ฉ”๋ผ ๊ถŒํ•œ ํ™•์ธ
  • RTSP/HLS read: ์ธ์ฆ ์—†์Œ (๋‚ด๋ถ€๋ง)

๋ฐ์ดํ„ฐ ๋ชจ๋ธ

ERD

erDiagram
    users ||--o{ user_cameras: has
    users ||--o{ notifications: receives
    users ||--o{ event_actions: performs
    cameras ||--o{ user_cameras: assigned_to
    cameras ||--o{ events: generates
    events ||--o{ event_actions: has
    events ||--o{ notifications: triggers

    users {
        UUID id PK
        VARCHAR email UK
        VARCHAR password
        VARCHAR name
        ENUM role
        BOOLEAN approved
        BOOLEAN deleted
        TIMESTAMP deleted_at
        TIMESTAMP created_at
        TIMESTAMP updated_at
    }

    cameras {
        UUID id PK
        VARCHAR name
        VARCHAR location
        BOOLEAN connected
        BOOLEAN enabled
        BOOLEAN analysis_enabled
        TIMESTAMP created_at
        TIMESTAMP updated_at
    }

    user_cameras {
        UUID id PK
        UUID user_id FK
        UUID camera_id FK
    }

    events {
        UUID id PK
        UUID camera_id FK
        ENUM risk
        ENUM type
        ENUM status
        TIMESTAMP occurred_at
        TEXT clip_url
        TEXT summary
        VARCHAR risk_score
        JSONB rag_references
        TEXT report
        TIMESTAMP created_at
        TIMESTAMP updated_at
    }

    event_actions {
        UUID id PK
        UUID event_id FK
        UUID user_id FK
        TEXT action
        TEXT description
        TIMESTAMP created_at
        TIMESTAMP updated_at
    }

    notifications {
        UUID id PK
        UUID user_id FK
        UUID event_id FK
        ENUM type
        VARCHAR title
        TEXT message
        TIMESTAMP created_at
    }
Loading

ํ…Œ์ด๋ธ” ์ƒ์„ธ

users

์ปฌ๋Ÿผ ํƒ€์ž… ์ œ์•ฝ์กฐ๊ฑด ๊ธฐ๋ณธ๊ฐ’ ์„ค๋ช…
id UUID PK auto ๊ณ ์œ  ์‹๋ณ„์ž
email VARCHAR(255) UNIQUE, NOT NULL - ๋กœ๊ทธ์ธ ์ด๋ฉ”์ผ
password VARCHAR(255) NOT NULL - BCrypt ์•”ํ˜ธํ™”
name VARCHAR(100) NOT NULL - ์‚ฌ์šฉ์ž ์ด๋ฆ„
role ENUM NOT NULL USER USER, ADMIN
approved BOOLEAN NOT NULL false ๊ด€๋ฆฌ์ž ์Šน์ธ ์—ฌ๋ถ€
deleted BOOLEAN NOT NULL false ํƒˆํ‡ด ์—ฌ๋ถ€
deleted_at TIMESTAMP - NULL ํƒˆํ‡ด ์ผ์‹œ
created_at TIMESTAMP NOT NULL auto ๊ฐ€์ž…์ผ
updated_at TIMESTAMP NOT NULL auto ์ˆ˜์ •์ผ

cameras

์ปฌ๋Ÿผ ํƒ€์ž… ์ œ์•ฝ์กฐ๊ฑด ๊ธฐ๋ณธ๊ฐ’ ์„ค๋ช…
id UUID PK auto ๊ณ ์œ  ์‹๋ณ„์ž
name VARCHAR(50) NOT NULL - MediaMTX ์ŠคํŠธ๋ฆผ ๊ฒฝ๋กœ๋ช…
location VARCHAR(100) NOT NULL =name ์‚ฌ์šฉ์ž ์ง€์ • ์žฅ์†Œ
connected BOOLEAN NOT NULL false MediaMTX ์—ฐ๊ฒฐ ์ƒํƒœ
enabled BOOLEAN NOT NULL false ์นด๋ฉ”๋ผ ํ™œ์„ฑํ™”
analysis_enabled BOOLEAN NOT NULL false AI ๋ถ„์„ ํ™œ์„ฑํ™”
created_at TIMESTAMP NOT NULL auto ์ƒ์„ฑ์ผ
updated_at TIMESTAMP NOT NULL auto ์ˆ˜์ •์ผ

user_cameras

์ปฌ๋Ÿผ ํƒ€์ž… ์ œ์•ฝ์กฐ๊ฑด ์„ค๋ช…
id UUID PK ๊ณ ์œ  ์‹๋ณ„์ž
user_id UUID FK โ†’ users.id ์‚ฌ์šฉ์ž
camera_id UUID FK โ†’ cameras.id ์นด๋ฉ”๋ผ

events

์ปฌ๋Ÿผ ํƒ€์ž… ์ œ์•ฝ์กฐ๊ฑด ๊ธฐ๋ณธ๊ฐ’ ์„ค๋ช…
id UUID PK auto ๊ณ ์œ  ์‹๋ณ„์ž
camera_id UUID FK โ†’ cameras.id - ์นด๋ฉ”๋ผ
risk ENUM NOT NULL - NORMAL, SUSPICIOUS, ABNORMAL
type ENUM NOT NULL - ASSAULT, BURGLARY, DUMP, SWOON, VANDALISM
status ENUM NOT NULL PROCESSING PROCESSING, ANALYZED
occurred_at TIMESTAMP NOT NULL - ๋ฐœ์ƒ ์‹œ๊ฐ
clip_url TEXT - NULL S3 ํด๋ฆฝ URL
summary TEXT - NULL AI ์š”์•ฝ
risk_score VARCHAR(10) - NULL ์œ„ํ—˜ ์ ์ˆ˜
rag_references JSONB - NULL RAG ์ฐธ์กฐ
report TEXT - NULL ์ƒ์„ธ ๋ณด๊ณ ์„œ
created_at TIMESTAMP NOT NULL auto ์ƒ์„ฑ์ผ
updated_at TIMESTAMP NOT NULL auto ์ˆ˜์ •์ผ

event_actions

์ปฌ๋Ÿผ ํƒ€์ž… ์ œ์•ฝ์กฐ๊ฑด ๊ธฐ๋ณธ๊ฐ’ ์„ค๋ช…
id UUID PK auto ๊ณ ์œ  ์‹๋ณ„์ž
event_id UUID FK โ†’ events.id, NOT NULL - ์ด๋ฒคํŠธ
user_id UUID FK โ†’ users.id NULL ์•ก์…˜ ์Šน์ธ/๊ฑฐ์ ˆํ•œ ์‚ฌ์šฉ์ž (null์ด๋ฉด ์ž๋™)
action TEXT NOT NULL - ์•ก์…˜ ์ข…๋ฅ˜ ๋˜๋Š” ์ด๋ฆ„
description TEXT NOT NULL - ์•ก์…˜ ์‹คํ–‰ ๊ฒฐ๊ณผ ๋˜๋Š” ์„ค๋ช…
created_at TIMESTAMP NOT NULL auto ์ƒ์„ฑ์ผ
updated_at TIMESTAMP NOT NULL auto ์ˆ˜์ •์ผ

notifications

์ปฌ๋Ÿผ ํƒ€์ž… ์ œ์•ฝ์กฐ๊ฑด ์„ค๋ช…
id UUID PK ๊ณ ์œ  ์‹๋ณ„์ž
user_id UUID FK โ†’ users.id, NOT NULL ์ˆ˜์‹  ์‚ฌ์šฉ์ž
event_id UUID FK โ†’ events.id ๊ด€๋ จ ์ด๋ฒคํŠธ (nullable)
type ENUM NOT NULL ALERT, WARNING, INFO, SUCCESS
title VARCHAR(200) NOT NULL ์ œ๋ชฉ
message TEXT NOT NULL ๋ฉ”์‹œ์ง€
created_at TIMESTAMP NOT NULL ์ƒ์„ฑ์ผ

์ธ๋ฑ์Šค

ํ…Œ์ด๋ธ” ์ธ๋ฑ์Šค๋ช… ์ปฌ๋Ÿผ
users idx_users_email email
users idx_users_approved approved
cameras idx_cameras_connected connected
cameras idx_cameras_enabled enabled
cameras idx_cameras_analysis_enabled analysis_enabled
events idx_events_camera_id camera_id
events idx_events_risk risk
events idx_events_type type
events idx_events_status status
events idx_events_occurred_at occurred_at
event_actions idx_event_actions_event_id event_id
notifications idx_notifications_user_id user_id
notifications idx_notifications_created_at created_at

์ธ์ฆ/์ธ๊ฐ€

JWT ํ† ํฐ

  • Access Token: 15๋ถ„, Authorization ํ—ค๋”
  • Refresh Token: 7์ผ, HttpOnly Cookie, Redis ์ €์žฅ

Spring Security FilterChain

  1. JwtAuthenticationFilter: JWT ๊ฒ€์ฆ ๋ฐ SecurityContext ์„ค์ •
  2. ์ธ์ฆ ์˜ˆ์™ธ ๊ฒฝ๋กœ: /api/auth/login, /api/auth/signup, /api/auth/refresh, /internal/**

๊ถŒํ•œ

  • USER: ๊ธฐ๋ณธ ์‚ฌ์šฉ์ž (ํ• ๋‹น๋œ ์นด๋ฉ”๋ผ๋งŒ ์ ‘๊ทผ)
  • ADMIN: ๋ชจ๋“  ์นด๋ฉ”๋ผ ์ ‘๊ทผ (์นด๋ฉ”๋ผ ๊ถŒํ•œ ์ˆ˜์ • ๋ถˆ๊ฐ€), ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ

SSE ์•Œ๋ฆผ ์‹œ์Šคํ…œ

SseEmitterService

  • ์‚ฌ์šฉ์ž๋ณ„ SSE ์—ฐ๊ฒฐ ๊ด€๋ฆฌ
  • ํƒ€์ž„์•„์›ƒ: 30๋ถ„

์ด๋ฒคํŠธ ํƒ€์ž…

์ด๋ฒคํŠธ ์„ค๋ช…
connect ์—ฐ๊ฒฐ ์„ฑ๊ณต
notification ์ƒˆ ์•Œ๋ฆผ
camera ์นด๋ฉ”๋ผ ์ƒํƒœ ๋ณ€๊ฒฝ
event ์ด๋ฒคํŠธ ์ƒ์„ฑ/์ˆ˜์ •
event-deleted ์ด๋ฒคํŠธ ์‚ญ์ œ
member ๋ฉค๋ฒ„ ๋ณ€๊ฒฝ
action-pending ์•ก์…˜ ์Šน์ธ ๋Œ€๊ธฐ (Human-in-the-Loop)
action-resolved ์•ก์…˜ ํ•ด๊ฒฐ๋จ

์—๋Ÿฌ ์ฝ”๋“œ

Auth

์ฝ”๋“œ HTTP ๋ฉ”์‹œ์ง€
EMAIL_NOT_FOUND 401 ๋“ฑ๋ก๋˜์ง€ ์•Š์€ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค.
INVALID_PASSWORD 401 ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
USER_NOT_APPROVED 403 ๊ด€๋ฆฌ์ž ์Šน์ธ ๋Œ€๊ธฐ ์ค‘์ž…๋‹ˆ๋‹ค.
DUPLICATE_EMAIL 400 ์ด๋ฏธ ๋“ฑ๋ก๋œ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค.
REFRESH_TOKEN_NOT_FOUND 401 Refresh token์ด ์—†์Šต๋‹ˆ๋‹ค.
INVALID_REFRESH_TOKEN 401 ์œ ํšจํ•˜์ง€ ์•Š์€ refresh token์ž…๋‹ˆ๋‹ค.
INVALID_USER 401 ์œ ํšจํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž์ž…๋‹ˆ๋‹ค.
AUTHENTICATION_REQUIRED 401 ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
USER_NOT_FOUND 401 ์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.
CURRENT_PASSWORD_MISMATCH 400 ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
PASSWORD_TOO_SHORT 400 ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 6์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
USER_DELETED 403 ํƒˆํ‡ดํ•œ ๊ณ„์ •์ž…๋‹ˆ๋‹ค.

User

์ฝ”๋“œ HTTP ๋ฉ”์‹œ์ง€
USER_ID_REQUIRED 400 ์‚ฌ์šฉ์ž ID๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
USER_NOT_FOUND_BY_ID 404 ์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

Camera

์ฝ”๋“œ HTTP ๋ฉ”์‹œ์ง€
CAMERA_NOT_FOUND 404 ์นด๋ฉ”๋ผ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.
CAMERA_ACCESS_DENIED 403 ํ•ด๋‹น ์นด๋ฉ”๋ผ์— ๋Œ€ํ•œ ์ ‘๊ทผ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.
CAMERA_NOT_CONNECTED 400 ์นด๋ฉ”๋ผ๊ฐ€ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Event

์ฝ”๋“œ HTTP ๋ฉ”์‹œ์ง€
EVENT_NOT_FOUND 404 ์ด๋ฒคํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

S3/Clip

์ฝ”๋“œ HTTP ๋ฉ”์‹œ์ง€
S3_UPLOAD_FAILED 500 S3 ์—…๋กœ๋“œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.
S3_DOWNLOAD_FAILED 500 S3 ๋‹ค์šด๋กœ๋“œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.
S3_DELETE_FAILED 500 S3 ์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.
CLIP_EXTRACTION_FAILED 500 ํด๋ฆฝ ์ถ”์ถœ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.

General

์ฝ”๋“œ HTTP ๋ฉ”์‹œ์ง€
FORBIDDEN 403 ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.
INTERNAL_SERVER_ERROR 500 ์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

์™ธ๋ถ€ ์—ฐ๋™

MediaMTX

  • ์นด๋ฉ”๋ผ ๋™๊ธฐํ™”: /internal/mediamtx/sync ํ˜ธ์ถœ ์‹œ MediaMTX API์—์„œ ์ŠคํŠธ๋ฆผ ๋ชฉ๋ก ์กฐํšŒ ํ›„ DB ๋™๊ธฐํ™”
  • ์ธ์ฆ ์œ„์ž„: MediaMTX โ†’ /internal/mediamtx/auth ํ˜ธ์ถœ
    • WebRTC read: JWT ๊ฒ€์ฆ
    • SRT publish: ID/PW ๊ฒ€์ฆ
    • RTSP/HLS read: ์ธ์ฆ ์—†์Œ (๋‚ด๋ถ€๋ง)

S3 (MinIO)

  • ํด๋ฆฝ ์ €์žฅ/์กฐํšŒ/์‚ญ์ œ
  • ๋ฒ„ํ‚ท: aegis (๊ธฐ๋ณธ๊ฐ’, ํ™˜๊ฒฝ๋ณ€์ˆ˜ AWS_S3_BUCKET์œผ๋กœ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ)
  • ํ‚ค ํ˜•์‹: clips/{eventId}.mp4

ํด๋ฆฝ ์ €์žฅ ๊ตฌ์กฐ:

aegis/
โ””โ”€โ”€ clips/                  # ์ด๋ฒคํŠธ ํด๋ฆฝ (Agent๊ฐ€ presigned URL๋กœ ์ง์ ‘ ์—…๋กœ๋“œ)
    โ””โ”€โ”€ {event_id}.mp4

์ฐธ๊ณ : temp/clips/ ๊ฒฝ๋กœ์™€ ๊ด€๋ จ ๋ฉ”์„œ๋“œ(tempClipExists, moveClipFromTemp)๋Š” ํ˜„์žฌ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค (Known Issues ์ฐธ์กฐ).

Redis

ํ‚ค ํƒ€์ž… ๋ฐธ๋ฅ˜ TTL ์„ค๋ช…
refresh_token:{token} String userId (UUID) 7์ผ Refresh Token โ†’ ์‚ฌ์šฉ์ž ๋งคํ•‘
mediamtx:sync:lock String "locked" 1์ดˆ MediaMTX ๋™๊ธฐํ™” ์ค‘๋ณต ๋ฐฉ์ง€ ์ž ๊ธˆ
analysis:cameras String [{"id":"uuid","name":"cam1","location":"1์ธต ๋กœ๋น„"},...] ์—†์Œ AI ๋ถ„์„ ๋Œ€์ƒ ์นด๋ฉ”๋ผ ๋ชฉ๋ก (JSON ๋ฐฐ์—ด)

Pub/Sub ์ฑ„๋„:

์ฑ„๋„ ๋ฉ”์‹œ์ง€ ์„ค๋ช…
camera:analysis:update "sync" ๋ถ„์„ ์นด๋ฉ”๋ผ ๋ชฉ๋ก ๋ณ€๊ฒฝ ์•Œ๋ฆผ (Python Agent ๊ตฌ๋…)

๋นŒ๋“œ ๋ฐ ๋ฐฐํฌ

JAR ๋นŒ๋“œ

./gradlew bootJar
java -jar build/libs/aegis-backend-1.0.0.jar

Docker ๋ฐฐํฌ

Caddy ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ๋ฅผ ํ†ตํ•ด /api/* ๊ฒฝ๋กœ๋กœ ์„œ๋น„์Šค๋ฉ๋‹ˆ๋‹ค.

  • ๋‚ด๋ถ€ ํฌํŠธ: 8080
  • ์™ธ๋ถ€ ์ ‘๊ทผ: https://localhost/api/*

๐Ÿ› Known Issues

์ตœ์ข… ๊ฐ์‚ฌ์ผ: 2026-02-12

๊ณ ์•„ ์ฝ”๋“œ

ํŒŒ์ผ ๋ฌธ์ œ ์ƒ์„ธ
S3Service.java tempClipExists() ๋ฏธ์‚ฌ์šฉ temp/clips ๊ฒฝ๋กœ ํ™•์ธ ๋ฉ”์„œ๋“œ, ํ˜ธ์ถœ์ฒ˜ ์—†์Œ
S3Service.java moveClipFromTemp() ๋ฏธ์‚ฌ์šฉ temp โ†’ clips ์ด๋™ ๋ฉ”์„œ๋“œ, ํ˜ธ์ถœ์ฒ˜ ์—†์Œ

๋ฏธ๊ตฌํ˜„ ์ฝ”๋“œ

ํŒŒ์ผ ๊ธฐ๋Šฅ ํ˜„์žฌ ์ƒํƒœ
Event.report ์ƒ์„ธ ๋ณด๊ณ ์„œ ํ•„๋“œ๋งŒ ์กด์žฌ, AI Agent์—์„œ ์ƒ์„ฑํ•˜์ง€ ์•Š์Œ

๋ณด์•ˆ ์ด์Šˆ

ํŒŒ์ผ ๋ฌธ์ œ ์‹ฌ๊ฐ๋„ ๊ถŒ์žฅ ์กฐ์น˜
application.properties JWT Secret ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ ๐Ÿ”ด ๋†’์Œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ์ฃผ์ž…, 256bit ์ด์ƒ ๋žœ๋ค๊ฐ’ ์‚ฌ์šฉ
DataInitializer.java Admin ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ธฐ๋ณธ๊ฐ’ ๐ŸŸก ์ค‘๊ฐ„ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ์ฃผ์ž… ๋˜๋Š” ์ฒซ ๋กœ๊ทธ์ธ ์‹œ ๋ณ€๊ฒฝ ๊ฐ•์ œ
/internal/** ๊ฒฝ๋กœ ์ธ์ฆ ์—†์Œ (๋‚ด๋ถ€๋ง ๊ฐ€์ •) ๐ŸŸก ์ค‘๊ฐ„ ์šด์˜ํ™˜๊ฒฝ์—์„œ IP ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ์ ์šฉ

๊ธฐํƒ€

ํ•ญ๋ชฉ ์„ค๋ช…
SSE ํƒ€์ž„์•„์›ƒ SseEmitterService 30๋ถ„ ํƒ€์ž„์•„์›ƒ, ์žฌ์—ฐ๊ฒฐ ํ•„์š”
์นด๋ฉ”๋ผ ์‚ญ์ œ ๋ฏธ์ง€์› MediaMTX ์ŠคํŠธ๋ฆผ ์ข…๋ฃŒ ์‹œ connected=false๋งŒ ์ฒ˜๋ฆฌ, ์‚ญ์ œ API ์—†์Œ

About

aegis-backend

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors