본문 바로가기

[Python] django 게시판 작성된 글 수정 및 삭제 기능 구현

액트 2022. 12. 1.
반응형

기존 게시판에 작성된 글을 수정 및 삭제 기능, 수정 일시 표시 등의 편집 기능을 추가해 보겠습니다.

수정은 질문 글 수정, 답변 글 수정

삭제는 질문 글 삭제, 답변 글 삭제

이렇게 구현할 예정입니다.


수정 일시

먼저 질문이나 답변이 언제 수정되었는지 확인할 수 있도록 Question과 Answer 모델에 수정 일시를 표시하는 modify_date 속성을 데이터베이스에 추가합니다. 데이터베이스에 컬럼을 추가하는 방법은 models.py 파일을 수정하는 것입니다.

mysite\pybo\models.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)		# 추가!!


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

null=True 는 데이터베이스에서 modify_date 칼럼에 null을 허용한다는 의미입니다.

blank=True 는 form.is_valid()를 통한 데이터 검증 시 값이 없어도 된다는 의미입니다. 

즉, 수정 데이터는 수정할 시에만 생성되는 데이터이므로 빈 값을 허용하였습니다.

모델이 변경되었으므로 makemigrations 명령을 수행합니다.

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

다음 migrate 명령어를 수행합니다.

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

1. 질문 수정

먼저 시나리오를 그리면 아래와 같습니다.

본인이 작성한 질문을 수정하기 위해서 ①"수정" 버튼을 클릭하여 수정 화면 페이지로 전환합니다.

페이지 전환에 앞서 ②로그인한 사용자와 글쓴이가 동일한 경우에만 수정 버튼이 활성화 되어야 합니다.

③수정 화면은 브라우저 화면 편집이므로 template 파일을 수정하면 됩니다.

"수정" 버튼과 수정 페이지를 만들었으면 ④URL 매핑을 통해 urls.py을 수정합니다.

수정 페이지로 넘어와서 글을 수정하고 ⑤"저장하기" 버튼을 클릭하여 저장합니다. 

마지막으로 잘 수정되었는지 ⑥확인합니다.

여기까지가 하나의 질문 수정 시나리오입니다. 이제 구현해 보도록 하겠습니다.

 

1-1) 수정 버튼 구현

{% extends 'base.html' %}
{% block content %}
<div class="container my-3">
    <!-- 질문 -->
    <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 %} 구문을 통해 로그인한 사용자와 글쓴이가 동일한 경우에만 노출되도록 했습니다. 

 

1-2) URL 매핑

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

mysite\pybo\urls.py 파일을 다음과 같이 수정합니다.

from django.urls import path
from . import views

app_name = 'pybo'

urlpatterns = [
    path('', views.index, name='index'),
    path('<int:question_id>/', views.detail, name='detail'),
    path('answer/create/<int:question_id>/', views.answer_create, name='answer_create'),
    path('question/create/', views.question_create, name='question_create'),
    #아래 추가!
    path('question/modify/<int:question_id>/', views.question_modify, name='question_modify'),
]

 

1-3) views.py 수정

urls.py 파일에 정의했던 views.question_modify 함수를 다음과 같이 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)가 다를 경우에는 "수정 권한이 없습니다"라는 오류를 발생시킵니다. 이 오류 메세지를 발생하기 위해 상단에 message 모듈을 추가했습니다.

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

위 구문은 instance를 기준으로 QuestionForm을 생성하지만 request.POST의 값으로 덮어쓰라는 의미입니다. 따라서 질문 문 수정화면에서 제목 또는 내용을 변경하여 POST 요청하면 변경된 내용이 QuestionForm에 저장될 것입니다.

question.modify_date = timezone.now() #수정일시 저장

위 구문은 질문의 수정 일시를 저장하는 구문이니다.

 

url 매핑 규칙에 의해

path('question/modify/<int:question_id>/', views.question_modify, name='question_modify'),

질문 상세 화면에서 "수정" 버튼을 클릭하면 http://localhost:8000/pybo/question/modify/글번호/ 페이지가 GET 방식으로 호출되어 질문 수정화면이 보여집니다. 수정 화면은 질문 등록시 사용했던 pybo/question_form.html 입니다.

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

GET 요청인 경우 질문 수정 화면에 조회된 질문의 제목과 내용이 반영됩니다.

else:
    form = QuestionForm(instance=question)

폼 생성시 위 처럼 instance 값을 지정하면 폼의 속성 값이 instance의 값으로 채워집니다. 따라서 질문을 수정하는 화면에서 제목과 내용이 채워진 것으로 보입니다.

 

1-4) 오류 표시

views.py 파일에서 정의한

messages.error(request, '수정 권한이 없습니다')

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

mysite\templates\pybo\question_detail.html 파일을 다음과 같이 수정합니다.

{% extends 'base.html' %}
{% block content %}
<div class="container my-3">
    <!-- message 표시 추가 시작 -->
    {% if messages %}
    <div class="alert alert-danger my-3" role="alert">
        {% for message in messaages %}
            <strong>{{ message.tags }}</strong>
            <ul><li>{{ message.messages }}</li></ul>
        {% endfor %}
    </div>
    {% endif %}
    <!-- message 표시 추가 끝 -->
    <!-- 질문 -->
    <h2 class="border-bottom py-2">{{ question.subject }}</h2>

 

1-5) 질문 수정 확인

잘 동작하는지 테스트합니다.

 

2. 질문 삭제

시나리오를 그리면 작성한 질문 글을 삭제하고자 삭제 버튼을 클릭합니다. "정말 삭제하시겠습니까?" 확인창이 발생합니다. "예"를 클릭하면 삭제가 진행됩니다.

삭제도 수정과 마찬가지로 작성한 글쓴이과 로그인한 사용자가 같아야 합니다.

2-1) 질문 삭제 버튼 구현

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>
</div>

수정 버튼 만든 곳 아래에 삭제 버튼을 만듭니다.

삭제 버튼을 클릭했을 때 바로 삭제하는 것이 아닌 "정말 삭제하시겠습니까?"와 같은 확인 창을 팝업합니다.

이 이유로 href 속성값에 javascript:void(0) 으로 설정한 것입니다. 이렇게 설정하면 해당 링크를 클릭해도 아무런 동작도 하지 않습니다. 그리고 삭제를 실행할 URL을 얻기 위해 data-uri 속성을 추가하고, 삭제 버튼일 눌리는 이벤트를 확인할 수 있도록 class 속성에 delelte 항목을 추가했습니다.

2-2) "정말 삭제하시겠습니까" 팝업창 띄우기

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

자바스크립트 코드는 화면 렌더링이 완료된 이후에 자바스크립트가 실행되어야 하기 때문에 body 태그가 끝나는 </body> 태그 바로 위에 작성합니다. 만약 화면 렌더링이 완료되지 않은 상태에서 자바스크립트를 실행하면 화면의 값을 읽지 못하는 오류가 발생할 수도 있고 화면 로딩이 지연될 수도 있습니다.

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

    <script src="{% static 'bootstrap.min.js' %}"></script>
    <!-- 자바스크립트 start -->
    {% block script %}
    {% endblock %}
    <!-- 자바스크립트 End -->
</body>
</html>

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

이제 mysite\templates\pybo\question_detail.html 파일 하반에 다음과 같이 자바스크립트 구문을 추가합니다.

{% 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 %}

 

2-3) urls.py 수정

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

삭제 버튼에서 정의한 url 에 대응하는 규칙을 url 맵핑 규칙에 추가합니다.

path('question/delete/<int:question_id>/', views.question_delete, name='question_delete'),

삭제 버튼을 클릭하면 "정말 삭제하시겠습니까?" 팝업창이 발생합니다. 팝업창에서 예를 클릭하면 URL 매핑 규칙의 의해 question_delete 함수가 호출됩니다.

 

2-4) views.py 수정

위에서 정의한 question_delete 함수를 mystite\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')

 

2-5) 질문 삭제 확인

작성된 글을 삭제하여 확인합니다.


반응형

3. 답변 수정

답변 수정도 질문 수정과 비슷한 방법입니다. 다만 답변 수정에는 답변 등록 템플릿이 따로 없으므로 답변 등록 템플릿을 추가할 예정입니다. 

3-1) 답변 수정 버튼

답변 목록 화면에 수정 버튼을 추가합니다.

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>

 

3-2) urls.py 수정

위에서 정의한 url answer_modify을 URL 매핑 규칙에 추가합니다.

path('answer/modify/<int:answer_id>/', views.answer_modify, name='answer_modify'),

 

3-3) views.py 수정

위에서 정의한 views.answer_modify 함수를 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)

 

3-4) 답변 수정 폼

답변 수정을 위한 템플릿 answer_form.html 파일을 다음과 같이 신규로 작성합니다.

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 %}

 

3-5) 답변 수정 확인

 

4. 답변 삭제

답변 삭제 기능 또한 질문 삭제와 같습니다.

4-1) 답변 삭제 버튼

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>
            <!-- 아래 추가!! -->
            <a href ="#" class="delete btn btn-sm btn-outline-secondary"
            data-uri="{% url 'pybo:answer_delete' answer.id %}">삭제</a>
            <!-- 아래 추가 끝-->
            {% endif %}
        </div>
    </div>
</div>
{% endfor %}

수정 버튼 옆에 삭제 버튼을 추가했습니다. 

 

4-2) urls.py

{% url 'pybo:answer_delete' answer.id %}

위 구문에 대한 url 매핑 규칙을 추가합니다.

path('answer/delete/<int:answer_id>/', views.answer_delete, name='answer_delete'),

 

4-3) views.py

위에서 정의한 views.answer_delete 함수를 정의합니다.

@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)

 

4-4) 삭제 확인

 

5. 수정일시 표시

질문 상세 화면에서 수정한 날짜와 시간을 표시합니다. 현재 표시되는 작성 일시 옆에 수정 일시를 표시합니다.

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 class="my-3">
<!-- 답변 -->
<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>

반응형

댓글