오늘의 추천 - 어제 기준으로 가장 높은 평점을 받은 음식점들 중 하나 음식점 추천
처음에는 try문없이 작성하였으나 데이터가 안정적이지 않아 테스트 할 때마다 테스트 데이터를 새로 만들어 주기 어렵기 때문에 예외문 처리해서 우선 오류 없이 작동되도록 만들었다.
datetime에서 날짜 끼리 연산하려고 한다면 timedelta를 이용해서 날짜를 연산할 수 있었다.
filter를 이용해서 어제 날짜에 쓰여진 각 유저들이 매긴 음식점의 평점들을 가져와서
어제의 최고 점수, 그리고 그 최고 점수을 받은 가게들을 가져와서 랜덤으로 하나 선택해서 화면에 출력되도록함
# restaurant/view.py
def main_view(request, category):
if request.method == 'GET':
...
# '오늘의 추천' - 어제 가장 높은 평점을 기록한 음식점 중 하나
try:
yesterday = datetime.now().date() - timedelta(days=1)
yesterday_top = Star.objects.filter(star_date=yesterday)
# 어제의 최고 점수, 최고점수 받은 가게 추출
top_score = 0
today_reco = []
for top in yesterday_top:
if top_score < top.star_avg_score:
top_score = top.star_avg_score
for top in yesterday_top:
if top_score == top.star_avg_score:
today_reco.append(top.star_restaurant.restaurant_name)
# 추출한 가게들 중 하나만 랜덤으로 선택 해서 출력
choice = random.choice(today_reco)
result = 'success'
today_res = Restaurant.objects.get(restaurant_name=choice)
except:
result = 'fail'
today_res = '어제 평점이 매겨진 음식점이 없습니다.'
...
return render(request, 'main/main.html', {..., 'today_res': today_res})
오류를 방지하기 위해서 우선 같이 return한 result로 성공여부를 판별한 다음에 그에 맞게 데이터 출력하도록 함
# main.html
<span class="user-info-title">오늘의 추천</span>
<div class="card" style="width: 100%">
{% if result == 'success' %}
<img src="/{{ today_res.restaurant_image }}" class="card-img-top" alt="...">
<div class="card-body">
<h5 class="card-title">{{ today_res.restaurant_name }}</h5>
<p class="card-text">{{ today_res.restaurant_category }}</p>
<a href="https://map.naver.com/v5/search/{{ today_res.restaurant_name }}" class="btn btn-primary">상세보기</a>
</div>
{% else %}
<div class="card-body"> {{ today_res }}</div>
{% endif %}
</div>
음식점 평점 수정 - 음식점에 평점을 등록할 때 음식점의 평균 평점이 수정됨
우선은 평점을 등록하는 코드는 내가 직접 짠 코드가 아닌 선민님께서 짜놓으신 코드다
html 단에서 각 레스토랑에 대해서 별점을 얻기 위해서 아래 처럼 별을 선택할 수 있는 부트스트랩을 이용해서 작성
별점을 선택하면 getRating({{es_id}})가 호출되면서 전역변수로 선언해둔 score에 값이 저장된다.
그리고 별점 선택 후 조금 더하기 클릭 시 scoring_completed() 호출되도록 해둠
# scoring.html
<div class="main_wrapper">
<div class="card_contents">
{% for res in random_restaurants %}
<div class="card_box">
<!-- / 앞에 = ../../ 와 같음-->
<div><img src="/{{ res.restaurant_image }}" alt="" class="card_img_item"></div>
<div class="card_items">
<div class="card_title_item">
<div class="card_item_left">{{ res.restaurant_name }}</div>
<div class="card_item_right">{{ res.restaurant_category }}</div>
</div>
<div class="card_content_item">
{{ res.restaurant_address }}
</div>
</div>
<div class="card_link_item"><a href="https://map.naver.com/v5/search/{{ res.restaurant_name }}"
target="_blank">자세히 보기</a></div>
<div class="card_star_item space-x-4 mx-auto">
<input type="radio" id="5-stars_{{ res.id }}" name="rating_{{ res.id }}" value="5"
v-model="ratings" onclick='getRating({{ res.id }})'/>
<label for="5-stars_{{ res.id }}" class="star_{{ res.id }} pr-4">★</label>
<input type="radio" id="4-stars_{{ res.id }}" name="rating_{{ res.id }}" value="4"
v-model="ratings" onclick='getRating({{ res.id }})'/>
<label for="4-stars_{{ res.id }}" class="star_{{ res.id }}">★</label>
<input type="radio" id="3-stars_{{ res.id }}" name="rating_{{ res.id }}" value="3"
v-model="ratings" onclick='getRating({{ res.id }})'/>
<label for="3-stars_{{ res.id }}" class="star_{{ res.id }}">★</label>
<input type="radio" id="2-stars_{{ res.id }}" name="rating_{{ res.id }}" value="2"
v-model="ratings" onclick='getRating({{ res.id }})'/>
<label for="2-stars_{{ res.id }}" class="star_{{ res.id }}">★</label>
<input type="radio" id="1-stars_{{ res.id }}" name="rating_{{ res.id }}" value="1"
v-model="ratings" onclick='getRating({{ res.id }})'/>
<label for="1-stars_{{ res.id }}" class="star_{{ res.id }}">★</label>
</div>
</div>
{% endfor %}
</div>
</div>
<div class="button_wrapper">
<button class="btn btn-primary btn-lg" onclick="scoring_completed()">조금 더하기!</button>
</div>
같은 페이지에 쓰여진 자바스크립트 코드다.
처음 ready function을 통해서 랜덤으로 가져온 레스토랑 리스트들을 dictionary에 저장함과 동시에
각 레스토랑의 score를 0으로 초기화 하도록 함
getRating 작업 이후에 score 에 선택한 점수가 저장 되며 scoring_completed() 함수에서
JSON 데이터 형태로 그대로 score를 전달하도록 코드를 짜둠
django에서는 이전에도 있었찌만 csrf_token 을 생성해서 인증을 거쳐야하는 코드가 꼭 필요하다!
여기서 선민님은 메타데이터로 csrf_token을 선언해두고 ajax 통신전에 변수로 가져온 다음
ajax 통신할때 headers에 {"X-CSRFToken": csrftoken} 의 형태로 넘겨주는 방식으로 코드를 작성하셨다.
# scoring.html
<head>
...
<meta name="csrf_token" content="{{ csrf_token }}">
...
</head>
<script>
var score = {};
$(document).ready(function () { // onload 보다 우선 실행됨.
var random_ids = {{random_ids}}; // 가져온 DB의 id들 모두 초기화
for (var i = 0; i < random_ids.length; i++) {
score[random_ids[i]] = 0; // Dictionary에 값 초기화 해서 넣기
}
})
function getRating(res_id) {
const rating = document.getElementsByName('rating_' + res_id);
console.log(rating, 'rating_' + res_id);
rating.forEach((star) => {
if (star.checked) {
console.log(star.value);
score[res_id] = parseInt(star.value);
}
});
console.log('==score==', score);
}
function scoring_completed() {
var csrftoken = document.querySelector("meta[name=csrf_token]").content
$.ajax({ // 비동기 방식
type: "POST",
dataType: "JSON",
contentType: "application/json; charset=utf-8",
url: "/user/put_score/",
data: JSON.stringify({'score': score}),
headers: {"X-CSRFToken": csrftoken},
success: function (response) {
alert(response["msg"])
window.location.reload()
}
});
}
</script>
# 레스토랑 평가 횟수, 평균 점수 update 위로는 이미 선민님께서 짜둔 put_score 함수다
json 데이터로 받아온 score 에는 각각의 restaurant_id 와 선택한 평점이 dictionary 형태로 받아올 수 있게 해두셨다.
ex) { '3' : 5, ..., ...} -> (resutarant_id = 3, 선택한 평점 5)
items() 함수로 key 와 value를 각각 가져와서 Star(각 유저가 매긴 음식점의 평점이 모여있는 테이블) Create 작업
여기에 이어서 붙여서 해당 restaurant_id 즉 k 값을 이용해서 해당 레스토랑의 restaurant_avg_score를 계산하기 하기위해
우선 count를 받아와서 0인지 아닌지 판별한 뒤 0인 경우는 value 그대로 저장하고 count(평가 횟수)를 1로 update
0이 아닌 경우에는 평균을 구해서 해당 평균 값을 update 할 수 있도록 코드를 작성하였습니다.
*평균 점수 = (평가횟수 * 현재 평균 점수 + 평가 점수) / (평가횟수 + 1)
이부분의 쿼리구문을 짤 때 count update하는 것과 restaurant_avg_score update하는 쿼리문을 따로 썻더니
update 되는 속도가 1.14초 정도 걸렸었었는데 선민님과 함께 속도를 줄일 방법이 있을까 보니
update 한번에 다 쓸 수 있는 걸 굳이 두번에 나눠쓰는 바보짓을 했다.
한줄로 변경하고 나서는 데이터를 update하는 속도를 약 800밀리초 정도로 감소시켰다!
# restaurant/views.py
def put_score(request):
if request.method == 'POST':
current_user = request.user
data = json.loads(request.body)
score = data['score']
print(score)
for k, v in score.items():
Star.objects.create(
star_avg_score=v, star_date=datetime.now().date(),
star_restaurant=Restaurant.objects.get(id=k), star_user=current_user
)
# print('== 저장되는 star ', star)
# 레스토랑 평가 횟수, 평균 점수 update
res = Restaurant.objects.get(id=k)
count = res.restaurant_count
if count == 0:
Restaurant.objects.filter(id=k).update(restaurant_count=1, restaurant_avg_score=v)
else:
avg_score = (count * res.restaurant_avg_score + v) / (count + 1)
Restaurant.objects.filter(id=k).update(restaurant_count=count + 1, restaurant_avg_score=avg_score)
return JsonResponse({'msg':'추천 정보 기록 완료~'})
평점순위 TOP5 - 모두, 한식, 중식, 일식, 양식 카테고리 별로 TOP5 출력
평점에 따른 TOP 5를 출력하기 위해 기존 테이블 변경 없이 가능한지 고민해보려다가 많은 쿼리를 사용해야하기 때문에 속도가 느려질게 너무나도 뻔해서 restaurant table에 column을 추가하기로 했다.
추가하려는 column - restaurant_count(평가 횟수), restaurant_avg_score(평균 평점)
# restaurant/models.py
class Restaurant(models.Model):
...
restaurant_count = models.IntegerField()
restaurant_avg_score = models.FloatField()
기존에 dp_upload 해줄때 restaurant_count 와 restaurant_avg_score는 csv 파일에 존재하지 않기 떄문에 명시적으로
각각 0 과 1.0 으로 초기화 시켜준다.
# recommadation/db_updload.py
...
Restaurant.objects.create(restaurant_name=name,
restaurant_address=address,
restaurant_image=image,
restaurant_category_id=category,
restaurant_count=0,
restaurant_avg_score=1.0)
...
DB동기화 시에도 컬럼순서에 맞춰 컬럼명 추가
# recommandation/recommand.py
def recommandation(login_user_id):
# 현재 DB에 맞게 동기화
restaurants = pd.DataFrame(Restaurant.objects.all().values())
stars = pd.DataFrame(Star.objects.all().values())
restaurants.columns = ['restaurant_id',
'restaurant_name',
'restaurant_address',
'restaurant_image',
'restaurant_count',
'restaurant_avg_score',
'restaurant_category_id']
...
처음에는 ajax로 비동기식 통신하여 object를 return 받아 장고내장함수를 이용해서 temp_html에 저장해서 html함수를 이용해서 div 자체를 갈아끼우는 코드로 짜보았으나 object 그대로를 받아와서 temp_html를 이용해서 백틱(`) 안에서 장고내장함수를 사용해서 한번에 5개를 뿌릴 수 있는 방법이 없어서 우선 동기식으로 구현을 먼저 해보았다.
url 연결 시 category_id 값을 전달 해주도록 만들고
# urls.py
...
# main
path('main/<int:category>', resview.main_view, name='main' ),
...
메인 화면으로 연결되는 url 들은 기본값으로 0을 함께 request 하도록 설정
<a href={% url 'main' 0 %}>나중에 할게요</a>
각 카테고리 버튼에 category_id 값에 맞게 onclick 이벤트 적용
# main.html
<div class="menu-box">
<div class="menu-all-box">
<button type="button" class="menu-btn" onClick="location.href='{% url 'main' 0 %}'">모두</button>
</div>
<div class="menu-korea-box">
<button type="button" class="menu-btn" onClick="location.href='{% url 'main' 1 %}'">한식</button>
</div>
<div class="menu-western-box">
<button type="button" class="menu-btn" onClick="location.href='{% url 'main' 4 %}'">양식</button>
</div>
<div class="menu-japan-box">
<button type="button" class="menu-btn" onClick="location.href='{% url 'main' 3 %}'">일식</button>
</div>
<div class="menu-china-box">
<button type="button" class="menu-btn" onClick="location.href='{% url 'main' 2 %}'">중식</button>
</div>
</div>
category 값을 함께 url에서 전달해준 것을 그대로 받아오기 위해 category 인자 추가
category_id 값을 그대로 사용하지 않고 sulg 형태로해서 문자열로 리턴해오니
이런 오류가 발생을 했다. 아마 저기 보이는 정규표현식중 한글이 들어가지 않기 때문에 사용하기 어렵다.
만약 테이블의 category_name 들을 '한식' 이 아닌 'ko' 같은 영어였다면 사용이 가능했을텐데 이제와서 다시 restaurant의 모든 데이터를 수정하기엔...할 수는 있지만 그냥 category_id 값으로 다 해결하는게 훨씬 깔끔하다고 생각을 했다.
어차피 카테고리도 4종류고 카테고리에 해당하는 버튼들을 만드는 거기 때문에 그에 맞게 category_id 값을 하드코딩해도 괜찮다고 생각했다.
# restaurant/views.py
def main_view(request, category):
...
if category == 0:
top5 = Restaurant.objects.order_by('-restaurant_avg_score')[:5]
print(top5)
else:
top5 = Restaurant.objects.filter(restaurant_category_id=category).order_by('-restaurant_avg_score')[:5]
print(top5)
return render(request, 'main/main.html', {..., 'top5': top5})
위의 쿼리문들 작성할 때 한가지 바보같은 짓! category == 0: 이라는 조건문을 작성할 때
앞서 slug 테스트 할때 문자열로 해둔 것을 깜빡하고 category == '0': 이라고 써놓고
왜 Restaurant.objects.all()에 결과값이 안넘어오지? 하는 짓을 2번다시 하지 말도록하자..
# main.html
<div class="card-box">
{% for t in top5 %}
<div class="card">
<img src="/{{ t.restaurant_image }}" class="card-img-top" alt="...">
<div class="card-body">
<h5 class="card-title">{{ t.restaurant_name }}</h5>
<p class="card-text">{{ t.restaurant_category }}</p>
<a href="https://map.naver.com/v5/search/{{ t.restaurant_name }}" class="btn" style="background-color: #EF5350; color: whitesmoke">상세보기</a>
</div>
</div>
{% endfor %}
</div>
'회고록(TIL&WIL)' 카테고리의 다른 글
TIL 2022.06.13 TODAY_LUNCH (bug_fix, refectoring, README) (0) | 2022.06.13 |
---|---|
TIL 2022.06.09 TODAY_LUNCH (랭킹TOP5 비동기식으로 재구현, 팝오버(부트스트랩)) (0) | 2022.06.09 |
TIL 2022.06.07 TODAY_LUNCH (사용자 기반 협업 필터링 추천시스템) (0) | 2022.06.07 |
TIL2022.06.05 TODAY_LUNCH(요기요 크롤링, csv load-DB 저장) (0) | 2022.06.05 |
TIL 2022.06.02 추천시스템 프로젝트 TODAY_LUNCH (S.A) (0) | 2022.06.02 |