Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions apps/detections/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ class Detection(models.Model):
]

# MSA: FK 대신 ID로 참조 (Vehicles Service)
vehicle_id = models.BigIntegerField(
null=True, blank=True, db_index=True, verbose_name="차량 ID"
)
vehicle_id = models.BigIntegerField(null=True, blank=True, verbose_name="차량 ID")
detected_speed = models.FloatField(verbose_name="감지 속도")
speed_limit = models.FloatField(default=60.0, verbose_name="제한 속도")
location = models.CharField(
Expand Down
33 changes: 11 additions & 22 deletions apps/detections/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,33 +37,22 @@ def pending(self, request):
@action(detail=False, methods=["get"])
def statistics(self, request):
"""위반 통계"""
from datetime import timedelta

from django.utils import timezone

queryset = Detection.objects.using("detections_db").all()

# 기간 필터 (선택)
period = request.query_params.get("period")
if period == "today":
from datetime import timedelta

from django.utils import timezone

queryset = queryset.filter(
detected_at__gte=timezone.now() - timedelta(days=1)
)
elif period == "week":
from datetime import timedelta

from django.utils import timezone

queryset = queryset.filter(
detected_at__gte=timezone.now() - timedelta(weeks=1)
)
elif period == "month":
from datetime import timedelta

from django.utils import timezone

period_map = {
"today": timedelta(days=1),
"week": timedelta(weeks=1),
"month": timedelta(days=30),
}
if period in period_map:
queryset = queryset.filter(
detected_at__gte=timezone.now() - timedelta(days=30)
detected_at__gte=timezone.now() - period_map[period]
)

# 카메라 필터 (선택)
Expand Down
2 changes: 1 addition & 1 deletion apps/notifications/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class Notification(models.Model):
]

# MSA: FK 대신 ID로 참조 (Detections Service)
detection_id = models.BigIntegerField(db_index=True, verbose_name="감지 내역 ID")
detection_id = models.BigIntegerField(verbose_name="감지 내역 ID")
fcm_token = models.CharField(
max_length=255, blank=True, null=True, verbose_name="FCM 토큰"
)
Expand Down
1 change: 0 additions & 1 deletion apps/vehicles/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ class Meta:
verbose_name = "차량"
verbose_name_plural = "차량 목록"
indexes = [
models.Index(fields=["plate_number"]),
models.Index(fields=["fcm_token"]),
]

Expand Down
22 changes: 22 additions & 0 deletions apps/vehicles/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import logging
import os

from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
Expand All @@ -9,6 +12,8 @@
VehicleSerializer,
)

logger = logging.getLogger(__name__)


class VehicleViewSet(viewsets.ModelViewSet):
"""차량 정보 관리 API (MSA: vehicles_db 사용)"""
Expand Down Expand Up @@ -58,6 +63,23 @@ def register_fcm(self, request):
plate_number=plate_number, defaults={"fcm_token": fcm_token}
)

# Dashboard 토큰은 FCM 토픽에 구독
if plate_number == "DASHBOARD":
try:
FCM_MOCK = os.getenv("FCM_MOCK", "false").lower() == "true"
if FCM_MOCK:
logger.info(
"[MOCK] Would subscribe token to dashboard_alerts topic"
)
else:
from core.firebase.fcm import get_fcm_client

fcm_client = get_fcm_client()
fcm_client.subscribe_to_topic([fcm_token], "dashboard_alerts")
logger.info("Dashboard token subscribed to dashboard_alerts topic")
except Exception as e:
logger.warning(f"Failed to subscribe dashboard token to topic: {e}")

return Response(
VehicleSerializer(vehicle).data,
status=status.HTTP_201_CREATED if created else status.HTTP_200_OK,
Expand Down
2 changes: 0 additions & 2 deletions config/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from __future__ import absolute_import, unicode_literals

from .celery import app as celery_app

__all__ = ("celery_app",)
2 changes: 0 additions & 2 deletions config/celery.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"""Celery Configuration"""

from __future__ import absolute_import, unicode_literals

import os

from celery import Celery
Expand Down
4 changes: 2 additions & 2 deletions config/settings/prod.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,5 @@
# ==================================================
# CORS 설정
# ==================================================
CORS_ALLOWED_ORIGINS = os.getenv("CORS_ALLOWED_ORIGINS", "").split(",")
CORS_ALLOW_CREDENTIALS = True
_cors_origins = os.getenv("CORS_ALLOWED_ORIGINS", "")
CORS_ALLOWED_ORIGINS = [o for o in _cors_origins.split(",") if o]
42 changes: 42 additions & 0 deletions core/firebase/fcm.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,36 @@ def send_to_tokens(
)
return result

def subscribe_to_topic(self, tokens: List[str], topic: str) -> Dict:
"""토큰을 FCM 토픽에 구독"""
from firebase_admin import messaging

response = messaging.subscribe_to_topic(tokens, topic)
logger.info(
f"FCM topic subscribe '{topic}': {response.success_count} success, "
f"{response.failure_count} failed"
)
return {
"success_count": response.success_count,
"failure_count": response.failure_count,
}

def send_to_topic(
self, topic: str, title: str, body: str, data: Optional[Dict[str, str]] = None
) -> str:
"""토픽으로 알림 전송"""
from firebase_admin import messaging

message = messaging.Message(
notification=messaging.Notification(title=title, body=body),
data=data or {},
topic=topic,
)

response = messaging.send(message)
logger.info(f"FCM sent to topic '{topic}': {response}")
return response


# 편의 함수
_fcm_client = None
Expand All @@ -125,3 +155,15 @@ def send_push_notification(
) -> str:
"""푸시 알림 전송 (편의 함수)"""
return get_fcm_client().send_to_token(token, title, body, data)


def subscribe_tokens_to_topic(tokens: List[str], topic: str) -> Dict:
"""토픽 구독 (편의 함수)"""
return get_fcm_client().subscribe_to_topic(tokens, topic)


def send_topic_notification(
topic: str, title: str, body: str, data: Optional[Dict[str, str]] = None
) -> str:
"""토픽 알림 전송 (편의 함수)"""
return get_fcm_client().send_to_topic(topic, title, body, data)
3 changes: 2 additions & 1 deletion tasks/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Celery Tasks Package
from .dlq_tasks import process_dlq_message
from .notification_tasks import send_notification
from .ocr_tasks import process_ocr

__all__ = ["process_ocr", "send_notification"]
__all__ = ["process_ocr", "send_notification", "process_dlq_message"]
1 change: 1 addition & 0 deletions tasks/dlq_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def process_dlq_message(self, *args, **kwargs):
Detection.objects.using("detections_db").filter(id=detection_id).update(
status="failed",
error_message=f"DLQ: {death_reason} from {original_queue}",
# QuerySet.update() bypasses auto_now, so set explicitly
updated_at=timezone.now(),
)
logger.info(f"Detection {detection_id} marked as failed via DLQ")
Expand Down
Loading