🧩

013 새 글 | 수정 | 삭제

1. urls.py 수정

url 패턴에 포스트 생성, 수정, 삭제 페이지 연결을 작성하겠습니다.
 
파일명 : post/urls.py
from django.urls import path from .views import * app_name = 'post' urlpatterns = [ path('', post_list, name='post_list'), path('new/', post_new, name='post_new'), path('edit/<int:pk>/', post_edit, name='post_edit'), path('delete/<int:pk>/', post_delete, name='post_delete'), ]
 
  • 새 포스트 작성 페이지는 /post/new/ 를 통해 post_new 로 연결됩니다.
  • 포스트 수정 페이지는 특정 포스트를 수정하는 것이기 때문에 키 값이 있어야 합니다. 따라서 <int:pk>를 URL에 넣습니다.
  • 포스트 삭제 페이지도 마찬가지로 특정 포스트의 키값을 받을 것입니다.
 

2. views.py

이전에 작성했던 것 아래에 함수를 추가할 것입니다. 포스트 생성, 수정, 삭제 기능의 함수들을 작성할 것입니다. 그리고 로그인 기능을 전에 구현을 해 놓았기 때문에 로그인 기능을 활용하도록 하겠습니다.
 

1. 새 포스트 작성 함수

@login_required 데코레이터를 써서 로그인 상태인 경우만 함수가 작동하게 합니다.
request 방식이 POST 방식일 때, PostForm이라는 폼을 만들어서 만약 폼 입력 값이 유효하다면 폼을 저장하는데, 유저 정보를 넣은 뒤 저장하기 위해 데이터베이스에 바로 저장되지 않도록 commit=False 옵션을 줍니다.
 
파일명 : post/views.py
@login_required def post_new(request): if request.method == 'POST': form = PostForm(request.POST, request.FILES) if form.is_valid(): post = form.save(commit=False) post.author = request.user post.save() #post.tag_save()
 
포스트를 저장합니다. 포스트의 태그도 저장할 것인데 태그 기능을 아직 구현 안 했기 때문에 주석 처리하겠습니다.
 
파일명 : post/views.py
def post_new(request): if request.method == 'POST': form = PostForm(request.POST, request.FILES) if form.is_valid(): post = form.save(commit=False) post.author = request.user post.save() # post.tag_save()
 
새 글이 등록되었습니다 라는 메세지를 띄울 겁니다.
 
파일명 : post/views.py
def post_new(request): if request.method == 'POST': form = PostForm(request.POST, request.FILES) if form.is_valid(): post = form.save(commit=False) #중복 방지 post.author = request.user post.save() # post.tag_save() messages.info(request, '새 글이 등록되었습니다')
 
post/post_list 페이지로 이동시킵니다.
 
파일명 : post/views.py
def post_new(request): if request.method == 'POST': form = PostForm(request.POST, request.FILES) if form.is_valid(): post = form.save(commit=False) #중복 방지 post.author = request.user post.save() # post.tag_save() messages.info(request, '새 글이 등록되었습니다') return redirect('post:post_list')
 
애초에 request가 잘못된 경우에는 다시 포스트 생성 페이지로 보내줍니다.
 
파일명 : post/views.py
@login_required def post_new(request): if request.method == 'POST': form = PostForm(request.POST, request.FILES) if form.is_valid(): post = form.save(commit=False) #중복 방지 post.author = request.user post.save() #post.tag_save() messages.info(request, '새 글이 등록되었습니다') return redirect('post:post_list') else: form = PostForm() return render(request, 'post/post_new.html', { 'form': form, })
 

2. 포스트 편집 함수

비슷한 방식으로 포스트 편집 함수도 만들 것입니다. 로그인 상태를 확인합니다.
 
파일명 : post/views.py
@login_required
 
requestpk 값을 받는 함수입니다. pk는 url 패턴에서 <int:pk>pk입니다. 두 곳의 이름이 반드시 같아야 매개변수로 받을 수 있습니다.
 
파일명 : post/views.py
@login_required def post_edit(request, pk):
 
포스트를 가져올 것입니다.
 
파일명 : post/views.py
@login_required def post_edit(request, pk): post = get_object_or_404(Post, pk=pk)
 
💡
get_object_or_404 함수는 모델에서 찾는 요소가 있다면 반환해주고, 없다면 404 에러를 발생시킵니다. objects.get의 경우 찾는 요소가 없다면 500번 서버에러가 발생하는데, 이는 사실 서버의 잘못이 아니라 사용자가 없는 요소를 찾은 것이기 때문에 500번 에러는 부적절합니다. 대신에 없는 페이지를 뜻하는 404 에러 메시지를 노출시켜야 사용자에게 없는 글을 찾으려고 시도했다는 것을 알릴 수 있습니다.
 
만약 포스트 작성자와 요청을 보낸 유저가 다르다면, 잘못된 접근이라고 경고 메세지가 나오도록 합니다. 그리고 post_list 페이지로 리다이렉트 시킵니다.
 
파일명 : post/views.py
@login_required def post_edit(request, pk): post = get_object_or_404(Post, pk=pk) if post.author != request.user: messages.warning(request, '잘못된 접근입니다') return redirect('post:post_list')
 
만약 POST 요청이라면, PostForm에 요청 정보를 넣습니다.
 
파일명 : post/views.py
@login_required def post_edit(request, pk): post = get_object_or_404(Post, pk=pk) if post.author != request.user: messages.warning(request, '잘못된 접근입니다') return redirect('post:post_list') if request.method == 'POST': form = PostForm(request.POST, request.FILES, instance=post)
 
폼이 유효한지 확인하고 폼을 저장하고 나중에 사용할 태그 기능을 적습니다.
 
파일명 : post/views.py
@login_required def post_edit(request, pk): post = get_object_or_404(Post, pk=pk) if post.author != request.user: messages.warning(request, '잘못된 접근입니다') return redirect('post:post_list') if request.method == 'POST': form = PostForm(request.POST, request.FILES, instance=post) if form.is_valid(): post = form.save() # post.tag_set.clear() # post.tag_save()
 
그리고 수정 완료 메세지를 보낸 후, 포스트 리스트로 연결합니다.
 
파일명 : post/views.py
@login_required def post_edit(request, pk): post = get_object_or_404(Post, pk=pk) if post.author != request.user: messages.warning(request, '잘못된 접근입니다') return redirect('post:post_list') if request.method == 'POST': form = PostForm(request.POST, request.FILES, instance=post) if form.is_valid(): post = form.save() #post.tag_set.clear() #post.tag_save() messages.success(request, '수정완료') return redirect('post:post_list')
 
만약 POST 요청이 아니라면, 폼을 만들고 다시 포스트 편집 페이지로 보냅니다. 그리고 post와 form을 전달합니다.
 
파일명 : post/views.py
@login_required def post_edit(request, pk): post = get_object_or_404(Post, pk=pk) if post.author != request.user: messages.warning(request, '잘못된 접근입니다') return redirect('post:post_list') if request.method == 'POST': form = PostForm(request.POST, request.FILES, instance=post) if form.is_valid(): post = form.save() #post.tag_set.clear() #post.tag_save() messages.success(request, '수정완료') return redirect('post:post_list') else: form = PostForm(instance=post) return render(request, 'post/post_edit.html', { 'post': post, 'form': form, })
 

3. 포스트 삭제 함수

로그인 상태를 확인합니다.
 
파일명 : post/views.py
@login_required
 
requestpk를 받습니다.
 
파일명 : post/views.py
@login_required def post_delete(request, pk):
 
포스트를 가져오고, 없다면 404 에러를 냅니다. pk 값이 같은 post를 찾아서 가져옵니다.
 
파일명 : post/views.py
@login_required def post_delete(request, pk): post = get_object_or_404(Post, pk=pk)
 
만약 포스트 작성자와 요청을 보낸 유저가 다르거나, POST 요청이 아니라면, 잘못된 접근이라고 경고 메세지가 나오도록 합니다.
 
파일명 : post/views.py
@login_required def post_delete(request, pk): post = get_object_or_404(Post, pk=pk) if post.author != request.user or request.method != 'POST': #URL을 통한 DB접근을 막는다 messages.warning(request, '잘못된 접근입니다.')
 
만약 그렇지 않다면 포스트를 삭제합니다.
 
파일명 : post/views.py
@login_required def post_delete(request, pk): post = get_object_or_404(Post, pk=pk) if post.author != request.user or request.method != 'POST': messages.warning(request, '잘못된 접근입니다.') else: post.delete()
 
삭제 완료 메세지를 보냅니다.
 
파일명 : post/views.py
@login_required def post_delete(request, pk): post = get_object_or_404(Post, pk=pk) if post.author != request.user or request.method != 'POST': messages.warning(request, '잘못된 접근입니다.') else: post.delete() # messages.success(request, '삭제완료')
 
삭제 성공이든 실패든 post_list 페이지로 리다이렉트 시킵니다.
 
파일명 : post/views.py
@login_required def post_delete(request, pk): post = get_object_or_404(Post, pk=pk) if post.author != request.user or request.method != 'POST': messages.warning(request, '잘못된 접근입니다.') else: post.delete() # messages.success(request, '삭제완료') return redirect('post:post_list')
 
데코레이터를 썼기 때문에 import를 파일 상단에 추가합니다.
 
파일명 : post/views.py
from django.contrib.auth.decorators import login_required
 
PostForm을 작성할 것이기 때문에 미리 import 합니다.
 
파일명 : post/views.py
from .forms import PostForm
 
완성된 코드는 아래와 같습니다.
 
파일명 : post/views.py
from django.contrib.auth import get_user_model from django.shortcuts import get_object_or_404, redirect, render from django.contrib.auth.decorators import login_required from .models import Post from .forms import PostForm def post_list(request): post_list = Post.objects.all() if request.user.is_authenticated: username = request.user user = get_object_or_404(get_user_model(), username=username) user_profile = user.profile return render(request, 'post/post_list.html', { 'user_profile': user_profile, 'posts': post_list, }) else: return render(request, 'post/post_list.html', { 'posts': post_list, }) @login_required def post_new(request): if request.method == 'POST': form = PostForm(request.POST, request.FILES) if form.is_valid(): post = form.save(commit=False) #중복 방지 post.author = request.user post.save() #post.tag_save() messages.info(request, '새 글이 등록되었습니다') return redirect('post:post_list') else: form = PostForm() return render(request, 'post/post_new.html', { 'form': form, }) @login_required def post_edit(request, pk): post = get_object_or_404(Post, pk=pk) if post.author != request.user: messages.warning(request, '잘못된 접근입니다') return redirect('post:post_list') if request.method == 'POST': form = PostForm(request.POST, request.FILES, instance=post) if form.is_valid(): post = form.save() #post.tag_set.clear() #post.tag_save() messages.success(request, '수정완료') return redirect('post:post_list') else: form = PostForm(instance=post) return render(request, 'post/post_edit.html', { 'post': post, 'form': form, }) @login_required def post_delete(request, pk): post = get_object_or_404(Post, pk=pk) if post.author != request.user or request.method != 'POST': messages.warning(request, '잘못된 접근입니다.') else: post.delete() # messages.success(request, '삭제완료') return redirect('post:post_list')
 

3. PostForm

form 태그 만들 때 사용했던 PostForm을 만들도록 하겠습니다. post/forms.py 파일을 만듭니다.
notion imagenotion image
notion imagenotion image
 
forms.py 파일 내용을 작성합니다. django.forms를 import 하고 Post 모델을 불러옵니다.
 
파일명 : post/forms.py
from django import forms from .models import Post
 
위에서 가져온 forms에서 ModelForm을 불러옵니다.
 
파일명 : post/forms.py
from django import forms from .models import Post class PostForm(forms.ModelForm):
 
게시글의 이미지를 저장할 이미지 필드를 사용할 것입니다.
 
파일명 : post/forms.py
from django import forms from .models import Post class PostForm(forms.ModelForm): photo = forms.ImageField(label='')
 
게시글 내용을 입력하는 필드를 만듭니다. 일반적인 CharField는 한 줄 짜리 입력창인 input 태그로 렌더링 되기 때문에 widget을 forms.Textarea로 설정합니다. 그러면 textarea 태그로 렌더링 됩니다.
 
파일명 : post/forms.py
from django import forms from .models import Post class PostForm(forms.ModelForm): photo = forms.ImageField(label='', required=False) content = forms.CharField(label='', widget=forms.Textarea(attrs={ 'class': 'post-new-content', 'rows': 5, 'cols': 50, 'placeholder': '140자 까지 등록 가능합니다' }))
 
메타 클래스로 폼에서 보여줄 필드들을 정해줍니다. PostForm을 HTML에서 보여줄 때 photo 필드와 content 필드만 보여줍니다.
 
파일명 : post/forms.py
from django import forms from .models import Post class PostForm(forms.ModelForm): photo = forms.ImageField(label='', required=False) content = forms.CharField(label='', widget=forms.Textarea(attrs={ 'class': 'post-new-content', 'rows': 5, 'cols': 50, 'placeholder': '140자 까지 등록 가능합니다' })) class Meta: model = Post fields = ['photo', 'content']
 
저번에 post/views.pypost_new라는 함수형 뷰를 만들었습니다.
notion imagenotion image
 

4. 포스트 작성

post_new 뷰 함수에서 post_new.html을 렌더링 했습니다. post/templates/post/post_new.html 파일을 작성해 보도록 하겠습니다.
notion imagenotion image
notion imagenotion image
 
같은 위치에 있는 layout.html 을 확장합니다.
 
파일명 : post/templates/post/post_new.html
{% extends "post/layout.html" %}
 
static 파일을 불러옵니다.
 
파일명 : post/templates/post/post_new.html
{% extends "post/layout.html" %} {% load static %}
 
block 영역을 만듭니다.
 
파일명 : post/templates/post/post_new.html
{% extends "post/layout.html" %} {% load static %} {% block head %} {% endblock %} {% block content %} {% endblock %}
 
head 부분에는 css가 들어갑니다.
 
파일명 : post/templates/post/post_new.html
{% extends "post/layout.html" %} {% load static %} {% block head %} <link rel="stylesheet" href="{% static 'css/new_post.css' %}"> {% endblock %} {% block content %} {% endblock %}
 
content 부분에는 새 글이 작성될 공간을 HTML 태그와 템플릿 태그를 활용해서 작성합니다.
 
파일명 : post/templates/post/post_new.html
{% extends "post/layout.html" %} {% load static %} {% block head %} <link rel="stylesheet" href="{% static 'css/new_post.css' %}"> {% endblock %} <!--새글이 작성될 공간--> {% block content %} <div id="main_container"> <div class="post_form_container"> <form action="" class="post_form" method="post" enctype="multipart/form-data"> <div class="title"> New Post </div> <div class="preview"> <label for="id_photo"> <div class="upload"> <div class="post_btn"> <div class="plus_icon"> <span></span> <span></span> </div> <p>포스트 이미지 추가</p> <canvas id="imageCanvas"></canvas> </div> </div> </label> </div> {% csrf_token %} <!-- form 을 보낼 때 보안에 도움이 되는 토큰을 추가 --> {{ form.as_p }} <!-- p 태그로 이루어진 form 형식 만듬 --> <input type="submit" class="submit_btn" value="저장"> </form> </div> </div> {% endblock %}
 
https://*.run.goorm.io/post/new/ URL로 접속하면 html 파일이 잘 적용된 것을 확인하실 수 있습니다.
notion imagenotion image
 

5. 이미지 미리보기

화면에서 포스트 이미지 추가를 할 때 파일 선택을 마쳤으면, 업로드한 이미지 파일을 업로드하기 전에 미리 볼 수 있으면 좋을것 같습니다. 그 기능은 JavaScript(자바스크립트)로 구현할 것입니다.
notion imagenotion image
 
canvas를 활용해서 미리 보기 기능을 구현할 것입니다. 나중에 더 다양하게 사용하실 수 있습니다.
html Element를 하나 가져오겠습니다. id_photo라고 하는 id값을 가진 Element를 가지고 올것입니다. form.as_p로 렌더링된 이미지 필드의 input 태그 입니다.
💡
장고 Form을 HTML에 렌더링 시, 각 필드의 id 값은 id_필드명 형식으로 자동으로 추가됩니다. 그래서 PostFormphoto 필드는 id_photo라는 id 값을 가진 input 태그가 됩니다.
 
post_new.html아래에 script 태그를 통해 자바스크립트를 추가하도록 하겠습니다.
 
파일명 : post/templates/post/post_new.html
<script> let fileInput = document.querySelector("#id_photo") fileInput.addEventListener('change', handleImage); let canvas = document.getElementById('imageCanvas') let ctx = canvas.getContext('2d'); function handleImage(e){ let reader = new FileReader(); reader.onload = function(event){ let img = new Image(); img.onload = function(){ canvas.width = 300; canvas.height = 300; ctx.drawImage(img,0,0,300,300); <!--실제 이미지를 넣는다--> }; img.src = event.target.result; <!--이미지 출력--> }; reader.readAsDataURL(e.target.files[0]); } </script>
 
이렇게 해서 이미지 미리보기 기능을 만들었습니다.
notion imagenotion image
 

6. 포스트 수정

포스팅을 수정하는 기능을 만들어 보도록 하겠습니다. 포스터의 고유한 키값, PRIMARY KEY를 확인해 그 글만 수정할 수 있도록 할 것입니다. PRIMARY KEY는 admin페이지의 Post 리스트를 보면 고유한 id 값이 나오는 것을 확인하실 수 있습니다. 글을 새로 만들면 1 씩 증가합니다.
닉네임 옆에 해당 포스트의 PRIMARY KEY를 출력해보도록 하겠습니다. post_list.html 에서 작성자 닉네임 옆에 추가합니다.
 
파일명 : post/templates/post/post_list.html
<div class="user_name"> <div class="nick_name m_text">{{ post.author.profile.nickname }} {{ post.id }}</div> <div class="country s_text">Seoul, South Korea</div> </div>
 
notion imagenotion image
 
닉네임 옆에 PRIMARY KEY가 표시된 것을 확인하실 수 있습니다. 게시물 마다 PRIMARY KEY 숫자가 다른것을 확인하실 수 있습니다.
이제 PRIMARY KEY를 이용해 이 글의 내용만 수정하도록 만들겠습니다. post/views.py에 가면 post_edit 함수가 있는데 post_edit.html 이라는 템플릿을 렌더링합니다. post/templates/post아래 post_edit.html 파일을 생성하도록 하겠습니다.
 
파일명 : post/templates/post/post_edit.html
{% extends "post/layout.html" %} {% load static %} {% block head %} <link rel="stylesheet" href="{% static 'css/new_post.css' %}"> {% endblock %} {% block content %} <div id="main_container"> <div class="post_form_container"> <form action="#" class="post_form" method="post" enctype="multipart/form-data"> <div class="title"> Edit Post </div> <!--새글 작성이랑 비슷하지만 수정은 이미 올라가 있는 이미지 불러오고 교체--> <div class="preview"> <div class="upload"> <div class="post_btn"> {% if post %} <p><img id="img_id" src="{{ post.photo.url }}" style="width: 300px; height: 300px; object-fit: cover" ></p> {% endif %} </div> </div> </div> {% csrf_token %} {{ form.as_p }}<!--form태그 자동 생성--> <input class="submit_btn" type="submit" value="수정완료"> </form> </div> </div> {% endblock %}
 
url을 생성할때 post/edit/<int:pk>/로 만들었습니다. url에 들어가겠습니다.
notion imagenotion image
 
post_list.html에서 게시글 내용이 보이도록 아직 수정을 안했기 때문에 수정되었는지 메인 페이지에서 확인이 불가능합니다. admin에 들어가서 바뀌었는지 확인해 보겠습니다.
notion imagenotion image
변경이 된 것을 확인하였습니다. 수정할 때 사진이 바뀌면 미리보기에 표시가 되었으면 좋겠는데 아직 표시가 되지 않습니다. 왜냐하면 아직 자바스크립트를 작성하지 않았기 때문입니다. post_edit.html에서 자바스크립트 부분을 작성하도록 하겠습니다.
 
파일명 : post/templates/post/post_edit.html
<script> const photo = document.querySelector('#img_id'); function upload_img(input) { var reader = new FileReader(); if (input.files && input.files[0]){ reader.onload = function (e) { photo.setAttribute('src',e.target.result); } reader.readAsDataURL(input.files[0]); } } let idPhoto = document.querySelector('#id_photo'); idPhoto.addEventListener('change',function(){ upload_img(this); }); </script>
notion imagenotion image
 
미리보기가 잘 들어가는 것을 확인하실 수 있습니다.
 

7. 포스트 삭제

포스트를 삭제하는 기능을 작성해보도록 하겠습니다. 삭제 버튼은 임시로 post_list 페이지에서 각 포스트의 작성자의 프로필 옆에 만들겠습니다. 지금은 삭제가 잘 되는지 기능만 보기 위함입니다. 뒷 과정에서 스타일링할 예정입니다.
post_list.html에서 div.user_container 태그 내부 마지막 부분에 추가합니다.
 
파일명 : post/templates/post/post_list.html
<div class="user_container"> <div class="profile_img"> <!--포스트 저자의 프로필 사진이 있다면--> {% if post.author.profile.picture %} <!-- 이미지 주소는 포스트 저자 프로필 사진의 URL입니다. --> <img src="{{ post.author.profile.picture.url }}" alt="프로필이미지"> <!--프로필 사진이 없다면 --> {% else %} <!-- 기본 이미지를 가져옵니다.--> <img src="{% static 'imgs/thumb.jpeg'%}" alt="프로필이미지"> {% endif %} </div> <div class="user_name"> <div class="nick_name m_text"> <!--포스트 저자 프로필의 닉네임--> {{ post.author.profile.nickname }} {{ post.pk }} </div> <div class="country s_text">Seoul, South Korea</div> </div> <div> <form action="{% url 'post:post_delete' post.pk %}" method="post"> {% csrf_token %} <input type="submit" value="삭제"> </form> </div> </div>
 
notion imagenotion image
 
삭제 버튼을 누르면 게시글이 잘 삭제되는 것을 볼 수 있습니다.