From d578660f2c78b6874f1c5862f3683969e96b0ff4 Mon Sep 17 00:00:00 2001 From: sanghun Date: Sat, 7 Feb 2026 00:27:24 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20FCM=20=ED=86=A0=ED=94=BD=20?= =?UTF-8?q?=EA=B5=AC=EB=8F=85=20=EC=A7=80=EC=9B=90=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=EC=9A=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FCMClient에 subscribe_to_topic(), send_to_topic() 메서드 추가 - register_fcm에서 DASHBOARD 토큰을 dashboard_alerts 토픽에 자동 구독 - FCM_MOCK 환경변수로 테스트 모드 지원 --- apps/vehicles/views.py | 20 ++++++++++++++++++++ core/firebase/fcm.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/apps/vehicles/views.py b/apps/vehicles/views.py index 7f9861b..b8de3a9 100644 --- a/apps/vehicles/views.py +++ b/apps/vehicles/views.py @@ -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 @@ -9,6 +12,8 @@ VehicleSerializer, ) +logger = logging.getLogger(__name__) + class VehicleViewSet(viewsets.ModelViewSet): """차량 정보 관리 API (MSA: vehicles_db 사용)""" @@ -58,6 +63,21 @@ 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, diff --git a/core/firebase/fcm.py b/core/firebase/fcm.py index 202b2c1..6316b99 100644 --- a/core/firebase/fcm.py +++ b/core/firebase/fcm.py @@ -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 @@ -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) From e6809ce67be0a077cc8a201b87371ad416ed7f0c Mon Sep 17 00:00:00 2001 From: sanghun Date: Sat, 7 Feb 2026 00:27:34 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=EB=AA=A8=EB=93=A0=20=EA=B0=90?= =?UTF-8?q?=EC=A7=80=EC=97=90=20=EB=8C=80=ED=95=B4=20=EB=8C=80=EC=8B=9C?= =?UTF-8?q?=EB=B3=B4=EB=93=9C=20=ED=86=A0=ED=94=BD=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ocr_tasks: send_notification 호출을 차량 매칭 조건 밖으로 이동 - notification_tasks: dashboard_alerts 토픽 브로드캐스트 추가 - 차량 개별 푸시와 토픽 브로드캐스트 이중 구조로 개편 - 토픽 알림 이력에 fcm_token="topic:dashboard_alerts" 저장 --- tasks/notification_tasks.py | 140 ++++++++++++++++++++++++------------ tasks/ocr_tasks.py | 10 ++- 2 files changed, 97 insertions(+), 53 deletions(-) diff --git a/tasks/notification_tasks.py b/tasks/notification_tasks.py index 4100b2a..dcb77bb 100644 --- a/tasks/notification_tasks.py +++ b/tasks/notification_tasks.py @@ -23,7 +23,8 @@ def send_notification(self, detection_id: int): """ FCM 푸시 알림 전송 Task - - Exponential Backoff 재시도 + - 대시보드 토픽 브로드캐스트 (모든 감지에 대해) + - 매칭된 차량 개별 푸시 (차량 있는 경우) - MSA: 각 서비스별 DB에서 조회 """ from apps.detections.models import Detection @@ -36,73 +37,118 @@ def send_notification(self, detection_id: int): id=detection_id, status="completed" ) - # 2. Vehicle 조회 (vehicles_db) - MSA: 별도 DB - vehicle = None - if detection.vehicle_id: - try: - vehicle = Vehicle.objects.using("vehicles_db").get( - id=detection.vehicle_id - ) - except Vehicle.DoesNotExist: - logger.warning(f"Vehicle {detection.vehicle_id} not found") - - if not vehicle or not vehicle.fcm_token: - logger.warning(f"No FCM token for detection {detection_id}") - return {"status": "skipped", "reason": "No FCM token"} - - # 3. FCM 메시지 생성 + # 2. 알림 메시지 생성 title = f"⚠️ 과속 위반 감지: {detection.ocr_result}" body = ( f"📍 위치: {detection.location}\n" f"🚗 속도: {detection.detected_speed}km/h " f"(제한: {detection.speed_limit}km/h)" ) + data = { + "detection_id": str(detection_id), + "plate_number": detection.ocr_result or "", + "speed": str(detection.detected_speed), + "speed_limit": str(detection.speed_limit), + "location": detection.location or "", + "detected_at": detection.detected_at.isoformat(), + } + + # 3. 대시보드 토픽으로 항상 전송 + topic_response = None + try: + if FCM_MOCK: + topic_response = f"mock-topic-{detection_id}" + else: + from core.firebase.fcm import send_topic_notification - if FCM_MOCK: - # Mock 모드 - import random - import time - - time.sleep(random.uniform(0.05, 0.1)) - response = f"mock-message-id-{detection_id}" - else: - # 실제 FCM 전송 (core/firebase/fcm.py 사용) - from core.firebase.fcm import send_push_notification - - response = send_push_notification( - token=vehicle.fcm_token, - title=title, - body=body, - data={ - "detection_id": str(detection_id), - "plate_number": detection.ocr_result or "", - "speed": str(detection.detected_speed), - "speed_limit": str(detection.speed_limit), - "location": detection.location or "", - "detected_at": detection.detected_at.isoformat(), - }, + topic_response = send_topic_notification( + "dashboard_alerts", title, body, data + ) + logger.info( + f"Dashboard topic notification sent for detection " + f"{detection_id}: {topic_response}" + ) + except Exception as e: + logger.warning( + f"Dashboard topic notification failed for detection " + f"{detection_id}: {e}" ) - # 5. 성공 이력 저장 (notifications_db) + # 4. 토픽 알림 이력 저장 (notifications_db) Notification.objects.using("notifications_db").create( detection_id=detection_id, - fcm_token=vehicle.fcm_token, + fcm_token="topic:dashboard_alerts", title=title, body=body, - status="sent", - sent_at=timezone.now(), + status="sent" if topic_response else "failed", + sent_at=timezone.now() if topic_response else None, + error_message=None if topic_response else "Topic send failed", ) - logger.info(f"Notification sent for detection {detection_id}: {response}") - return {"status": "sent", "fcm_response": response} + # 5. 매칭된 차량에 개별 푸시 (기존 동작) + vehicle = None + if detection.vehicle_id: + try: + vehicle = Vehicle.objects.using("vehicles_db").get( + id=detection.vehicle_id + ) + except Vehicle.DoesNotExist: + logger.warning(f"Vehicle {detection.vehicle_id} not found") + + if vehicle and vehicle.fcm_token: + try: + if FCM_MOCK: + vehicle_response = f"mock-message-id-{detection_id}" + else: + from core.firebase.fcm import send_push_notification + + vehicle_response = send_push_notification( + token=vehicle.fcm_token, + title=title, + body=body, + data=data, + ) + + Notification.objects.using("notifications_db").create( + detection_id=detection_id, + fcm_token=vehicle.fcm_token, + title=title, + body=body, + status="sent", + sent_at=timezone.now(), + ) + logger.info( + f"Vehicle notification sent for detection " + f"{detection_id}: {vehicle_response}" + ) + except Exception as e: + logger.warning( + f"Vehicle notification failed for detection " + f"{detection_id}: {e}" + ) + Notification.objects.using("notifications_db").create( + detection_id=detection_id, + fcm_token=vehicle.fcm_token, + title=title, + body=body, + status="failed", + error_message=str(e), + ) + + return { + "status": "sent", + "topic": bool(topic_response), + "vehicle": bool(vehicle and vehicle.fcm_token), + } except Detection.DoesNotExist: - logger.error(f"Detection {detection_id} not found") + logger.error(f"Detection {detection_id} not found or not completed") return {"status": "error", "reason": "Detection not found"} except Exception as exc: - # FCM 실패 시 이력 저장 후 재시도 try: + from apps.notifications.models import Notification + Notification.objects.using("notifications_db").create( detection_id=detection_id, status="failed", diff --git a/tasks/ocr_tasks.py b/tasks/ocr_tasks.py index 567b57a..892b8f9 100644 --- a/tasks/ocr_tasks.py +++ b/tasks/ocr_tasks.py @@ -127,15 +127,13 @@ def process_ocr(self, detection_id: int, gcs_uri: str): using="detections_db", update_fields=["vehicle_id", "updated_at"], ) - - # 7. FCM 토큰이 있으면 알림 Task 발행 - if vehicle.fcm_token: - send_notification.apply_async( - args=[detection_id], queue="fcm_queue" - ) except Exception as e: logger.warning(f"Vehicle lookup failed: {e}") + # 7. Always send notification for completed detections + # (dashboard gets topic notification; matched vehicle gets individual push) + send_notification.apply_async(args=[detection_id], queue="fcm_queue") + logger.info(f"OCR completed for detection {detection_id}: {plate_number}") return { "detection_id": detection_id, From 2767b32f14f4729a83878005a341ad028b4a10f6 Mon Sep 17 00:00:00 2001 From: sanghun Date: Sat, 7 Feb 2026 00:43:20 +0900 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20=EC=95=8C=EB=A6=BC=20=ED=83=9C?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=20=EC=95=88=EC=A0=95=EC=84=B1=20=EB=B0=8F=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=ED=92=88=EC=A7=88=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 토픽 알림 중복 전송 방지 (autoretry 시 idempotency 체크) - null 안전성 강화 (ocr_result, location 미확인 시 기본값 처리) - Detection.DoesNotExist 시 retry 전략으로 변경 (타이밍 이슈 대응) - apply_async 실패 시 OCR 완료 상태 보호 (try/except 래핑) - 에러 로깅 개선 (silent pass 제거) --- tasks/notification_tasks.py | 75 +++++++++++++++++++++---------------- tasks/ocr_tasks.py | 5 ++- 2 files changed, 47 insertions(+), 33 deletions(-) diff --git a/tasks/notification_tasks.py b/tasks/notification_tasks.py index dcb77bb..4537c9e 100644 --- a/tasks/notification_tasks.py +++ b/tasks/notification_tasks.py @@ -38,9 +38,9 @@ def send_notification(self, detection_id: int): ) # 2. 알림 메시지 생성 - title = f"⚠️ 과속 위반 감지: {detection.ocr_result}" + title = f"⚠️ 과속 위반 감지: {detection.ocr_result or '미확인'}" body = ( - f"📍 위치: {detection.location}\n" + f"📍 위치: {detection.location or '알 수 없음'}\n" f"🚗 속도: {detection.detected_speed}km/h " f"(제한: {detection.speed_limit}km/h)" ) @@ -53,38 +53,49 @@ def send_notification(self, detection_id: int): "detected_at": detection.detected_at.isoformat(), } - # 3. 대시보드 토픽으로 항상 전송 + # 3. 대시보드 토픽으로 항상 전송 (중복 방지) topic_response = None - try: - if FCM_MOCK: - topic_response = f"mock-topic-{detection_id}" - else: - from core.firebase.fcm import send_topic_notification + already_sent_topic = Notification.objects.using("notifications_db").filter( + detection_id=detection_id, fcm_token="topic:dashboard_alerts", status="sent" + ).exists() + + if not already_sent_topic: + try: + if FCM_MOCK: + topic_response = f"mock-topic-{detection_id}" + else: + from core.firebase.fcm import send_topic_notification - topic_response = send_topic_notification( - "dashboard_alerts", title, body, data + topic_response = send_topic_notification( + "dashboard_alerts", title, body, data + ) + logger.info( + f"Dashboard topic notification sent for detection " + f"{detection_id}: {topic_response}" ) - logger.info( - f"Dashboard topic notification sent for detection " - f"{detection_id}: {topic_response}" + except Exception as e: + logger.warning( + f"Dashboard topic notification failed for detection " + f"{detection_id}: {e}" + ) + + # 4. 토픽 알림 이력 저장 (notifications_db) + Notification.objects.using("notifications_db").create( + detection_id=detection_id, + fcm_token="topic:dashboard_alerts", + title=title, + body=body, + status="sent" if topic_response else "failed", + sent_at=timezone.now() if topic_response else None, + error_message=None if topic_response else "Topic send failed", ) - except Exception as e: - logger.warning( - f"Dashboard topic notification failed for detection " - f"{detection_id}: {e}" + else: + topic_response = "already_sent" + logger.info( + f"Dashboard topic notification already sent for detection " + f"{detection_id}, skipping" ) - # 4. 토픽 알림 이력 저장 (notifications_db) - Notification.objects.using("notifications_db").create( - detection_id=detection_id, - fcm_token="topic:dashboard_alerts", - title=title, - body=body, - status="sent" if topic_response else "failed", - sent_at=timezone.now() if topic_response else None, - error_message=None if topic_response else "Topic send failed", - ) - # 5. 매칭된 차량에 개별 푸시 (기존 동작) vehicle = None if detection.vehicle_id: @@ -142,8 +153,8 @@ def send_notification(self, detection_id: int): } except Detection.DoesNotExist: - logger.error(f"Detection {detection_id} not found or not completed") - return {"status": "error", "reason": "Detection not found"} + logger.warning(f"Detection {detection_id} not found or not completed, retrying") + raise self.retry(countdown=3, max_retries=3) except Exception as exc: try: @@ -155,8 +166,8 @@ def send_notification(self, detection_id: int): retry_count=self.request.retries, error_message=str(exc), ) - except Exception: - pass + except Exception as db_err: + logger.error(f"Failed to record notification failure for detection {detection_id}: {db_err}") logger.error(f"Notification failed for detection {detection_id}: {exc}") raise diff --git a/tasks/ocr_tasks.py b/tasks/ocr_tasks.py index 892b8f9..a105e6b 100644 --- a/tasks/ocr_tasks.py +++ b/tasks/ocr_tasks.py @@ -132,7 +132,10 @@ def process_ocr(self, detection_id: int, gcs_uri: str): # 7. Always send notification for completed detections # (dashboard gets topic notification; matched vehicle gets individual push) - send_notification.apply_async(args=[detection_id], queue="fcm_queue") + try: + send_notification.apply_async(args=[detection_id], queue="fcm_queue") + except Exception as e: + logger.warning(f"Failed to enqueue notification for detection {detection_id}: {e}") logger.info(f"OCR completed for detection {detection_id}: {plate_number}") return { From 32ead5c1d1f6ae01631e45ebf3fb127585591c46 Mon Sep 17 00:00:00 2001 From: sanghun Date: Sat, 7 Feb 2026 00:55:33 +0900 Subject: [PATCH 4/5] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20=ED=92=88?= =?UTF-8?q?=EC=A7=88=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CORS 빈 문자열 폴백 방지 및 중복 설정 제거 (prod.py) - 중복 DB 인덱스 제거 (Detection, Notification, Vehicle) - statistics() 메서드 import 중복 제거 및 period_map 패턴 적용 - 불필요한 __future__ import 제거 (Python 3 전용) - DLQ 태스크 export 누락 보완 (tasks/__init__.py) - QuerySet.update()의 auto_now 우회 관련 주석 추가 --- apps/detections/models.py | 2 +- apps/detections/views.py | 33 +++++++++++---------------------- apps/notifications/models.py | 2 +- apps/vehicles/models.py | 1 - config/__init__.py | 2 -- config/celery.py | 2 -- config/settings/prod.py | 4 ++-- tasks/__init__.py | 3 ++- tasks/dlq_tasks.py | 1 + 9 files changed, 18 insertions(+), 32 deletions(-) diff --git a/apps/detections/models.py b/apps/detections/models.py index 3208a5d..a1fb949 100644 --- a/apps/detections/models.py +++ b/apps/detections/models.py @@ -18,7 +18,7 @@ class Detection(models.Model): # MSA: FK 대신 ID로 참조 (Vehicles Service) vehicle_id = models.BigIntegerField( - null=True, blank=True, db_index=True, verbose_name="차량 ID" + null=True, blank=True, verbose_name="차량 ID" ) detected_speed = models.FloatField(verbose_name="감지 속도") speed_limit = models.FloatField(default=60.0, verbose_name="제한 속도") diff --git a/apps/detections/views.py b/apps/detections/views.py index aeae6d1..3a3a469 100644 --- a/apps/detections/views.py +++ b/apps/detections/views.py @@ -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] ) # 카메라 필터 (선택) diff --git a/apps/notifications/models.py b/apps/notifications/models.py index f88a785..49efd97 100644 --- a/apps/notifications/models.py +++ b/apps/notifications/models.py @@ -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 토큰" ) diff --git a/apps/vehicles/models.py b/apps/vehicles/models.py index 487b782..6c660da 100644 --- a/apps/vehicles/models.py +++ b/apps/vehicles/models.py @@ -28,7 +28,6 @@ class Meta: verbose_name = "차량" verbose_name_plural = "차량 목록" indexes = [ - models.Index(fields=["plate_number"]), models.Index(fields=["fcm_token"]), ] diff --git a/config/__init__.py b/config/__init__.py index 8a891ca..53f4ccb 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, unicode_literals - from .celery import app as celery_app __all__ = ("celery_app",) diff --git a/config/celery.py b/config/celery.py index 634af32..9f3b672 100644 --- a/config/celery.py +++ b/config/celery.py @@ -1,7 +1,5 @@ """Celery Configuration""" -from __future__ import absolute_import, unicode_literals - import os from celery import Celery diff --git a/config/settings/prod.py b/config/settings/prod.py index 17c6217..fa9df62 100644 --- a/config/settings/prod.py +++ b/config/settings/prod.py @@ -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] diff --git a/tasks/__init__.py b/tasks/__init__.py index 4009146..c3d404a 100644 --- a/tasks/__init__.py +++ b/tasks/__init__.py @@ -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"] diff --git a/tasks/dlq_tasks.py b/tasks/dlq_tasks.py index 8e7b1d2..4109516 100644 --- a/tasks/dlq_tasks.py +++ b/tasks/dlq_tasks.py @@ -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") From b529a4f8590f6d522cdfa0d99d639716a8675293 Mon Sep 17 00:00:00 2001 From: sanghun Date: Sat, 7 Feb 2026 21:12:17 +0900 Subject: [PATCH 5/5] =?UTF-8?q?style:=20black=20=ED=8F=AC=EB=A7=A4?= =?UTF-8?q?=ED=84=B0=20=EC=A0=81=EC=9A=A9=20(CI=20Code=20Quality=20?= =?UTF-8?q?=EC=88=98=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/detections/models.py | 4 +--- apps/vehicles/views.py | 4 +++- tasks/notification_tasks.py | 19 +++++++++++++------ tasks/ocr_tasks.py | 4 +++- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/apps/detections/models.py b/apps/detections/models.py index a1fb949..2ab2d74 100644 --- a/apps/detections/models.py +++ b/apps/detections/models.py @@ -17,9 +17,7 @@ class Detection(models.Model): ] # MSA: FK 대신 ID로 참조 (Vehicles Service) - vehicle_id = models.BigIntegerField( - null=True, blank=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( diff --git a/apps/vehicles/views.py b/apps/vehicles/views.py index b8de3a9..f36c849 100644 --- a/apps/vehicles/views.py +++ b/apps/vehicles/views.py @@ -68,7 +68,9 @@ def register_fcm(self, request): try: FCM_MOCK = os.getenv("FCM_MOCK", "false").lower() == "true" if FCM_MOCK: - logger.info("[MOCK] Would subscribe token to dashboard_alerts topic") + logger.info( + "[MOCK] Would subscribe token to dashboard_alerts topic" + ) else: from core.firebase.fcm import get_fcm_client diff --git a/tasks/notification_tasks.py b/tasks/notification_tasks.py index 4537c9e..770a3c8 100644 --- a/tasks/notification_tasks.py +++ b/tasks/notification_tasks.py @@ -55,9 +55,15 @@ def send_notification(self, detection_id: int): # 3. 대시보드 토픽으로 항상 전송 (중복 방지) topic_response = None - already_sent_topic = Notification.objects.using("notifications_db").filter( - detection_id=detection_id, fcm_token="topic:dashboard_alerts", status="sent" - ).exists() + already_sent_topic = ( + Notification.objects.using("notifications_db") + .filter( + detection_id=detection_id, + fcm_token="topic:dashboard_alerts", + status="sent", + ) + .exists() + ) if not already_sent_topic: try: @@ -134,8 +140,7 @@ def send_notification(self, detection_id: int): ) except Exception as e: logger.warning( - f"Vehicle notification failed for detection " - f"{detection_id}: {e}" + f"Vehicle notification failed for detection " f"{detection_id}: {e}" ) Notification.objects.using("notifications_db").create( detection_id=detection_id, @@ -167,7 +172,9 @@ def send_notification(self, detection_id: int): error_message=str(exc), ) except Exception as db_err: - logger.error(f"Failed to record notification failure for detection {detection_id}: {db_err}") + logger.error( + f"Failed to record notification failure for detection {detection_id}: {db_err}" + ) logger.error(f"Notification failed for detection {detection_id}: {exc}") raise diff --git a/tasks/ocr_tasks.py b/tasks/ocr_tasks.py index a105e6b..a547939 100644 --- a/tasks/ocr_tasks.py +++ b/tasks/ocr_tasks.py @@ -135,7 +135,9 @@ def process_ocr(self, detection_id: int, gcs_uri: str): try: send_notification.apply_async(args=[detection_id], queue="fcm_queue") except Exception as e: - logger.warning(f"Failed to enqueue notification for detection {detection_id}: {e}") + logger.warning( + f"Failed to enqueue notification for detection {detection_id}: {e}" + ) logger.info(f"OCR completed for detection {detection_id}: {plate_number}") return {