본문 바로가기
Research/Django

[Django] 점프 투 장고 튜토리얼 - 3-12 추천(좋아요)

by RIEM 2021. 11. 25.

Django 점프 투 장고 정리

작성일 : 2021-11-25

문서버전 : 1.0 

개요

이 문서는 점프 투 장고 사이트의 장고 튜토리얼 학습 내용을 정리한 내용입니다.

레퍼런스

점프 투 장고 https://wikidocs.net/72242

3-12 추천

질문과 답변에 ‘좋아요’ 추천 기능을 추가해보자.

모델변경

Question 모델이 추천인(voter) 속성을 추가해주어야 한다.

여기서 추천인은 ManyToManyField를 사용한다. 이는 하나의 질문에 다수가 추천할 수 있고 한명이 여러 개의 질문에 추천을 누를 수 있기 때문이다. 

> ../mysite/pybo/modles.py

 
(... 생략 ...)
class Question(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    subject = models.CharField(max_length=200)
    content = models.TextField()
    create_date = models.DateTimeField()
    modify_date = models.DateTimeField(null=True, blank=True)
    voter = models.ManyToManyField(User)  # 추천인 추가

    def __str__(self):
        return self.subject
(... 생략 ...)

 

Voter = models.ManyToManyField(User)과 같이 추천인(voter)을 ManyToManyField 관계로 추가했다. 모델을 수정했으니 makemigrations 명령을 실행해보자.

(mysite) c:\projects\mysite>python manage.py makemigrations
SystemCheckError: System check identified some issues:

ERRORS:
pybo.Question.author: (fields.E304) Reverse accessor for 'pybo.Question.author' clashes with reverse accessor for 'pybo.Question.voter'.
        HINT: Add or change a related_name argument to the definition for 'pybo.Question.author' or 'pybo.Question.voter'.
pybo.Question.voter: (fields.E304) Reverse accessor for 'pybo.Question.voter' clashes with reverse accessor for 'pybo.Question.author'.
        HINT: Add or change a related_name argument to the definition for 'pybo.Question.voter' or 'pybo.Question.author'.

(mysite) c:\projects\mysite>

 

오류 내용은 Question 모델에서 사용한 author와 voter가 모두 User 모델과 연결되어 있기 때문에 User.question_set처럼 User 모델 통해서 Question 데이터 접근 시 author 기준으로 할지 voter 기준으로 할지 명확하지 않다는 오류다.

위의 오류 문구와 같이 related_name 인수를 추가하면 해결 가능하다.


> ../mysite/pybo/models.py

 
class Question(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='author_question')
    subject = models.CharField(max_length=200)
    content = models.TextField()
    create_date = models.DateTimeField()
    modify_date = models.DateTimeField(null=True, blank=True)
    voter = models.ManyToManyField(User, related_name='voter_question')

    def __str__(self):
        return self.subject

 

Author에 related_name=’author_question’라는 인수를 지정하고 voter에는 related_name=’voter_question’라는 인수를 지정해주었다.

 

이렇게 되면 특정 사용자가 작성한 질문을 얻기 위해서 some_user.author_question.all() 처럼 사용할 수 있게 된다. 마찬가지로 특정 사용자가 투표한 질문을 얻기 위해 some_user.voter_question.all()처럼 사용할 수 있다. 


> ../mysite/pybo/models.py

위와 마찬가지 방법으로 Aswer모델에도 추천인(voter) 속성을 추가해주자.

 
(... 생략 ...)
class Answer(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='author_answer')
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    content = models.TextField()
    create_date = models.DateTimeField()
    modify_date = models.DateTimeField(null=True, blank=True)
    voter = models.ManyToManyField(User, related_name='voter_answer')
(... 생략 ...)


> makemigrations, migrate 명령

(mysite) c:\projects\mysite>python manage.py makemigrations
Migrations for 'pybo':
  pybo\migrations\0006_auto_20200423_1358.py
    - Add field voter to answer
    - Add field voter to question
    - Alter field author on answer
    - Alter field author on question

(mysite) c:\projects\mysite>python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, pybo, sessions
Running migrations:
  Applying pybo.0006_auto_20200423_1358(... 생략 ...) OK

(mysite) c:\projects\mysite>



질문 추천

위에서 Question 모델에 추천인 속성을 추가했다. 이제 질문 추천 기능을 구현해보자.

질문 추천 버튼

질문 추천 버튼 위치는 질문 상세 화면에 만들어주자. 앞으로 템플릿 수정 사항이 많으니 정신줄 꽉 잡고 따라가야 한다.

> ../mysite/templates/pybo/question_detail.html

{% extends 'base.html' %}

{% block content %}

<div class="container my-3">

    (... 생략 ...)

    <h2 class="border-bottom py-2">{{ question.subject }}</h2>

    <div class="row my-3">

        <div class="col-1"> <!-- 추천영역 -->

            <div class="bg-light text-center p-3 border font-weight-bolder mb-1">{{question.voter.count}}</div>

            <a href="#" data-uri="{% url 'pybo:vote_question' question.id  %}"

               class="recommend btn btn-sm btn-secondary btn-block my-1">추천</a>

        </div>

        <div class="col-11"> <!-- 질문영역 -->

            <!-- 기존내용 -->

            <div class="card"my-3<!-- my-3 삭제 -->

                <div class="card-body"

                   (... 생략 ...)

                </div>

            </div>

        </div>

    </div>

    <h5 class="border-bottom my-3 py-2">{{question.answer_set.count}}개의 답변이 있습니다.</h5>

    {% for answer in question.answer_set.all %}

    (... 생략 ...)

(... 생략 ...)



추천 영역을 강조하기 위해 분리한 영역의 구조는 아래와 같다.

<div class="row">
    <div class="col-1">
        추천영역
    </div>
    <div class="col-11">
        질문영역
    </div>
</div>

 

추천영역은 1/12 만큼, 기존 질문영역은 11/12 만큼 차지하도록 부트스트랩의 row와 col-1, col-11을 이용해 구성해주었다. 해당 내용에 관한 상세한 정보는 부트스트랩 공식문서를 참조하자. (*부트스트랩 그리드 - https://getbootstrap.com/docs/4.4/layout/grid/)

 

추천 버튼 확인 창

‘추천’버튼 눌렀을 때 ‘정말 추천하시겠습니까?’라는 확인 창이 나타나게 해주자.

> ../templates/pybo/question_detail.html

 
(... 생략 ...)
{% block script %}
<script type='text/javascript'>
$(document).ready(function(){
    (... 생략 ...)
    $(".recommend").on('click', function() {
        if(confirm("정말로 추천하시겠습니까?")) {
            location.href = $(this).data('uri');
        }
    });
});
</script>
{% endblock %}

 

추천 버튼에 class=”recommend”가 적용되어 있으므로 추천 버튼을 클릭하면 “정말 추천하시겠습니까?”라는 질문창이 표시되고 확인 무르면 data-uri 속성에 정의한 url이 호출된다.

 

질문 추천 urls.py

위에서 {% url ‘pybo:vote_question’ question.id %} URL이 추가되었으므로 pybo/urls.py에 다음과 같이 URL 매핑을 추가해주자.


> ../mysite/pybo/urls.py

 
(... 생략 ...)
from .views import base_views, question_views, answer_views, comment_views, vote_views
(... 생략 ...)

urlpatterns = [
    (... 생략 ...)
    # vote_views.py
    path('vote/question/<int:question_id>/', vote_views.vote_question, name='vote_question'),
]

 

Vote_views.py 파일이 없는 상태에서 일단 추가해주었다. Vote_views.py 파일을 만들어주자.

 

질문 추천 views.py

Url 매핑에 의해 실행되는 vote_views.py 파일과 vote_question 함수는 다음과 같이 생성해주자.

> ../mysite/pybo/views/vote_views.py

 
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect

from ..models import Question


@login_required(login_url='common:login')
def vote_question(request, question_id):
    """
    pybo 질문추천등록
    """
    question = get_object_or_404(Question, pk=question_id)
    if request.user == question.author:
        messages.error(request, '본인이 작성한 글은 추천할수 없습니다')
    else:
        question.voter.add(request.user)
    return redirect('pybo:detail', question_id=question.id)

 

본인이 쓴 글을 직접 추천하는 것을 방지해야 한다. 이를 위해 로그인 유저와 추천하려는 질문의 작성자의 동일 여부를 확인하고 동일할 경우 오류가 발생한다.

 

Question 모델의 voter는 여러 사람을 추가할 수 있는 ManyToManyField이다. 따라서 question.voter.add(request.user)과 같이 add 함수를 사용해 추천인을 추가한다. 동일한 사용자가 동일한 질문에 추천을 여러번 하더라도 추천수는 증가하지 않는데 중복을 허용하지 않는 ManyToManyField의 특성 때문이다.

 

질문 추천 확인

추천 버튼이 정상적으로 작동하는지 확인해보자.

 

자신이 작성한 질문글의 추천 버튼을 누를 경우 정상적으로 오류가 잘 작동한다.

 

답변 추천

답변 추천 기능은 질문 추천 기능과 동일하다.

답변 추천 버튼

답변의 추천수 표시하고 답변 추천 버튼을 질문 상세 템플릿에 아래와 같이 추가해주자.

>../templates/pybo/question_detail.html

 
(... 생략 ...)
<h5 class="border-bottom my-3 py-2">{{question.answer_set.count}}개의 답변이 있습니다.</h5>
{% for answer in question.answer_set.all %}
<div class="row my-3">
    <div class="col-1">  <!-- 추천영역 -->
        <div class="bg-light text-center p-3 border font-weight-bolder mb-1">{{answer.voter.count}}</div>
        <a href="#" data-uri="{% url 'pybo:vote_answer' answer.id  %}"
            class="recommend btn btn-sm btn-secondary btn-block my-1">추천</a>
    </div>
    <div class="col-11">  <!-- 답변영역 -->
        <!-- 기존내용 -->
        <div class="card"my-3>  <!-- my-3 삭제 -->
            <div class="card-body">
                (... 생략 ...)
            </div>
        </div>
    </div>
</div>
{% endfor %}
(... 생략 ...)

 

답변 추천 urls.py

{% url ‘pybo:vote_answer’ answer.id %}이 추가되었으므로 이를 pybo/urls.py에 url매핑해주자.

> ../mysite/pybo/urls.py

 
(... 생략 ...)

urlpatterns = [
    (... 생략 ...)
    path('vote/answer/<int:answer_id>/', vote_views.vote_answer, name='vote_answer'),
]

 

답변 추천 views.py

URL 매핑에 의해 실행되는 vote_views.vote_answer 함수는 아래와 같이 vote_views.py에 작성해주자.

> ../mysite/pybo/views/vote_views.py

 
(... 생략 ...)

from ..models import Question, Answer

(... 생략 ...)

@login_required(login_url='common:login')
def vote_answer(request, answer_id):
    """
    pybo 답글추천등록
    """
    answer = get_object_or_404(Answer, pk=answer_id)
    if request.user == answer.author:
        messages.error(request, '본인이 작성한 글은 추천할수 없습니다')
    else:
        answer.voter.add(request.user)
    return redirect('pybo:detail', question_id=answer.question.id)

 

답변 추천 확인

 

질문 목록에 추천수 표시

질문 목록에 추천수를 표시해보자. 질문 목록 템플릿에 추천수를 추가해보자.

> ../mysite/templates/pybo/question_list.html

(... 생략 ...)

<table class="table">

    <thead>

    <tr class="text-center thead-dark">

        <th>번호</th>

        <th>추천</th>

        <th style="width:50%">제목</th>

        <th>글쓴이</th>

        <th>작성일시</th>

    </tr>

    </thead>

    <tbody>

    {% if question_list %}

    {% for question in question_list %}

    <tr class="text-center">

        <td>

            <!-- 번호 = 전체건수 - 시작인덱스 - 현재인덱스 + 1 -->

            {{ question_list.paginator.count|sub:question_list.start_index|sub:forloop.counter0|add:1 }}

        </td>

        <td>

            {% if question.voter.all.count > 0 %}

            <span class="badge badge-warning px-2 py-1">{{ question.voter.all.count }}</span>

            {% endif %}

        </td>

        (... 생략 ...)

    </tr>

    {% endfor %}

    {% else %}

    (... 생략 ...)

    {% endif %}

    </tbody>

</table>

(... 생략 ...)



부트스트랩의 badge 컴포넌트로 표시된 것을 확인해보자.

 

 

 

댓글