장고 신고 기능 만들기

소요 시간: 10분

사용자가 부적절한 글이나 댓글을 신고할 수 있도록 하는 기능은 포럼 운영에 매우 중요하다. 이를 통해 사용자들은 안전한 커뮤니케이션 환경을 느낄 수 있고, 운영자는 문제를 조기에 인지하여 대처할 수 있다. 


신고 모델 설계

사용자가 신고한 글(Post)이나 댓글(Comment)을 저장할 수 있는 Report 모델을 만들었다. 이 모델에는 신고한 사용자, 신고 대상의 타입(글 또는 댓글), 신고 사유, 상태 등을 저장할 수 있도록 했다. 특히, 신고된 글이나 댓글을 공통으로 처리할 수 있도록 GenericForeignKey를 활용하기로 했다.

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

class Report(models.Model):
    REPORT_STATUS_CHOICES = [
        ('pending', 'Pending'),
        ('reviewed', 'Reviewed'),
        ('resolved', 'Resolved'),
        ('rejected', 'Rejected'),
    ]

    user = models.ForeignKey(User, on_delete=models.CASCADE)  # 신고한 사용자
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)  # 신고된 객체의 타입 (글 또는 댓글)
    object_id = models.PositiveIntegerField()  # 신고된 객체의 ID
    content_object = GenericForeignKey('content_type', 'object_id')  # 신고된 객체 (글 또는 댓글)
    reason = models.TextField()  # 신고 사유
    status = models.CharField(max_length=10, choices=REPORT_STATUS_CHOICES, default='pending')  # 신고 상태
    created_at = models.DateTimeField(auto_now_add=True)  # 신고 날짜
    
    # 관리용 필드
    is_resolved = models.BooleanField(default=False)  # 관리자가 신고를 처리했는지 여부
    resolved_at = models.DateTimeField(null=True, blank=True)  # 신고가 처리된 시간
    resolved_by = models.ForeignKey(User, null=True, blank=True, related_name='reports_resolved', on_delete=models.SET_NULL)  # 처리한 관리자

    def __str__(self):
        return f'{self.user.username} - {self.get_status_display()}'

여기서 **GenericForeignKey**를 사용한 이유는 다수의 객체를 하나의 모델로 처리할 수 있기 때문이다. 예를 들어, `Post`와 `Comment` 같은 서로 다른 모델을 신고할 때, 각 모델을 위한 별도의 필드를 만들 필요 없이, 하나의 `Report` 모델에서 `content_type`과 `object_id`를 사용하여 신고할 수 있다. 이로 인해 코드가 훨씬 간결해지고 중복을 피할 수 있었다.

만약 각 모델에 대해 별도의 `ForeignKey`를 사용한다면 다음과 같은 문제들이 발생할 수 있다. `post`와 `comment` 필드를 모두 포함시키면, 하나는 항상 `null`이 되어야 하므로 이로 인해 복잡한 조건 처리가 필요해진다. 또한, 새로운 신고 대상 모델이 추가될 경우, 기존의 `Report` 모델에 필드를 추가해야 하고 데이터베이스 마이그레이션도 필요하다. 이렇게 되면 유지보수가 어려워질 수 있다.

is_resolved와 resolved_at, resolved_by를 추가했다. 24시간 내 처리되어야 하는데 여러 사람들이 이 필드들을 이용하면 신고들이 효율적으로 관리될 것 같다.


신고 뷰 구현

신고 기능의 뷰는 Django의 FormView를 활용하여 작성했다. 사용자가 신고 버튼을 클릭하면 신고할 대상이 글인지 댓글인지에 따라 적절한 폼이 표시되도록 설계했다. 뷰 클래스에서 dispatch 메서드를 오버라이드하여 신고할 객체를 동적으로 가져오는 로직을 구현했다.

from django.views.generic.edit import FormView
from django.contrib import messages
from django.shortcuts import get_object_or_404
from .forms import ReportForm
from django.contrib.contenttypes.models import ContentType

class ReportView(FormView):
    template_name = 'report_form.html'
    form_class = ReportForm

    def dispatch(self, request, *args, **kwargs):
        # 신고할 객체의 모델과 ID를 기반으로 대상 가져오기
        model = ContentType.objects.get(app_label='posts', model=self.kwargs['model']).model_class()
        self.content_object = get_object_or_404(model, id=self.kwargs['object_id'])
        return super().dispatch(request, *args, **kwargs)

    def form_valid(self, form):
        report = form.save(commit=False)
        report.user = self.request.user
        report.content_object = self.content_object  # 신고 대상 객체 설정
        report.save()
        messages.success(self.request, '신고가 접수되었습니다.')
        return super().form_valid(form)

    def get_success_url(self):
        # 신고된 객체가 글인지 댓글인지에 따라 리다이렉트 URL 결정
        if hasattr(self.content_object, 'post'):
            return reverse('post_detail', kwargs={'post_id': self.content_object.post.id})
        else:
            return reverse('post_detail', kwargs={'post_id': self.content_object.id})

신고가 성공적으로 처리되면 사용자는 신고가 접수되었다는 메시지를 확인할 수 있도록 했다.


신고 폼 템플릿

신고 폼을 위한 템플릿 코드는 다음과 같다. 사용자가 신고 사유를 입력할 수 있는 간단한 양식을 제공한다.

<!-- report_form.html -->
<h2>신고하기</h2>
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">신고 제출</button>
</form>

<a href="{% url 'my_reports' %}">내 신고 내역 보기</a>

신고 상태를 확인할 수 있는 페이지도 작성했다. 사용자가 자신의 신고 내역을 쉽게 확인할 수 있도록 하고, 신고 대상의 종류에 따라 링크를 다르게 표시하도록 했다.

<h2>내 신고 목록</h2>
<table>
    <tr>
        <th>신고 대상</th>
        <th>사유</th>
        <th>상태</th>
        <th>신고 날짜</th>
    </tr>
    {% for report in reports %}
    <tr>
        <td>
            {% if report.content_type.model == 'post' %}
                <a href="{% url 'post_detail' report.object_id %}">{{ report.content_object.title }}</a>
            {% elif report.content_type.model == 'comment' %}
                <a href="{% url 'post_detail' report.content_object.post.id %}">댓글 보기</a>
            {% endif %}
        </td>
        <td>{{ report.reason }}</td>
        <td>{{ report.get_status_display }}</td>
        <td>{{ report.created_at }}</td>
    </tr>
    {% endfor %}
</table>

이렇게 하면 사용자는 자신이 신고한 신고들과 결과를 쉽게 확인할 수 있을 것이다.


신고 URL 패턴

신고 요청을 처리하는 URL 패턴도 설정했다. model과 object_id를 받아서 모든 신고 요청을 하나의 뷰로 처리할 수 있게 하였다. 이로 인해 URL이 간결해지고 코드가 깔끔해졌다.

from django.urls import path
from .views import ReportView, MyReportsView

urlpatterns = [
    path('report/<str:model>/<int:object_id>/', ReportView.as_view(), name='report'),
    path('my-reports/', MyReportsView.as_view(), name='my_reports'),
]


신고 기능은 필수다. 신고 기능이 없다면 앱 스토어에 앱을 배포할 수 없다. 또한 Django의 강력한 기능인 GenericForeignKey와 contenttypes를 잘 활용할 수 있었던 것 같다. 이러한 설계를 통해 코드의 간결함과 유지보수의 용이성을 동시에 확보할 수 있었다. 특히, 다양한 유형의 객체를 하나의 필드에서 처리하는 방식이 매우 유용하다는 것을 깨달았다.

장고 리스트