03_Web

16_글과 댓글 모델(블로그) 구현

chuuvelop 2025. 2. 7. 17:31
728x90
글과 댓글 모델(블로그) 구현

 

01. 1대다 연결
  • 하나의 부모 요소에 여러 자식 요소가 연결된 연결
    • 블로그 댓글은 수에 상관없이 반드시 하나의 글과 연결되므로 글과 댓글은 1대다 연결
  • 하나의 글에 여러 댓글이 달렸을 때, 이를 하나의 테이블로 나타낸다면 반드시 중복 데이터가 나타나게 됨
  • 이 경우 글 제목을 수정하려면 반드시 3개의 데이터가 동시에 수정되어야 함
    • 이런 비효율적인 구조를 막기 위해 1대다 관계를 가지는 데이터는 각각의 테이블에 나누어 저장
  • ID열은 테이블에 있는 데이터를 구분할 수 있는 유일한 값
    • 하지만 이렇게 나누면 각 댓글이 어떤 글에 달린 글인지 알 수 없게 됨
    • 따라서 댓글 테이블의 각 행에 해당 댓글이 어떤 글과 연결되는지 나타내야함

 

1대다 관계 모델 구현

# blog/models.py

class Post(models.Model):
    # 포스트제목, 포스트내용은 주석
    title = models.CharField("포스트 제목", max_length = 100)
    content = models.TextField("포스트 내용")

    def __str__(self):
        return self.title
    
class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete = models.CASCADE)
    content = models.TextField("댓글 내용")

    def __str__(self):
        return f"{self.post.title}의 댓글 (ID: {self.id})"

 

 

 

관리자 페이지 등록

# blog/admin.py

from django.contrib import admin
from blog.models import Post

# Register your models here.
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    pass

 

 

 

02. 글과 댓글 보여주기

 

  • 전체 글 목록 보여주기
# blog.view.py

def post_list(request):
    posts = Post.objects.all()

    print("전체 블로그 글 리스트: ", posts)

    context = {
        "posts" : posts,
    }

    return render(request, 'post_list.html', context)

 

# blog/templates/post_list.html

<!DOCTYPE html>
<html lang="ko">

<body>
    <h1>게시글 목록</h1>
    <!-- 글 목록을 ul 요소 내에 표시 -->
    <ul>
        {% for post in posts %}
        <li>
            <h2>{{ post.title }}</h2>
            <p>{{ post.content }}</p>
        </li>
        {% endfor %}
    </ul>
    
</body>
</html>

 

 

03. 댓글 목록 보여주기

Django ORM을 사용한 1:N 객체 접근

  • Comment 모델은 post속성으로 자신과 연결된 Post에 직접 엑세스 할 수 있음(정방향 관계)
  • 반대로 1:N 에서 1 방면의 객체에서 N 방면의 객체로 접근하는 것은 역방향 관계
    • 역방향 접근을 위한 속성은 {N방향 모델명의 소문자화}_set 이라는 이름으로 Django ORM이 자동 생성해줌
  • post.comment_set 은 RelatedManager 객체
    • N방향의 객체로 접근할 수 있도록 도와주는 역할
    • Comment 입장에서 연결된 Post는 1개 뿐이기 때문에 곧바로 접근이 가능하지만
    • Post 입장에서는 연결된 Comment들이 여럿이므로 Manager객체를 통해서 접근
# blog/templates/post_list.html

{% for post in posts %}
    <li>
        <h2>{{ post.title }}</h2>
        <p>{{ post.content }}</p>
        <!-- 순회 중인 post와 연결된 모든 comment QuerySet -->
        <ul>
        {% for comment in post.comment_set.all %}
            <li>{{ comment.content }}</li>
        {% empty %} <!-- post.comment_set.all 에 순회할 항목이 없는 경우 -->
            <li>아직 댓글이 없습니다</li>
        {% endfor %}
        </ul>
    </li>
    {% endfor %}

 

  • 순회할 항목이 없는 경우를 뜻하는 {% empty %} 태그를 중간에 사용하여 댓글이 없는 경우에는 "아직 댓글이 없습니다" 라는 문장을 출력

 

04. CSS와 정적파일
  • 정적 파일(Static files)
    • 사전적으로는 변화가 없는 파일
    • 웹 프레임워크에서 정적파일이란 프레임워크의 소스코드를 제외한 나머지 이미지, 동영상, CSS, JavaScript파일 등
    • 소스코드는 일반적으로 동적인 결과물을 만들어내는 데 사용하지만 정적파일은 사용자에게 변형 없이 언제나 동일한 형태로 제공됨
# pylog/settings.py

# Static files (CSS, JavaScript, Images)
https://docs.djangoproject.com/en/5.1/howto/static-files/

STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]

 

 

05. 템플릿에서 정적파일 사용

 

  • 템플릿에서 정적파일을 불러올 때는 {% static '정적파일경로' %} 태그를 사용
{% load static %}
<!DOCTYPE html>
<html lang="ko">
    <head>
        <link rel="stylesheet" href="{% static 'css/style.css' %}">
    </head>

 

 

06. 유저가 업로드하는 정적파일

정적파일의 분류

  • Django에서 정적파일은 두 가지로 나뉨
    • 소스코드에 포함되는 정적파일
    • 유저가 업로드하는 정적파일
  • 이전에 정적파일을 넣어뒀던 디렉터리는 소스코드에 포함되는 정적파일을 두는 곳
    • 프로젝트의 일부분으로 취급됨
  • 유저가 업로드하는 정적파일은 프로젝트에 포함되지 않음
    • 블로그라는 전체 프로젝트와는 별개로 블로그를 사용하는 사용자들이 업로드하는 글에 포함된 이미지와 같은 데이터를 의미
  • Django에서 소스코드에 포함되는 정적파일은 Staticfile이라 부르고, 유저가 업로드하는 정적파일은 User-uploaded static file이라 부름

 

유저가 업로드하는 정적파일 설정

  • settings.py 에서 소스코드에 포함되는 정적파일의 설정은 STATIC_ 으로 시작하고, 유저가 업로드하는 정적파일과 관련된 설정은 MEDIA_ 로 시작함
  • MEDIA_URL
    • 유저가 업로드한 파일에 접근할 수 있도록 브라우저에 제공하는 경로 접두어
    • 소스코드에 포함되는 정적파일은 STATIC_URL 이라는 설정값을 사용하며 기본값은 "/static/"
    • 유저가 업로드한 파일의 경로 접두어는 "/media/"를 사용
  • MEDIA_ROOT
    • 실제로 유저가 업로드한 파일이 저장될 경로
    • 유저가 업로드한 정적파일은 프로젝트 디렉터리 하위의 media 디렉터리를 사용
# pylog/settings.py

# Static files (CSS, JavaScript, Images)
https://docs.djangoproject.com/en/5.1/howto/static-files/

STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]

MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media"

 

  • 위와 같이 설정하면 pylog 프로젝트에서 유저가 업로드한 파일들은 BASE_DIR/media/ 디렉터리에 업로드됨
    • 사용자가 처음으로 파일을 업로드할 때 MEDIA_ROOT 설정에 의해 Django가 자동으로 생성하므로 직접 해당 디렉터리를 생성할 필요는 없음

 

정적파일을 저장하는 필드 추가

  • 각각의 글에 썸네일 이미지를 저장할 필드를 추가
 
  • 이미지필드에서 선택한 파일은 BASE_DIR/media/post/ 디렉터리 안에 업로드됨
    • settings.py 에서 MEDIA_ROOT 가 BASE_DIR/media/ 까지의 경로를 만들고, Post모델의 ImageField내의 upload_to="post"가 그 다음 경로를 생성하고 최종 경로에 업로드한 파일명으로 저장됨
 
  • Django는 STATIC_URL 을 사용해 소스코드에 포함된 정적파일을 불러오는 것은 기본적으로 설정되어 있으나
  • MEDIA_URL 을 사용해 유저가 업로드한 정적파일을 불러오는 것은 별도의 설정을 추가해야함
# pylog/ 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("", include("blog.urls")),
]

urlpatterns += static(
    # URL의 접두어가 MEDIA_URL일 때는 정적파일을 돌려준다
    prefix = settings.MEDIA_URL,

    # 돌려줄 디렉터리는 MEDIA_ROOT를 기준으로 한다
    document_root = settings.MEDIA_ROOT,
)

 

템플릿에 업로드된 파일 보여주기

# blog/admin.py

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ["title", "thumbnail"]

 

# blog/templates/post_list.html

{% for post in posts %}
            <li class="post">
                <h2>{{ post.title }}</h2>
                <p>{{ post.content }}</p>
                <!-- 순회 중인 post와 연결된 모든 comment QuerySet -->
                <ul class="comments">
                    {% for comment in post.comment_set.all %}
                        <li class="comment">{{ comment.content }}</li>
                    {% empty %} <!-- post.comment_set.all 에 순회할 항목이 없는 경우 -->
                        <li>아직 댓글이 없습니다</li>
                    {% endfor %}
                </ul>
                <img src="{{ post.thumbnail.url }}">
            </li>
            {% endfor %}

 

 

 

07. 유저가 업로드하는 정적파일 설정

 

  • settings.py 에서 소스코드에 포함되는 정적파일의 설정은 STATIC_ 으로 시작하고, 유저가 업로드하는 정적파일과 관련된 설정은 MEDIA_ 로 시작함
  • MEDIA_URL
    • 유저가 업로드한 파일에 접근할 수 있도록 브라우저에 제공하는 경로 접두어
    • 소스코드에 포함되는 정적파일은 STATIC_URL 이라는 설정값을 사용하며 기본값은 "/static/"
    • 유저가 업로드한 파일의 경로 접두어는 "/media/"를 사용
  • MEDIA_ROOT
    • 실제로 유저가 업로드한 파일이 저장될 경로
    • 유저가 업로드한 정적파일은 프로젝트 디렉터리 하위의 media 디렉터리를 사용
# pylog/settings.py

# Static files (CSS, JavaScript, Images)
https://docs.djangoproject.com/en/5.1/howto/static-files/

STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]

MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media"

 

  • 위와 같이 설정하면 pylog 프로젝트에서 유저가 업로드한 파일들은 BASE_DIR/media/ 디렉터리에 업로드됨
    • 사용자가 처음으로 파일을 업로드할 때 MEDIA_ROOT 설정에 의해 Django가 자동으로 생성하므로 직접 해당 디렉터리를 생성할 필요는 없음

 

 

08. 정적파일을 저장하는 필드 추가

 

  • 각각의 글에 썸네일 이미지를 저장할 필드를 추가
  • 이미지필드에서 선택한 파일은 BASE_DIR/media/post/ 디렉터리 안에 업로드됨
    • settings.py 에서 MEDIA_ROOT 가 BASE_DIR/media/ 까지의 경로를 만들고, Post모델의 ImageField내의 upload_to="post"가 그 다음 경로를 생성하고 최종 경로에 업로드한 파일명으로 저장됨
  • Django는 STATIC_URL 을 사용해 소스코드에 포함된 정적파일을 불러오는 것은 기본적으로 설정되어 있으나
  • MEDIA_URL 을 사용해 유저가 업로드한 정적파일을 불러오는 것은 별도의 설정을 추가해야함
# pylog/ 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("", include("blog.urls")),
]

urlpatterns += static(
    # URL의 접두어가 MEDIA_URL일 때는 정적파일을 돌려준다
    prefix = settings.MEDIA_URL,

    # 돌려줄 디렉터리는 MEDIA_ROOT를 기준으로 한다
    document_root = settings.MEDIA_ROOT,
)

 

09. 템플릿에 업로드된 파일 보여주기

 

# blog/admin.py

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ["title", "thumbnail"]
# blog/templates/post_list.html

{% for post in posts %}
            <li class="post">
                <h2>{{ post.title }}</h2>
                <p>{{ post.content }}</p>
                <!-- 순회 중인 post와 연결된 모든 comment QuerySet -->
                <ul class="comments">
                    {% for comment in post.comment_set.all %}
                        <li class="comment">{{ comment.content }}</li>
                    {% empty %} <!-- post.comment_set.all 에 순회할 항목이 없는 경우 -->
                        <li>아직 댓글이 없습니다</li>
                    {% endfor %}
                </ul>
                <img src="{{ post.thumbnail.url }}">
            </li>
            {% endfor %}

 

 

10. 파일이 없는 ImageField 처리
  • Post 의 thumbnail 에 이미지를 저장하지 않은 경우, ImageField와 연결되는 파일이 없기 때문에 url을 만들어낼 수 없다는 오류
    • 파일이 저장되지 않은 경우에도 오류없이 화면을 표시하도록 템플릿을 수정해야함
# blog/templates/post_list.html

        </ul>
        {% if post.thumbnail %}
            <img src="{{ post.thumbnail.url }}">
        {% else %}
            <img src="" alt="">
        {% endif %}
    </li>
    {% endfor %}

 

 

 

글 상세 페이지
  • 상세 페이지 기본 구조
    • view : blog/views.py의 post_detail
    • url : ID가 1번인 글은 /post/1/, ID가 2번인 글은 /posts/2/를 사용
    • template : templates/post_detail.html을 사용
  • 전체 글 목록을 볼 수 있는 /posts/와는 다르게 글의 상세 페이지는 자신의 ID값에 따라 서로 다른 동적인 URL을 가져야 함

 

01. 동적 URL 경로
  • 새로 만들어질 글은 어떤 ID를 가지게 될 지 알 수 없음
    • 어떤 숫자를 입력하든 입력받은 숫자의 ID에 해당하는 글의 상세 화면을 보여주어야 함
    • 동적으로 숫자를 받을 수 있도록 URL을 수정해야함
#blog/views.py

def post_detail(request):
    return render(request, "post_detail.html")

 

# blog/templates/post_detail.html

{% load static %}
<!DOCTYPE html>
<html lang="ko">
<head>
    <link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
    <body>
        <div id="navbar">
            <span>pylog</span>
        </div>
        <div id="post-detail">
            <span>Post Detail</span>
        </div>        
    </body>
</html>

 

# blog/urls.py

urlpatterns = [
    path("", views.index),
    path("posts/", views.post_list),
    path("posts/<int:post_id>/", views.post_detail),
]
  • int: 정수 형태의 값을 받도록 제한한다는 의미
  • post_id: <와 >사이의 영역이 post_id라는 이름을 가진다는 의미


인수를 전달받을 수 있는 view 함수

  • post_detail 함수는 현재 request 매개변수를 하나 받을 수 있음
    • 모든 view 역할을 하는 함수는 request라는 인수를 받을 수 있어야 하며, 이는 Django의 규칙임
    • view 함수의 request 매개변수에 값을 전달하는 것은 자동으로 Django가 처리하며 우리가 직접 값을 전달할 수 없음
  • post_detail과 연결된 URL에 접근하면 post_detail을 호출
    • 동적으로 값을 받을 수 있도록 패턴이 정의되었다면 패턴 부분에 해당하는 값을 view함수로 전달
    • 예) /posts/1000/ 에 접근한다면 post_detail(post_id = 1000)으로 인수를 전달
# blog/views.py

def post_detail(request, post_id):
    # id값이 URL에서 받은 post_id 값인 Post 객체
    post = Post.objects.get(id = post_id)
    print(post)

    context = {
        "post" : post,
    }
    return render(request, "post_detail.html", context)

 

# blog/templates/post_detail.html

    <body>
        <div id="navbar">
            <span>pylog</span>
        </div>
        <div id="post-detail">
            <h1>Post Detail</h1>
            <h1>{{ post.title }}</h1>
            <p>{{ post.content }}</p>
            <p>{{ post.thumbnail }}</p>
        </div>        
    </body>

 

 

02. CSS 적용
  • 스타일은 잘 적용되어도 줄바꿈은 적용되지 않음
    • HTML에는 줄바꿈 한 내용이 전달되지만, 기본적으로 HTML요소 내의 텍스트는 줄바꿈이 무시됨
    • linebreaker를 추가하면 줄바꿈을 적용할 수 있음
# blog/templates/post_detail.html

    <body>
        <div id="navbar">
            {% if post.thumbnail %}
                <img src="{{ post.thumbnail.url }}">
            {% endif %}
            <span>{{ post.title }}</span>
        </div>
        <div id="post-detail">
            <p>{{ post.content|linebreaksbr }}</p>
        </div>        
    </body>

 

 

 

03. 댓글 기능

 

# blog/templates/post_detail.html

        <div id="post-detail">
            <p>{{ post.content|linebreaksbr }}</p>
            <ul class="comments">
                {% for comment in post.comment_set.all %}
                    <li class="comment">{{ comment.content }}</li>
                {% empty %} <!-- post.comment_set.all 에 순회할 항목이 없는 경우 -->
                    <li>아직 댓글이 없습니다</li>
                {% endfor %}
            </ul>
        </div>

 

# 글/댓글 작성

- 글 작성 페이지 기본 구조
    - view : blog/views.py 의 post_add
    - url : /posts/add
    - Template : blog/templates/post_add.html

 

 

04. 사용자의 입력을 받는 Template
  • HTML에서 사용자의 입력을 받는 요소는
    • 입력에 대한 제목 : <label>
    • 한 줄 짜리 텍스트 입력 : <input type="text">
    • 여러 줄의 텍스트 입력 : <textarea>
    • 버튼 : <button>
# blog/templates/post_add.html

    <body>
        <div>
            <h1>POST ADD</h1>
            <form method="GET">
                <div>
                    <label>제목</label>
                    <input type="text">
                </div>
                <div>
                    <label>내용</label>
                    <textarea></textarea>
                </div>
                <button>작성</button>
            </form>
        </div>
    </body>
  • 사용자의 입력을 받는 요소는 form 태그로 감싸줌
    • 사용자가 입력한 값을 처리하려면 단순히 input이나 textarea로 값을 받는 것 외에, 그 요소들을 반드시 form 태그 내부에 넣어야 함
  • div로 각각의 입력할 항목들을 감싸고, 내부에 label로 해당 항목의 이름을 표시
    • 제목은 여러 줄일 필요가 없으므로 input 요소를 사용
    • 글 내용은 여러 줄로 이루어져 있으므로 textarea 요소를 사용
# blog/templates/post_add.html

<form method="GET">
    <div>
        <label>제목</label>
        <input type="text" name="title">
    </div>
    <div>
        <label>내용</label>
        <textarea name="content" cols="50" rows="10"></textarea>
    </div>
    <button type="submit">작성</button>
</form>
  • name 속성을 추가해서 각각의 요소에 입력된 값이 어떤 이름으로 view에 전달될지 지정
  • button에 type="submit" 속성을 추가하여 버튼 클릭 시 form의 데이터를 전송하도록 지정
  • URL로 전달한 값은 Django View에서 사용할 수 있음
  • 사용자가 입력한 값을 단순한 조회가 아닌 DB에 새로운 객체를 생성하기 위해 사용할 때는 일반적으로 데이터를 URL로 전달하지 않음
    • URL은 데이터의 길이 제한이 있어서 긴 텍스트는 전달할 수도 없고, 보안상으로도 좋지 않기 때문
    • URL로 데이터를 전달하는 방식을 GET 메서드를 통한 전송이라 부르는데
    • URL을 통하지 않고 더 많은 데이터를 제약없이 보내려면 POST 메서드를 사용해야함
  • 어떤 방식으로 데이터를 전달할지는 form태그의 method속성에서 정의
# blog/templates/post_add.html

<div>
    <h1>POST ADD</h1>
    <form method="POST">
        <div>

 

  • Post 요청에 대한 Forbidden 오류
    • 403 Forbidden : 요청은 받았으나 그 요청을 처리할 권한이 없기 때문에 서버에서 거부함
  • CSRF 공격
    • CSRF 인증에 실패하여 요청이 중단됨
    • CSRF : Cross-Site Request Forgery(사이트 간 요청 위조)
  • Django에서 처리하는 GET과 POST 요청
    • 지금까지 CSRF 인증 오류가 발생하지 않은 이유는 Django 가 데이터를 처리하는 방식이 GET/POST에 따라 다르기 때문
    • GET은 사이트의 특정 페이지에 접속하거나, 검색을 하는 등의 읽기/조회 행동을 수행하는 데 쓰임
    • POST는 사이트의 특정 데이터를 변경/작성 하는데 쓰임
    • 따라서 Django는 POST 요청에 대해서 GET 요청보다 더 높은 보안 수준을 적용
    • GET 방식의 요청에서는 CSRF 보안을 적용하지 않음
  • Django의 CSRF 공격 방어기법
    • CSRF 공격 방어의 핵심은 로그인한 사용자가 의도하지 않은 POST 요청을 거부하는 것
      1. Django는 새로운 요청을 하는 브라우저마다 구분되는 값을 서버에 저장
      2. POST 요청을 하는 form이 브라우저별로 구분되는 값을 가지지 않는다면 요청을 거부
    • 브라우저별로 구분되는 값은 서버에 저장되므로 브라우저를 이용하는 사람(이용자 또는 해커)은 그 값을 알 수 없음
    • 이 기능을 이용하면 실 사용자의 POST 요청도 거부되게 되므로 Django는 실 사용자가 해당 값을 사용할 수 있는 기능을 제공
    • Template 파일에서 {% csrf_token %} 태그를 사용하면 이 영역은 브라우저별로 구분되는 값으로 치환됨
    • 이 값을 확인할 수 있는 시점은 HTML파일이 사용자에게 전달되어 브라우저에 그려진 다음이며, 해커는 이 값을 미리 알 수 없음
# blog/templates/post_add.html

<form method="POST">
    {% csrf_token %}
    <div>
  • hidden type을 갖게 되면 해당 input은 브라우저에 나타나지 않으며 단순히 고정된 데이터를 담게 됨

 

05. view에서 POST요청 처리
# blog/views.py

def post_add(request):
    if request.method == "POST": # method 가 POST일 때
        # print(request.POST) # POST 메서드로 전달된 데이터를 출력
        title = request.POST["title"]
        content = request.POST["content"]
        print(title)
        print(content)

    # method가 POST가 아닐 때
    # else: 
    #     print("method GET")
    return render(request, "post_add.html")
  • GET 메서드로 보낸 데이터는 request.GET 에 담겨오며, POST메서드로 보낸 데이터는 request.POST에 담겨서 전달됨
  • View로 전달된 요청이 GET인지, POST인지는 request.method 속성에 정의됨

 

06. POST 데이터를 사용한 DB row 생성
  • ORM을 사용해서 DB에 데이터를 생성할 때는 create메서드를 사용
    • created_instance = 모델.objects.create(필드명=필드값)
# blog/views.py

def post_add(request):
    if request.method == "POST": # method 가 POST일 때
        # print(request.POST) # POST 메서드로 전달된 데이터를 출력
        title = request.POST["title"]
        content = request.POST["content"]
        # print(title)
        # print(content)
        Post.objects.create(
            title=title,
            content=content,
        )

    # method가 POST가 아닐 때
    # else: 
    #     print("method GET")
    return render(request, "post_add.html")

 

# blog/views.py

from django.shortcuts import render, redirect

...

def post_add(request):
    if request.method == "POST": # method 가 POST일 때
        # print(request.POST) # POST 메서드로 전달된 데이터를 출력
        title = request.POST["title"]
        content = request.POST["content"]
        # print(title)
        # print(content)
        new_post = Post.objects.create(
            title=title,
            content=content,
        )

        return redirect(f"/posts/{new_post.id}/")

    # method가 POST가 아닐 때
    # else: 
    #     print("method GET")
    return render(request, "post_add.html")

 

  • 기존에는 작성 버튼을 누르면 글이 생성된 후 다시 글 작성 페이지로 이동했음
    • 다시 글 작성 페이지로 오는건 어색하고 작성한 글을 볼 수 있는 글 목록 페이지나 글 상세 페이지가 자연스러움
  • 글 목록 페이지의 URL은 언제나 변하지 않지만 상세페이지는 각각의 게시글마다 URL이 달라짐
    • 포매팅을 이용해 생성된 게시글의 id를 포함한 URL로 이동

 

07. 댓글 작성
  • 글 작성과의 차이점
    • 제목이 없음
      • 작성한 댓글이 반드시 어떤 글(Post)에 소속되어야 함

 

08. 댓글 생성 form을 template에 추가
  • 일반적으로 댓글 목록은 글 상세 페이지의 가장 하단에 보임
# templates/post_detail.html

<div id="navbar">
            {% if post.thumbnail %}
                <img src="{{ post.thumbnail.url }}">
            {% endif %}
            <span>{{ post.title }}</span>
        </div>
        <div id="post-detail">
            <p>{{ post.content|linebreaksbr }}</p>
            <ul class="comments">
                {% for comment in post.comment_set.all %}
                    <li class="comment">{{ comment.content }}</li>
                {% empty %} <!-- post.comment_set.all 에 순회할 항목이 없는 경우 -->
                    <li>아직 댓글이 없습니다</li>
                {% endfor %}
            </ul>

            <form method="POST">
                {% csrf_token %}
                <div>
                    <label>내용</label>
                    <textarea name="comment" cols="1" rows="1"></textarea>
                </div>
                <button type="submit" class="btn btn-primary">댓글작성</button>
            </form>

        </div>        
    </body>
</html>

 

 

09. View에서 Comment생성
  • form이 POST요청을 하므로 view에서 POST요청을 처리할 수 있도록 해야함
# blog/views.py

def post_detail(request, post_id):
    # id값이 URL에서 받은 post_id 값인 Post 객체
    post = Post.objects.get(id = post_id)
    print(post)

    if request.method == "POST":
        # textarea의 name 속성값("comment")을 가져옴
        comment_content = request.POST["comment"]
        print(comment_content)

        # 전달된 "comment"의 값으로 Comment 객체를 생성
        Comment.objects.create(
            post = post,
            content = comment_content,
        )
        # GET 요청으로 글 상세 페이지를 보여주거나
        # POST 요청으로 댓글이 생성되거나
        # 두 경우 모두,  이 글의 상세 페이지를 보여주면 됨
        
    context = {
        "post" : post,
    }
    return render(request, "post_detail.html", context)

 

 

10. 글 작성시 이미지 업로드
  • 파일을 첨부할 때는 <input type="file"> 태그를 사용
# blog/templates/post_add.html

            <form method="POST" enctype="multipart/form-data">
                {% csrf_token %}
                <div>
                    <label>제목</label>
                    <input type="text" name="title">
                </div>
                <div>
                    <label>내용</label>
                    <textarea name="content" cols="50" rows="10"></textarea>
                </div>
                <div>
                    <label>썸네일</label>
                    <input name="thumbnail" type="file">
                </div>
                <button type="submit" class="btn btn-primary">작성</button>
            </form>
  • 파일을 전송해야 하는 form에는 enctype="multipart/form-data" 속성을 추가
    • enctype 속성은 데이터를 서버로 전송할 때 어떤 인코딩 유형을 사용할 것인지 나타냄
    • form에 별도의 enctype을 지정하지 않으면 텍스트 데이터만 보낼 수 있는 인코딩 방식을 사용
  • view에서 POST로 전달받은 데이터는 request.POST에서 가져옴
    • 전송된 파일은 request.FILES에서 가져와야함
# blog/views.py

def post_add(request):
    if request.method == "POST": # method 가 POST일 때
        # print(request.FILES)
        # print(request.POST) # POST 메서드로 전달된 데이터를 출력
        title = request.POST["title"]
        content = request.POST["content"]
        thumbnail = request.FILES["thumbnail"] # 이미지 파일
        # print(title)
        # print(content)
        new_post = Post.objects.create(
            title=title,
            content=content,
            thumbnail = thumbnail, # 이미지 파일을 객체 생성 시에 전달
        )

        return redirect(f"/posts/{new_post.id}/")

    # method가 POST가 아닐 때
    # else: 
    #     print("method GET")
    return render(request, "post_add.html")
  • request.FILES 에는 thumbnail이라는 이름으로 InMemoryUploadFile이 전달됨
728x90

'03_Web' 카테고리의 다른 글

18_ 블로그 Django 프로젝트  (1) 2025.02.17
17_인스타그램 구현  (1) 2025.02.08
15_검색기능이 되는 화면 만들기  (2) 2025.02.06
14_VIEW, URL, Template  (1) 2025.02.06
13_Django_게시물 작성/수정/삭제 기능  (0) 2025.02.05