본문 바로가기
Research/Django

[Django] 점프 투 장고 튜토리얼 - 3-10 댓글(Comment)

by RIEM 2021. 11. 25.

Django 점프 투 장고 정리

작성일 : 2021-11-24

문서버전 : 1.0 

개요

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

레퍼런스

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

3-10 댓글(Comment)

답변에 댓글을 달 수 있는 댓글(Comment) 기능을 추가해보자.

댓글 모델

댓글 작성을 위해선 댓글 모델이 필요하다. 

> ../mysite/pybo/models.py

 
class Comment(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    content = models.TextField()
    create_date = models.DateTimeField()
    modify_date = models.DateTimeField(null=True, blank=True)
    question = models.ForeignKey(Question, null=True, blank=True, on_delete=models.CASCADE)
    answer = models.ForeignKey(Answer, null=True, blank=True, on_delete=models.CASCADE)

 

Comment 모델의 속성은 아래와 같다.

  • Author : 댓글 작성자
  • Content : 댓글 내용
  • Create_date : 댓글 작성일시
  • Modify_date : 댓글 수정일시
  • Question : 댓글 달린 질문
  • Answer : 댓글 달린 답변

 

질문에 댓글이 작성될 경우 question 변수에 값이 저장되고, 답변에 댓글이 작성될 경우 answer 변수에 값이 저장된다. 두개 변수 중 하나에만 값이 저장되므로 두 개의 속성은 모두 null=True, blank=True를 설정한다. Question, answer 속성에 있는 on_delete=models.CASCADE 옵션은 질문이나 답변이 삭제될 경우 해당 댓글도 삭제되도록 한다. 

 

Comment 모델이 추가되었으니 makemigrations, migrate 명령 실행한다.

(mysite) c:\projects\mysite>python manage.py makemigrations
Migrations for 'pybo':
  pybo\migrations\0005_comment.py
    - Create model Comment

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

 

모델의 migrate가 잘 진행되었다.

 

질문 댓글

질문에 댓글을 다는 기능을 추가하자. 장고 새 기능 추가하는 패턴을 정리하자면 아래와 같다.


장고 기능 개발 패턴 정리

  1. 템플릿 : 추가 기능을 위한 링크, 버튼 추가
  2. Urls.py : 링크에 해당하는 URL 매핑
  3. Forms.py : 폼 작성(필요한 경우)
  4. Views.py : URL 매핑 관련 함수 작성
  5. 함수에서 사용할 템플릿 작성

 

질문 댓글 링크

질문 상세 템플릿을 다음과 같이 수정하자

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

(... 생략 ...)

<div class="card-body">

    <div class="card-text" style="white-space: pre-line;">{{ question.content }}</div>

    <div class="d-flex justify-content-end">

        {% if question.modify_date %}

        <div class="badge badge-light p-2 text-left mx-3">

            <div class="mb-2">modified at</div>

            <div>{{ question.modify_date }}</div>

        </div>

        {% endif %}

        <div class="badge badge-light p-2 text-left">

            <div class="mb-2">{{ question.author.username }}</div>

            <div>{{ question.create_date }}</div>

        </div>

    </div>

    {% if request.user == question.author %}

    <div class="my-3">

        <a href="{% url 'pybo:question_modify' question.id  %}"

            class="btn btn-sm btn-outline-secondary">수정</a>

        <a href="#" class="delete btn btn-sm btn-outline-secondary"

            data-uri="{% url 'pybo:question_delete' question.id  %}">삭제</a>

    </div>

    {% endif %}

    <!-- 질문 댓글 Start -->

    {% if question.comment_set.count > 0 %}

    <div class="mt-3">

    {% for comment in question.comment_set.all %}  <!-- 등록한 댓글을 출력 -->

        <div class="comment py-2 text-muted"<!-- 댓글 각각에 comment 스타일 지정 -->

            <span style="white-space: pre-line;">{{ comment.content }}</span>

            <span>

                - {{ comment.author }}, {{ comment.create_date }}

                {% if comment.modify_date %}

                (수정:{{ comment.modify_date }})

                {% endif %}

            </span>

            {% if request.user == comment.author %}

            <a href="{% url 'pybo:comment_modify_question' comment.id  %}" class="small">수정</a>,

            <a href="#" class="small delete" 

               data-uri="{% url 'pybo:comment_delete_question' comment.id  %}">삭제</a>

            {% endif %}

        </div>

    {% endfor %}

    </div>

    {% endif %}

    <div>

        <a href="{% url 'pybo:comment_create_question' question.id  %}" 

           class="small"><small>댓글 추가 ..</small></a<!-- 댓글 추가 링크 -->

    </div>

    <!-- 질문 댓글 End -->

</div>

(... 생략 ...)



{% for comment in question.comment_set.all %}와 {% endfor %} 사이에 댓글 내용과 글쓴이, 작성일시를 출력하여 질문에 달린 댓글 내용을 표시해주었다. 댓글 글쓴이와 로그인 유저가 동일한 경우 ‘수정’과 ‘삭제’ 링크가 보이도록 했다. For문 외부에는 댓글 작성할 수 있는 ‘댓글 추가..’ 링크도 추가했다.

 

For문에 의해 반복되는 댓글 각각은 comment라는 스타일 클래스를 따로 지정해주었다. 이 스타일은 댓글 영역의 폰트를 작게 만들어준다. 이를 위해 style.css의 comment 클래스의 내용을 추가해주자.

> ../mysite/static/style.css

.comment {
    border-top:dotted 1px #ddd;
    font-size:0.7em;
}

 

질문 댓글 URL 매핑

위에서 추가한 ‘수정’, ‘삭제’, ‘댓글 추가..’ 을 pybo/urls.py에서 URL 매핑해주자.

> ../mysite/pybo/urls.py

 
(... 생략 ...)

urlpatterns = [
    (... 생략 ...)
    path('comment/create/question/<int:question_id>/', views.comment_create_question, name='comment_create_question'),
    path('comment/modify/question/<int:comment_id>/', views.comment_modify_question, name='comment_modify_question'),
    path('comment/delete/question/<int:comment_id>/', views.comment_delete_question, name='comment_delete_question'),
]

댓글 등록, 수정, 삭제 관련 URL 매핑을 모두 해주었다. 댓글 등록시 질문의 id 번호 <int:question_id>가 필요하고, 수정하거나 삭제할 경우 댓글의 id 번호 <int:comment_id>가 필요한 것이 특징이다.

 

질문 댓글 폼

댓글 등록 시 사용할 CommentForm을 아래와 같이 작성해주자.

> ../mysite/pybo/forms.py

 
from django import forms
from pybo.models import Question, Answer, Comment

(... 생략 ...)

class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ['content']
        labels = {
            'content': '댓글내용',
        }

댓글 작성 시 content 필드만 사용된다는 것이 특이사항이다.



질문 댓글 등록 함수

> ../mysite/pybo/views.py

pybo/views.py 파일에 댓글 등록 함수 comment_create_question을 추가하자

 
(... 생략 ...)
from .forms import QuestionForm, AnswerForm, CommentForm
(... 생략 ...)

@login_required(login_url='common:login')
def comment_create_question(request, question_id):
    """
    pybo 질문댓글등록
    """
    question = get_object_or_404(Question, pk=question_id)
    if request.method == "POST":
        form = CommentForm(request.POST)
        if form.is_valid():
            comment = form.save(commit=False)
            comment.author = request.user
            comment.create_date = timezone.now()
            comment.question = question
            comment.save()
            return redirect('pybo:detail', question_id=question.id)
    else:
        form = CommentForm()
    context = {'form': form}
    return render(request, 'pybo/comment_form.html', context)

댓글 저장 후 pybo:detail로 redirect해주었다. 질문에 대한 댓글이기 때문에 comment.question = question처럼 comment에 question을 저장했다.

 

질문 댓글 템플릿

> ../mysite/templates/pybo/comment_form.html

댓글 등록을 위해 comment_form.html 템플릿을 간단한 형태로 생성해주자.

 
{% extends 'base.html' %}

{% block content %}
<div class="container my-3">
    <h5 class="border-bottom pb-2">댓글등록하기</h5>
    <form method="post" class="post-form my-3">
        {% csrf_token %}
        {% include "form_errors.html" %}
        <div class="form-group">
            <label for="content">댓글내용</label>
            <textarea class="form-control"name="content" id="content"
                      rows="3">{{ form.content.value|default_if_none:'' }}</textarea>
        </div>
        <button type="submit" class="btn btn-primary">저장하기</button>
    </form>
</div>
{% endblock %}

 

질문 댓글 수정 함수

질문에 등록된 댓글 수정을 위해 comment_modify_question 함수를 추가해주자.

> ../mysite/pybo/views.py

 
(... 생략 ...)
from .models import Question, Answer, Comment
(... 생략 ...)

@login_required(login_url='common:login')
def comment_modify_question(request, comment_id):
    """
    pybo 질문댓글수정
    """
    comment = get_object_or_404(Comment, pk=comment_id)
    if request.user != comment.author:
        messages.error(request, '댓글수정권한이 없습니다')
        return redirect('pybo:detail', question_id=comment.question.id)

    if request.method == "POST":
        form = CommentForm(request.POST, instance=comment)
        if form.is_valid():
            comment = form.save(commit=False)
            comment.modify_date = timezone.now()
            comment.save()
            return redirect('pybo:detail', question_id=comment.question.id)
    else:
        form = CommentForm(instance=comment)
    context = {'form': form}
    return render(request, 'pybo/comment_form.html', context)

 

질문 댓글 수정 함수는 질문 댓글 등록 함수와 거의 유사하다. GET 방식일 경우 기존 댓글을 조회해서 폼에 반영하고 POST 방식일 경우 입력된 값으로 댓글을 업데이트한다. 업데이트 시 modify_date에 수정일시를 반영한다.

질문 댓글 삭제 함수

질문에 등록된 댓글을 삭제하는 기능인 comment_delete_question을 추가하자

> ../mysite/pybo/views.py

(... 생략 ...)

@login_required(login_url='common:login')
def comment_delete_question(request, comment_id):
    """
    pybo 질문댓글삭제
    """
    comment = get_object_or_404(Comment, pk=comment_id)
    if request.user != comment.author:
        messages.error(request, '댓글삭제권한이 없습니다')
        return redirect('pybo:detail', question_id=comment.question.id)
    else:
        comment.delete()
    return redirect('pybo:detail', question_id=comment.question.id)
 

 

잘 구현되는지 확인해보자.

질문에 댓글을 달 수 있는 기능을 추가했다. 일반적으로 질문 대신 답변에 댓글을 많이 달기 때문에 답변 댓글 기능을 만들어보자.

 

답변 댓글

질문에 댓글 기능 추가하는 과정과 크게 다르지 않다. 

 

답변 댓글 링크

아래와 같이 질문 상세 템플릿의 답변 목록을 수정해주자.

> ../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="card my-3">

    <div class="card-body">

        <div class="card-text" style="white-space: pre-line;">{{ answer.content }}</div>

        <div class="d-flex justify-content-end">

            {% if answer.modify_date %}

            <div class="badge badge-light p-2 text-left mx-3">

                <div class="mb-2">modified at</div>

                <div>{{ answer.modify_date }}</div>

            </div>

            {% endif %}

            <div class="badge badge-light p-2 text-left">

                <div class="mb-2">{{ answer.author.username }}</div>

                <div>{{ answer.create_date }}</div>

            </div>

        </div>

        {% if request.user == answer.author %}

        <div class="my-3">

            <a href="{% url 'pybo:answer_modify' answer.id  %}"

               class="btn btn-sm btn-outline-secondary">수정</a>

            <a href="#" class="delete btn btn-sm btn-outline-secondary "

               data-uri="{% url 'pybo:answer_delete' answer.id  %}">삭제</a>

        </div>

        {% endif %}

        {% if answer.comment_set.count > 0 %}

        <div class="mt-3">

        {% for comment in answer.comment_set.all %}

            <div class="comment py-2 text-muted">

                <span style="white-space: pre-line;">{{ comment.content }}</span>

                <span>

                    - {{ comment.author }}, {{ comment.create_date }}

                    {% if comment.modify_date %}

                    (수정:{{ comment.modify_date }})

                    {% endif %}

                </span>

                {% if request.user == comment.author %}

                <a href="{% url 'pybo:comment_modify_answer' comment.id  %}" class="small">수정</a>,

                <a href="#" class="small delete"

                   data-uri="{% url 'pybo:comment_delete_answer' comment.id  %}">삭제</a>

                {% endif %}

            </div>

        {% endfor %}

        </div>

        {% endif %}

        <div>

            <a href="{% url 'pybo:comment_create_answer' answer.id  %}"

               class="small"><small>댓글 추가 ..</small></a>

        </div>

    </div>

</div>

{% endfor %}

(... 생략 ...)



위에서 추가한 질문에 대한 댓글 기능과 큰 차이는 없어보인다. Question.comment_set 대신 answer.comment_set을 사용한 것과 답변의 댓글을 <수정>, <삭제>, <추가> 시 호출되는 URL만 다르다.

답변 댓글 URL 매핑

위 템플릿에서 지정한 url을 Urls.py 파일에 반영해주자.

 
(... 생략 ...)

urlpatterns = [
    (... 생략 ...)
    path('comment/create/answer/<int:answer_id>/', views.comment_create_answer, name='comment_create_answer'),
    path('comment/modify/answer/<int:comment_id>/', views.comment_modify_answer, name='comment_modify_answer'),
    path('comment/delete/answer/<int:comment_id>/', views.comment_delete_answer, name='comment_delete_answer'),
]

 

댓글을 등록할 때는 답변 id 번호(<int:answer_id>)를 사용했고, 수정하거나 삭제할 경우는 댓글의 id 번호(<int:comment_id>)를 사용했다.

 

답변 댓글 등록, 수정, 삭제 함수

Views.py 파일에 답변의 댓글을 등록, 수정, 삭제하기 위한 함수를 추가해주자.

> ../mysite/pybo/views.py

 
(... 생략 ...)

@login_required(login_url='common:login')
def comment_create_answer(request, answer_id):
    """
    pybo 답글댓글등록
    """
    answer = get_object_or_404(Answer, pk=answer_id)
    if request.method == "POST":
        form = CommentForm(request.POST)
        if form.is_valid():
            comment = form.save(commit=False)
            comment.author = request.user
            comment.create_date = timezone.now()
            comment.answer = answer
            comment.save()
            return redirect('pybo:detail', question_id=comment.answer.question.id)
    else:
        form = CommentForm()
    context = {'form': form}
    return render(request, 'pybo/comment_form.html', context)


@login_required(login_url='common:login')
def comment_modify_answer(request, comment_id):
    """
    pybo 답글댓글수정
    """
    comment = get_object_or_404(Comment, pk=comment_id)
    if request.user != comment.author:
        messages.error(request, '댓글수정권한이 없습니다')
        return redirect('pybo:detail', question_id=comment.answer.question.id)

    if request.method == "POST":
        form = CommentForm(request.POST, instance=comment)
        if form.is_valid():
            comment = form.save(commit=False)
            comment.modify_date = timezone.now()
            comment.save()
            return redirect('pybo:detail', question_id=comment.answer.question.id)
    else:
        form = CommentForm(instance=comment)
    context = {'form': form}
    return render(request, 'pybo/comment_form.html', context)


@login_required(login_url='common:login')
def comment_delete_answer(request, comment_id):
    """
    pybo 답글댓글삭제
    """
    comment = get_object_or_404(Comment, pk=comment_id)
    if request.user != comment.author:
        messages.error(request, '댓글삭제권한이 없습니다')
        return redirect('pybo:detail', question_id=comment.answer.question.id)
    else:
        comment.delete()
    return redirect('pybo:detail', question_id=comment.answer.question.id)

 

답변 댓글과 관련된 함수는 질문 댓글 함수와 거의 유사하다. 답변의 댓글을 등록하거나 수정하기 위해 사용한 폼과 템플릿은 질문 댓글에서 사용한 CommentForm과 comment_form 파일을 다시 활용하면 된다.

 

답변의 댓글인 경우 question_id 값을 알기 위해 comment.answer.question 처럼 답변(answer)을 통해 질문(question)을 얻을 수 있었다.

 

 

 

댓글