본문 바로가기
DEV/Web 개발

Web개발 :: 페이지네이션, permission class, LocalStorage _TIL71

by 올커 2022. 12. 14.

■ JITHub 개발일지 71일차

□  TIL(Today I Learned) ::

페이지네이션, permission class, LocalStorage

 1. 페이지네이션 적용
  - 페이지네이션은 장소 추천 리스트에 적용했다. 페이지네이션은 rest framewokr에서 이미 제공해주고 있는 모듈을 사용하면 편리하게 구현할 수 있다. 아래와 같이 프로젝트 폴더에 페이지네이션을 구현할 모듈을 지정해둔다.

# project_folder/pagination.py

from rest_framework.pagination import PageNumberPagination


class BasePagination(PageNumberPagination):
    # 페이지 사이즈를 지정할 query_param 문자열 지정 ex) /?page_size=5
    page_size_query_param = "page_size"


class PaginationHandlerMixin(object):
    @property
    def paginator(self):
        # 페이지네이터와 연결된 뷰 확인 hasattr(속성 포함 여부 확인)
        if not hasattr(self, "_paginator"):
            if self.pagination_class is None:
                self._paginator = None
            else:
                self._paginator = self.pagination_class()
        else:
            pass
        return self._paginator

    def paginate_queryset(self, queryset):
        # 결과 한 페이지를 반환하거나, 페이지 분할을 사용하지 않을 경우 '없음'을 반환합니다.
        if self.paginator is None:
            return None
        return self.paginator.paginate_queryset(queryset, self.request, view=self)

    def get_paginated_response(self, data):
        # 지정된 출력 데이터에 대해 페이지 유형 'Response' 개체를 반환합니다.
        assert self.paginator is not None
        return self.paginator.get_paginated_response(data)

 - views.py에서 불러와 사용을 하는데, 아래 19번째 행부터가 페이지네이션을 적용하여 변경된 부분을 보여준다.

   쿼리셋을 세팅한 후 페이지네이션을 거치고, serializer도 페이지네이션에 맞게 만들어주어야 한다.

# places/views.py

from rest_framework.pagination import PageNumberPagination
...

##### 맛집(유저일 경우) #####
class UserPlaceListView(PaginationHandlerMixin, APIView):
    permission_classes = [IsAuthenticated]
    pagination_class = PlaceListPagination

    # 맛집 리스트 추천
    @swagger_auto_schema(operation_summary="맛집 리스트 추천(유저)",
                    responses={200 : '성공', 500 : '서버 에러'})
    def get(self, request, cate_id):
        place_list = rcm_place_user(user_id = request.user.id, cate_id=cate_id)
        preserved = Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(place_list)])
        place = Place.objects.filter(id__in=place_list).order_by(preserved)
        
        page = self.paginate_queryset(place)
        serializer = self.get_paginated_response(PlaceSerializer(page, many=True).data)
        return Response(serializer.data, status=status.HTTP_200_OK)

 - 사실 가장 번거로웠던 부분은 프론트엔드 부분이었다. 페이지가 1페이지인지, 마지막 페이지인지 각 조건에 따라 페이지네이션도 보여져야 하는 부분이 달라져야 하기 때문이다. (사실 이 로직도 완전하지 않아 수정이 필요하다. 예를들어 페이지가 1~3페이지 밖에 없는 경우 등)

// 페이지네이션
    const page_no = response_json.next.split('=')[1].split('/')[0]
    const last_page_no = parseInt(response_json.count/10)
    if (page_no-1 == 1) {
        $('#pagenation').empty()
        $('#pagenation').append(
        `
            <
            <a href="#"><div class="current_page">${page_no-1}</div></a>
            <a href="#"><div onclick="NewUserPlaceListView(${place_id}, '${category}', ${page_no})">${page_no}</div></a>
            <div>...</div>
            <a href="#"><div onclick="NewUserPlaceListView(${place_id}, '${category}', ${last_page_no})">${last_page_no}</div></a>
            >
        `
    )
    } else if (page_no-1 == 2)  {
        $('#pagenation').empty()
        $('#pagenation').append(
        `
            <
            <a href="#"><div onclick="NewUserPlaceListView(${place_id}, '${category}', 1)">1</div></a>
            <a href="#"><div class="current_page">${page_no-1}</div></a>
            <a href="#"><div onclick="NewUserPlaceListView(${place_id}, '${category}', ${page_no})">${page_no}</div></a>
            <div>...</div>
            <a href="#"><div onclick="NewUserPlaceListView(${place_id}, '${category}', ${last_page_no})">${last_page_no}</div></a>
            >
        `)
    }else if (page_no-1 == last_page_no) {
        $('#pagenation').empty()
        $('#pagenation').append(
        `
            <
            <a href="#"><div onclick="NewUserPlaceListView(${place_id}, '${category}', 1)">1</div></a>
            <div>...</div>
            <a href="#"><div onclick="NewUserPlaceListView(${place_id}, '${category}', ${page_no-2})">${page_no-2}</div></a>
            <a href="#"><div class="current_page">${page_no-1}</div></a>
            >
        `
    )
    } else if (page_no-1 == last_page_no-1) {
        $('#pagenation').empty()
        $('#pagenation').append(
        `
            <
            <a href="#"><div onclick="NewUserPlaceListView(${place_id}, '${category}', 1)">1</div></a>
            <div>...</div>
            <a href="#"><div onclick="NewUserPlaceListView(${place_id}, '${category}', ${page_no-2})">${page_no-2}</div></a>
            <a href="#"><div class="current_page">${page_no-1}</div></a>
            <a href="#"><div onclick="NewUserPlaceListView(${place_id}, '${category}', ${last_page_no})">${last_page_no}</div></a>
            >
        `)
    }else {
        $('#pagenation').empty()
        $('#pagenation').append(
        `
            <
            <a href="#"><div onclick="NewUserPlaceListView(${place_id}, '${category}', 1)">1</div></a>
            <div>...</div>
            <a href="#"><div onclick="NewUserPlaceListView(${place_id}, '${category}', ${page_no-2})">${page_no-2}</div></a>
            <a href="#"><div class="current_page">${page_no-1}</div></a>
            <a href="#"><div onclick="NewUserPlaceListView(${place_id}, '${category}', ${page_no})">${page_no}</div></a>
            <div>...</div>
            <a href="#"><div onclick="NewUserPlaceListView(${place_id}, '${category}', ${last_page_no})">${last_page_no}</div></a>
            >
        `
    )
    }

 

 

 

 

 2. LocalStorage의 Payload 사용할 수 있는 형태로 변환하기
  - LocalStorage에는 기본적으로 string형태로 데이터가 저장된다. 아래 사진처럼 겉으로 보기에는 json형식? 또는 딕셔너리 형식처럼 보이지만 Javascript에서 key값으로 불러온다거나, 인덱싱하면 괄호부터 각각 단어 하나하나가 string이다. 

 - 이를 사용하기 편한 Json형태로 바꾸어주는 것은 생각보다 간단하다.

   아래 코드처럼 JSON.parse를 사용하면 json형식으로 변환하여 str_payload 변수에 넣어주게 된다. 그리고 4번째 행처럼 불러오고자 하는 key를 '.'을 사용해 호출하면 된다.

    const storage = localStorage.getItem("payload");
    const str_payload = JSON.parse(storage)
    
    console.log(str_payload.is_admin)

 

 

 3. Admin 계정에만 특정 기능 부여하기

 - 먼저 users앱에 jwt_claim_serializer.py를 아래와 같이 사용하고 있다.

# users/jwt_claim_serializer.py

from rest_framework_simplejwt.serializers import TokenObtainPairSerializer, PasswordField
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.settings import api_settings
from rest_framework import serializers, exceptions

from django.contrib.auth import authenticate, get_user_model
from django.contrib.auth.models import update_last_login
from django.utils.translation import gettext_lazy as _
from django.utils import timezone

from .models import User

class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
    username_field = get_user_model().USERNAME_FIELD
    token_class = RefreshToken

    default_error_messages = {"no_active_account": _("아이디와 비밀번호를 확인해주세요. ")}
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.fields[self.username_field] = serializers.CharField()
        self.fields["password"] = PasswordField()
    
    def validate(self, attrs):
        authenticate_kwargs = {self.username_field: attrs[self.username_field], "password": attrs["password"],}
        
        try:
            authenticate_kwargs["request"] = self.context["request"]
            
        except KeyError:
            pass
            
        self.user = authenticate(**authenticate_kwargs)
            
        try:
            username = attrs[self.username_field]
            self.target_user =  User.objects.get(username=username)
            self.is_active = self.target_user.is_active
            self.withdraw = self.target_user.withdraw
            
            account_lock_count = self.target_user.account_lock_count
            
            # account_lock_count counting
            if self.user == None:
                self.target_user.account_lock_count += 1
                self.target_user.save()

            # account_lock_count 4이면 잠금
            if account_lock_count == 4:
                self.target_user.is_active = False   
                self.target_user.account_lock_time = timezone.now()
                self.target_user.save()
                
            # is_active False 제한 시간 확인 후 True
            self.now_today_time = timezone.now()

            if self.is_active == False:
                target_user_lock_time = self.target_user.lock_time + timezone.timedelta(minutes=5)
                
                if self.now_today_time >= target_user_lock_time:
                    self.target_user.is_active = True
                    self.target_user.lock_count = 0
                    self.target_user.save()

            # withdraw True이면 로그인 시 False
            if self.withdraw == True:
                self.target_user.withdraw = False
                self.target_user.save()
            
        except:
            pass
        
        if User.objects.filter(username=username).exists():
            
            # is_active False 계정잠금 
            if self.is_active == False:
                raise serializers.ValidationError("로그인 시도가 너무 많습니다. 나중에 다시 시도해 주세요.")
            
        # login error
        if not api_settings.USER_AUTHENTICATION_RULE(self.user):
            raise exceptions.AuthenticationFailed(self.error_messages["no_active_account"],"no_active_account",)
        
        # login token 
        refresh = self.get_token(self.user)

        attrs["refresh"] = str(refresh)
        attrs["access"] = str(refresh.access_token)

        if api_settings.UPDATE_LAST_LOGIN:
            update_last_login(None, self.user)

        return {"access":attrs["access"], "refresh":attrs["refresh"]}

    @classmethod
    def get_token(cls, user):
        token = super().get_token(user)
        token["nickname"] = user.user_profile.nickname
        token["review_cnt"] = user.user_profile.review_cnt
        token["password_expired"] = user.password_expired
        token["is_confirmed"] = user.is_confirmed
        token["is_admin"] = user.is_admin

        return token

 - 여기서 가장 아래 "get_token" 함수에서는 jwt를 통해 localstorage에 payload값으로 불러올 키들을 지정해준다. 해당 유저가 리뷰를 작성한 이력이 있는지, 그리고 관리자인지 확인을 해주기 위해 "review_cnt"와 "is_admin"이라는 키로 DB에서 불러온 값들을 각 키에 저장해주고, token을 리턴하여 payload로 보내줄 수 있다.

# users/views.py

class KakaoLoginView(APIView):
    ...
    profile = Profile.objects.create(nickname=kakao_nickname, user=new_user)
    ...
    return Response({'refresh': str(refresh), 'access': str(refresh.access_token), 'nickname':kakao_nickname, 'review_cnt':profile.review_cnt}, status=status.HTTP_200_OK)

 

 

 

반응형

댓글