1. 다음 url이 실제 작동하도록 해주세요.
1.1 'blog/' : 블로그 글 목록
1.2 'blog/<int:pk>/' : 블로그 상세 글 읽기
1.3 'blog/create/' : 블로그 글 작성 - 로그인한 사용자
1.4 'blog/update/<int:pk>/' : 블로그 글 업데이트(수정하기) - 내 글인 경우
1.5 'blog/delete/<int:pk>/' : 블로그 글 삭제 - 내 글인 경우
###################################
앱이름: blog views 함수이름 html 파일이름 비고
'blog/' blog_list blog_list.html
'blog/<int:pk>/' blog_details blog_details.html
'blog/create/' blog_create create.html
'blog/update/<int:pk>/' blog_update update.html
'blog/delete/<int:pk>/' blog_delete delete.html
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=100)
contents = models.TextField()
main_image = models.ImageField(upload_to="blog/%Y/%m/%d/", blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
데이터베이스와의 상호작용을 관리하는 models.py
의 코드이다. VARCHAR
타입
title
: 이라는 최대 길이가 100글자인 문자열 필드를 설정contents
: 텍스트 데이터 저장 필드main_image
: 이미지 파일을 저장하는 필드. upload_to
: 업로드된 이미지가 저장될 경로settings.py
의 MEDIA_ROOT 설정에 따름created_at
: 레코드가 생성된 시간을 저장하는 필드. 레코드 생성 시 현재 시간을 자동으로 저장(1번만 작동)updated_at
: 레코드가 마지막으로 수정된 시간을 저장하는 필드. auto_now=True
: 레코드가 저장될 때마다 현재 시간을 자동으로 저장__str__
: 모델의 문자열 표현을 반환하는 메서드이미지의 이름과, 경로는 성능에 있어서 중요하다.
python manage.py makemigrations
python manage.py migrate
from django.contrib import admin
from .models import Post
admin.site.register(Post)
관리자 사이트에서 models.py
에 선언한 Post
모델을 관리할 수 있도록 설정하는 코드이다.
python manage.py createsuperuser
leehojun
leehojun@gmail.com
이호준1234!
media
: 사용자가 업로드한 이미지static
: 우리가 사용할 이미지# settings.py
STATIC_URL = "static/" # 이 URL로 들어오면
STATICFILES_DIRS = [BASE_DIR / "static"] # 여기서 처리해주겠다!
MEDIA_URL = "/media/" # 이 URL로 들어오면
MEDIA_ROOT = BASE_DIR / "media" # 여기서 처리해주겠다!
BASE_DIR
는 루트 디렉토리이다.
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path("admin/", admin.site.urls),
path("blog/", include("blog.urls")),
]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
settings.MEDIA_URL
에 정의된 URL 경로로 접속하면 settings.MEDIA_ROOT
에 지정된 경로에서 파일을 찾아 서비스할 수 있다.models.py
에서 정의한 Post
모델의 모든 객체를 데이터베이스에서 조회하며, db
라는 이름의 context
변수에 담아 템플릿으로 전달한다.
from django.shortcuts import render
from .models import Post
def blog_list(request):
db = Post.objects.all()
context = {"db": db}
return render(request, "blog/blog_list.html", context)
def blog_details(request, pk):
db = Post.objects.get(pk=pk)
context = {"db": db}
return render(request, "blog/blog_details.html", context)
blog_details
뷰의 경우에는 pk
라는 매개변수를 받아 Post
모델에서 해당 ID
를 가진 객체를 조회한다. 이를 db
라는 이름의 context
변수에 담아 템플릿으로 전달한다.
<h1>게시판</h1>
<form action="" method="get">
<input type="search" name="q">
<button type="submit">검색</button>
</form>
<ul>
{% for post in db %}
<li>
<a href="{% url 'blog_details' post.id %}">{{ post.title }}</a>
<p>{{ post.contents }}</p>
</li>
{% endfor %}
</ul>
db
라는 context
변수를 사용하여 각 포스트의 제목과 내용을 리스트로 출력한다. 각 제목은 해당 포스트의 세부 페이지로의 링크가 된다.
{% url 'blog_details' post.id %}
: blog_details
URL로 이동한다.<h1>게시판</h1>
<p>{{db.title}}</p>
<p>{{db.contents}}</p>
<p>{{db.created_at}}</p>
<p>{{db.updated_at}}</p>
<p>{{db.id}}</p>
{% if db.main_image %}
<img src="{{ db.main_image.url }}" alt="">
{% endif %}
<a href="{% url 'blog_list' %}">뒤로가기</a>
db
라는 context
변수를 사용하여 선택된 포스트의 세부 정보를 출력한다. 포스트의 제목, 내용, 생성 시간, 수정 시간, ID를 출력하고, 메인 이미지가 있으면 이미지를 출력한다. "뒤로가기" 링크를 클릭하면 포스트 리스트 페이지로 돌아간다.
def blog_list(request):
if request.GET.get("q"):
db = Post.objects.filter(
Q(title__icontains=request.GET.get("q"))
| Q(contents__icontains=request.GET.get("q"))
).distinct()
# sqlite3에서는 대소문자 구분이 안됩니다. 나중에 배울 postgresql에서는 대소문자 구분이 됩니다.
# namefield__icontains는 대소문자를 구분하지 않고
# namefield__contains는 대소문자를 구분합니다.
else:
db = Post.objects.all()
context = {"db": db}
return render(request, "blog/blog_list.html", context)
request.GET.get("q")
: 사용자가 입력한 검색어를 가져온다. GET 메서드의 쿼리 파라미터에서 "q"라는 이름의 값을 가져오는 코드이며 검색어가 입력되지 않았으면 None을 반환한다.
Post.objects.filter(...)
: 검색어가 포함된 포스트를 데이터베이스에서 조회한다. filter
메서드는 주어진 조건에 맞는 객체를 조회하는 메서드이다.
Q(title__icontains=request.GET.get("q")) | Q(contents__icontains=request.GET.get("q"))
:
검색어가 제목이나 내용에 포함된 경우를 조회하는 조건이다. Q
객체는 복잡한 조회 조건을 표현할 수 있게 해주며, |
연산자는 OR
연산을 의미한다. __icontains
는 대소문자를 구분하지 않는 포함 관계를 검사하는 필드 조회이다.
dinstinct()
: 중복된 결과를 제거한다. 같은 포스트가 제목과 내용 양쪽에 검색어가 포함되어 두 번 조회되는 경우를 방지하기 위함이다.
else: db = Post.objects.all()
: 검색어가 입력되지 않았으면 모든 포스트를 조회한다.
from django.urls import path
from . import views
urlpatterns = [
path("", views.blog_list, name="blog_list"),
path("<int:pk>/", views.blog_details, name="blog_details"),
path("create/", views.blog_create, name="blog_create"),
path("update/<int:pk>/", views.blog_update, name="blog_update"),
path("delete/<int:pk>/", views.blog_delete, name="blog_delete"),
]
def blog_create(request):
form = PostForm()
'''
이렇게 생성된 form은 자동으로 form을 만들어주는 기능을 가지고 있습니다.
이렇게 안하면 일일이 form을 하나씩 만들어야 합니다. 이해하긴 일일이 만드는 것이 더 좋을 수도 있습니다.
'''
context = {"form": form}
return render(request, "blog/blog_create.html", context)
def blog_update(request, pk):
pass
def blog_delete(request, pk):
pass
해당 파일에서는 form
데이터를 처리하는 데 사용하는 Form
클래스를 정의한다. Django에서는 다양한 기능을 제공한다.
폼 렌더링
: HTML 폼을 자동으로 생성하고 렌더링하는 기능을 제공한다. 데이터 검증
: Django Form은 데이터의 유효성을 검사하는 기능을 제공한다. 보안
: Cross-Site Request Forgery (CSRF) 공격 등을 방어하는 기능을 제공한다. form
에 자동으로 CSRF
토큰을 포함하여, CSRF
공격을 방어한다.모델 연동
: Django 모델과 연동되어, form
데이터를 직접 데이터베이스에 저장하거나 조회하는 기능을 제공한다.from django import forms
class PostForm(forms.Form): # PostForm은 여러분이 원하는 이름으로 바꿔도 됩니다. forms.Form은 기본 form입니다. 이는 추후 forms.ModelForm로 바뀌어야 합니다.
title = forms.CharField()
contents = forms.CharField()
블로그 포스트 생성 폼을 HTML로 렌더링하는 코드이다.
<form action="{% url 'blog_create' %}" method="post">
{% csrf_token %}
{% comment %}
{{ form }}
{{ form.as_p }}
{{ form.as_div }}
<ul>
{{ form.as_ul }}
</ul>
<table>
{{ form.as_table }}
</table>
{{ form.title }}
{{ form.contents }}
{% endcomment %}
{{ form }}
<button type="submit">저장</button>
</form>
<form action="{% url 'blog_create' %}" method="post">
: action
속성은 폼 데이터를 보낼 URL을 지정한다.{% url 'blog_create' %}
는 blog_create
라는 이름의 URL 패턴을 찾아 해당 URL로 대체한다.{% csrf_token %}
: CSRF
공격을 방어하기 위한 토큰을 폼에 추가
{% comment %} ... {% endcomment %}
: 주석
{{ form }}
: Django Form 객체를 HTML로 렌더링한다. 모든 폼 필드를 포함한 HTML을 출력한다.
<button type="submit">저장</button>
: 폼 데이터를 제출하는 버튼을 추가
blog_create
함수를 추가하여 사용자가 블로그 포스트를 생성하는 요청을 처리한다.
def blog_create(request):
if request.method == "GET":
print("GET으로 들어왔습니다!")
form = (
PostForm()
) # 이렇게 생성된 form은 자동으로 form을 만들어주는 기능을 가지고 있습니다.
# 이렇게 안하면 일일이 form을 하나씩 만들어야 합니다. 이해하긴 일일이 만드는 것이 더 좋을 수도 있습니다.
context = {"form": form}
return render(request, "blog/blog_create.html", context)
elif request.method == "POST":
print("POST로 들어왔습니다!")
print(request.POST)
form = PostForm(request.POST)
if form.is_valid():
# form.is_valid()를 통과하면 form.cleaned_data를 통해 데이터를 가져올 수 있습니다. form.is_valid() 이걸 안하면 form.cleaned_data 사용할 수 없습니다. 호출도 불가합니다!
print(form)
print(form.data)
print(form.cleaned_data["title"])
print(type(form))
print(dir(form))
"""
'add_error', 'add_initial_prefix', 'add_prefix', 'as_div', 'as_p', 'as_table', 'as_ul', 'auto_id', 'base_fields', 'changed_data', 'clean', 'cleaned_data', 'data', 'declared_fields', 'default_renderer', 'empty_permitted', 'error_class', 'errors', 'field_order', 'fields', 'files', 'full_clean', 'get_context', 'get_initial_for_field', 'has_changed', 'has_error', 'hidden_fields', 'initial', 'is_bound', 'is_multipart', 'is_valid', 'label_suffix', 'media', 'non_field_errors', 'order_fields', 'prefix', 'render', 'renderer', 'template_name', 'template_name_div', 'template_name_label', 'template_name_p', 'template_name_table', 'template_name_ul', 'use_required_attribute', 'visible_fields'
"""
return render(request, "blog/blog_create.html")
else:
context = {"form": form}
return render(request, "blog/blog_create.html", context)
HTTP GET
요청을 처리하는 부분 : 블로그 포스트 생성 폼을 렌더링하는 역할을 한다. PostForm()
을 통해 폼 객체를 생성하고, 이를 템플릿으로 전달한다. 그 결과, 사용자는 블로그 포스트 생성 폼을 웹 페이지에서 볼 수 있다.
HTTP POST
요청을 처리하는 부분: 사용자가 폼에 입력한 데이터를 받아서 처리하는 역할을 한다. PostForm(request.POST)
를 통해 폼 객체를 생성하되, 사용자가 입력한 데이터를 함께 전달한다. 그리고 form.is_valid()
를 통해 데이터의 유효성을 검사한다.
유효성 검사를 통과한 경우, form.cleaned_data
를 통해 사용자가 입력한 데이터를 가져올 수 있다. 이 데이터는 필요에 따라 데이터베이스에 저장하거나 다른 처리를 할 수 있다. 위 코드에서는 데이터를 출력하고 다시 form
을 렌더링 한다.
유효성 검사를 통과하지 못한 경우, 폼 객체를 다시 템플릿으로 전달하여 폼을 다시 렌더링한다. 이때 폼 객체에는 사용자가 입력한 데이터와 함께 유효성 검사에서 발생한 오류 정보도 함께 전달된다. 이 정보를 템플릿에서 사용하여 사용자에게 오류를 알릴 수 있다.
forms.py
를 수정하여 model
과 forms
를 연결한다.
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
title = forms.CharField()
contents = forms.CharField()
class Meta:
model = Post
fields = ["title", "contents"]
class Meta
: PostForm
이 처리할 모델과 필드를 지정model = Post
: PostForm
이 처리할 모델을 Post
로 지정fields = ["title", "contents"]
: PostForm
이 처리할 필드를 title
과 contents
로 지정<p style="color:red;">{{ error }}</p> 추가
def blog_delete(request, pk):
# post = Post.objects.get(pk=pk)
post = get_object_or_404(Post, pk=pk)
print(post)
if request.method == "POST":
post.delete()
return redirect("blog_list")
get_object_or_404(Post, pk=pk)
: Post
모델에서 pk
가 일치하는 객체를 DB에서 조회한다. 만약, 존재하지 않으면 404 에러를 반환한다.
return redirect("blog_list")
: 포스트를 삭제한 후에는 사용자를 블로그 포스트 리스트 페이지로 리다이렉트(재연결)한다.
선택한 블로그 포스트를 삭제하는 버튼이 추가하였다.
<!-- 삭제하기 버튼 -->
<form action="{% url 'blog_delete' db.id %}" method="post">
{% csrf_token %}
<button type="submit">삭제하기</button>
</form>
기존의 파일에서 max_length
와 main_image
필드를 추가하였다.
class PostForm(forms.ModelForm): # PostForm은 여러분이 원하는 이름으로 바꿔도 됩니다.
title = forms.CharField(max_length=100)
contents = forms.CharField(widget=forms.Textarea)
class Meta:
model = Post
fields = ["title", "contents", "main_image"]
# fields = '__all__'
def blog_create(request):
if request.method == "GET":
form = PostForm()
context = {"form": form}
return render(request, "blog/blog_create.html", context)
elif request.method == "POST":
form = PostForm(request.POST, request.FILES) # 수정
if form.is_valid():
post = form.save()
# detail로 가야한다!
# return redirect("blog_details", pk=post.pk)
return redirect("blog_list")
else:
context = {
"form": form,
"error": "입력이 잘못되었습니다. 알맞은 형식으로 다시 입력해주세요!",
}
return render(request, "blog/blog_create.html", context)
form = PostForm(request.POST)
부분을 form = PostForm(request.POST, request.FILES)
로 변경하여 사용자의 입력 데이터와 파일을 함께 받도록 수정하였다.<form action="{% url 'blog_create'%}" method="post" enctype="multipart/form-data"> 로 수정
<form action="{% url 'blog_create'%}" method="post" enctype="multipart/form-data">
: 파일을 보낼 수 있도록 enctype
을 추가하여 수정하였다.def blog_update(request, pk):
post = get_object_or_404(Post, pk=pk)
if request.method == "POST":
form = PostForm(request.POST, request.FILES, instance=post)
if form.is_valid():
form.save()
return redirect("blog_details", pk=post.pk)
else:
form = PostForm(instance=post)
context = {"form": form, "pk": pk}
return render(request, "blog/blog_update.html", context)
form = PostForm(request.POST, request.FILES, instance=post)
:PostForm
에 전달하여 폼 객체를 생성한다. 수정하기 버튼을 추가하였다.
<!-- 수정하기 버튼 -->
<a href="{% url 'blog_update' db.id %}">수정하기</a>
수정하기 버튼을 눌렀을 경우에 실행되는 것들을 정의한다.
<p style="color:red;">{{ error }}</p>
<form action="{% url 'blog_update' pk %}" method="post" enctype="multipart/form-data">
{# 해킹 공격 방어를 위한 토큰입니다. #}
{% csrf_token %}
{{ form }}
<button type="submit">저장</button>
</form>