2020-04-18

Django 26. 댓글 쓰기 / 삭제 구현 (AJAX)

Ajax를 사용하여 게시글에 댓글을 달고, 삭제하는 기능을 구현합니다.


게시글에 댓글달기를 구현하기에 앞서 이전의 포스팅에서는 공지사항 앱인 notice app을 활용하여 포스팅을 진행하였습니다. 하지만 공지사항에는 댓글을 달 수 없도록 설정하였기에 이 프로젝트에서는 자유롭게 게시글을 작성하고 소통할 수 있는 자유게시판 앱을 추가하였습니다. 구현 방법은 공지사항 앱과 같으며 freeapp을 생성했다는 가정하에 포스팅을 진행하겠습니다. 댓글 포스팅은 총 2개로 나누어 포스팅할 예정입니다. 기본적인 구현 로직은 다음과 같습니다.

level0 개발자, level1 관리자 : 모든 댓글의 입력, 삭제

level2 사용자 : 댓글의 입력 (본인 글일시 댓글의 입력, 삭제)

  1. 본인의 글일시 댓글에 (글쓴이) 표시를 한다.
  2. 댓글 작성, 삭제시 그 갯수를 동적으로 표시한다.
  3. 본 게시글이 삭제될시 댓글은 DB상에서 삭제된다.
  4. 댓글 삭제시 DB상에서 내용은 보존한다.
  5. 댓글 삭제시 삭제된 댓글이라는 표시를 한다.
  6. 댓글 입력, 삭제는 비동기로 구현한다.

1. 댓글 models.py 작성

댓글을 작성하기 위한 models을 생성하기 위해 적용할 게시판앱의 models.py에 아래의 Comment 클래스를 추가합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# free/models.py

class Comment(models.Model):
post = models.ForeignKey(Free, on_delete=models.CASCADE, verbose_name='게시글')
writer = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, verbose_name='댓글작성자')
content = models.TextField(verbose_name='댓글내용')
created = models.DateTimeField(auto_now_add=True, verbose_name='작성일')
deleted = models.BooleanField(default=False, verbose_name='삭제여부')

def __str__(self):
return self.content

class Meta:
db_table = '자유게시판 댓글'
verbose_name = '자유게시판 댓글'
verbose_name_plural = '자유게시판 댓글'

댓글을 작성할 게시글 필드인 post와 댓글작성자 필드인 wirter는 ForeignKey관계로 설정합니다. 작성된 댓글을 삭제할 시 ‘삭제된 댓글입니다.’ 로 표시하기 위해 deleted 필드를 추가합니다.

아래의 명령어를 통해 생성한 Comment를 django DB에 적용하기 위해 migrate합니다.

1
2
$ python manage.py makemigrations free
$ python manage.py migrate

2. admin.py 작성

생성한 Model을 django admin페이지에서 관리할 수 있도록 admin.py에 아래의 코드를 추가합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# free/admin.py

from .models import Comment

class CommentAdmin(admin.ModelAdmin):
list_display = (
'post',
'content',
'writer',
'created',
'deleted',
)
search_fields = ('post__title', 'content', 'writer__user_id',)

admin.site.register(Comment, CommentAdmin)

search_fields의 incontains에러를 피하기 위하여 ForeignKey 관계로 연결된 필드는 ‘post__title’과 같은 형식으로 추가합니다.

3. 댓글쓰기 views.py 작성

templates에서 Ajax로 댓글작성 비동기요청이 들어왔을때 응답을 해주기 위해 게시글의 id값을 인자로 받는 comment_write_view를 아래와 같이 작성합니다.

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
# free/views.py

from django.http import HttpResponse
import json
from django.core import serializers
from django.core.serializers.json import DjangoJSONEncoder

@login_message_required
def comment_write_view(request, pk):
post = get_object_or_404(Free, id=pk)
writer = request.POST.get('writer')
content = request.POST.get('content')
if content:
comment = Comment.objects.create(post=post, content=content, writer=request.user)
post.save()
data = {
'writer': writer,
'content': content,
'created': '방금 전',
'comment_id': comment.id
}
if request.user == post.writer:
data['self_comment'] = '(글쓴이)'

return HttpResponse(json.dumps(data, cls=DjangoJSONEncoder), content_type = "application/json")

templates에서 json으로 받은 댓글의 내용과 작성자를 id값에 맞는 post에 create메소드를 사용하여 댓글을 생성합니다. 성공적으로 DB에 댓글이 생성되었을시 댓글의 id값, 글작성자와 댓글작성자를 비교해 글쓴이 여부 등과 같은 data를 딕셔너리형으로 담아 response합니다.

4. 댓글삭제 views.py 작성

댓글삭제도 댓글쓰기 view와 마찬가지로 게시글의 id값을 인자로 받는 comment_delete_view를 아래와 같이 작성합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# free/views.py

@login_message_required
def comment_delete_view(request, pk):
post = get_object_or_404(Free, id=pk)
comment_id = request.POST.get('comment_id')
target_comment = Comment.objects.get(pk = comment_id)

if request.user == target_comment.writer or request.user.level == '1' or request.user.level == '0':
target_comment.deleted = True
target_comment.save()
post.save()
data = {
'comment_id': comment_id,
}
return HttpResponse(json.dumps(data, cls=DjangoJSONEncoder), content_type = "application/json")

댓글삭제 요청이 들어왔을 경우 pk값으로 받은 게시글의 id를 사용해 게시글을 찾아내고, 댓글의 id값인 comment_id와 비교하여 삭제요청을 한 사용자의 등급이 관리자거나 글쓴이 본인일 경우에 댓글의 deleted 필드를 True로 변경합니다. templates에서는 deleted가 True인 댓글을 템플릿 필터를 활용해 삭제메세지로 표시할 수 있게 됩니다.

5. urls.py 작성

구현한 views를 연결하기 위해 urls.pyurlpatterns에 아래와 같이 path를 추가합니다.

1
2
3
4
5
6
# free/urls.py

urlpatterns = [
path('<int:pk>/comment/write/', views.comment_write_view, name='comment_write'),
path('<int:pk>/comment/delete/', views.comment_delete_view, name='comment_delete'),
]

6. 댓글표시 templates 작성

사용자가 게시글을 GET으로 접근했을시 보여지는 템플릿을 우선적으로 작성합니다. 템플릿 필터를 활용하여 삭제댓글, 글쓴이 표시를 하고 비동기로 댓글이 append될 hidden 타입의 comment_list div를 추가합니다. 작성자를 얻기 위해 현재 접속된 사용자를 value값으로 설정합니다.

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<!-- templates/free/free_detail.html -->

<div class="card">
<div class="card-header">
<div class="col-md-12">
{% csrf_token %}
<div class="form-group row">
<textarea class="form-control" id="content_id" rows="3" placeholder="댓글을 입력해주세요."></textarea>
</div>
<div class="text-right" style="float:right">
<button id="comment_write" class="btn btn-sm">댓글달기</button>
</div>
</div>
<hr>
<div id="more_comment">
{% if comments %}
{% for comment in comments %}
<div id='{{ comment.id }}'>
{% if comment.deleted %}
<span>삭제된 댓글입니다.</span><hr>
{% else %}
{% if comment.writer == free.writer %}
<strong>{{ comment.writer }}&nbsp;<span>(글쓴이)</span></strong>
{% else %}
<strong>{{ comment.writer }}</strong>
{% endif %}
<span style="float:right">{{ comment.created }}</span>
{% if comment.writer == request.user or request.user.level == '0' or request.user.level == '1' %}
<div>
<div style="white-space:pre-wrap; text-align:left;">{{ comment.content }}</div>
<div style="text-align: right;">
<a onclick="commentDelete('{{comment.id}}');">댓글삭제</a>
</div>
</div>
<hr>
{% else %}
<div>
<div style="white-space:pre-wrap; text-align:left;">{{ comment.content }}</div>
</div>
<hr>
{% endif %}
{% endif %}
</div>
<div class='{{ comment.id }}'></div>
{% endfor %}
{% endif %}
<input type="hidden" id="comment_writer" value={{request.user}}>
<div id="comment_list"></div>
</div>
</div>
</div>

7. 댓글쓰기 AJAX Script 작성

댓글 달기 button인 comment_write를 click하였을때 실행되는 Ajax Script는 아래와 같습니다.

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!-- templates/free/free_detail.html -->

<script type="text/javascript">
$(document).ready(function () {
$('#comment_write').click(function () {
var content= $("#content_id").val();
var writer= $("#comment_writer").val();
$.ajax({
type: "POST",
url: "{% url 'free:comment_write' free.id %}",
dataType: "json",
data: {
'writer': writer,
'content': content,
'csrfmiddlewaretoken': '{{csrf_token}}',
},
success: function (response) {
if (response.self_comment) {
$('#comment_list').append(
'<div><div id='+response.comment_id+'><strong>'+response.writer+'&nbsp;<span>'+response.self_comment+'</span></strong>'+
'<span style="float:right;">'+response.created+'</span>'+
'<div><div style="white-space:pre-wrap; text-align:left;">'+response.content+
'</div><div style="text-align:right;"><a onclick="commentDelete('+response.comment_id+');">댓글삭제</a></div></div><hr></div><div class='+response.comment_id+'></div>'
);
}
else{
$('#comment_list').append(
'<div><div id='+response.comment_id+'><strong>'+response.writer+'</strong>'+
'<span style="float:right;">'+response.created+'</span>'+
'<div><div style="white-space:pre-wrap; text-align:left;">'+response.content+
'</div><div style="text-align:right;"><a onclick="commentDelete('+response.comment_id+');">댓글삭제</a></div></div><hr></div><div class='+response.comment_id+'></div>'
);
}
$('#content_id').val("");
},
error: function () {
if ($('#content_id').val()=="") {
alert('댓글을 입력해주세요.');
}
},
})
});
});
</script>

댓글의 내용과 작성자를 이전에 구현한 comment_write_viewjson형태로 전달합니다. comment_wirte_view에서 성공적으로 응답이 오면 본인 글에 댓글을 작성할시 작성자 옆에 (글쓴이)표시를 하기 위해서 self_comment의 여부를 확인하고 hidden type으로 설정해둔 comment_list에 적절한 html을 append시킵니다.

8. 댓글삭제 templates 작성

댓글 삭제를 위한 Script는 아래와 같습니다. 사용자가 댓글 삭제를 클릭할 시 confirm 창으로 삭제여부를 확인하고 comment_delete_view에서 본인의 댓글인지 확인 후 성공적으로 삭제가 되었을시 templates에서 응답을 받아 replaceWith메소드를 통해 삭제된 댓글입니다.라는 표시를 하게 됩니다.

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
<!-- templates/free/free_detail.html -->

<script type="text/javascript">
function commentDelete(value) {
var comment_id = value;
var delete_warning = confirm('댓글을 삭제하시겠습니까?');
if (delete_warning == true) {
$.ajax({
type: "POST",
url: "{% url 'free:comment_delete' free.id %}",
dataType: "json",
data: {
'comment_id': comment_id,
'csrfmiddlewaretoken': '{{csrf_token}}',
},
success: function (response) {
$('#'+response.comment_id).replaceWith('<span style="color:gray;">삭제된 댓글입니다.</span><hr>');
},
error: function () {
alert('본인 댓글이 아닙니다.');
},
});
}
}
</script>

9. 결과

django-project-26

*본 프로젝트에서는 댓글에 답글쓰기 기능이 추가되어 있으나 Django 포스팅과 성격이 달라 따로 포스팅하지 않았습니다. 제 Github에 답글쓰기 소스가 있으니 참고하시길 바랍니다.

*전체 html, css 등은 자세하게 포스팅하지 않습니다. 제 Github에서 소스를 확인하실 수 있습니다.