필자는 미디움을 참고하여 결과물을 만들고 있다. 장고를 이제 좀 알겠다 싶었으나 제자리 걸음을 하고 있는 듯한 느낌이다. 이게 장고에 익숙하지 않아서 그런건지 데이터 베이스를 아직도 잘 모르는 건지 판단이 안된다. 여튼 좋아요 버튼을 만들어 보았다.


1.MODEL

“좋아요는 댓글처럼 모델을 따로 만들어야 하나?”라는 고민을 했었지만 ManyToManyField를 사용하면 한 사용자가 한 포스트에 좋아요를 하거나 취소하도록 할 수 있다.

1
2
3
class Post(models.Model):
    ...
    likes = models.ManyToManyField(User, related_name='likes')

그래서 좋아요는 포스트의 하나의 테이블로 들어간다.


2.URL

필자는 좋아요 버튼을 누르면 URL로 접속해서 포스트와 상호작용을 시킬 것이다.

1
2
3
urlpatterns = [
    ...
    path('posts/<int:pk>/like', views.post_like, name='post_like'),

간단하게 포스트의 주요키만 받으면 할 수 있겠다 싶어서 위와같은 URL을 만들었다.


3.VIEW

좋아요 버튼은 SUBMIT되는 것이 아니므로 POST인지 아닌지 판단할 필요가 없다. (이걸로 삽질 오래함)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def post_like(request, pk):
    # 포스트 정보 받아옴
    post = get_object_or_404(Post, pk=pk)

    # 사용자가 로그인 된건지 확인
    if not request.user.is_active:
        return redirect('post_detail', username=post.author, url=post.url)    

    # 사용자 정보 받아옴
    user = User.objects.get(username=request.user)
    # 좋아요에 사용자가 존재하면
    if post.likes.filter(id = user.id).exists():
        # 사용자를 지움
        post.likes.remove(user)
    else:
        # 아니면 사용자를 추가
        post.likes.add(user)
    # 포스트로 리디렉션
    return redirect('post_detail', username=post.author, url=post.url)


4.TEMPLATE

1
<a href="{% url 'post_like' pk=post.pk%}">좋아요! {{ post.total_likes }}</a>

이제 템플릿에서 포스트 내부에 위와같은 링크를 만들어주면 링크를 눌렀을때 좋아요를 시켜줄 수 있다!

완성된 기능


5.AJAX

좋아요는 대부분 AJAX를 사용하여 페이지 전환을 없앤다. 필자는 아직 AJAX의 묘미인 JSON을 전달받는 방법은 잘 모르지만 LIKE는 단지 토탈수만 전달받아도 충분하므로 쉽게 적용할 수 있었다.

1 - Model 수정
1
2
3
4
5
6
class Post(models.Model):
    ...
    likes = models.ManyToManyField(User, related_name='likes')

    def total_likes(self):
        return self.likes.count()

우선 모델에서 좋아요의 전체 갯수를 파악할 수 있도록 메서드를 작서앟였다.

2 - View 수정
1
2
3
4
5
6
7
8
def post_like(request, pk):
    post = get_object_or_404(Post, pk=pk)
    user = User.objects.get(username=request.user)
    if post.likes.filter(id = user.id).exists():
        post.likes.remove(user)
    else:
        post.likes.add(user)
    return HttpResponse(str(post.total_likes()))

상단의 코드에서는 자용자가 로그인을 했는지 확인했으나, 실제로 ManyToMany는 외래키가 없으면 서버에러가 발생하므로 빼주었다. return 값은 리디렉션이 아닌 포스트의 좋아요의 토탈 값을 전달했다.

3 - Template 수정

post_derail 템플릿에서 아래 태그를 추가한다. 좋아요 버튼이 될 SVG와 그 아래에는 토탈 좋아요 값을 출력한다.

1
2
3
4
<a href="javascript:void(0);" onclick="post_like();">
    <SVG></SVG>
</a>
<a id="like-count"><a>


post_detail 템플릿에서 아래 스크립트를 추가한다. 아래 코드에는 나열되지 않았지만 당연히 jQuery도 삽입해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<script type="text/javascript">
var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
function csrfSafeMethod(method) {
    // these HTTP methods do not require CSRF protection
    return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
$.ajaxSetup({
    beforeSend: function (xhr, settings) {
        if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
            xhr.setRequestHeader("X-CSRFToken", csrftoken);
        }
    }
});
function post_like() {
    {% if request.user.is_active %}
    $.ajax({
        url: "{% url 'post_like' pk=post.pk%}",
        type: "post",
    }).done(function (data) {
        document.getElementById('like-count').innerHTML = data;
    });
    {% else %}
    alert("로그인 된 사용자만 사용할 수 있습니다.");
    {% endif %}
}
</script>

기본적으로 중요한 함수는 post_like라는 함수지만 장고에선 ajaxSetupcsrfSafeMethod를 적용해주어야 정상적으로 사용이 가능했다. 여하지간 post_like 함수의 핵심은 해당 포스트의 주소로 AJAX를 사용하여 성공적으로 끝난경우 like-count는 엘리먼트의 값을 전달받은 좋아요 토탈 값으로 변경한다.


6.PERPECT

이번엔 좀 더 완성도를 올리기 위해서 좋아요를 한 경우 버튼의 색상을 변경하고 취소하면 다시 색상을 변경할 것이다.

1
2
3
4
...
check_like = post.likes.filter(id=request.user.id).exists()
...
return render(request, 'board/post_detail.html', {...,'check_like':check_like,...})

포스트 디테일을 호출하기 전에 위와같이 좋아요를 했는지 알 수 있도록 변수를 생성하여 템플릿으로 전달하자

1
2
3
4
<a href="javascript:void(0);" onclick="post_like();">
    <SVG class="{%if check_like %}like-active{% endif %}"></SVG>
</a>
<a id="like-count">{{ post.total_likes }}<a>

필자의 경우에는 좋아요가 활성화 된 경우 SVGlike-active라는 클래스를 추가해 주도록 하였다. 포스트 내부에서 좋아요를 클릭하는 경우 자바스크립트를 이용해서 간단하게 제어했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function post_like() {
    {% if request.user.is_active %}
    $.ajax({
        url: "{% url 'post_like' pk=post.pk%}",
        type: "post",
    }).done(function (data) {
        if($("#like").hasClass("like-active")) {
            $("#like").removeClass("like-active");
        } else {
            $("#like").addClass("like-active");
        }
        document.getElementById('like-count').innerHTML = data;
    });
    {% else %}
    alert("로그인 된 사용자만 사용할 수 있습니다.");
    {% endif %}
}


결과물

WRITTEN BY

배진오

소비적인 일보단 생산적인 일을 추구하며, 좋아하는 일을 잘하고 싶어합니다 :D
im@baejino.com