Django 연습

17. Django 알림 시스템 구현

수달1234 2025. 4. 8. 15:21

📢 시작 🏷️

오늘은 소셜 미디어나 커뮤니티 사이트에서 사용자 경험을 향상시키는 기능중 하나인 알림 시스템을 구현하고자 합니다. 사용자들은 자신의 콘텐츠나 작게는 글, 댓글에 대한 반응을 실시간을 받기를 원합니다. 이 글에서는 데이터베이스 기반의 알림 시스템을 구축하여 이를 구현하고자 합니다.

 

📢 개요

구현할 알림 시스템은 다음 유형의 활동에 대해 알림을 생성합니다:

  • 게시글에 대한 좋아요
  • 게시글에 달린 댓글
  • 댓글에 달린 대댓글
  • 사용자 팔로우

 

📝 모델 정의

데이터베이스에 저장해야하므로 새롭게 app을 추가해야합니다. 

from django.db import models
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType

User = get_user_model()

class Notification(models.Model):
    # 알림 유형
    LIKE = 'like'
    COMMENT = 'comment'
    FOLLOW = 'follow'
    MENTION = 'mention'
    REPLY = 'reply'
    
    NOTIFICATION_TYPES = (
        (LIKE, '좋아요'),
        (COMMENT, '댓글'),
        (FOLLOW, '팔로우'),
        (MENTION, '멘션'),
        (REPLY, '답글'),
    )
    
    # 알림 받는 사용자
    recipient = models.ForeignKey(User, on_delete=models.CASCADE, related_name='notifications')
    
    # 알림 보낸 사용자
    actor = models.ForeignKey(User, on_delete=models.CASCADE, related_name='actions')
    
    # 알림 유형
    notification_type = models.CharField(max_length=20, choices=NOTIFICATION_TYPES)
    
    # 알림 관련 객체 (게시글, 댓글 등)
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')
    
    # 알림 텍스트
    text = models.CharField(max_length=255)
    
    # 읽음 여부
    is_read = models.BooleanField(default=False)
    
    # 생성 시간
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        ordering = ['-created_at']
        
    def __str__(self):
        return f"{self.actor} {self.get_notification_type_display()} to {self.recipient}"

✔️ GenericForeignKey : 다양한 유형의 객체(댓글, 글 등)를 참조합니다.

✔️ NOTIFICATION_TYPES : 알림 받고싶은 유형을 지정합니다.

 

📝 유틸 추가

from .models import Notification
from django.contrib.contenttypes.models import ContentType

def create_notification(recipient, actor, notification_type, content_object, text=None):
    """
    알림 생성 함수
    """
    # 자기 자신에게는 알림을 보내지 않음
    if recipient == actor:
        return None
        
    content_type = ContentType.objects.get_for_model(content_object)
    
    # 기본 텍스트 설정
    if text is None:
        if notification_type == Notification.LIKE:
            text = f"{actor.nickname}님이 회원님의 게시글을 좋아합니다."
        elif notification_type == Notification.COMMENT:
            text = f"{actor.nickname}님이 회원님의 게시글에 댓글을 남겼습니다."
        elif notification_type == Notification.FOLLOW:
            text = f"{actor.nickname}님이 회원님을 팔로우합니다."
        elif notification_type == Notification.MENTION:
            text = f"{actor.nickname}님이 회원님을 언급했습니다."
        elif notification_type == Notification.REPLY:
            text = f"{actor.nickname}님이 회원님의 댓글에 답글을 남겼습니다."
    
    notification = Notification.objects.create(
        recipient=recipient,
        actor=actor,
        notification_type=notification_type,
        content_type=content_type,
        object_id=content_object.id,
        text=text
    )
    
    return notification

 

 

🔍 views.py

from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from .models import Notification

@login_required
def notification_list(request):
    """알림 목록 보기"""
    notifications = Notification.objects.filter(recipient=request.user)
    unread_count = notifications.filter(is_read=False).count()
    
    return render(request, 'notifications/notification_list.html', {
        'notifications': notifications,
        'unread_count': unread_count
    })

@login_required
def mark_as_read(request, notification_id):
    """알림 읽음 표시"""
    notification = get_object_or_404(Notification, id=notification_id, recipient=request.user)
    notification.is_read = True
    notification.save()
    
    # 알림 클릭 시 해당 객체로 이동
    content_object = notification.content_object
    
    if notification.notification_type == Notification.LIKE or notification.notification_type == Notification.COMMENT:
        # 게시글이나 댓글 관련 알림
        if hasattr(content_object, 'post'):
            # 댓글인 경우 게시글로 이동
            return redirect('post_detail', post_id=content_object.post.id)
        else:
            # 게시글인 경우
            return redirect('post_detail', post_id=content_object.id)
    elif notification.notification_type == Notification.FOLLOW:
        # 팔로우 알림은 해당 사용자 프로필로 이동
        return redirect('profile')
    
    # 기본적으로 알림 목록으로 리디렉션
    return redirect('notifications:list')

@login_required
def mark_all_as_read(request):
    """모든 알림 읽음 표시"""
    if request.method == 'POST':
        notifications = Notification.objects.filter(recipient=request.user, is_read=False)
        notifications.update(is_read=True)
    
    return redirect('notifications:list')

 

🔍 이벤트 알림 추가

댓글, 팔로우, 좋아요의 view에 알림을 생성하도록 수정해야합니다. 일단은 팔로우에 대한 알림을 추가하였습니다.

@login_required
def follow_toggle(request, user_id):
    """사용자 팔로우/언팔로우 토글 기능"""
    if request.method != 'POST':
        return redirect('profile')
        
    target_user = get_object_or_404(CustomUser, id=user_id)
    
    # 자기 자신을 팔로우할 수 없음
    if request.user == target_user:
        messages.error(request, '자기 자신을 팔로우할 수 없습니다.')
        return redirect('profile')
    
    # 이미 팔로우 중인지 확인
    follow_relation = Follow.objects.filter(follower=request.user, following=target_user)
    
    if follow_relation.exists():
        # 팔로우 관계가 있으면 삭제 (언팔로우)
        follow_relation.delete()
        messages.success(request, f'{target_user.nickname}님 팔로우를 취소했습니다.')
    else:
        # 팔로우 관계가 없으면 생성 (팔로우)
        follow = Follow.objects.create(follower=request.user, following=target_user)
        messages.success(request, f'{target_user.nickname}님을 팔로우합니다.')
        
        # 팔로우 알림 생성
        create_notification(
            recipient=target_user,
            actor=request.user,
            notification_type=Notification.FOLLOW,
            content_object=follow
        )
    
    # 이전 페이지로 리디렉션
    return redirect(request.META.get('HTTP_REFERER', 'profile'))

 

🔍 컨텍스트 프로세서

모든 페이지에서 읽지 않은 알림수를 표시하기 위해 추가합니다.

def notifications_count(request):
    if request.user.is_authenticated:
        unread_count = request.user.notifications.filter(is_read=False).count()
        return {'unread_notifications_count': unread_count}
    return {'unread_notifications_count': 0}

 

settings.py에 등록합니다.

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        'DIRS': [os.path.join(BASE_DIR, 'templates')],  
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
                'messages_app.context_processors.unread_messages_count',
                # 컨텍스트 프로세서서
                'notifications.context_processors.notifications_count',
            ],
        },
    },
]