본문 바로가기

TIL 및 WIL/TIL (Today I Learned)

[TIL] 2022.06.23 (Django 심화, DRF 6)

오늘은 어제 만들었던 product 앱 안에 있는 Product 모델의 테이블들을 추가해주고, Review라는 새로운 모델을 만들어서 설정을 해주었다.

그리고 기존 serializer 안에 있는 validate / create / update 기능을 직접 custom 해보고, 새로운 serializer와 permission을 만들어서 사용해보는 공부를 했다.

 

우선 product/models.py에서 기존에 사용하였던 Product 모델에 테이블을 몇 가지 추가해주었다.

 

# product/models.py

class Product(models.Model):
    # 기존에 있던 테이블
    # 작성자 / 제목 / 썸네일 / 설명 / 등록일자 / 노출시작일 / 노출종료일
    user = models.ForeignKey('user.User', verbose_name="작성자", on_delete=models.CASCADE)
    title = models.CharField("제목", max_length=50)
    # upload_to='img/product_img/%Y%m%d'
    thumbnail = models.ImageField("썸네일", upload_to='product/img/%Y%m%d', width_field=None, height_field=None, max_length=100)
    description = models.TextField("설명")
    created_at = models.DateTimeField("등록일자", auto_now_add=True)
    show_date_start = models.DateTimeField("노출 시작 일자", default=timezone.now)
    show_date_end = models.DateTimeField("노출 종료 일자", default=timezone.now)

    # 추가 테이블
    # 가격 / 수정일자 / 활성화여부
    price = models.IntegerField("가격", default=0)
    update_at = models.DateTimeField("수정일자", auto_now=True)
    is_active = models.BooleanField("활성화 여부", default=True)

    def __str__(self):
    	return f'{self.user.username}님이 등록한 {self.title}입니다.'

 

가격, 수정 일자, 활성화 여부 테이블을 추가해주었다.

가격은 기본값(default)으로 0을 주었고, 수정 일자는 수정이 될 때마다 현재 시간으로 저장, True/False를 정해주는 BooleanField를 사용하여 활성화 여부를 정해주었다.

 

이제 다음으로 serializer에서 직접 custom 한 validate / create / update 기능을 사용해보기 위해 기존에 사용하던 ProductSerializer에 해당 기능들 안에 조건을 추가하여 연습을 해보았다.

 

# product/serializers.py

class ProductSerializer(serializers.ModelSerializer):
    class Meta:
        model = ProductModel
        fields = ["user", "title", "thumbnail", "description",
        "created_at", "update_at","show_date_start", "show_date_end",
        "price", "is_active"]

    # 노출 종료 일자가 현재보다 더 이전 시점이라면 상품 등록 불가능
    def validate(self, data):
        now_date = timezone.now()

        if now_date > data.get("show_date_end"):
            raise serializers.ValidationError(
                detail = {"error": "노출 종료 일자가 현재보다 과거에 있습니다."}
            )
        return data

    # 상품 설명의 마지막에 "<등록일자>에 등록된 상품입니다." 라는 문구 추가
    def create(self, validated_data):
        product = ProductModel(**validated_data)

        create_at = product.create_at
        msg = f'{create_at}에 등록된 상품입니다.'

        product.description = product.description + msg
        product.save()

        return product

    # 상품 수정 시 상품 설명의 첫줄에 "<수정일자>에 수정되었습니다." 라는 문구 추가
    def update(self, instance, validated_data):
        # instance에는 입력된 object가 담긴다.
        for key, value in validated_data.items():
            setattr(instance, key, value)
        instance.save()

        update_at = instance.update_at
        msg = f'{update_at}에 수정되었습니다.'

        instance.description = msg + instance.description
        instance.is_active = True
        instance.save()

        return instance

 

 

validate 기능으로는 노출 종료 일자(show_date_end)가 현재 시간보다 이전이라면 상품 등록이 불가능하도록 설정을 해주었다.

우선 now_date라는 변수 안에 timezone.now()를 사용하여 현재 시간을 넣어주었고, 현재 시간과 노출 종료 일자를 비교하여 해당 조건에 만족하면 error를 발생시키도록 만들어주었다.

error가 발생하지 않는다면 request 받은 data를 반환해주도록 했다.

 

create 기능에는 제품 등록 시 등록일자(create_at)를 설명(description) 뒤에 추가하여 저장을 해주었다.

 

update 기능으로는 create 기능과 반대로 수정 일자(update_at)를 설명 앞에 추가하여 수정을 하도록 만들어주었다.

 

이렇게 세 개의 기능을 custom 하여 만들어주고 해당 ProductSerializer가 실행되면 해당 기능들이 사용되도록 해주었다.

 

다음으로 제품(product)에 대한 리뷰를 작성할 수 있도록 Review 모델을 만들어주었다.

 

# product/models.py

USER_RATING = {
    (1, "1점"),
    (2, "2점"),
    (3, "3점"),
    (4, "4점"),
    (5, "5점"),    
}

# 리뷰 모델
class Review(models.Model):
    # 작성자 / 상품 / 내용 / 평점 / 작성일
    user = models.ForeignKey('user.User', verbose_name="작성자", on_delete=models.CASCADE)
    product = models.ForeignKey(Product, verbose_name="상품", on_delete=models.CASCADE)
    content = models.TextField("내용")
    rating = models.IntegerField("평점", choices=USER_RATING, default=1)
    created_at = models.DateTimeField("작성일", auto_now_add=True)

    def __str__(self):
        return f'{self.user.username}님이 {self.product.title} 제품에 대해 남긴 리뷰입니다.'

 

해당 Review 모델에는 작성자, 상품, 내용, 평점, 작성일에 대한 테이블을 만들어주었고, 작성자와 상품에 대해서는 ForeignKey 관계로 User 모델과 Product 모델에 대해 관계를 설정해주었다.

 

평점에 관련된 rating은 choices를 사용하여 USER_RATING 정보를 받아와서 사용하도록 해주었다.

맨 처음 기본값으로는 1을 설정해주었다.

 

이후 새롭게 생성된 Review 모델을 관리자 페이지에서 관리할 수 있도록 product/admin.py에 해당 모델을 연결해주었다.

 

어드민 페이지에 추가된 Review 모델

 

이제 새로운 serializer를 생성하고 새로운 APIView를 만들어서 제품에 대한 review 정보를 볼 수 있도록 product 앱의 serializers.py와 views.py에 다음과 같이 코드를 추가해주었다.

 

# product/serializers.py

class ProductSecondSerializer(serializers.ModelSerializer):
    user = serializers.SerializerMethodField()

    # 로그인된 사용자
    def get_user(self, obj):
        return obj.user.username

    class Meta:
        model = ProductModel
        fields = ["user", "title", "thumbnail", "description",
        "created_at", "update_at","show_date_start", "show_date_end",
        "price", "is_active"]
        

# product/views.py

class ProductSecondView(APIView):
    def get(self, request):
        user = request.user # 로그인된 사용자

        if not user.is_authenticated:
            return Response({"message": "로그인이 필요합니다."}, status=status.HTTP_401_UNAUTHORIZED)

        show_date_now = timezone.now() # 현재 시간

        # 현재 일자를 기준으로 노출 종료 일자가 지나지 않았거나, 활성화 여부가 True
        # 로그인한 사용자가 등록한 상품들의 정보를 Serializer를 사용해 반환
        terms = Q(show_date_end__gte=show_date_now) & Q(is_active=True)

        products = ProductModel.objects.filter(terms)

        return Response(ProductSecondSerializer(products, many=True).data, status=status.HTTP_200_OK)

 

기존에 있던 ProductSerializer와 ProductView의 get 내용을 가져와서 일부만 수정해주었다.

그대로 사용하면 이름이 겹치기 때문에 ProductSecond- 로 이름을 바꿔서 내용을 바꿔주도록 했다.

 

우선 APIView에서 현재 시간을 기준으로 노출 종료 일자가 지나지 않거나, 활성화 여부가 True면 로그인된 사용자가 등록한 제품 정보를 보여줄 수 있도록 만들어주었다.

 

저번에 했던 코드에서 추가된 것은 활성화 여부밖에 없는데 활성화 여부는 is_active로 설정해 주었기 때문에 Q(is_active=True)를 추가하여 조건을 달아주었다.

 

이제 제품들에 대한 review 내용을 확인할 수 있도록 ReviewSerializer를 만들어주고 해당 APIView 조회 시 제품 목록과 리뷰를 볼 수 있도록 만들어주었다.

 

# product/serializers.py

class ReviewSerializer(serializers.ModelSerializer):
    user = serializers.SerializerMethodField()

    def get_user(self, obj):
        return obj.user.username

    class Meta:
        model = ReviewModel
        fields = ["user", "product", "content", "rating", "created_at"]

class ProductSecondSerializer(serializers.ModelSerializer):
    user = serializers.SerializerMethodField()
    average_rating = serializers.SerializerMethodField()
    review = serializers.SerializerMethodField()

    # 로그인된 사용자
    def get_user(self, obj):
        return obj.user.username

    # 평균 점수는 (리뷰 평점의 합/리뷰 갯수)로 구해주세요.
    def get_average_rating(self, obj):
        # 하나의 제품(product)의 review(obj)
        reviews = ReviewModel.objects.filter(product=obj)

        if 0 < len(reviews):
            total_rating = 0

            for review in reviews:
                total_rating += review.rating

            avg_rating = total_rating / len(reviews)

            return avg_rating
        else:
            return None

    # 작성된 리뷰는 모두 return하는 것이 아닌 가장 최근 리뷰 1개만 리턴해주세요.
    def get_review(self, obj):
        # 하나의 제품(product)의 review(obj)
        reviews = ReviewModel.objects.filter(product=obj).order_by("-created_at")
        if 0 < len(reviews):
            recent_review = ReviewSerializer(reviews[0]).data
            return recent_review
        else:
            return None

    class Meta:
        model = ProductModel
        fields = ["user", "title", "thumbnail", "description",
        "created_at", "update_at","show_date_start", "show_date_end",
        "price", "is_active", "average_rating", "review"]

 

ReviewModel에 있는 테이블 필드들을 ReviewSerializer 안에 넣어주고, ProductSecondSerializer에 조건에 부합하는 내용을 넣어줄 수 있도록 추가적인 설정을 해주었다.

 

우선 제품에 대한 리뷰들을 가져와서 해당 리뷰의 평균을 낼 수 있도록 코드를 만들어주었다.

 

리뷰 정보를 가져오기 위해 제품들에 대한 review를 reviews에 넣어주었다.

if문을 사용해 review가 하나 이상이라도 존재하면 total_rating에 리뷰들에 적힌 평점(rating)을 저장하고, 전체 평점 값과 리뷰의 수를 나눠준 뒤 해당 값을 반환해주었다.

 

이때 else를 사용하여 0<len(reviews) 조건이 만족하지 않으면 None 값을 반환하도록 만들어 주었는데, 리뷰가 없을 경우 None값을 반환해주지 않는다면 오류가 발생하게 된다.

그렇기에 제품에 대한 리뷰가 존재하지 않는다면 None 값을 반환하여 코드에 오류가 발생하지 않도록 설정해주었다.

 

다음은 가장 최근 리뷰 하나만 반환받기 위한 조건을 만들어주었다.

 

평점 평균값을 구할 때와 비슷하게 제품들에 대한 review를 가져오고, 해당 review를 order_by("-created_at")을 사용하여 생성일을 기준으로 내림차순 정렬을 해주었다.

내림차순 정렬을 해주면 가장 최근에 작성한 리뷰가 처음으로 저장되게 된다.

이후 if문을 통해 리뷰가 있는지 확인한 다음, 제일 첫 번째로 저장된 리뷰를 반환해주도록 하였다.

 

평점 평균값과 마찬가지로 리뷰가 없을 경우에 발생하는 오류를 방지하기 위해서 리뷰가 없을 경우에는 None 값을 반환하도록 설정해주었다.

 

serializer 조건 설정을 마친 뒤 마지막으로 fields에 average_rating과 review를 추가해주는 것으로 serializer 설정을 마무리했다.

 

이제 마지막으로 permission을 사용해 사용자 권한에 대한 설정을 해주었다.

 

# drf_project/permissions.py

class IsNotAuthenticatedReadOnlyOrMoreThanThreeDaysUserCreate(BasePermission):
    # 관리자(admin)는 읽기/쓰기 가능
    # 로그인 하지 않는 사용자 : 읽기(GET)
    # 회원가입 후 3일 이상 지난 사용자 : 읽기/쓰기
    SAFE_METHODS = ('GET', ) # 일반 사용자 권한 허용 : GET
    # '접근 권한이 없습니다.'
    message = '관리자 또는 회원가입 후 3일이 지나야 합니다.'

    def has_permission(self, request, view):
        user = request.user

        if not user.is_authenticated:
            response = {
                "detail": "서비스를 이용하기 위해서는 로그인이 필요합니다.",
            }
            raise GenericAPIException(status_code=status.HTTP_401_UNAUTHORIZED, detail=response)

        # 인증된 사용자가 관리자(admin)일 경우
        if user.is_authenticated and user.is_admin:
            return True

        # 사용자가 인증되었고, 가입 후 3일이 지난 사용자일 경우
        if user.is_authenticated and request.user.join_date < (timezone.now() - timedelta(days=3)):
            return True

        # 사용자가 인증되었고, 권한이 GET일 경우
        elif user.is_authenticated and request.method in self.SAFE_METHODS:
            return True
        
        return False

 

사용자 권한 설정으로 관리자 또는 가입 후 3일이 지난 사용자는 읽기/쓰기가 가능하고, 일반 사용자는 읽기만 가능하도록 설정을 해주었다.

 

이제 Postman에서 잘 작동하는지 확인하기 전에 관리자 페이지에서 리뷰를 생성해주도록 하였다.

 

작성된 리뷰들

 

마지막으로 Postman에서 API 동작이 되는지 확인해주었다.

 

API 동작

 

제품과 리뷰들에 대한 내용이 정상적으로 출력되는 것을 볼 수 있었다.

리뷰가 없으면 None 값을 반환하도록 설정하였기에 리뷰가 없으면 average_rating과 review가 null 값으로 나오는 것도 볼 수 있었다.

 

Github : https://github.com/J1NU2/DRF_Practice

 

GitHub - J1NU2/DRF_Practice

Contribute to J1NU2/DRF_Practice development by creating an account on GitHub.

github.com

 

-

 

장고 심화 과정으로 DRF에 대해 공부하는 시간을 가져보았다.

처음에는 많이 어렵고 이해가 되지 않는 부분이 많이 있었으나, 팀장님과 팀원분들의 도움으로 이해해가면서 공부하는 과정이 재미있게 느껴졌었다.

 

그래도 아직은 다소 어려운 부분도 있고 완벽하게 이해한 부분 또한 확실하다고 말할 수 없으나, 처음 공부했을 때보다는 이해도 많이 되었고 아예 모르겠다고 말할 정도까지는 아닌 것 같다.

 

아직 배운 지 얼마 안 되기도 했고 앞으로 남은 날도 많이 있기에 계속 공부하다 보면 익숙해지고 잘 다룰 수 있을 것 같은 느낌이 든다.

 

이번 DRF 연습을 하면서는 Postman을 통해 API 통신만 해봤는데, 아직 Frontend를 다뤄보지 않아서 다소 걱정되는 부분 또한 있는 것 같다.

 

:P