될때까지

((WnB)) 호스트 집 인포만 등록하기 본문

프로젝트/wecode2차 : WnB

((WnB)) 호스트 집 인포만 등록하기

랖니 2022. 10. 20. 22:59
728x90

# FLOW

호스트가 집을 등록하는 기능을 구현해보자. 이미지 파일 업로드는 일단 빼고 대략적인 flow를 그려보자.

  1. url : POST /hosts/rooms
  2. 집 등록 기능은 로그인한 유저 + 호스트 일때만 가능하다.
  3. header에 첨부된 토큰을 복호화하여 호스트 여부를 확인한다.
  4. 호스트가 아니라면, 403 에러를 반환한다.
  5. 집이름, 지역, 주소, 가격, 설명, 위도, 경도, 최대수용인원, 방, 화장실, 침대의 갯수, 카테고리, 룸타입, 기타시설의 정보를 body로 받는다.
  6. 외래키로 연결되는 카테고리, 룸타입, 기타시설 id는 존재유무를 확인한다.
  7. 집이름은 중복 불가능하다.
  8. 집을 등록한다.

 

# 포스트맨에서 리스트로 데이터를 보내는 방법

포스트맨으로 여러개의 데이터를 리스트로 담아 요청하는 방법은 아래와 같다.

코드에서는 아래와 같이 받아올 수 있다.

class RegisterRoomView(View):
    @signin_decorator
    def post(self, request):
        data         = request.POST

        try:
            host = Host.objects.get(user=request.user)   
            
            name              = data['room_name']
            address           = data['state/province/region']
            detail_address    = data['detail_address']
            price             = data['price']
            description       = data['description']
            latitude          = data['latitude']
            longitude         = data['longitude']
            maximum_occupancy = data['maximum_occupancy']
            bedroom           = data['bedroom']
            bathroom          = data['bathroom']
            bed               = data['bed']
            category          = data['category_id']
            room_type         = data['room_type_id']
            facility_ids      = data.getlist('facility_ids')  
            
            print('facility_ids', facility_ids)  # ==> 리스트로 담긴다 ['1', '2', '5', '111']

 

# aggregate

집을 등록할 때 기타시설 ID는 에어컨, 난방, 무료주차 등 1개가 아닌 여러개가 들어올 수도 있다.
그래서 facility_ids를 리스트로 받았다.

기타 시설의 존재 여부를 확인하기 위해서 로직을 어떻게 작성해야할까?
처음에는 len을 사용하여 구현했다.
두 갯수가 일치하지 않으면 정확히 에러를 일으킨다. 하지만 코드가 가독성도 안좋고 안예쁘다 마음에 안들어!!

            # 5. facility_ids 존재여부
            print('DB 기타 시설과 일치하는 ID의 갯수', len(Facility.objects.filter(id__in=facility_ids)))   # 일치하는 객체들이 쿼리셋으로 리스트에 담겨서 반환된다.
            print('입력받은 기타 시설 ID의 갯수', len(facility_ids))
            
            count_facility_ids       = len(facility_ids)
            count_exist_facility_ids = len(Facility.objects.filter(id__in=facility_ids))
            
            if count_exist_facility_ids != count_facility_ids:
                return JsonResponse({'message':'FACILITY_DOES_NOT_EXIST'}, status=404)

장고 공식문서를 살펴봤고, len말고 aggregate를 사용해서 갯수를 비교하면 될 것 같았다.

그리고 리스트에 담긴 id와 일치하는 객체들을 한번에 찾고 싶었고 이 역시 in을 사용하면 된다.

  • aggregate : 쿼리셋의 계산된 값(평균, 합계 등)을 딕셔너리 형태로 반환한다.
  • in : iterable한 객체(주로 list, tuple, queryset)에서 값을 찾을 수 있다.
print('test', Facility.objects.filter(id__in=facility_ids))
# test <QuerySet [<Facility: Facility object (1)>, <Facility: Facility object (2)>, <Facility: Facility object (5)>]>
# facility_ids에는 총 4개가 담겼는데 일치하는 3개의 객체만 담아서 반환한다.
print('test', Facility.objects.filter(id__in=facility_ids).aggregate(Count('id')))
# test {'id__count': 3}
# facility 테이블에는 id, name2개의 컬럼이 있다. facility객체가 반환되므로 해당 객체의 정보 중 id로 카운트한다.

print('test', Facility.objects.filter(id__in=facility_ids).aggregate(change_name = Count('id')))
# test {'change_name': 3}
# 반환되는 딕셔너리의 키의 이름도 변경할 수 있다.

위에서 print로 찍어봤듯이,

Facility.objects.filter(id__in=facility_ids).aggregate(count = Count('id')) 의 결과값은 딕셔너리다.
딕셔너리의 키 이름인 count를 이용하여 id가 일치하는 기타 시설의 갯수만 가져오기 위해서 .get을 사용했다.
Facility.objects.filter(id__in=facility_ids).aggregate(count = Count('id')).get('count')

그러면 3 이렇게 일치하는 갯수만 반환된다.
해당 데이터를 가지고 클라이언트로부터 입력받은 리스트의 갯수와 비교한다.
갯수가 일치하면 입력한 기타시설의 id들이 모두 존재한다는 뜻이고,
일치하지 않는다면 존재하지 않는 기타시설 id를 입력했다는 뜻이 된다.

count_exist_facility_ids = Facility.objects.filter(id__in=facility_ids).aggregate(count=Count('id')).get('count')

if count_exist_facility_ids != len(facility_ids):
    return JsonResponse({'message':'FACILITY_DOES_NOT_EXIST'}, status=404)

이렇게 하면 리스트로 전달받은 기타시설 ID의 존재 여부도 확인 완료!

 

# transaction

호스트가 집을 등록하기 위해 데이터 요청을 보낸다.

데이터에 아무 문제가 없다면 Room 테이블을 생성하고 RoomFacility 테이블도 생성해야한다.
(RoomFacility 테이블은 룸과 기타시설의 연결관계를 나타내는 중간테이블이다.
1번 방에 기타시설 1,3,4가 존재한다면 RoomFacility테이블에 1-1, 1-3, 1-4 데이터가 생성되야한다.)

이 둘을 엮어주기 위해 transaction을 사용했다.

  • transaction : 데이터베이스의 상태를 변화시키기 위해서 수행되는 여러가지의 쿼리들을 하나의 작업 단위로 묶는 것
    with transaction.atomic():
        room, is_created = Room.objects.get_or_create(
            name = name,
            defaults = {
                "address"           : address,
                "detail_address"    : detail_address,
                "price"             : price,
                "description"       : description,
                "latitude"          : latitude,
                "longitude"         : longitude,
                "maximum_occupancy" : maximum_occupancy,
                "bedroom"           : bedroom,
                "bathroom"          : bathroom,
                "bed"               : bed,
                "host"              : host,
                "category"          : Category.objects.get(id=category),
                "room_type"         : RoomType.objects.get(id=room_type)
            }
        )

먼저 Room객체를 생성해야한다. 이 때 get_or_create를 사용했고 defaults속성을 사용했다. 
코드를 위처럼 작성하면 Room 객체들 중에 name이 name인 아이들만 찾는다.
그리고 name이 일치하는 객체가 없다면 새로운 Room객체를 생성하는데 이 때 defaults안에 있는 값들을 사용해서 객체를 생성한다.

get_or_create의 첫번쨰 반환값에는 get으로 찾은 객체가 담긴다.
'room'을 이용해서 방 이름이 똑같은 숙소가 있는지 검증하는 로직을 작성할 수 있다.

이제 Room을 생성했으면 RoomFacility 테이블에도 데이터를 집어넣어야한다.

Room은 1개의 데이터만 생성하면 됐지만 RoomFacility는 여러개의 데이터를 생성해야한다.
이를 위해 bulk_create를 사용했다.

여러개의 기타시설 id가 facility_ids에 담겨있기 때문에 반복문을 돌면서 하나씩 꺼내서 데이터를 생성하게끔 코드를 작성했다.

    # 7. room생성하고 room_facilities도 생성해야함. => 트랜잭션 처리
    RoomFacility.objects.bulk_create([
        RoomFacility(
            room_id = room.id,
            room_facility_id = facility_id
        ) for facility_id in facility_ids ])

그렇게 입력받은 데이터로 Room과 RoomFacility까지 생성했고 생성된 Room데이터를 반환하게 작성했다.

이미지 등록하기는 내일합시다!

728x90