django - airbnb project 후기

yj-leee·2021년 2월 9일
1
post-thumbnail

☑ AirBnB

에어비앤비는 숙박 공유 서비스로 예약과 결제가 이루어지는 사이트이다. 온라인 숙소 예약 플렛폼을 모델링부터 배포까지 재개발 하였으며 날짜입력을 통한 온라인 예약시스템 구현을 목표로 했다. 1차 프로젝트 갓챠피디아(링크)와 마찬가지로 스크럼 방식을 통해 진행되었으며 트레일로와 노션을 활용했다.

  1. 인원
    프론트(고은정, 장재원)
    백엔드(이성보, 김기용, 이영주)

  2. 스크럼 방식으로 진행(트레일로, 노션 활용)

  3. 모델링(A-query tool 사용)
    모델링을 토대로 user, property, reservation 로 app을 나누게 되었다.
    A-query tool을 사용하는 이유는 무겁고 복잡하고 불편한 기존 ERD 프로그램을 가벼우며 깔끔, 간결하게 쓰기 좋다. 그리고 무료이다.

☑ 기억에 남는 코드

✔ 소셜 로그인 - OAuth란?

접근 위임을 위한 개방형 프로토콜(인증방식의 표준)이다. 보안수준이 검증된 아마존, 구글 등의 사이트의 API 를 이용해 인증을 받는 방법이다. 즉, 비밀번호를 직접 제공하지 않아도 타사의 웹 서비스에 저장된 계정에 관한 정보를 이용해 인증을 받을 수 있다.


[1] Client가 User에게 OAuth 로그인 버튼을 보여준다.
[2] 로그인 버튼을 누르면 Resource Server(Google, Facebook 등)는 user에게 로그인 화면을 보여준다. 로그인 과정에서 scope에 정의된 정보 공개 범위에 대한 동의를 구한다. 사용자로부터 동의를 얻게되면 Server는 사용자 아이디와 제공할 scope를 알게 된다.
[3] 로그인 인증이 끝나면 google은 user에게 Authorization Code를 전달한다.
[4] 이 때 redirect_url로 전환되면서 authorization_code가 전달되며 이 과정에서 Client가 authorization_code를 알게 된다.
[5] Client는 google에게 Client ID, Client Secret, Redirect URL, Authorization Code 를 모두 보낸다.
[6] 위 네 가지 정보가 모두 일치하면 google는 Client에게 AccessToken을 발급한다.

✔ id를 통해 할 수 있는것

resource server에서 resource owner가 사용하는 id를 resource owner가 client서비스에 직접 회원가입해서 발급하는 id대신에 사용 가능
resource owner가 우리의 서비스를 패스워드를 통해서 자기가 맞는지 식별하지만 패스워드 대신에 resource server가 전달준 id의 주인이 확신하다라는 것을 안심할 수 있다 -> 인증의 목적 달성

▶ 작성 코드

class SocialLoginView(View):

    def post(self, request):
        try:
            token        = request.headers.get('token', None)
            data         = id_token.verify_oauth2_token(token, requests.Request())
            email        = data['email']
            user, flag   = User.objects.get_or_create(email=email)
            if not flag:
                token        = jwt.encode({'id':user.id}, SECRET_KEY_JWT, ALGORITHM)
                access_token = token.decode('utf-8')
                return JsonResponse({'accessToken':access_token}, status=200)
            return JsonResponse({'message':'Success'}, status=200)
        except ValueError:
            return JsonResponse({'message':'Invalid_token'}, status=400)

✔ ** 로그인 - 데코레이터 쓰는 이유?**

데코레이터의 사용은 코드의 중복을 줄이고 가독성을 증가와 유지보수의 효율을 증대시킨다.

  • 로그인 필수 (required=True) request.user=user.id
  • 로그인 필수 아님 (required=False), request.user=None

▶ 작성 코드

def login_decorator(required=True):
    def decorator(func):
        def wrapper(self, request, *args, **kwargs):

            access_token = request.headers.get('Authorization', None)

            if not required and not access_token:
                request.user = None
                return func(self, request, *args, **kwargs)
            payload      = jwt.decode(access_token, SECRET_KEY_JWT, algorithm=ALGORITHM)
            user         = User.objects.get(id=payload['id'])
            request.user = user

            return func(self, request, *args, **kwargs)

        return wrapper
    return decorator

✔ check_availability 함수

[1] reservation 테이블에 해당 숙소의 id를 통해 모든 예약 데이터를 가지고 있는 querySet 을 bookings 에 저장한다.
[2] check_in, check_out 날짜를 예약된 데이터와 비교하여 True, False 값을 저장한다.
[3] all() 메소드를 사용해서 리스트에 false가 하나라도 존재하면 false를 리턴한다.availiability 리스트가 True를 반환한다면 해당 날짜, 숙소의 예약이 비어있음으로 예약가능하며 False 를 반환한다면 해당 날짜에 예약되어있는 숙소가 존재한다는 의미이기 때문에 예약불가능으로 처리가 된다.

▶ 작성 코드

def check_availability(property, check_in, check_out):
    bookings      = Reservation.objects.filter(property_id=property.id)
    check_in      = datetime.date(check_in)
    check_out     = datetime.date(check_out)
    availability = []
    for booking in bookings:
        if booking.check_in > check_out or booking.check_out < check_in:
            availability.append(True)
        else:
            availability.append(False)
    return all(availability)

✔ Q()객체 사용하기


위와 같은 다양한 조건들을 한번에 적용하기 위해서 q()객체를 사용한다.
필터 조건 뿐만 아니라 검색 기능도 한번에 구현할 수 있다는 장점이 있다.

▶ 작성 코드

class PropertyListView(View):

    @login_decorator(required=False)
    def get(self, request):
        try:
            sort         = request.GET.get('sort', '0')
            limit        = int(request.GET.get('limit', 100000000000))
            offset       = int(request.GET.get('offset', 0))

            category     = request.GET.get('category', None)
            type         = request.GET.get('type', None)
            facility     = request.GET.getlist('facility', None)
            rule         = request.GET.get('rule', None)
            is_supered   = request.GET.get('is_super', None)
            max          = request.GET.get('max', '100000000')
            min          = request.GET.get('min', '0')

            check_in = request.GET.get('check_in', '5000-01-01')


            sort_set={
                '0': 'id',
                '1': '-review_count'
            }

[1] 숙소의 조건을 쿼리스트링으로 받기 위한 코드를 작성했다.
먼저 리뷰의 개수를 카운트해서 내림차순을 하기 위한 sort_set 딕셔너리를 만들어주었다.

            if not validate_date_format(check_in):
                return JsonResponse({'message':'Invalid_date_format'}, status=400)

            check_out = request.GET.get('check_out', '5000-01-10')
            if not validate_date_format(check_out):
                return JsonResponse({'message':'Invalid_date_format'}, status=400)

            number_of_guest = request.GET.get('guest', None)
            search          = request.GET.get('search', None)
            conditions      = {}
            available_rooms = []

[2] check_in, check_out 날짜에 대한 validate를 확인한다.
검색과 예약 가능한 룸을 확인하기 위해 딕셔너리와 리스트를 만들어준다.

▶ 작성 코드

            query  = Q()
            if search:
                query  &=\
                    Q(country__name__contains  = search) |\
                    Q(province__name__contains = search) |\
                    Q(city__name__contains     = search) |\
                    Q(district__name__contains = search) |\
                    Q(street__contains         = search)

            if category:
                conditions['category__id'] = category
            if type:
                conditions['type__id'] = type
            if facility:
                conditions['facility__id__in'] = facility
            if number_of_guest:
                conditions['capacity__lte'] = number_of_guest
            if check_in:
                check_in  = date_parser(check_in)
            if check_out:
                check_out = date_parser(check_out)

            if not validate_check_in_out_date(check_in, check_out):
                return JsonResponse({'message':'Invalid_date'}, status=400)

            if rule:
                conditions['rule__in'] = rule
            if is_supered:
                conditions['host__is_super'] = is_supered
            if min:
                conditions['price__gte'] = min
            if max:
                conditions['price__lte'] = max

[3] q() 객체를 사용해서 or 조건에 검색어에 일치하는 숙소의 객체를 필터링한다.
쿼리스트링으로 해당 값이 들어왔을 때 conditions 라는 딕셔너리에 필터링 값이 담기도록 한다.

▶ 작성 코드

            properties = Property.objects.\
                prefetch_related('propertyimage_set',
                                 'bookmark_set',
                                 'review_set',
                                 'type').\
                annotate(review_count=Count('review')).\
                filter(query, **conditions).order_by(sort_set[sort])

            available_rooms = [property for property in properties if check_availability(property, check_in, check_out)]

            context = [
                {
                    'cateoryName'     : property.category.name,
                    'porpertyType'    : property.type.name,
                    'propertyId'      : property.id,
                    'propertyName'    : property.title,
                    'price'           : property.price,
                    'propertyImages'  : [image.url for image in property.propertyimage_set.all()],
                    'hostName'        : property.host.name,
                    'isSuper'         : property.host.is_super,
                  'isBookmarked'    : property.bookmark_set.filter(user=request.user).exists(),
                    'longitude'       : property.longitude,
                    'latitude'        : property.latitude,
                    'capacity'        : property.capacity,
                    'facilities'      : [facility.name for facility in property.facility.all()[0:3]],
                    'size'            : [size.name for size in property.size.all()],
                    'numberOfReviews' : property.review_set.count(),
                    'rate'            : validate_review_set(property)
                }
                for property in  available_rooms[offset:offset+limit]
            ]

            return JsonResponse({'result':context}, status=200)
        except KeyError:
            return JsonResponse({'message':'KeyError'}, status=400)

[4] prefetch_related 를 통해 역참조관계인 propertyimage_set, bookmark, review, type 테이블, 숙소의 review 개수를 카운트 하기 위해 annotate를 사용하고 내림차순 정렬과 q객체와 딕셔너리로 필터를 적용한다....

[5] 예약이 가능한 룸인지 check_availability 함수를 통해서 확인하고 offset:offset+limit 로 Pagination을 사용해서 데이터를 return 한다.

✔ 페이지네이션

Pagination(Paging)기능은 백엔드에서 보내는 데이터를 한 화면에 전부 보여줄 수 없는 경우에 일정 길이로 끊어서 전달한다.

  • limit(또는 page size)
    한 페이지에 보여줄 데이터 수

  • offset
    데이터가 시작하는 위치(index)

✔ annotate

if review:
	queryset = queryset.annotate(num_reviews=Count('review')).order_by(-num_reviews)
    

annotate는 revuew 컬럼에 개수가 몇개 있는지 count해준다.

✔ get_or_create 함수

flag란, TRUE 또는 FALSE를 갖는 온오프 스위치라고 생각하면 된다. 딸깍하고 스위치를 키면 TRUE, 스위치를 끄면 FALSE이다.
TRUE 라면 인스턴스가 get_or_create 메서드에 의해 생성되었다는 걸 의미한다.
FALSE 라면 인스턴스가 데이터베이스에서 꺼내왔음을 의미한다. (즉, 기존에 있던 것임)

✔ aggregate

필드 전체의 합, 평균, 개수 등을 계산할 때 사용한다
aggregate를 사용하면 결과값이 딕셔너리 형태로 나오기 때문에 딕셔너리에서 꺼내는 형태로 바로 저장해준다.

▶ 작성 코드

def validate_review_set(property):

    if property.review_set.exists():
        rate = {}
        rate['propertyCleanliness']   = property.review_set.aggregate(cleanliness=Avg('cleanliness'))['cleanliness']
        rate['propertyCommunication'] = property.review_set.aggregate(communication=Avg('communication'))['communication']
        rate['propertyCheckIn']       = property.review_set.aggregate(check_in=Avg('check_in'))['check_in']
        rate['propertyAccuracy']      = property.review_set.aggregate(accuracy=Avg('accuracy'))['accuracy']
        rate['propertyLocation']      = property.review_set.aggregate(location=Avg('location'))['location']
        rate['propertyAffordability'] = property.review_set.aggregate(affordability=Avg('affordability'))['affordability']


        rate['propertyRate'] = (rate['propertyCleanliness'] +\
                               rate['propertyCommunication'] +\
                               rate['propertyCheckIn'] +\
                               rate['propertyAccuracy'] +\
                               rate['propertyLocation'] +\
                               rate['propertyAffordability'])/6
        return rate
    return False
    

0개의 댓글