[day-36] 웹으로 진행하는 CRUD, 사용자 입력 받기

Joohyung Park·2024년 2월 26일
0

[모두연] 오름캠프

목록 보기
68/95

게시판 생성 및 검색 기능

기획

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

blog > models.py

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__ : 모델의 문자열 표현을 반환하는 메서드

이미지의 이름과, 경로는 성능에 있어서 중요하다.

DB 반영

python manage.py makemigrations
python manage.py migrate

blog > admin.py

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!

static, media 폴더 선언 및 생성

  • media : 사용자가 업로드한 이미지
  • static : 우리가 사용할 이미지
# settings.py

STATIC_URL = "static/" # 이 URL로 들어오면
STATICFILES_DIRS = [BASE_DIR / "static"] # 여기서 처리해주겠다!

MEDIA_URL = "/media/" # 이 URL로 들어오면
MEDIA_ROOT = BASE_DIR / "media" # 여기서 처리해주겠다!

BASE_DIR 는 루트 디렉토리이다.

루트 > urls.py 수정

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)
    미디어 파일을 서비스하기 위한 URL 설정을 추가한다. 이 설정에 따라 settings.MEDIA_URL에 정의된 URL 경로로 접속하면 settings.MEDIA_ROOT에 지정된 경로에서 파일을 찾아 서비스할 수 있다.
    해당 설정은 개발 서버에서 미디어 파일을 서비스할 때 사용하며, 실제 운영 환경에서는 웹 서버 등 다른 방법으로 미디어 파일을 서비스하는 것이 일반적이라고 한다.

blog > views.py

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 변수에 담아 템플릿으로 전달한다.

blog > blog_list.html(템플릿)

<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로 이동한다.

blog > blog_details.html(템플릿)

<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를 출력하고, 메인 이미지가 있으면 이미지를 출력한다. "뒤로가기" 링크를 클릭하면 포스트 리스트 페이지로 돌아간다.

검색기능 구현 (blog > views.py)

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() : 검색어가 입력되지 않았으면 모든 포스트를 조회한다.

CRUD URL 추가 (blog > urls.py)

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"),
]

CRUD 함수 추가 (blog > views.py)

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

blog > forms.py

해당 파일에서는 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()

blog > blog_create.html

블로그 포스트 생성 폼을 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 > views.py 수정

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을 렌더링 한다.

    • 유효성 검사를 통과하지 못한 경우, 폼 객체를 다시 템플릿으로 전달하여 폼을 다시 렌더링한다. 이때 폼 객체에는 사용자가 입력한 데이터와 함께 유효성 검사에서 발생한 오류 정보도 함께 전달된다. 이 정보를 템플릿에서 사용하여 사용자에게 오류를 알릴 수 있다.

models.py와 forms.py 연결

forms.py를 수정하여 modelforms를 연결한다.

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이 처리할 필드를 titlecontents로 지정

에러메세지 출력

  • blog > templates > blog > create.html
<p style="color:red;">{{ error }}</p> 추가

Delete 구현

blog > views.py

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") : 포스트를 삭제한 후에는 사용자를 블로그 포스트 리스트 페이지로 리다이렉트(재연결)한다.

blog > blog_details.html(템플릿)

선택한 블로그 포스트를 삭제하는 버튼이 추가하였다.

<!-- 삭제하기 버튼 -->
<form action="{% url 'blog_delete' db.id %}" method="post">
    {% csrf_token %}
    <button type="submit">삭제하기</button>
</form>

이미지 업로드 기능 추가

blog > forms.py 이미지 필드 추가

기존의 파일에서 max_lengthmain_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__'

blog > views.py 이미지 필드 추가

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)로 변경하여 사용자의 입력 데이터와 파일을 함께 받도록 수정하였다.

blog > blog_create.html 이미지 필드 추가

<form action="{% url 'blog_create'%}" method="post" enctype="multipart/form-data"> 로 수정
  • <form action="{% url 'blog_create'%}" method="post" enctype="multipart/form-data"> : 파일을 보낼 수 있도록 enctype을 추가하여 수정하였다.

Update 구현

blog > views.py

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) :
    사용자가 입력한 데이터(request.POST, request.FILES)와 기존 포스트 객체(instance=post)를 PostForm에 전달하여 폼 객체를 생성한다.

blog > blog_details.html

수정하기 버튼을 추가하였다.

<!-- 수정하기 버튼 -->
<a href="{% url 'blog_update' db.id %}">수정하기</a>

blog > blog_update.html

수정하기 버튼을 눌렀을 경우에 실행되는 것들을 정의한다.

<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>
profile
익숙해지기 위해 기록합니다

0개의 댓글