■ 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)
'DEV > Web 개발' 카테고리의 다른 글
Web개발 :: BackData 전처리, Json 파일 변환 등 _TIL74 (0) | 2022.12.19 |
---|---|
Web개발 :: 데이터 처리, 변형, Pandas Dataframe, Pagenation _TIL72 (0) | 2022.12.15 |
Web 개발 :: 12월 둘째주 WIL15 (0) | 2022.12.14 |
Web개발 :: 머신러닝_Header를 사용한 네이버 지도 웹 크롤링 _TIL70 (0) | 2022.12.14 |
Web개발 :: 네이버 지도 Web Dynamic map API 적용하기 _TIL69 (0) | 2022.12.13 |
댓글