본문 바로가기

회고록(TIL&WIL)

TIL 2022.06.09 TODAY_LUNCH (랭킹TOP5 비동기식으로 재구현, 팝오버(부트스트랩))

랭킹TOP5 비동기식으로 재구현

어제 동기식으로 구현해봤던 랭킹 TOP5 출력을 다시한번 생각해서 ajax로 request 줘서 비동기식으로 구현해보았다.

우선 ajax 통신할 준비 <head> 부분에 meta 태그를 이용해서 csrf_token 가져올 준비 해두고

<meta name="csrf_token" content="{{ csrf_token }}">

버튼마다 함수 호출하도록 설정하고 인자값으로 caterogy_id 값을 전달하도록 작성

<div class="menu-box">
    <div class="menu-all-box">
        <button type="button" class="menu-btn" onClick="select_top5(0)">모두</button>
    </div>
    <div class="menu-korea-box">
        <button type="button" class="menu-btn" onClick="select_top5(1)">한식</button>
    </div>
    <div class="menu-western-box">
        <button type="button" class="menu-btn" onClick="select_top5(4)">양식</button>
    </div>
    <div class="menu-japan-box">
        <button type="button" class="menu-btn" onClick="select_top5(3)">일식</button>
    </div>
    <div class="menu-china-box">
        <button type="button" class="menu-btn" onClick="select_top5(2)">중식</button>
    </div>
</div>

ajax 통신을 위해서 meta태그에서 가져온 csrftoken을 headers에 꼭 같이 보내주어야하고

자바스크립트 select_top5(category) 함수 작성 어제는 생각하지 못했던 자바스크립트 자체에서 제공하는

JSON 데이터 형태를 이용해서 JSON으로 가공해서 request 보내고 response 데이터를 받아서

JSON.parse(json데이터)로 풀어서 가져온 다음에  innerText 함수와 scr 함수를 통해서 내용, 이미지등을 바꿔서 페이지가 reload 되지않고도 변경되도록 코드 작성

<script>
    function select_top5(category){
        var csrftoken = document.querySelector("meta[name=csrf_token]").content
        $.ajax({
            type: "POST",
            data: {'category': category},
            url: {% url 'main' %},
            headers: {"X-CSRFToken":csrftoken},
            success: function (response){
                let data = JSON.parse(response['data'])
                for(i = 0; i < data.length; i++){
                    document.getElementById("name" + i).innerText=data[i]['name'];
                    document.getElementById("category" + i).innerText=data[i]['category'];
                    document.getElementById("image" + i).src="/" + data[i]['image'];
                }
            }

        })
    }
</script>

기존에 지정해둔 main 을 호출할 때 POST로 호출 시의 내용으로만 바꿔서 하나의 함수에 같이 작성했고

이후 받은 restaurant object를 for문을 사용해서 name, image, category 의 정보를 하나하나 가져와서 JSON형태로 저장하는 top5_append(object) 함수를 정의해서 코드의 중복을 최대한 줄임

# restaurant/views.py
def main_view(request):
    if request.method == 'GET':
    ...
    if request.method == "POST":
    print('POST 로 호출됨!')
    category = request.POST.get('category')
    print(category)
    # 카테고리 분류
    if category == '0':
        # 평균 점수 기준으로 내림차순으로 정렬해서 5개까지 출력 
        top5 = Restaurant.objects.order_by('-restaurant_avg_score')[:5]
        json_data = top5_append(top5)
        return JsonResponse({'data': json_data})
    else:
        # 해당 카테고리에서의 평균 점수 기준으로 내림차순으로 정렬해서 5개까지 출력 
        top5 = Restaurant.objects.filter(restaurant_category_id=category).order_by('-restaurant_avg_score')[:5]
        json_data = top5_append(top5)
        return JsonResponse({'data': json_data})

category를 받아올 때 그냥 category를 받아서 저장하게 되면 <categories: '한식'> 이러한 형태로 불러오게 되어 있어서

category_id를 받아온 뒤, 각 id 마다 이름을 제대로 부여해줬어야했는데 (1 > 한식,  2 > 중식, 3 > 일식, 4 > 양식)

처음에는 if - elif 문을 통해 작성하려니 쓸데없이 길이만 차지하게되고 swich-case문을 쓰려고했는데 파이썬에서는 지원하지 않는 문법이었다.

그래서 파이썬에서 swich-case문 처럼 쓸 수 있는 방법이 categories 딕셔너리를 만들어 각각 key, value 입력해둔 다음에 받아온 id 값에 해당하는 key의 value를 얻도록 딕셔너리의 get 함수를 이용해서 value을 얻어서 그 값을 다시 category 에 저장해서 최종적으로 바로 출력될 수 있도록 JSON 형태로 저장하게 만들었다.

# for문 돌려서 restaurant objects 안에 있는 각각의 값들 json 형태로 저장해서 return
def top5_append(objects):
    top5_list = []
    for t in objects:
        name = t.restaurant_name
        image = t.restaurant_image
        category = t.restaurant_category_id
        categories = {1: '한식', 2: '중식', 3: '일식', 4: '양식'}
        category = categories.get(category, '잘못된 카테고리')
        top5_list.append({'name': name, 'image': image, 'category': category})
        print(top5_list)
    json_data = json.dumps(top5_list, ensure_ascii=False)
    return json_data

그리고 마지막으로 기본적으로 메인페이지를 열었을 때 전체랭킹 TOP5가 나와야되니  GET으로 호출 시 맨끝에 

top5 로 정렬된 restaurant object 들을 render에 함께 return 해주었다.

# restaurant/views.py
def main_view(request):
    if request.method == 'GET':
    ...
    top5 = Restaurant.objects.order_by('-restaurant_avg_score')[:5]

    return render(request, 'main/main.html', {..., 'top5': top5})

그래서 우선은 기본적으로 전체 top5가 뿌려질 수 있도록 장고내장함수를 통해 출력하였으며

이후 카테고리들 클릭 시 내용물들이 바뀌어야하므로 id값으로 각각의 정보+index 번호로 지정해서 구분될 수 있도록했다.

index 번호를 얻기 위해서 jinja 에서는 {{ loop.index }} 의 형태로 사용하였으나

django 에서는 {{ forloop.counter }} 또는 {{ forloop.counter0 }} 로 index 값을 얻어 올 수 있었다 counter 뒤에 0 이 붙고 안붙고의 차이는 index 번호가 1번부터 시작하는지 0번 부터 시작하는지에 대한 차이이다.

<div class="card-box">
    {% for t in top5 %}
    <div class="card">
        <img src="/{{ t.restaurant_image }}" class="card-img-top" alt="..." id="image{{ forloop.counter0 }}">
        <div class="card-body">
            <h5 class="card-title" id="name{{ forloop.counter0 }}">{{ t.restaurant_name }}</h5>
            <p class="card-text" id="category{{ forloop.counter0 }}">{{ t.restaurant_category }}</p>
            <a href="https://map.naver.com/v5/search/{{ t.restaurant_name }}" class="btn" style="background-color: #EF5350; color: whitesmoke" id="search{{ forloop.counter0 }}">상세보기</a>
        </div>
    </div>
    {% endfor %}
</div>

팝오버(부트스트랩)

그림처럼 해당 부분을 클릭했을 때 저런식으로 설명창 나오도록 하는 컴포넌트를 팝오버라고 부른다.

팝오버를 쓰기 위해선 자바스크립트로 연결을 해주어야만 쓸 수 있다!

<script>
    $(document).ready(function () {
        $('[data-toggle="popover"]').popover({container: "body"});
    });
</script>

부트스트랩 가져 올 때도 /bootstrap.bundle.min.js" 붙어있는 스크립트를 가져와야 사용이 가능하다고 한다.

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
        crossorigin="anonymous"></script>

title="" 에 내용을 작성 하면 제목부분이 활성화되고 제목이 따로 출력된다. 나는 필요없어서 작성하지 않았다

data-bs-html="true" 입력값으로 html 태그를 인식 할 수 있도록 해주는 코드다 이 코드를 알기 전까지는 줄바꿈을 하지못해서 꽤나 고전했다.. <br> 이라던다 \n 이라던가 줄바꿈 기능이 전혀 작동하지 않았는데 해당 코드를 추가하고 나서 <br>태그가 먹혀서 간단히 줄바꿈에 성공했다.

그리고 여러가지 기능들이 들어가 있는데 기본적으로는 위에 팝오버 시작을 위한 자바스크립트들과 연결되는 방식이었고

한번씩 태그가 안먹힐 경우에 가운데 bs 를 추가하면 기능이 제대로 작동했다.

data-html 도 가운데 bs를 붙였고 data-placement 도 가운데 bs를 추가하니 제대로 작동했었다.

<div class="card-section-title text1">
    사용자님과 가장 유사한 <a type="button" title="" data-bs-html="true" data-container="body"
            data-toggle="popover" data-bs-placement="top"
            data-bs-content="{{ similar_top10 }}">
        {{ similar.fullname }}
    </a>님의 추천 음식점입니다!
</div>

받아온 데이터프레임 similar_top10 을 dictionary 형태로 변환해서

for 문으로 key 와 value를 따로따로 받아와서 key의 경우는 필터링한번 거쳐서 이름으로 가져오고

value의 경우 너무 소수점이 길기 때문에 일정 자리 까지만 출력할 수 있도록 설정을 해서 output 에 결과 값 저장해서

리턴 하여 출력하는 형태로 완성을 하였다.

# 사용자 기반 추천 시스템 필터링 거쳐 가장 비슷한 유저가 가본 음식점 중 평점 높은 순으로 리스트 가져옴
reco, similar_user, similar_top10 = recommandation(current_user.id)
similar_dict = similar_top10.to_dict()
output = ''
for key, value in similar_dict.items():
    name = UserModel.objects.get(id=key).fullname
    output = output + name + ' 유사도: ' + str(value)[:8] + '<br>'