Django Review APIView 설계

2star_·2025년 1월 8일
0

최종 프로젝트

목록 보기
10/32

전체 Views.py 코드

from django.db.models import Count, Q
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from django.shortcuts import get_object_or_404
from .models import Review, ReviewComment, ReviewLike, ReviewCommentLike
from .serializers import ReviewSerializer, ReviewCommentSerializer, ReviewLikeSerializer, ReviewCommentLikeSerializer
from django.db.models import Count


class ReviewAPIView(APIView):
    """
    리뷰 목록 조회 및 새 리뷰 생성
    """

    def get_permissions(self):
        """
        메서드별 권한 부여
        """
        if self.request.method == "GET":
            return [AllowAny()]
        if self.request.method == "POST":
            return [IsAuthenticated()]
        return super().get_permissions()
    
    def get(self, request):
        """리뷰 목록 조회"""
        sort_by = request.query_params.get('sort_by', 'recent')  # 기본 정렬 기준: 최신순

        # annotate로 좋아요 및 비추천 수 계산 / 'related_name="likes"로 연결된 ReviewLike 모델의 is_active 필드 값이 1인 경우 추천, -1인 경우 비추천'
        reviews = Review.objects.annotate(
            total_likes=Count('likes', filter=Q(likes__is_active=1)),
            total_dislikes=Count('likes', filter=Q(likes__is_active=-1))
        )

        # 정렬 기준 적용
        if sort_by == 'popular':  # 인기순
            reviews = reviews.order_by('-total_likes', '-created_at')
        elif sort_by == 'views':  # 조회순
            reviews = reviews.order_by('-view_count', '-created_at')
        else:  # 최신순
            reviews = reviews.order_by('-created_at')

        serializer = ReviewSerializer(reviews, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)


    def post(self, request):
        """새 리뷰 생성 """
        serializer = ReviewSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save(user=request.user)  # 현재 요청 유저를 저장
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

class ReviewDetailAPIView(APIView):
    """
    특정 리뷰 조회, 수정, 삭제
    """

    def get_permissions(self):
        """
        메서드별 권한 설정
        """
        if self.request.method == "GET":
            return [AllowAny()]
        if self.request.method in ["PUT", "DELETE"]:
            return [IsAuthenticated()]
        return super().get_permissions()

    def get(self, request, pk):
        """특정 리뷰 조회"""
        review = get_object_or_404(Review, pk=pk)

        # 조회수 증가 로직
        review.view_count += 1
        review.save()

        serializer = ReviewSerializer(review)
        return Response(serializer.data, status=status.HTTP_200_OK)

    def put(self, request, pk):
        """특정 리뷰 수정"""
        review = get_object_or_404(Review, pk=pk)

        if review.user != request.user:
            return Response({"detail": "권한이 없습니다."}, status=status.HTTP_403_FORBIDDEN)

        serializer = ReviewSerializer(review, data=request.data, partial=True)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_200_OK)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request, pk):
        """특정 리뷰 삭제"""
        review = get_object_or_404(Review, pk=pk)

        if review.user != request.user:
            return Response({"detail": "권한이 없습니다."}, status=status.HTTP_403_FORBIDDEN)

        review.delete()
        return Response({"detail": "리뷰가 삭제되었습니다."}, status=status.HTTP_204_NO_CONTENT)

class ReviewCommentAPIView(APIView):
    """
    특정 리뷰에 댓글 생성 및 조회
    """
    def get_permissions(self):
        """
        메서드별 권한 설정
        """
        if self.request.method == "GET":
            return [AllowAny()]
        if self.request.method == "POST":
            return [IsAuthenticated()]
        return super().get_permissions()

    def get(self, request, review_id):
        """특정 리뷰의 댓글 목록 조회"""
        review = get_object_or_404(Review, pk=review_id)
        comments = review.comments.all()  # Review 모델의 related_name="comments"로 연결된 댓글 가져오기
        serializer = ReviewCommentSerializer(comments, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)

    def post(self, request, review_id):
        """특정 리뷰에 댓글 생성"""
        review = get_object_or_404(Review, pk=review_id)
        serializer = ReviewCommentSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save(user=request.user, review=review)
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

class ReviewCommentDetailAPIView(APIView):
    """
    특정 댓글 수정 및 삭제
    """
    permission_classes = [IsAuthenticated]

    def put(self, request, pk):
        """댓글 수정"""
        comment = get_object_or_404(ReviewComment, pk=pk)

        if comment.user != request.user:
            return Response({"detail": "권한이 없습니다."}, status=status.HTTP_403_FORBIDDEN)

        serializer = ReviewCommentSerializer(comment, data=request.data, partial=True)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_200_OK)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request, pk):
        """댓글 삭제"""
        comment = get_object_or_404(ReviewComment, pk=pk)

        if comment.user != request.user:
            return Response({"detail": "권한이 없습니다."}, status=status.HTTP_403_FORBIDDEN)

        comment.delete()
        return Response({"detail": "댓글이 삭제되었습니다."}, status=status.HTTP_204_NO_CONTENT)

class ReviewLikeAPIView(APIView):
    """
    특정 리뷰에 좋아요 또는 비추천 생성 및 상태 변경
    """
    permission_classes = [IsAuthenticated]

    def post(self, request, review_id):
        """좋아요/비추천 생성 및 상태 변경"""
        review = get_object_or_404(Review, pk=review_id)

        serializer = ReviewLikeSerializer(data=request.data)
        if serializer.is_valid():
            # 기존 좋아요/비추천 업데이트 또는 새로 생성
            like, created = ReviewLike.objects.update_or_create(
                user=request.user, review=review, defaults={"is_active": serializer.validated_data["is_active"]}
            )
            like_serializer = ReviewLikeSerializer(like)
            return Response(like_serializer.data, status=status.HTTP_200_OK if not created else status.HTTP_201_CREATED)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    
class ReviewCommentLikeAPIView(APIView):
    """
    특정 댓글에 좋아요 또는 비추천 생성 및 상태 변경
    """
    permission_classes = [IsAuthenticated]

    def post(self, request, comment_id):
        """좋아요/비추천 생성 및 상태 변경"""
        comment = get_object_or_404(ReviewComment, pk=comment_id)

        serializer = ReviewCommentLikeSerializer(data=request.data)
        if serializer.is_valid():
            # 기존 좋아요/비추천 업데이트 또는 새로 생성
            like, created = ReviewCommentLike.objects.update_or_create(
                user=request.user, comment=comment, defaults={"is_active": serializer.validated_data["is_active"]}
            )
            like_serializer = ReviewCommentLikeSerializer(like)
            return Response(
                like_serializer.data,
                status=status.HTTP_200_OK if not created else status.HTTP_201_CREATED
            )

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Django Review APIView

Django로 리뷰 및 댓글 관련 API를 설계하면서 APIView를 활용해 전반적인 기능을 구현하였습니다.


1. 메서드별 권한 분리

  • 결정: get_permissions 메서드를 사용해 각 HTTP 메서드별로 권한을 분리.
    • GET: 누구나 접근 가능 (AllowAny)
    • POST, PUT, DELETE: 인증된 사용자만 접근 가능 (IsAuthenticated)
  • 이유: 리뷰와 댓글은 읽기와 쓰기 권한이 명확히 나뉩니다. 읽기는 공개적이어야 하지만, 쓰기는 인증된 사용자만 허용하도록 설정해 보안과 사용자 신뢰를 유지.

2. 좋아요 및 비추천 수 계산

  • 결정: annotateQ 객체를 활용해 Review 모델의 좋아요(is_active=1)와 비추천(is_active=-1) 수를 동적으로 계산.
  • 이유:
    • 별도 필드로 좋아요와 비추천 수를 저장하지 않고 동적 계산을 통해 데이터 일관성 유지.
    • 성능 최적화를 위해 쿼리 수준에서 계산.

3. 정렬 기준 제공

  • 결정: sort_by 쿼리 파라미터로 최신순, 인기순, 조회순 정렬 기능 구현.
    • 기본값: 최신순 (-created_at)
    • 인기순: 좋아요 수 기준 (-total_likes)
    • 조회순: 조회수 기준 (-view_count)
  • 이유: 사용자 경험을 개선하기 위해 다양한 정렬 옵션 제공. 유저가 원하는 기준에 맞게 데이터를 조회할 수 있도록 함.

4. 조회수 증가 로직

  • 결정: 리뷰 상세 조회 시(GET /reviews/<pk>/), view_count 필드를 증가시키는 로직 추가.
  • 이유:
    • 상세 페이지의 조회수를 정확히 반영.
    • 유저의 관심도를 파악해 향후 기능 개선이나 분석에 활용 가능.

5. 좋아요 및 비추천 상태 관리

  • 결정: update_or_create를 사용해 좋아요/비추천 상태를 생성하거나 업데이트.
    • ReviewLikeReviewCommentLike 모델 모두 동일한 로직 적용.
  • 이유:
    • 동일한 유저가 동일 리뷰/댓글에 대해 여러 번 요청을 보내는 것을 방지.
    • 유저의 의도에 따라 좋아요 상태를 쉽게 수정할 수 있도록 구현.

6. 댓글 관련 설계

  • 결정:
    • ReviewCommentAPIView: 특정 리뷰의 댓글 목록 조회 및 댓글 생성.
    • ReviewCommentDetailAPIView: 댓글 수정 및 삭제 기능 구현.
  • 이유:
    • 댓글은 리뷰와의 관계가 명확하므로 related_name을 통해 관리.
    • 댓글별 수정/삭제 권한을 제한해 데이터 무결성을 유지.

7. Serializer를 활용한 데이터 검증 및 응답

  • 결정: 모든 데이터 검증과 직렬화는 DRF의 Serializer를 활용.
  • 이유:
    • 코드의 재사용성 향상.
    • 입력 데이터의 유효성을 RESTful API 레이어에서 검증하여 안전한 데이터 처리 가능.

8. 에러 처리 및 응답 코드

  • 결정:
    • 유효하지 않은 데이터: 400 BAD REQUEST
    • 인증되지 않은 접근: 403 FORBIDDEN
    • 요청 성공: 200 OK, 201 CREATED, 204 NO CONTENT
  • 이유: HTTP 표준 응답 코드를 사용해 API의 가독성과 디버깅 효율성 향상.

향후 개선 가능성

  1. 캐싱 도입: 조회수가 높은 리뷰에 대한 요청 최적화를 위해 캐싱을 고려.
  2. 페이징 추가: 리뷰 및 댓글 목록 조회 시 많은 데이터가 반환될 수 있으므로 DRF의 Pagination 활용.
  3. 좋아요/비추천 로직 개선: 쿼리 최적화를 위해 DB 레벨의 트리거나 캐싱 사용 고려.

이렇게 사용자 경험과 데이터 일관성을 우선으로 하고, Django와 DRF의 장점을 최대한 활용하는 방향으로 진행했습니다.

profile
안녕하세요.

0개의 댓글

관련 채용 정보