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)
})
})
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를 날리는 것은 문제가 되지 않지만, 항상 이렇게 데이터를 날릴 순 없어 구글링을 해봤는데 답이 나오질 않아 좀 더 시간을 두고 서칭을 하려 한다.