Programming

; develop a program

Framework/Django

[Django] 장고(Django) 서비스 개발 - 수정과 삭제

Clloud_ 2022. 12. 20. 22:39
반응형

작성한 질문과 답변을 수정하고 삭제할 수 있는 기능을 추가하려고 한다.

비슷한 기능을 반복적으로 구현해야 하므로 조금 지루할 수 있지만 장고 패턴에 익숙해질 수 있는 좋은 기회라고 생각하고 따라 해 보자.

 

이번 포스팅에서는 장고를 사용하여 게시판 서비스 개발에 필요한 수정과 삭제에 대하여 공부를 해보고자 한다.

 


수정 일시

질문이나 답변이 언제 수정되었는지 확인할 수 있도록 Question과 Answer 모델에 수정 일시를 의미하는 modify_date 속성을 추가한다.

 

[파일명: projects\mysite\pybo\models.py]

(... 생략 ...)

class Question(models.Model):
    (... 생략 ...)
    modify_date = models.DateTimeField(null=True, blank=True)
    (... 생략 ...)

class Answer(models.Model):
    (... 생략 ...)
    modify_date = models.DateTimeField(null=True, blank=True)

 

null=True는 데이터베이스에서 modify_date 칼럼에 null을 허용한다는 의미이며, blank=True는 form.is_valid( )를 통한 입력 데이터 검증 시 값이 없어도 된다는 의미이다.

즉, null=True, blank=True는 어떤 조건으로든 값을 비워둘 수 있음을 의미한다.

 

수정일시는 수정한 경우에만 생성되는 데이터이므로 null=True, blank=True를 지정했다.

 

모델이 변경되었으므로 makemigrations, migrate 명령을 수행한다.

(mysite) c:\projects\mysite>python manage.py makemigrations
Migrations for 'pybo':
     pybo\migrations\0004_auto_20200331_0945.py
        - Add field modify_date to answer
        - Add field modify_date to question

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

(mysite) c:\projects\mysite>

 


질문 수정

작성한 질문을 수정하려면 질문 상세 화면에서 "수정" 버튼을 클릭하여 수정 화면으로 진입해야 한다.

 

질문 수정 버튼

질문 상세 화면에 다음과 같이 질문 수정 버튼을 추가한다.

 

[파일명: projects\mysite\templates\pybo\question_detail.html]

(... 생략 ...)
<!-- 질문 -->
<h2 class="border-bottom py-2">{{ question.subject }}</h2>
<div class="card my-3">
    <div class="card-body">
        <div class="card-text" style="white-space: pre-line;">{{ question.content }}</div>
        <div class="d-flex justify-content-end">
            <div class="badge bg-light text-dark p-2 text-start">
                <div class="mb-2">{{ question.author.username }}</div>
                <div>{{ question.create_date }}</div>
            </div>
        </div>
        <div class="my-3">
            {% if request.user == question.author %}
            <a href="{% url 'pybo:question_modify' question.id  %}" 
               class="btn btn-sm btn-outline-secondary">수정</a>
            {% endif %}
        </div>
    </div>
</div>
(... 생략 ...)

 

수정 버튼은 로그인한 사용자와 글쓴이가 동일한 경우에만 노출되도록 {% if request.user == question.author %}을 적용한다.

만약 로그인한 사용자와 글쓴이가 다르다면 수정 버튼은 보이지 않는다.

 


urls.py

{% url 'pybo:question_modify' question.id %} URL이 추가되었으므로 pybo/urls.py에 다음처럼 URL매핑 규칙을 추가한다.

 

[파일명: projects\mysite\pybo\urls.py]

(... 생략 ...)

urlpatterns = [
    (... 생략 ...)
    path('question/modify/<int:question_id>/', views.question_modify, name='question_modify'),
]

 


views.py

views.question_modify 함수를 다음처럼 작성한다.

 

[파일명: projects\mysite\pybo\views.py]

(... 생략 ...)
from django.contrib import messages
(... 생략 ...)

@login_required(login_url='common:login')
def question_modify(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    if request.user != question.author:
        messages.error(request, '수정권한이 없습니다')
        return redirect('pybo:detail', question_id=question.id)
    if request.method == "POST":
        form = QuestionForm(request.POST, instance=question)
        if form.is_valid():
            question = form.save(commit=False)
            question.modify_date = timezone.now()  # 수정일시 저장
            question.save()
            return redirect('pybo:detail', question_id=question.id)
    else:
        form = QuestionForm(instance=question)
    context = {'form': form}
    return render(request, 'pybo/question_form.html', context)

 

question_modify 함수는 로그인한 사용자(request.user)와 수정하려는 질문의 글쓴이(question.author)가 다를 경우에는 "수정권한이 없습니다"라는 오류를 발생시킨다.

이 오류를 발생시키기 위해 messages 모듈을 이용하였다.

messages는 장고가 제공하는 모듈로 넌필드 오류(non-field error)를 발생시킬 경우에 사용한다.

 

질문 상세 화면에서 "수정" 버튼을 클릭하면 http://localhost:8000/pybo/question/modify/2/ 페이지가 GET 방식으로 호출되어 질문수정 화면이 보인다.

질문 수정화면에서 사용한 템플릿은 질문 등록 시 사용했던 pybo/question_form.html 파일과 동일하다.

 

그리고 질문 수정 화면에서 "저장하기" 버튼을 클릭하면 http://localhost:8000/pybo/question/modify/2/ 페이지가 POST 방식으로 호출되어 데이터가 수정된다.

form 태그에 action 속성이 없는 경우 디폴트 action은 현재 페이지가 되기 때문이다.

 

GET 요청인 경우 질문수정 화면에 조회된 질문의 제목과 내용이 반영될 수 있도록 다음과 같이 폼을 생성해야 한다.

form = QuestionForm(instance=question)

 

폼 생성 시 이처럼 instance 값을 지정하면 폼의 속성 값이 instance의 값으로 채워진다.

따라서 질문을 수정하는 화면에서 제목과 내용이 채워진 채로 보일 것이다.

 

POST 요청인 경우 수정된 내용을 반영해야 하는 케이스이므로 다음처럼 폼을 생성해야 한다.

form = QuestionForm(request.POST, instance=question)

 

위 코드의 의미는 instance를 기준으로 QuestionForm을 생성하지만 request.POST의 값으로 덮어쓰라는 의미이다.

따라서 질문 수정화면에서 제목 또는 내용을 변경하여 POST 요청하면 변경된 내용이 QuestionForm에 저장될 것이다. 

 

그리고 질문의 수정일시는 다음처럼 현재일시로 저장되도록 했다.

question.modify_date = timezone.now() 

 


오류 표시

messages 모듈에 의해 발생되는 "수정권한이 없습니다"라는 오류가 표시될 수 있도록 질문 상세 화면 위쪽에 오류 영역을 추가한다.

 

[파일명: projects\mysite\templates\pybo\question_detail.html]

{% extends 'pybo/base.html' %}
{% block content %}
<div class="container my-3">
    <!-- message 표시 -->
    {% if messages %}
    <div class="alert alert-danger my-3" role="alert">
    {% for message in messages %}
        <strong>{{ message.tags }}</strong>
        <ul><li>{{ message.message }}</li></ul>
    {% endfor %}
    </div>
    {% endif %}
    <h2 class="border-bottom py-2">{{ question.subject }}</h2>
    (... 생략 ...)
(... 생략 ...)

 

수정은 로그인 한 사용자와 글 작성자가 동일한 경우에만 가능하기 때문에 이 오류가 표시될 일은 없을 것이다.

하지만 비 정상적인 방법으로 질문을 수정할 경우 오류를 보여주어야 하므로 필요한 부분이다.

 


질문 수정 확인

이제 로그인 사용자와 글쓴이가 같으면 질문 상세 화면에 <수정> 버튼이 보일 것이다.

 

 


질문 삭제

작성한 질문을 삭제하려면 질문 상세 화면에서 "삭제" 버튼을 클릭하여 삭제해야 한다.

 

질문 삭제 버튼

이번에는 작성한 글을 삭제할 수 있는 버튼을 다음처럼 추가한다.

 

[파일명: projects\mysite\templates\pybo\question_detail.html]

<!-- 질문 -->
(... 생략 ...)
<div class="my-3">
    {% if request.user == question.author %}
    <a href="{% url 'pybo:question_modify' question.id  %}"
       class="btn btn-sm btn-outline-secondary">수정</a>
    <a href="javascript:void(0)" class="delete btn btn-sm btn-outline-secondary"
       data-uri="{% url 'pybo:question_delete' question.id  %}">삭제</a>
    {% endif %}
</div>
(... 생략 ...)

 

<삭제> 버튼은 <수정> 버튼과 달리 href 속성값을 javascript:void(0)로 설정했다. 

href 속성값을 javascript:void(0)로 설정하면 해당 링크를 클릭해도 아무런 동작도 하지 않는다.

그리고 삭제를 실행할 URL을 얻기 위해 data-uri 속성을 추가하고, <삭제> 버튼이 눌리는 이벤트를 확인할 수 있도록 class 속성에 "delete" 항목을 추가했다.

data-uri 속성은 자바스크립트에서 클릭 이벤트 발생 시 this.dataset.uri와 같이 사용하여 그 값을 얻을 수 있다.

href에 삭제 URL을 직접 사용하지 않고 이러한 방식을 사용하는 이유는 삭제 버튼을 클릭했을 때 "정말로 삭제하시겠습니까?" 와 같은 확인창이 필요하기 때문이다.

 


자바스크립트

삭제 버튼을 눌렀을 때 확인창을 호출하기 위해서는 다음과 같은 자바스크립트 코드가 필요하다.

지금은 아래 코드를 추가하지 않는다.

 

<script type='text/javascript'>
const delete_elements = document.getElementsByClassName("delete");
Array.from(delete_elements).forEach(function(element) {
    element.addEventListener('click', function() {
        if(confirm("정말로 삭제하시겠습니까?")) {
            location.href = this.dataset.uri;
        };
    });
});
</script>

 

이 자바스크립트의 의미는 delete라는 클래스를 포함하는 컴포넌트(예:버튼이나 링크)를 클릭하면 "정말로 삭제하시겠습니까?" 라는 질문을 하고 "확인"을 선택했을 때 해당 컴포넌트의 data-uri 값으로 URL 호출을 하라는 의미이다.

"확인" 대신 "취소"를 선택하면 아무런 일도 발생하지 않을 것이다.

※ delete 클래스는 답변 삭제에도 사용된다.

따라서 이와 같은 스크립트를 추가하면 "삭제" 버튼을 클릭하고 "확인"을 선택하면 data-uri 속성에 해당하는  {% url 'pybo:question_delete' question.id %}이 호출될 것이다.

 


자바스크립트 블록

자바스크립트는 HTML 구조에서 다음과 같이 </body> 태그 바로 위에 삽입하는 것을 추천한다.

<html>
<head>
(... 생략 ...)
</head>
<body>
(... 생략 ...)
<!-- 이곳에 추가 -->
</body>
</html>

 

왜냐하면 이렇게 해야 화면 렌더링이 완료된 후에 자바스크립트가 실행되기 때문이다.

화면 렌더링이 완료되지 않은 상태에서 자바스크립트를 실행하면 화면의 값을 읽지 못하는 오류가 발생할 수도 있고 화면 로딩이 지연되는 문제가 발생할 수도 있다.

따라서 자바스크립트는 </body> 태그 바로 위에 삽입하는 것이 좋다.

 

자바스크립트를 </body> 태그 바로 위에 삽입하기 위해 다음처럼 base.html을 수정한다.

 

[파일명: projects\mysite\templates\base.html]

(... 생략 ...)
<!-- Bootstrap JS -->
<script src="{% static 'bootstrap.min.js' %}"></script>
<!-- 자바스크립트 Start -->
{% block script %}
{% endblock %}
<!-- 자바스크립트 End -->
</body>
</html>

 

base.html 을 상속하는 템플릿들에서 content 블록을 구현하게 했던 것과 마찬가지 방법으로 script 블록을 구현할 수 있도록 했다.

</body> 태그 바로 위에  {% block script %}{% endblock %} 블록을 추가했다.

이렇게 하면 이제 base.html을 상속하는 템플릿은 자바스크립트의 삽입 위치를 신경 쓸 필요 없이 스크립트 블록을 사용하여 자바스크립트를 작성하면 된다.

 

question_detail.html 하단에  {% block script %}{% endblock %} 블록을 다음처럼 추가하자.

 

[파일명: projects\mysite\templates\pybo\question_detail.html]

(... 생략 ...)
{% endblock %}
{% block script %}
<script type='text/javascript'>
const delete_elements = document.getElementsByClassName("delete");
Array.from(delete_elements).forEach(function(element) {
    element.addEventListener('click', function() {
        if(confirm("정말로 삭제하시겠습니까?")) {
            location.href = this.dataset.uri;
        };
    });
});
</script>
{% endblock %}

 

{% block script %}과 {% endblock %} 사이에 질문을 삭제할 수 있는 자바스크립트를 작성하였다.

 


urls.py

그리고 data-uri {% url 'pybo:question_delete' question.id %} URL이 추가되었으므로 pybo/urls.py에 다음처럼 URL 매핑 규칙을 추가한다.

 

[파일명: projects\mysite\pybo\urls.py]

(... 생략 ...)

urlpatterns = [
    (... 생략 ...)
    path('question/delete/<int:question_id>/', 
    	  views.question_delete, name='question_delete'),
]

 


views.py

위에서 정의한 views.question_delete 함수를 다음처럼 작성한다.

 

[파일명: projects\mysite\pybo\views.py]

(... 생략 ...)

@login_required(login_url='common:login')
def question_delete(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    if request.user != question.author:
        messages.error(request, '삭제권한이 없습니다')
        return redirect('pybo:detail', question_id=question.id)
    question.delete()
    return redirect('pybo:index')

 

question_delete 함수 역시 로그인이 필요하므로 @login_required 애너테이션을 적용하고 로그인한 사용자와 글쓴이가 동일한 경우에만 삭제할 수 있도록 했다.


질문 삭제 확인

질문을 작성한 사용자와 로그인한 사용자가 동일할 경우 다음처럼 상세조회 화면에 "삭제" 버튼이 노출될 것이다.

 

 


답변 수정

이번에는 답변 수정 기능을 추가하려고 한다.

질문 수정과 거의 비슷한 방법이지만 답변 수정은 답변 등록 템플릿이 따로 없으므로 답변 수정에 사용할 템플릿이 추가로 필요하다.

답변 등록은 질문 상세 화면 아래쪽에 텍스트 입력 창을 추가하여 만든 것이므로 질문 상세 템플릿을 답변 수정용으로 사용하는 데는 적합하지 않다. 

 

답변 수정 기능은 질문 수정과 크게 차이 나지 않으므로 간단히 설명할 예정이다.

 


답변 수정 버튼

답변 목록이 출력되는 부분에 답변 수정 버튼을 추가한다.

 

[파일명: projects\mysite\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">
            <div class="badge bg-light text-dark p-2 text-start">
                <div class="mb-2">{{ answer.author.username }}</div>
                <div>{{ answer.create_date }}</div>
            </div>
        </div>
        <div class="my-3">
            {% if request.user == answer.author %}
            <a href="{% url 'pybo:answer_modify' answer.id  %}" 
               class="btn btn-sm btn-outline-secondary">수정</a>
            {% endif %}
        </div>
    </div>
</div>
{% endfor %}
(... 생략 ...)

 


urls.py

{% url 'pybo:answer_modify' answer.id %} 가 추가되었으므로 pybo/urls.py 파일에 다음처럼 URL 매핑 규칙을 추가하자.

 

[파일명: projects\mysite\pybo\urls.py]

urlpatterns = [
    (... 생략 ...)
    path('answer/modify/<int:answer_id>/', views.answer_modify, name='answer_modify'),
]

 


views.py

위에서 정의한 views.answer_modify 함수를 다음처럼 작성한다.

 

[파일명: projects\mysite\pybo\views.py]

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

(... 생략 ...)

@login_required(login_url='common:login')
def answer_modify(request, answer_id):
    answer = get_object_or_404(Answer, pk=answer_id)
    if request.user != answer.author:
        messages.error(request, '수정권한이 없습니다')
        return redirect('pybo:detail', question_id=answer.question.id)
    if request.method == "POST":
        form = AnswerForm(request.POST, instance=answer)
        if form.is_valid():
            answer = form.save(commit=False)
            answer.modify_date = timezone.now()
            answer.save()
            return redirect('pybo:detail', question_id=answer.question.id)
    else:
        form = AnswerForm(instance=answer)
    context = {'answer': answer, 'form': form}
    return render(request, 'pybo/answer_form.html', context)

 

질문 수정과 동일하므로 별도의 설명은 하지 않으려고 한다.

 


답변 수정 폼

답변 수정을 위한 템플릿 answer_form.html 파일은 다음처럼 신규로 작성해야 한다.

 

[파일명: projects\mysite\templates\pybo\answer_form.html]

{% extends 'base.html' %}
{% block content %}
<!-- 답변 수정-->
<div class="container my-3">
    <form method="post">
        {% csrf_token %}
        {% include "form_errors.html" %}
        <div class="mb-3">
            <label for="content" class="form-label">답변내용</label>
            <textarea class="form-control" name="content" id="content"
                      rows="10">{{ form.content.value|default_if_none:'' }}</textarea>
        </div>
        <button type="submit" class="btn btn-primary">저장하기</button>
    </form>
</div>
{% endblock %}

 

답변 내용을 확인하고 수정할 수 있는 간단한 템플릿이다.

 


답변 수정 확인

답변 수정도 질문 수정과 마찬가지로 답변 등록 사용자와 로그인 사용자가 동일할 때만 <수정> 버튼이 나타난다.

 

 


답변 삭제

답변 삭제도 질문 삭제와 동일한 방법으로 진행된다.

 

답변 삭제 버튼

질문 상세 화면에서 답변을 삭제할 수 있는 버튼을 다음과 같이 추가한다.

 

[파일명: projects\mysite\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="my-3">
            {% if request.user == answer.author %}
            <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>
            {% endif %}
        </div>
    </div>
</div>
(... 생략 ...)

 

<수정> 버튼 옆에 <삭제> 버튼을 추가했다.

질문의 <삭제> 버튼과 마찬가지로 <삭제> 버튼에 delete 클래스를 적용했으므로 <삭제> 버튼을 누르면 data-uri 속성에 설정한 url이 실행될 것이다.

 


urls.py

{% url 'pybo:answer_delete' answer.id %}이 추가되었으므로 pybo/urls.py 파일에 URL 매핑 규칙을 추가한다.

 

[파일명: projects\mysite\pybo\urls.py]

(... 생략 ...)

urlpatterns = [
    (... 생략 ...)
    path('answer/delete/<int:answer_id>/', views.answer_delete, name='answer_delete'),
]

 


views.py

위에서 정의한 views.answer_delete 함수를 다음처럼 작성한다.

 

[파일명: projects\mysite\pybo\views.py]

(... 생략 ...)

@login_required(login_url='common:login')
def answer_delete(request, answer_id):
    answer = get_object_or_404(Answer, pk=answer_id)
    if request.user != answer.author:
        messages.error(request, '삭제권한이 없습니다')
    else:
        answer.delete()
    return redirect('pybo:detail', question_id=answer.question.id)

 


답변 삭제 확인

질문 상세 화면에서 답변을 작성한 사용자와 로그인한 사용자가 같으면 <삭제> 버튼이 나타날 것이다.

 

 


수정일시 표시하기

마지막으로 질문 상세 화면에서 수정일시를 확인할 수 있도록 템플릿을 수정해야 한다.

질문과 답변에는 이미 작성일시를 표시하고 있으므로 작성일시 바로 왼쪽에 수정일시를 추가한다.

 

[파일명: projects\mysite\templates\pybo\question_detail.html]

(... 생략 ...)
<!-- 질문 -->
<h2 class="border-bottom py-2">{{ question.subject }}</h2>
<div class="card my-3">
    <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 bg-light text-dark p-2 text-start mx-3">
                <div class="mb-2">modified at</div>
                <div>{{ question.modify_date }}</div>
            </div>
            {% endif %}
            <div class="badge bg-light text-dark p-2 text-start">
                <div class="mb-2">{{ question.author.username }}</div>
                <div>{{ question.create_date }}</div>
            </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="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 bg-light text-dark p-2 text-start mx-3">
                <div class="mb-2">modified at</div>
                <div>{{ answer.modify_date }}</div>
            </div>
            {% endif %}
            <div class="badge bg-light text-dark p-2 text-start">
                <div class="mb-2">{{ answer.author.username }}</div>
                <div>{{ answer.create_date }}</div>
            </div>
        </div>
        (... 생략 ...)
        </div>
    </div>
</div>
{% endfor %}
(... 생략 ...)

 

이제 질문이나 답변을 수정하면 다음처럼 수정일시가 표시된다

 

 


반응형