[0723] 다중 옵션을 활용한 공원 검색 기능

nikevapormax·2022년 7월 23일
0

TIL

목록 보기
82/116
post-custom-banner

iPark Project

  • 검색 기능을 맡고 나서 제일 먼저 생각했던 검색 옵션은 공원의 option이었다.
  • 공원의 option은 총 8 가지가 있었다.
["조경", "운동", "놀이공원", "역사", "학습테마", "교양", "편익", "주차장"]
  • 해당 option들은 우리가 구한 공공 데이터에서는 작성되어 있지 않았고, 팀원들과 같이 공원의 정보를 살펴가며 하나씩 각 공원의 option으로 만들어 주었다.

8 가지의 option으로만 공원 검색

  • option만을 활용한 공원 검색에 대한 코드는 아래와 같았다.
    • __contains : options 변수에 하나의 값만 있을 때 사용
    • __in : options 변수에 여러 값이 있을 때 사용
class ParkSearchView(APIView):
    def get(self, request):
        options = request.query_params.getlist("option", "")
        
        if len(options) == 1:
            results = ParkModel.objects.filter(option__option_name__contains=request.query_params.get("option", "")).distinct()
        else:
            results = ParkModel.objects.filter(option__option_name__in=options).distinct()
            
        if results.exists():
            serializer = ParkSerializer(results, many=True)
            return Response(serializer.data, status=status.HTTP_200_OK)
  • 이때의 코드는 postman으로 테스트만 해 작성한 코드이다.
  • query parameter를 사용할 생각으로 위와 같이 작성하였다.

공원 db에 있는 zone을 활용한 공원 검색

  • 그러다 팀원들과 회의 중 공원의 지역을 활용한 검색이 있으면 좋을 것 같다는 의견이 나왔다.
  • 따라서 이를 구현하기 위해 코드를 작성했고, 코드는 위와 비슷하게 작성하였다.
class ParkSearchView(APIView):
    def get(self, request):
        options = request.query_params.getlist("option", "")
        
        for option in options:
            if option in ["조경", "운동", "놀이공원", "역사", "학습테마", "교양", "편익", "주차장"]:
                results = ParkModel.objects.filter(option__option_name__contains=option).distinct()
            else:
                results = ParkModel.objects.filter(zone=option).distinct()
                
        if results.exists():
            serializer = ParkSerializer(results, many=True)
            return Response(serializer.data, status=status.HTTP_200_OK)
  • 별도의 코드를 작성하지 않기 위해 front에서 보내주는 값을 모두 option으로 설정해 보내주었었다.
  • 해당 코드의 문제점은 front에서 함수를 생성하길 하나의 option이나 zone에 대해서만 결과가 나오도록 만들어놨던 것과 back에서도 여러 개의 query parameter를 받아도 활용하지 못한다는 것이다.
  • 따라서 여러 개의 option이나 zone을 활용할 수도 있고, 이 둘을 섞어서도 활용할 수 있도록 만들고 싶었다.

공원의 option과 zone을 모두 활용한 검색

  • 해당 부분을 설계할 때 front 부분을 먼저 작성하였다.
  • addEventListener를 활용해 여러 개의 option이나 zone을 back으로 보낼 수 있도록 했다.
// 옵션값을 여러 개 받음 //
var values = document.querySelectorAll("#park-option a")
var valueList = []
values.forEach(value => {
  value.addEventListener("click", () => {
    valueList.push(value.title)
  })
})

var zones = document.querySelectorAll("#park-zone a")
var zoneList = []
zones.forEach(zone => {
  zone.addEventListener("click", () => {
    valueList.push(zone.title)
  })
})


// 쿼리 파라미터를 통한 공원 정보 get //
function getParks() {
  $("#parks").empty()

  let option_param = ""
  for (let i = 0; i < valueList.length; i++) {
    if (valueList[i] != undefined) {
      option_param += "param=" + valueList[i] + "&"
    }
  }
  let zone_param = ""
  for (let i = 0; i < zoneList.length; i++) {
    if (zoneList[i] != undefined) {
      zone_param += "param=" + zoneList[i] + "&"
    }
  }

  valueList = []
  zoneList = []

  $.ajax({
    type: "GET",
    url: `${backendBaseUrl}park/option/` + "?" + option_param + zone_param,
    data: {},
    success: function (response) {
      for (let i = 0; i < response.length; i++) {
        get_parks_html(
          response[i].id,
          response[i].park_name,
          response[i].image,
          response[i].check_count
        )
      }
    }
  });
}
  • front에서 보내주는 값들을 따로 받아서 쿼리하기 위해 back의 부분을 아래와 같이 변경하였다.
    • 먼저 option의 값들을 option_name에서 option_id로 변경해주었다.
    • 그 이유는 through table을 활용하기 위해서이다.
    • 원래는 값을 그대로 보내주고 받았었는데, 송파구의 놀이공원을 선택하면 모든 놀이공원들이 나오게 되었다.
    • id 값을 사용해 쿼리하는게 제일 정확하다는 것을 알고 있었으면서도 name을 활용했었는데, through table을 활용하게 되면서 id를 쓸 수 밖에 없어 아래와 같이 변경해주었다.
    • zone의 경우, 원래 공원의 정보 중 하나여서 그대로 이름값을 사용했다.
    • 원래는 query parameter를 보내줄 때 option과 zone을 분리해서 보내고 back에서도 분리해서 받을 수 있을 줄 알았는데 하나의 변수로 모두 하나의 내용으로 들어오게 되었다.
    • 따라서 각각의 리스트를 생성해 값을 먼저 저장해주었다.
    • 그리고 아래의 3 가지 조건을 통해 결과값을 돌려줄 수 있었다.
      • option과 zone을 사용하는 경우
      • option만 사용하는 경우
      • zone만 사용하는 경우
# 검색 페이지
class OptionView(APIView):
    def get(self, request):
        param = request.query_params.getlist("param")

        option_list = []
        zone_list = []

        for p in param:
            if p in ["1", "2", "3", "4", "5", "6", "7", "8"]:
                option_list.append(p)
            else:
                zone_list.append(p)

        if len(option_list) > 0 and len(zone_list) > 0:
            results = ParkModel.objects.filter(zone__in=zone_list).filter(parkoption__option_id__in=option_list).distinct()
        elif len(option_list) > 0:
            results = ParkModel.objects.filter(parkoption__option_id__in=option_list).distinct()
        elif len(zone_list) > 0:
            results = ParkModel.objects.filter(zone__in=zone_list).distinct()
        
        if results.exists():
            serializer = ParkSerializer(results, many=True)
            return Response(serializer.data, status=status.HTTP_200_OK)

        return Response({"message": "공원을 찾을 수 없습니다."}, status=status.HTTP_404_NOT_FOUND)
  • 그렇다면 가장 중요한 through table은 어떻게 생성할까?
  • 먼저 중간 테이블은 다들 알다시피 ManyToMany 관계일 경우 django가 자동으로 생성해준다.
  • 나의 중간 테이블의 이름은 park_park_option이다.
  • 해당 정보를 가지고 아래와 같이 model을 다시 세팅해주었다.
  • Park 모델에 through="parkOption"를 작성해준다.
  • ParkOption이라는 테이블을 만들어 주고 class Meta 부분에 나의 중간 테이블 이름을 넣어주면 된다.
    • 현재 중간 테이블에 있는 모델들을 ForeignKey로 받아주면 된다.
class Park(models.Model):
    option = models.ManyToManyField(Option, verbose_name="공원 옵션", related_name="options", through="parkOption")
    park_name = models.CharField("공원명", max_length=200)
    addr = models.CharField("공원주소", max_length=200)
    zone = models.CharField("지역", max_length=100)
    admintel = models.CharField("전화번호", max_length=20)
    main_equip = models.TextField("주요시설", blank=True)
    template_url = models.URLField("바로가기", max_length=200, blank=True)
    list_content = models.TextField("공원개요")
    image = models.CharField("이미지", max_length=300)
    longitude = models.CharField("X좌표", max_length=50, blank=True)
    latitude = models.CharField("Y좌표", max_length=50, blank=True)
    check_count = models.PositiveIntegerField("조회수", default=0)
    created_at = models.DateField("공원 정보 생성시간", auto_now_add=True)
    updated_at = models.DateField("공원 정보 수정시간", auto_now=True)

    def __str__(self):
        return self.park_name
    
    @property
    def update_counter(self):
        self.check_count = self.check_count + 1
        self.save()
        
        
class ParkOption(models.Model):
    park = models.ForeignKey(Park, on_delete=models.CASCADE)
    option = models.ForeignKey(Option, on_delete=models.CASCADE)
    
    class Meta:
        db_table = "park_park_option"
  • 현재 모델이 변경된 상태이니 migrate를 진행해야 한다. makemigrations는 그대로 해도 되지만, migrate를 할 때는 --fake를 붙여주어야 한다.
$ python manage.py migrate --fake
  • migrate도 진행하고 프론트로 가 데이터를 보내보니 결과가 잘 나오는 것을 볼 수 있었다.
  • shell을 통해서도 쿼리를 날려봤는데 데이터가 똑같이 잘 나오는 것을 볼 수 있었다.
  • 그런데 test code를 실행하는 도중 아래와 같은 에러가 났다.
django.db.utils.OperationalError: table "park_park_option" already exists
  • 어짜피 개발 중이라 db를 날리는 것은 문제가 되지 않지만, 항상 이렇게 데이터를 날릴 순 없어 구글링을 해봤는데 답이 나오질 않아 좀 더 시간을 두고 서칭을 하려 한다.
profile
https://github.com/nikevapormax
post-custom-banner

0개의 댓글