Django CRUD를 학습하면서, Dear 프로젝트를 만들어 볼 것이다.
Dear은 Daily Events and Affective Reactions의 약어다.
서비스의 주요 특징은 사용자가 오늘 하루 있었던 일과 관련된
경험 태그
하나와 그로 인해 느낀감정 태그
하나를 선택하여 글을 작성하고, 다른 사람들이 작성한 글 중, 태그가 일치하는 글을 볼 수 있는 것이다.
Dear은 일상의 사건과 감정을 중심으로 하는 소셜 네트워크로서, 사용자들이 자신의 경험을 기반으로 비슷한 경험을 한 사용자를 찾아 서로의 이야기에 공감하며 상호작용하는 공간을 제공할 수 있다.
Dear 프로젝트를 생성하기 전,
가상환경 설정
을 위해 django 디렉토리를 하나 생성해준다.
mkdir django
django 디렉토리에 기존에 생성해둔
django-envs
가상환경을 적용해준다
cd django
pyenv local django-envs
Dear 프로젝트를 생성한다.
django-admin startproject dear
프로젝트 루트로 이동 후 vscode를 실행한다.
cd dear
code .
settings.py
에서 타임존을 한국 시간으로 변경한다.
#settings.py
TIME_ZONE = 'Asia/Seoul'
posts 앱을 생성한다. 터미널에서 명령어를 입력한다.
python manage.py startapp posts
앱을 생성한 뒤에는
settings.py
에서 등록해야 한다.
#settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'posts' #등록
]
위를 살펴보면 posts 말고도 장고가 기본적으로 제공하는 앱들이 있다. 해당 앱들이 필요로 하는 데이터 구조를 생성하기 위해서
migrate
명령어를 입력한다.
python manage.py migrate
명령어를 입력하면
db.sqlite3
파일이 생성된 것을 확인할 수 있다.
개발 서버를 실행하고 서버가 정상적으로 동작하는지 확인한다.
python manage.py runserver
아래와 같이 잘 동작하는 것을 알 수 있다.
Dear의 url 구조는 다음과 같다.
다만 현재 학습하는 토픽에서는 기본적인 포스팅 구조만을 배우고 있기에 하단의 url 구조를 따르고, 추후 변경하는 것을 목표로 한다.
프로젝트 구성 디렉토리의
urls.py
를 작성한다.
#dear/urls.py
from django.contrib import admin
from django.urls import path, include #include 추가
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('posts.urls')), #작성
]
이렇게 작성하면 로컬호스트:8000으로 접속하거나 로컬호스트:8000/빈url로 접속하면 가장 먼저
posts
앱의urls
를 확인하게 된다.
posts 앱에 urls가 없으니 새로 생성하고 코드를 작성해 준다.
#posts/urls.py
from django.urls import path #작성
from . import views #작성
urlpatterns = [
path('', views.index),
path('posts/', views.post_list),
path('posts/new', views.post_create),
path('posts/<int:post_id>', views.post_detail),
path('posts/<int:post_id>/edit', views.post_update),
path('posts/<int:post_id>/delete', views.post_delete),
]
기본적으로 로컬호스트:8000/ 으로 접속하면 장고는 프로젝트 구성 디렉토리의 url 패턴에 따라 posts 앱의 url 패턴을 확인한다.
path('', views.index)
는로컬호스트:8000/{빈 url}
로 접속하면 views의 index 함수를 확인하라는 내용이다.
path('posts/', views.post_list)
는로컬호스트:8000/posts
로 접속하면 views의 post_list 함수를 확인하라는 내용이다.
path('posts/new', views.post_create)
는로컬호스트:8000/posts/new
로 접속하면 views의 post_create 함수를 확인하라는 내용이다.
path('posts/<int:post_id>', views.post_detail)
는로컬호스트:8000/posts/{url}
로 접속하면 {url}을 정수로 보고 post_id에 담아서 views의 post_detail 함수를 확인하라는 내용이다.
path('posts/<int:post_id>/edit', views.post_update)
는로컬호스트:8000/posts/{url}/edit
으로 접속하면 {url}을 정수로 보고 post_id에 담아서 views의 post_update 함수를 확인하라는 내용이다.
path('posts/<int:post_id>/delete', views.post_delete)
는로컬호스트:8000/posts/{url}/delete
로 접속하면 {url}을 정수로 보고 post_id에 담아서 views의 post_delete 함수를 확인하라는 내용이다.
현재는 각 url 패턴과 매칭되는 views가 정의되지 않았으니 개발 서버를 실행하면 에러가 날 것이다. 따라서 url 패턴을 주석처리 해둔다.
from django.urls import path
from . import views
urlpatterns = [
# path('', views.index),
# path('posts/', views.post_list),
# path('posts/new', views.post_create),
# path('posts/<int:post_id>', views.post_detail),
# path('posts/<int:post_id>/edit', views.post_update),
# path('posts/<int:post_id>/delete', views.post_delete),
]
데이터베이스에 접근하기 위해서는 model을 정의해줘야 한다. posts 앱 디렉토리의
models.py
를 작성한다.
#posts/models.py
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=50)
content = models.TextField()
dt_created = models.DateTimeField(verbose_name="Date Created", auto_now_add=True)
dt_modified = models.DateTimeField(verbose_name="Date Modified", auto_now=True)
def __str__(self):
return self.title
from django.db import models
장고 프레임워크의 db 모듈에서 models 클래스를 불러온다
class Post(models.Model):
Post 라는 이름의 모델 클래스를 생성한다.
title = models.CharField(max_length=50)
Post 모델에 최대 길이가 50인 문자열 컬럼 title을 추가한다.
content = models.TextField()
Post 모델에 최대 길이 제한이 없는 문자열 컬럼 content를 추가한다.
dt_created = models.DateTimeField(verbose_name="Date Created", auto_now_add=True)
Post 모델에 Date Created라는 별명을 가졌으며 인스턴스 생성 시 생성 시간이 자동으로 입력되는 컬럼 dt_created를 추가한다.
dt_modified = models.DateTimeField(verbose_name="Date Modified", auto_now=True)
Post 모델에 Date Modified라는 별명을 가졌으며 인스턴스 변경 시 변경 시간이 자동으로 입력되는 컬럼 dt_modified를 추가한다.
마지막 던더 str 함수는 title을 문자열로 반환하는 역할을 한다.
shell 환경에서 데이터를 불러오면 각 데이터의 title을 표기한다.def __str__(self): return self.title
모델이 생성되거나 변경되면
makemigrations
으로 새로운 migrations 파일을 생성하고migrate
를 통해 실제 데이터베이스에 적용해야 한다. 터미널에서 아래의 명령어를 입력한다.
python manage.py makemigrations #migration 파일 생성
python manage.py migrate #데이터베이스에 적용
더미데이터를 생성하는 방법을 알아보자
shell
환경을 불러온다.python manage.py shell #shell 환경을 불러오는 명령어
모델의
Post
클래스를 불러온다.from posts.models import Post #posts 디렉토리의 모델 파일에서 Post 클래스를 불러온다
데이터가 있는지 확인하기 위해 모든 데이터를 불러온다.
Post.objects.all() #모든 데이터를 불러오는 명령어
아직 생성된 데이터가 없으니 데이터를 수동으로 생성해본다.
Post.objects.create(title = "기분 좋은 날",content = "오늘은 평소와 달리 기분 좋은 아침을 맞이했다.")
데이터가 생성 되었다면 다음과 같이 출력될 것이다.
출력 결과에 위처럼 데이터의
title
이 호출되는 이유는던더 str
함수에서 self.title을 반환하기 때문이다def __str__(self): return self.title
생성된 데이터를 자세히 살펴보기 위해 아래의 명령어를 실행하면
Post.objects.all().values() #모델의 모든 데이터를 모든 컬럼의 값을 포함하여 호출
다음과 같이 출력된다.
Post 클래스의
dt_created
와dt_modified
컬럼은auto_now
와auto_now_add
속성을 사용했기 때문에 직접 입력하지 않아도자동
으로 생성되는 것을 확인할 수 있다.
이번에는 더미데이터를 수정하는 방법을 알아보자.
데이터 수정을 위해서는 먼저 데이터를 불러와야 한다. 따라서 post라는 변수에 id가 1인 객체를 지정한다.
post = Post.objects.get(id=1) #id가 1인 객체를 post 변수에 삽입
post.title을 입력해 현재 title을 확인한다.
post.title #post변수의 안쪽 속성에 접근해 title 컬럼의 값을 확인
이제 post 변수에 담긴 데이터의 title을 변경해보자.
post.title = "기분이 너어무 좋은 날"
post.title로 확인해보니 변경이 완료됐다.
데이터베이스에서도 변경되었는지 확인해보자.
Post.objects.all()
변경되지 않았다. 여전히 "기분 좋은 날"로 나타난다.
그 이유는 저장을 하지 않았기 때문이다.
post.save() #데이터 변경내용 저장
저장하고 다시 확인해보면
title
과dt_modified
가 변경된 것을 확인할 수 있다.
분명
settings.py
의 타임존을 Asia/Seoul 로 변경했는데 왜 모델 인스턴스의 시간은UTC
로 표기가 되는 것일까?
이는
settings.py
의USE_TZ
와 관련있다. 장고는USE_TZ = True
인 경우 설정한 타임존이 템플릿과 폼에만 영향을 끼친다. 따라서USE_TZ = False
로 변경하면 해결된다.
이번에는 shell 환경이 아닌 관리자 페이지에서 데이터를 수정해보자.
admin.py
에 Post 모델을 등록한다.#posts/admin.py from django.contrib import admin from .models import Post #모델에서 Post 클래스를 불러온다 # Register your models here. admin.site.register(Post) #Post를 관리자 페이지에 등록한다
shell 환경을 종료한다.
exit() #shell 환경 종료
관리자 계정을 생성한다.
python manage.py createsuperuser #관리자 계정 생성
개발서버를 실행한다.
python manage.py runserver
로컬호스트:8000/admin으로 접속한다.
관리자로 로그인하면 Posts가 생성된 것을 확인할 수 있다.
Posts를 클릭하면 shell 환경으로 작성한 데이터를 확인할 수 있다.
posts 앱 디렉토리의
urls.py
의url 패턴
중포스트 목록 페이지
에 해당하는 코드를주석 해제
한다.
from django.urls import path
from . import views
urlpatterns = [
# path('', views.index),
path('posts/', views.post_list), #주석 해제
# path('posts/new', views.post_create),
# path('posts/<int:post_id>', views.post_detail),
# path('posts/<int:post_id>/edit', views.post_update),
# path('posts/<int:post_id>/delete', views.post_delete),
]
로컬호스트:8000/posts/ 경로로 접속 시 매칭되는 views의
post_list
함수를 작성한다.
from django.shortcuts import render
# Create your views here.
def post_list(request):
return render(request, 'posts/post_list.html')
post_list
함수는 요청이 들어오면 posts 앱 디렉토리의post_list.html
템플릿을 렌더한다.
먼저 templates 디렉토리를 생성한다. templates과 정적 파일은
샌드위치 구조
의 디렉토리를 생성해야 한다.
자주 사용되며 변경되지 않는 내용은 부모 템플릿을 통해 상속받도록
temlpates/posts
에base.html
을 생성하고 작성한다.
<!-- base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dear :: Daily Events and Affective Reactions</title>
</head>
<body>
{% block post_container %} <!--상속받지 않는 부분-->
{% endblock post_container %}
</body>
</html>
상속받지 않는 부분은
block
영역을 지정하고post_container
로 명명한다.
base.html을 상속받는
post_list.html
을 생성하고 내용을 작성한다.
{% extends "./base.html" %} <!--부모 템플릿 상속-->
{% block post_container %} <!--상속받지 않는 영역-->
<h1>글 목록 페이지입니다.</h1>
{% endblock post_container %}
base.html을 상속받는
extends
템플릿 태그를 작성하고, container 블록에 <h1> 태그를 입력한다.
개발 서버를 실행하고 접속하면 다음과 같이 나타난다.
post_list 함수에 Post 모델을 통해 데이터를 가져오는 로직을 추가한다.
# views.py
from django.shortcuts import render
from .models import Post
# Create your views here.
def post_list(request):
posts = Post.objects.all()
context = {"posts" : posts}
return render(request, 'posts/post_list.html', context=context)
현재 디렉토리의
models
파일에서Post
모델을 불러온다.from .models import Post
posts
변수에Post 모델의 모든 데이터
를 불러오고,context
변수에 사전형으로 담아서 post_list.html 템플릿을 렌더할 때 함께 전달한다.def post_list(request): posts = Post.objects.all() context = {"posts" : posts} return render(request, 'posts/post_list.html', context=context)
view로부터 context로 전달받은 데이터를 호출할 수 있도록
템플릿 태그
를 사용해 템플릿을 수정한다.
<!--post_list.html-->
{% extends "./base.html" %}
{% block post_container %}
<h1>글 목록 페이지입니다.</h1>
<table>
<tr>
<td>제목</td>
<td>작성일</td>
<td>수정일</td>
</tr>
{% for post in posts %}
<tr>
<td>{{post.title}}</td>
<td>{{post.dt_created}}</td>
<td>{{post.dt_modified}}</td>
</tr>
{% endfor %}
</table>
{% endblock post_container %}
각 게시글이 쉽게 구분될 수 있도록
테이블 태그
를 사용했다.
for 템플릿 태그
를 사용해사전형
으로 전달받은 데이터를 post 이름으로 안쪽 속성에 접근해title
,de_created
,dt_modified
를 각 <td> 태그에 작성하는 것을 반복한다.{% for post in posts %} <tr> <td>{{post.title}}</td> <td>{{post.dt_created}}</td> <td>{{post.dt_modified}}</td> </tr> {% endfor %}
개발 서버를 실행하고 접속해보면 아래와 같이 호출된다.
기존에 작성한
urls.py
에서 포스트 상세 페이지에 대한 url 패턴을 주석해제 한다.
#posts/urls.py
from django.urls import path
from . import views
urlpatterns = [
# path('', views.index),
path('posts/', views.post_list),
# path('posts/new', views.post_create),
path('posts/<int:post_id>', views.post_detail), #주석 해제
# path('posts/<int:post_id>/edit', views.post_update),
# path('posts/<int:post_id>/delete', views.post_delete),
]
post_detail
뷰를 작성한다.
#posts/views.py
from django.shortcuts import render
from .models import Post
# Create your views here.
def post_list(request):
posts = Post.objects.all()
context = {"posts" : posts}
return render(request, 'posts/post_list.html', context=context)
def post_detail(request, post_id): #작성
post = Post.objects.get(id=post_id)
context = {"post": post}
return render(request, 'posts/post_detail.html', context=context)
post_detail
함수는 request와post_id
를 파라미터로 받는다.post_id
는posts/{url}
인 경우{url}
을정수
로 보고post_id
라는 이름으로 넘겨받는 것으로 url 패턴에서 정의했다.def post_detail(request, post_id):
post 변수에 Post 모델에서 post_id와 일치하는 값의 id를 지닌 데이터를 지정하고 context에 사전형으로 지정한다.
post = Post.objects.get(id=post_id) context = {"post": post}
context
와post_detail.html
템플릿을 렌더해 반환한다.return render(request, 'posts/post_detail.html', context)
post_detail.html
파일을 생성하고 작성한다.
<!--post_detail.html-->
{% extends "./base.html" %}
{% block post_container %}
<h2>{{post.title}}</h2>
<div>작성일: {{post.dt_created}}</div>
<hr>
<div>{{post.content}}</div>
<hr>
{% endblock post_container %}
먼저 부모 템플릿을 상속받는
extends
템플릿 태그를 작성한다.{% extends "./base.html" %}
부모 템플릿을 상속받지 않는
post_container
블록 영역을 추가한다.{% block post_container %} {% endblock post_container %}
템플릿 변수
를 사용해view
로 부터 전달받은post
의 안쪽 속성에 접근한다.{% block post_container %} <h2>{{post.title}}</h2> <div>작성일: {{post.dt_created}}</div> <hr> <div>{{post.content}}</div> <hr> {% endblock post_container %}
개발 서버를 실행하고
posts/3
으로 접속해보면 아래와 같이 출력된다.
그런데 문제가 있다. 생성한 데이터의 내용 중 엔터가 줄바꿈으로 인식되지 않는다.
그 이유는 장고에서는 엔터를
\n
로 저장하지만 html에서는<br>
태그로 저장하기 때문이다. 따라서\n
을<br>
로 변경하는 작업이 필요하다.
post_detail.html
템플릿에서post.content
변수에linebreaksbr
템플릿 필터를 작성하면 된다.<!--post_detail.html--> {% extends "./base.html" %} {% block post_container %} <h2>{{post.title}}</h2> <div>작성일: {{post.dt_created}}</div> <hr> <div>{{post.content|linebreaksbr}}</div> <!--작성--> <hr> {% endblock post_container %}
작성하면 아래와 같이 장고의
\n
이 html의<br>
로 변경된 것을 확인할 수 있다.
포스트 목록 페이지와 포스트 상세 페이지를 만들었다. 현재는 포스트 상세 페이지로 이동하려면
posts/{url}
을 통해 url을 직접 입력해 이동하는 방법만 있다. 포스트 목록 페이지와 상세 페이지를 연결해보자.
post_list.html
템플릿에서 포스트 제목에<a>
태그를 추가한다.
<!--post_list.html-->
{% extends "./base.html" %}
{% block post_container %}
<h1>글 목록 페이지입니다.</h1>
<table>
<tr>
<td>제목</td>
<td>작성일</td>
<td>수정일</td>
</tr>
{% for post in posts %}
<tr>
<td><a href="/posts/{{post.id}}">{{post.title}}</a></td>
<td>{{post.dt_created}}</td>
<td>{{post.dt_modified}}</td>
</tr>
{% endfor %}
</table>
{% endblock post_container %}
<td>
태그 안에<a>
태그가 있고<a>
태그의 경로는posts/{{post.id}}
이며 대체 텍스트는{{post.title}}
이다.<td><a href="/posts/{{post.id}}">{{post.title}}</a></td>
포스트 목록 페이지를 렌더할 때 url로 부터
posts/{url}
의{url}
을 정수로 보고post_id
로 전달받고 view는post_id
를 이용해 각각의 포스트의title
,dt_created
,de_modified
데이터에 접근해 출력할 수 있다.
따라서
post.id
로 각 포스트의id
데이터에 접근할 수 있고, 이를 통해<a>
태그의 경로를posts/{각 포스트의 id}
로 지정해 제목을 클릭하면 각 포스트의상세 페이지로 연결
할 수 있다.
포스트 목록 페이지에서 포스트 제목을 클릭하면
posts/{포스트의 id}
로 이동되며
포스트의 상세 페이지를 확인할 수 있다.
post_detail.html
템플릿에<a>
태그를 추가한다.
<!--post_detail.html-->
{% extends "./base.html" %}
{% block post_container %}
<h2>{{post.title}}</h2>
<div>작성일: {{post.dt_created}}</div>
<hr>
<div>{{post.content|linebreaksbr}}</div>
<hr>
<a href="/posts/">돌아가기</a> <!--추가-->
{% endblock post_container %}
<a>
태그에/posts/
경로를 입력하면 클릭 시 포스트 목록 페이지로 이동한다.
목록 페이지에서 상세 페이지로 이동하는 url은
/posts/{{post.id}}
로 정의되어 있고, 상세 페이지에서 목록 페이지로 이동하는 url은/posts/
로 정의되어 있다.
문제는 url의 구조 또는 url 패턴이 변경되는 경우, 많은 수정이 필요하다는 것이다. 이번에는 하드코딩으로 인한 유지보수의 어려움을 해결하기 위해 url에 이름을 붙인다.
urls.py 에서 url 패턴을 살펴보자
#posts/urls.py
from django.urls import path
from . import views
urlpatterns = [
# path('', views.index),
path('posts/', views.post_list),
# path('posts/new/', views.post_create),
path('posts/<int:post_id>/', views.post_detail),
# path('posts/<int:post_id>/edit/', views.post_update),
# path('posts/<int:post_id>/delete/', views.post_delete),
]
name
속성을 추가해 각 url에 이름을 붙여보자.
#posts/urls.py
from django.urls import path
from . import views
from django.urls import path
from . import views
urlpatterns = [
# path('', views.index),
path('posts/', views.post_list, name="post-list"), #name 속성 추가
# path('posts/new/', views.post_create),
path('posts/<int:post_id>/', views.post_detail, name="post-detail"), # name 속성 추가
# path('posts/<int:post_id>/edit/', views.post_update),
# path('posts/<int:post_id>/delete/', views.post_delete),
]
하드코딩된 url 경로를 템플릿 태그
{% url {url name} {인자} %}
를 이용해 수정한다.
기존
<a>
태그에서는 포스트 상세 페이지 경로를/posts/{{post.id}}
로 설정했다.
템플릿 태그
url
에 포스트 상세 페이지의url name
을 경로로 입력한 뒤, 한 칸 띄어서post.id
인자를 넘겨준다.
<!--post_list.html-->
{% extends "./base.html" %}
{% block post_container %}
<h1>글 목록 페이지입니다.</h1>
<table>
<tr>
<td>제목</td>
<td>작성일</td>
<td>수정일</td>
</tr>
{% for post in posts %}
<tr>
<td><a href={% url "post-detail" post.id %}>{{post.title}}</a></td> <!--수정-->
<td>{{post.dt_created}}</td>
<td>{{post.dt_modified}}</td>
</tr>
{% endfor %}
</table>
{% endblock post_container %}
기존
<a>
태그에서는 포스트 목록 경로를/posts/
로 설정했다.
템플릿 태그
url
에 포스트 목록 페이지의url name
을 경로로 입력한다.
<!--post_detail.html-->
{% extends "./base.html" %}
{% block post_container %}
<h2>{{post.title}}</h2>
<div>작성일: {{post.dt_created}}</div>
<hr>
<div>{{post.content|linebreaksbr}}</div>
<hr>
<a href={% url "post-list" %}>돌아가기</a> <!--수정-->
{% endblock post_container %}
기존에 하드코딩된 방식과 똑같이 작동한다. 추후 url이 변경되는 경우 하드코딩된 방식은 연관된 모든 파일을 수정해야 하지만,
url name
속성을 입력한 방식은 연관된 파일을 수정하지 않아도 된다.
html과 css를 직접 작성하지 않고, 다른 사람이 작성한 템플릿을 프로젝트에 적용하는 방법을 알아본다.
base.html
은 다음과 같이 작성되어 있다.
<!--base.html-->
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<title>DEAR :: Daily Events and Affective Reactions</title>
<meta charset="UTF-8">
{% block css %}
{% endblock css %}
<link rel="stylesheet" type="text/css"
href="https://fonts.googleapis.com/earlyaccess/nanummyeongjo.css">
</head>
<body>
<div id="warp">
<div id="header">
<div id="nav">
<div class="logo">
<a href="[A]"><img src="{% static 'posts/images/logo.png' %}"></a>
</div>
</div>
{% block post_header %}
{% endblock post_header %}
</div>
{% block logo_text %}
{% endblock logo_text %}
<div id="content">
<div class="container">
{% block content %}
{% endblock content %}
</div>
</div>
<div id="footer">
<div class="footer">
<p>Costory</p>
</div>
</div>
</div>
</body>
</html>
부모 템플릿의
block
영역은 총 4개로 구분된다.
<head>
태그 안에 작성된block css
는 자식 템플릿 별로 다른css
파일을 적용하기 위한 영역이다.{% block css %} {% endblock css %}
<body>
태그 안에 작성된block header
은 포스트 상세 페이지의 제목을 입력하기 위한 영역이다.{% block post_header %} {% endblock post_header %}
<body>
태그 안에 작성된block logo_text
는 포스트 목록 페이지의 상단 로고를 작성하기 위한 영역이다.{% block logo_text %} {% endblock logo_text %}
<body>
태그 안에 작성된block content
는 포스트 목록 페이지의 각 포스트를 보여주기 위한 영역이다.{% block content %} {% endblock content %}
부모 템플릿의 상단 로고를 클릭하면 이동하는 url이 비어있다. 하드코딩이 아닌,
url name
속성을 입력한다. dear의 메인페이지는 포스트 목록 페이지니까url 템플릿 태그
에 포스트 목록 페이지의name 속성
인post-list
를 입력한다.
<!--base.html-->
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<title>DEAR :: Daily Events and Affective Reactions</title>
<meta charset="UTF-8">
{% block css %}
{% endblock css %}
<link rel="stylesheet" type="text/css"
href="https://fonts.googleapis.com/earlyaccess/nanummyeongjo.css">
</head>
<body>
<div id="warp">
<div id="header">
<div id="nav">
<div class="logo">
<a href={% url "post-list" %}><img src="{% static 'posts/images/logo.png' %}"></a> <!--수정-->
</div>
</div>
{% block post_header %}
{% endblock post_header %}
</div>
{% block logo_text %}
{% endblock logo_text %}
<div id="content">
<div class="container">
{% block content %}
{% endblock content %}
</div>
</div>
<div id="footer">
<div class="footer">
<p>Costory</p>
</div>
</div>
</div>
</body>
</html>
post_list.html
은 다음과 같이 작성되어 있다.
<!--post_list.html-->
{% block css %}
<link rel="stylesheet" href="./css/post_list.css">
{% endblock css %}
{% block logo_text %}
<div id="header">
<div class="container">
<h1><img src="{% static 'posts/images/headertxt.png' %}"></h1>
</div>
</div>
{% endblock logo_text %}
{% block content %}
<div class="post_container">
</div>
{% endblock content %}
우선 부모 템플릿을 상속받는 템플릿 태그와 정적파일을 불러오는 템플릿 태그를 작성한다.
<!--post_list.html-->
{% extends "./base.html" %} <!--작성-->
{% load static %} <!--작성-->
{% block css %}
<link rel="stylesheet" href="./css/post_list.css">
{% endblock css %}
{% block logo_text %}
<div id="header">
<div class="container">
<h1><img src="{% static 'posts/images/headertxt.png' %}"></h1>
</div>
</div>
{% endblock logo_text %}
{% block content %}
<div class="post_container">
</div>
{% endblock content %}
다음은 하드 코딩된 css 경로를 정적파일 경로로 수정한다.
<!--post_list.html-->
{% extends "./base.html" %}
{% load static %}
{% block css %}
<link rel="stylesheet" href="{% static "posts/css/post_list.css" %}"> <!--수정-->
{% endblock css %}
{% block logo_text %}
<div id="header">
<div class="container">
<h1><img src="{% static 'posts/images/headertxt.png' %}"></h1>
</div>
</div>
{% endblock logo_text %}
{% block content %}
<div class="post_container">
</div>
{% endblock content %}
다음은
block content
영역이다. 여기에는 각 포스트의제목
,작성일
,본문의 일부
를 표기해야 한다. css를 반영할 수 있는 class를 포함한 코드를 추가하면 다음과 같다.
<!--post_list.html-->
{% extends "./base.html" %}
{% load static %}
{% block css %}
<link rel="stylesheet" href="{% static "posts/css/post_list.css" %}">
{% endblock css %}
{% block logo_text %}
<div id="header">
<div class="container">
<h1><img src="{% static 'posts/images/headertxt.png' %}"></h1>
</div>
</div>
{% endblock logo_text %}
{% block content %}
<div class="post_container">
<div class="post"><a href="[A] 개별 포스트로 이동하는 URL"> <!---->
<h2 class="title">[B] 글 목록에서 보여질 제목</h2>
<p class="date">[C] 글 목록에서 보여질 작성일</p>
<p class="text">[D] 글 목록에서 보여질 본문 100자</p>
</a></div>
</div>
{% endblock content %}
각 포스트는 클릭 시 해당 포스트의 상세 페이지로 이동하는 기능을 해야 하니,
<a>
태그로 감싼다.
우리는 이전 과정에서 for 문을 사용해 posts에 담긴 각각의 게시글을 post로 생성했다. 이를 변경된 디자인 템플릿에도 적용될 수 있도록 작성한다.
<!--post_list.html-->
{% extends "./base.html" %}
{% load static %}
{% block css %}
<link rel="stylesheet" href="{% static "posts/css/post_list.css" %}">
{% endblock css %}
{% block logo_text %}
<div id="header">
<div class="container">
<h1><img src="{% static 'posts/images/headertxt.png' %}"></h1>
</div>
</div>
{% endblock logo_text %}
{% block content %}
<div class="post_container">
{% for post in posts %}
<div class="post"><a href="{% url "post-detail" post.id %}">
<h2 class="title">{{post.title}}</h2>
<p class="date">{{post.dt_created}}</p>
<p class="text">{{post.content}}</p>
</a></div>
{% endfor %}
</div>
{% endblock content %}
이전 과정과 크게 다르지 않다. 부모 템플릿을 상속받는 템플릿 태그와 정적 파일을 불러오는 템플릿 태그를 작성한다.
<!--post_detail.html-->
{% extends "./base.html" %}
{% load static %}
css
를 불러오는css 블럭
을 작성한다.
<!--post_detail.html-->
{% extends "./base.html" %}
{% load static %}
{% block css %}
<link rel="stylesheet" href="{% static "posts/css/post_detail.css" %}">
{% endblock css %}
post_header 블럭
을 작성한다.
<!--post_detail.html-->
{% extends "./base.html" %}
{% load static %}
{% block css %}
<link rel="stylesheet" href="{% static "posts/css/post_detail.css" %}">
{% endblock css %}
{% block post_header %}
<div class="container">
<h1 class="title">{{post.title}}</h1>
<p class="date">{{post.dt_created}}</p>
</div>
{% endblock post_header %}
content 블럭
을 작성한다.
<!--post_detail.html-->
{% extends "./base.html" %}
{% load static %}
{% block css %}
<link rel="stylesheet" href="{% static "posts/css/post_detail.css" %}">
{% endblock css %}
{% block post_header %}
<div class="container">
<h1 class="title">{{post.title}}</h1>
<p class="date">{{post.dt_created}}</p>
</div>
{% endblock post_header %}
포스트 목록 페이지는 다음과 같이 출력된다.
포스트 상세 페이지는 다음과 같이 출력된다.
포스트를 작성하고 저장하는 페이지를 만든다.
포스트 작성 페이지를 만들기 위해 posts 앱 디렉토리에
forms.py
파일을 생성한다.
장고의
forms 모듈
을 불러온다.
#forms.py
from django import forms
PostForm
클래스를 만들고forms
의Form
클래스를 부모클래스로 두어상속
받는다.
from django import forms
class PostForm(forms.form):
PostForm
클래스의 내용을 입력한다.
from django import forms
class PostForm(forms.form):
title = forms.CharField(max_length=50, label="제목")
content = forms.CharField(label="내용", widget=forms.Textarea)
기본적으로 폼 클래스는 모델 클래스와 작성 방식이 유사하다. 하나하나 살펴보자.
title
필드는 forms 모듈의CharField
이며최대 길이
는 50이고필드 이름
은 제목이다.title = forms.CharField(max_length=50, label="제목")
content
필드는 forms 모듈의CharField
이며필드 이름
은 내용이다.위젯 스타일
은 forms 모듈의 Textarea로 설정했다.content = forms.CharField(label="내용", widget=forms.Textarea)
Charfield
는 기본적으로 한 줄 입력의 위젯을 제공하는데Textarea
위젯을 지정하면 여러 줄 입력의 위젯으로 변경된다.
포스트 작성 페이지의 url 주석을 해제하고
url name
을 입력한다.
#urls.py
from django.urls import path
from . import views
urlpatterns = [
# path('', views.index),
path('posts/', views.post_list, name="post-list"),
path('posts/new/', views.post_create, name="post-create"), #수정
path('posts/<int:post_id>/', views.post_detail, name="post-detail"),
# path('posts/<int:post_id>/edit/', views.post_update),
# path('posts/<int:post_id>/delete/', views.post_delete),
]
url 경로와 매칭되는 view 함수를 작성한다.
생성한 forms 파일의 PostForm 클래스를 불러온다.
#views.py
from django.shortcuts import render
from .models import Post
from .forms import PostForm #PostForm 클래스 불러오기
# Create your views here.
def post_list(request):
posts = Post.objects.all()
context = {"posts" : posts}
return render(request, 'posts/post_list.html', context=context)
def post_detail(request, post_id):
post = Post.objects.get(id=post_id)
context = {"post": post}
return render(request, 'posts/post_detail.html', context=context)
post_create 함수를 작성한다.
#views.py
from django.shortcuts import render
from .models import Post
from .forms import PostForm
# Create your views here.
def post_list(request):
posts = Post.objects.all()
context = {"posts" : posts}
return render(request, 'posts/post_list.html', context=context)
def post_detail(request, post_id):
post = Post.objects.get(id=post_id)
context = {"post": post}
return render(request, 'posts/post_detail.html', context=context)
def post_create(request): #작성
post_form = PostForm()
context = {"post_form": post_form}
return render(request, 'posts/post_form.html', context=context)
post_form 변수
에PostForm
을빈 양식
으로 지정한다.posts_form = PostForm()
context
변수에 post_form키
와 post_form밸류
를 쌍으로 저장한다.context = {"post_form": post_form}
context 변수
를 context 인자로 담아서posts/post_form.html
페이지를 렌더한다.
post_form.html
템플릿을 생성하고 내용을 작성한다.
<!--post_form.html-->
<form>
{{post_form}}
<input type="submit" value="전송">
</form>
Post_Form 클래스
를 만들었고 views의post_create
함수에서 이를 템플릿으로 전달했으니<form>
태그 안에 템플릿 변수인{{post_form}}
을 입력하면 폼 형식이 렌더된다. 추가적으로 전송 버튼만<input>
태그로 작성한다.
결과는 다음과 같다.
{{post_form}}
템플릿 변수에 .as_p를 입력하면<p>
태그의 형태로 변환된다.
이 밖에도
리스트 형태
(as_ul),테이블 형태
(as_table)도 가능하다. 본 프로젝트는 미리 작성된 css 스타일을 적용하기 위해리스트 형태
로 변환한다.
<!--post_form.html-->
<form>
{{post_form.as_ul}} <!--작성-->
<input type="submit" value="전송">
</form>
다음은
<form>
태그의 속성을 작성한다.method
속성은post
로 작성하고action
속성은 작성하지 않는다.
<!--post_form.html-->
<form method="post"> <!--작성-->
{{post_form.as_ul}}
<input type="submit" value="전송">
</form>
이렇게 하면
action
속성을 작성하지 않았기 때문에post
방식으로현재 url
로 데이터를 전송한다.
다음으로는
csrf_token
템플릿 태그를 추가한다. 이 템플릿 태그는교차 사이트 위조 검증
으로 <요청하지 않은 요청>을 마치 <요청한 것 처럼 위조>하는 것을방지
하는 기술이다.
<!--post_form.html-->
<form method="post">{% csrf_token %} <!--csrf토큰-->
{{post_form.as_ul}}
<input type="submit" value="전송">
</form>
뷰를 수정해야하는데, 기존에 작성된 뷰를 살펴보면 다음과 같다.
#views.py
def post_create(request):
post_form = PostForm()
context = {"post_form": post_form}
return render(request, 'posts/post_form.html', context=context)
우리가 해야할 것은 두 가지다.
하나는
posts/new
로 처음 접속 했을 때는 폼을 띄우는 것이고,다른 하나는
posts/new
에서 데이터를 입력하고 전송 버튼을 눌렀을 때입력된 데이터로 새 게시글을 저장
한 다음,해당 게시글의 상세 페이지로 이동
하는 것이다.
이는
if
문과else
문으로 구분해서 작성할 수 있다. 작성된 코드는 다음과 같다.
#views.py
from django.shortcuts import render, redirect
from .models import Post
from .forms import PostForm
# Create your views here.
def post_list(request):
posts = Post.objects.all()
context = {"posts" : posts}
return render(request, 'posts/post_list.html', context=context)
def post_detail(request, post_id):
post = Post.objects.get(id=post_id)
context = {"post": post}
return render(request, 'posts/post_detail.html', context=context)
def post_create(request):
if request.method =="POST":
title = request.POST["title"]
content = request.POST["content"]
new_post = Post(
title = title,
content = content
)
new_post.save()
return redirect("post-detail", post_id=new_post.id)
else:
post_form = PostForm()
context = {"post_form": post_form}
return render(request, 'posts/post_form.html', context=context)
우리는 여기서 post_create 함수만 살펴본다.
#views.py > post_create
def post_create(request):
if request.method =="POST":
title = request.POST["title"]
content = request.POST["content"]
new_post = Post(
title = title,
content = content
)
new_post.save()
return redirect("post-detail", post_id=new_post.id)
else:
post_form = PostForm()
context = {"post_form": post_form}
return render(request, 'posts/post_form.html', context=context)
우선 render 함수 외에 redirect 함수를 사용하기 때문에 redirect 함수를 불러온다.
from django.shortcuts import render, redirect
먼저 if문으로 작성된 코드를 살펴본다.
if request.method =="POST":
title = request.POST["title"]
content = request.POST["content"]
new_post = Post(
title = title,
content = content
)
new_post.save()
return redirect("post-detail", post_id=new_post.id)
만약
요청 방식
이POST
라면:if request.method =="POST":
변수 title
에POST 요청이 담고 있는 데이터
중키
를title
로 하는값
을 지정한다.title = request.POST["title"]
변수 content
에POST 요청이 담고 있는 데이터
중키
를content
로 하는값
을 지정한다.content = request.POST["content"]
변수 new_post
에Post 모델
을 사용해title 필드
에는변수 title
에 저장한 값을 넣고,content 필드
에는변수 content
에 저장한 값을 넣는다.new_post = Post( title = title, content = content )
new_post
를데이터베이스
에 저장한다.new_post.save()
그리고
redirect
함수를 사용해post-detail
(상세 페이지) url로 이동한다. 그런데post-detail
(상세 페이지)는<int:post_id>
를 필요로 하니,post_id
에 방금 생성한new_post의 id
를 전달한다.return redirect("post-detail", post_id=new_post.id)
간단히 설명하면, posts/new에 접속해 폼에 데이터를 입력하고 전송 버튼을 누르면 입력된 데이터로 게시글을 생성하고, 해당 게시글의 상세 페이지로 이동하는 로직이다.
else
문으로 작성된 코드를 살펴본다.
else:
post_form = PostForm()
context = {"post_form": post_form}
return render(request, 'posts/post_form.html', context=context)
만약 그렇지 않으면 (여기서는 요청 방식이 POST가 아니라면, 즉
요청 방식
이GET
이라면)else:
변수 post_form
에PostForm
폼을 지정한다.post_form = PostForm()
그리고
변수 context
에 post_form을 지정한다.context = {"post_form": post_form}
렌더 함수를 사용해
posts/post_form.html
을 렌더하고,파라미터 context의 인자
로변수 context
를 전달한다.
간단히 설명하면,
posts/new
url에 처음 접속(GET)하면forms.py
의PostForm
클래스를 포함하는게시글 작성 페이지
로 이동하는 로직이다.
form
을 통해 게시글 작성 페이지를 만들었으니,게시글 목록 페이지
에서글쓰기 버튼
을 추가한다.
post_list.html
의content 블럭
에<a>
태그를 추가한다.
<!--post_list.html-->
{% extends "./base.html" %}
{% load static %}
{% block css %}
<link rel="stylesheet" href="{% static "posts/css/post_list.css" %}">
{% endblock css %}
{% block logo_text %}
<div id="header">
<div class="container">
<h1><img src="{% static 'posts/images/headertxt.png' %}"></h1>
</div>
</div>
{% endblock logo_text %}
{% block content %}
<div class="btn_post">
<a href={% url "post-create" %}>기록하기</a> <!--추가-->
</div>
<div class="post_container">
{% for post in posts %}
<div class="post"><a href="{% url "post-detail" post.id %}">
<h2 class="title">{{post.title}}</h2>
<p class="date">{{post.dt_created}}</p>
<p class="text">{{post.content}}</p>
</a></div>
{% endfor %}
</div>
{% endblock content %}
btn_post
<div>
태그로 감싼 이유는css 파일을 적용
하기 위함이다.
#11. 포스트 작성 페이지에서 폼을 활용해 사용자가 입력한 데이터를 기반으로 새로운 게시글을 생성하는 기능을 구현했다.
이번에는 장고의
모델 폼
기능을 활용해 폼을 더욱 간편하게 만드는 방법을 알아본다.
기존에 작성된 폼은 다음과 같다.
#forms.py
from django import forms
class PostForm(forms.Form):
title = forms.CharField(max_length=50, label="제목")
content = forms.CharField(label="내용", widget=forms.Textarea)
모델 폼을 활용하기 위한 코드는 다음과 같다.
#forms.py
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ["title", "content"]
모델 폼
은 모델을 기반으로 폼을 간편하게 생성할 수 있는 기능이다.
먼저 모델을 불러와야 한다.
from .models import Post
forms.ModelForm
클래스를 상속받는PostForm
클래스를 생성한다.class PostForm(forms.ModelForm):
Post
모델을 참고해title 필드
와content 필드
를 입력받는 폼을 생성한다.class Meta: model = Post #Post 모델을 참고 fields = ["title", "content"] #폼을 통해 입력받을 필드를 추가
만약 필드가 너무 많아 일일이 입력하기가 힘들다면
모든 필드
를 추가하는 방법이 있다.fields = "__all__"
기존에 작성한
post_create
함수를 살펴본다.
def post_create(request):
if request.method =="POST":
title = request.POST["title"]
content = request.POST["content"]
new_post = Post(
title = title,
content = content
)
new_post.save()
return redirect("post-detail", post_id=new_post.id)
else:
post_form = PostForm()
context = {"post_form": post_form}
return render(request, 'posts/post_form.html', context=context)
if
문을 모델폼에 맞게 변경한다.
def post_create(request):
if request.method =="POST":
post_form = PostForm(request.POST)
new_post = post_form.save()
return redirect("post-detail", post_id=new_post.id)
변경된 코드의 의미는 다음과 같다.
만약 요청 방식이 POST라면
if request.method =="POST":
요청에 포함되어 있는 데이터(폼에 입력되고 전송된)를 PostForm과 바인딩하고 변수 post_form에 지정한다.
post_form = PostForm(request.POST)
변수 post_form에 지정된 바운드폼을 데이터베이스에 저장한 뒤, new_post에 지정한다.
new_post = post_form.save()
new_post의 상세 페이지로 이동한다.
return redirect("post-detail", post_id=new_post.id)
앞으로는 더 간단한 방법인 모델 폼을 사용하도록 하자.
이번에는 모델폼으로 작성하는 데이터의
유효성
을 검증하는 방법을 알아본다.
유효성을 검증하는 방법은
두 가지
가 있다.모델 필드 또는 폼 필드
를 통한 유효성 검증 방법과Validator
를 통한 유효성 검증 방법이 있다.
우리는 모델 폼을 사용하기 때문에 폼 필드가 아닌,
모델 필드
를 통해유효성을 검증
하는 방법을 알아본다.
만약,
게시글의 제목은 중복될 수 없음
이라는 정책이 있다면, 우리는 게시글 작성 페이지에서 게시글을 생성하기 전, 제목에 입력된 데이터가 기존에 생성된 게시글의 제목과 중복되지 않는지 유효성을 검사해야 한다.
그러기 위해서
Post 모델
의title 필드
에unique 옵션
을 추가한다.
from django.db import models
# Create your models here.
class Post(models.Model):
title = models.CharField(max_length=50, unique=True)
content = models.TextField()
dt_created = models.DateTimeField(verbose_name="Date Created", auto_now_add=True)
dt_modified = models.DateTimeField(verbose_name="Date Modified", auto_now=True)
def __str__(self):
return self.title
중복된 제목으로 인해 유효성 검증에 실패하면 유저에게 보여줄 에러메세지를 변경하는
error_messages 옵션
을 추가한다.
from django.db import models
# Create your models here.
class Post(models.Model):
title = models.CharField(max_length=50, unique=True, error_messages={"unique":"이미 같은 제목의 게시글이 존재합니다"})
content = models.TextField()
dt_created = models.DateTimeField(verbose_name="Date Created", auto_now_add=True)
dt_modified = models.DateTimeField(verbose_name="Date Modified", auto_now=True)
def __str__(self):
return self.title
post_create 뷰에서 유효성 검증을 통과한 경우와 그렇지 않은 경우에 따른 로직을 구분한다.
#views.py > post_create
def post_create(request):
if request.method =="POST":
post_form = PostForm(request.POST)
if post_form.is_valid():
new_post = post_form.save()
return redirect("post-detail", post_id=new_post.id)
else:
context = {"post_form": post_form}
else:
post_form = PostForm()
context = {"post_form": post_form}
return render(request, 'posts/post_form.html', context=context)
변경된 코드의 의미는 다음과 같다.
만약 요청 방식이
POST
라면if request.method =="POST":
PostForm 폼
과 요청이 담고있는 데이터를 바인딩 하고변수 post_form
에 지정한다.post_form = PostForm(request.POST)
만약
post_form
이 유효성 검증에 통과하면if post_form.is_valid():
post_form
을 기반으로 게시글을 생성하고변수 new_post
에 지정한다.new_post = post_form.save()
그리고 해당 게시글의 상세 페이지로 이동한다.
return redirect("post-detail", post_id=new_post.id)
만약
post_form
이 유효성 검증에 실패하면else:
변수 context
에post_form
을 지정한다.context = {"post_form": post_form}
이때,
else
문과if
문 밖으로 이동해 마지막return
문으로 이동한다. 결과적으로는post_form
(입력한 데이터)를유지
한 채로post_form.html
을 다시 렌더한다.만약 요청 방식이
POST
가 아니라면,else:
변수 post_form
에비어있는
Post_form 폼
을 지정한다.post_form = PostForm()
변수 context
에post_form을
지정한다.context = {"post_form": post_form}
context
에 담긴 데이터를 가지고post_form.html
을 렌더한다.return render(request, 'posts/post_form.html', context=context)
만약 보다 복잡한 유효성 검증이 필요한 경우, 장고에서 제공하는 유효성 검증인
validator
를 통한 유효성 검증을 방법을 선택할 수 있다.
validator
는 장고에서 제공하는bulit-in validator
와 직접 구현한validator
가 있는데, 여기서는 전자를 사용할 것이다.
django built-in validators에 접속하면 다양한 밸리데이터를 찾을 수 있다.
게시글 내용
에최소길이제한
유효성 검증을 적용해본다.
djnago.core.validators
모듈의최소길이제한
클래스를 불러온 뒤,content 필드
에 적용할최소길이
와에러메세지
를 입력한다.
밸리데이터를 추가하기 위해서는
리스트 형식
으로 작성해야 한다.
# models.py
from django.db import models
from django.core.validators import MinLengthValidator #최소길이제한 클래스
# Create your models here.
class Post(models.Model):
title = models.CharField(max_length=50, unique=True, error_messages={"unique":"이미 같은 제목의 게시글이 존재합니다"})
content = models.TextField(validators=[MinLengthValidator(10, "10자 이상 입력해주세요")]) #입력
dt_created = models.DateTimeField(verbose_name="Date Created", auto_now_add=True)
dt_modified = models.DateTimeField(verbose_name="Date Modified", auto_now=True)
def __str__(self):
return self.title
개발서버를 실행한 결과 아래와 같이 잘 적용되는 것을 확인할 수 있다.
이번에는 validator를 직접 만드는 방법을 알아본다.
validators.py을 생성하고 ValidationError 객체를 불러온 뒤, 함수를 작성한다.
#validators.py
from django.core.exceptions import ValidationError
def validate_symbols(value):
if ("@" in value) or ("#" in value):
raise ValidationError("'@'와 '#'은 포함될 수 없습니다.", code="symbol_error")
ValidationError
는 유효성 검증 에러를 일으키는 객체다.from django.core.exceptions import ValidationError
validate_symbols
함수는if
문을 사용해@ 또는 #이 포함
되면 유효성검증 에러를 발생시킨다.def validate_symbols(value): if ("@" in value) or ("#" in value): raise ValidationError("'@'와 '#'은 포함될 수 없습니다.", code="symbol_error")
작성한
validate_symbols
를Post 모델
의content 필드
에 적용한다.
#models.py
from django.db import models
from django.core.validators import MinLengthValidator
from .validators import validate_symbols #밸리데이터 불러오기
# Create your models here.
class Post(models.Model):
title = models.CharField(max_length=50, unique=True, error_messages={"unique":"이미 같은 제목의 게시글이 존재합니다"})
content = models.TextField(validators=[MinLengthValidator(10, "10자 이상 입력해주세요"),
validate_symbols]) #밸리데이터 적용
dt_created = models.DateTimeField(verbose_name="Date Created", auto_now_add=True)
dt_modified = models.DateTimeField(verbose_name="Date Modified", auto_now=True)
def __str__(self):
return self.title
개발서버를 실행한 결과 잘 작동되는 것을 확인할 수 있다.
지금의 폼 구조는 이쁘지가 않다. css를 적용하기 전 폼의 구조를 변경하는 방법을 알아본다.
현재는
PostForm 폼
을템플릿 태그 {{post_form}}
으로 일괄적으로 불러온다. 각 폼의 인풋을 별도로 불러오는 코드로 변경해 본다.
<!--post_form.html-->
<form method="post">{% csrf_token %}
<h3>제목</h3>
<p>{{post_form.title}}</p>
<h3>내용</h3>
<p>{{post_form.content}}</p>
<input type="submit" value="전송">
</form>
코드를 살펴보면 폼의 안쪽 속성으로 필드에 접근해서
<p>
태그로 감싼 형태로 작성했다. 결과는 아래와 같다.
좀 더 깔끔한 모습으로 변했다. 그러나
에러메세지
가 호출되지 않는다. 기존에는템플릿 태그 {{post_form}}
로 폼을 일괄적으로 호출해서 에러메세지가 포함되었다. 이번에는에러메세지도 별도로 호출
하는 코드를 작성한다.
<!--post_form.html-->
<form method="post">{% csrf_token %}
<h3>제목</h3>
<p>{{post_form.title}}</p>
{% for error in post_form.title.errors %}
<p>{{error}}</p>
{% endfor %}
<h3>내용</h3>
<p>{{post_form.content}}</p>
{% for error in post_form.content.errors %}
<p>{{error}}</p>
{% endfor %}
<input type="submit" value="전송">
</form>
에러메세지
가 여러 개일 수 있으니for
문을 활용해 각 필드의 에러 중, 각각의 에러를 호출한다. 에러는.errors
로 접근할 수 있다.
개발서버를 실행한 결과 에러메세지가 잘 호출되는 것을 확인할 수 있다.
폼 디자인을 적용하는 방법을 단계별로 알아본다.
정적파일 디렉토리
에form.css
파일을 생성하고 작성한다.
/*form.css*/
.error {
color: red;
}
css
를 적용하기 위해부모 템플릿을 상속
받는템플릿 태그
와정적파일
을 불러오는템플릿 태그
를 작성한다.
그리고
css 블럭 영역
에 방금 생선한form.css
파일을 적용한다.
다음으로
<form>
태그를content 블럭 영역
으로 감싼다.
마지막으로 에러메세지를 담고있는
<p>
태그에error 클래스
를 적용한다.
<!--post_form.html-->
{% extends "./base.html" %}
{% load static %}
{% block css %}
<link rel="stylesheet" href="{% static "posts/css/form.css" %}">
{% endblock css %}
{% block content %}
<form method="post">{% csrf_token %}
<h3>제목</h3>
<p>{{post_form.title}}</p>
{% for error in post_form.title.errors %}
<p class="error">{{error}}</p>
{% endfor %}
<h3>내용</h3>
<p>{{post_form.content}}</p>
{% for error in post_form.content.errors %}
<p class="error">{{error}}</p>
{% endfor %}
<input type="submit" value="전송">
</form>
{% endblock content %}
결과는 아래와 같다.
에러메세지
의 경우클래스
를 지정해서 해당 클래스에css
를 적용할 수 있었다.
그러나
인풋
의 경우필드별로 장고가 지정한 위젯
을 사용하기 때문에장고 공식 문서
에서해당 필드의 기본 위젯
이 무엇인지 확인한 뒤,해당 위젯에 클래스를 지정
한 뒤, 해당 클래스에css
를 적용해야 한다.
장고 공식문서를 통해 확인한 결과
CharField
의 기본 위젯은TextInput
이다.
forms.py
의Meta 클래스
에서widgets
은필드를 키
로 하고값으로 위젯
이름을 입력하면,다른 위젯
을 기본 위젯으로 사용할 수 있다. 또는 지금처럼직접 기본 위젯에 접근
해야 할 때 사용할 수 있다.
#forms.py
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ["title", "content"]
widgets = {"title": forms.TextInput(attrs={"class": "title", "placeholder": "제목이 입력하세요",}),
"content": forms.Textarea(attrs={"placeholder": "내용을 입력하세요"})}
위 코드를 확인해보면
Meta 클래스
에widgets
을 두고title 필드
의 위젯을TextInput
으로 지정하고attrs
로 해당 위젯의속성
에 접근해title 클래스
를 지정했다.
또한
title 필드
와content 필드
의placeholder
를 지정했다.
이후
form.css
에서title 클래스
에 대한css
를 선언한다.
/*form.css*/
.error {
color: red;
}
.title {
width: 400px;
}
이미 작성되어 있는 css를 적용하는 방법을 알아본다.
전달 받은 css는 다음과 같다.
/*post_form.css*/
h1,h2,h3,p,a,ul,li{
margin: 0;
padding: 0;
font-family : 'NanumMyeongjo';
font-weight: 200;
list-style: none;
text-decoration: none;
outline: none;
color: black;
}
a:hover,a:active,button:hover {
text-decoration: none;
}
html{
position: relative;
min-height: 100%;
margin: 0;
}
body {
min-height: 100%;
margin: 0;
padding: 0;
background: #2b2a2e;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: auto;
}
img {
margin: 0;
padding: 0;
border: 0;
}
#nav{
height: 60px;
}
#header{
margin: 20px 0 20px;
}
#content{
margin-bottom: 300px;
}
#footer{
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 75px;
background: rgba(41, 41, 44, 0.7);
}
.container{
width: 800px;
margin: 0 auto;
}
.logo{
width: 65%;
min-width: 899px;
margin: 0 auto;
padding-top: 17px;
}
.logo img{
width: 88px;
}
h1{
text-align: center;
font-size: 32px;
letter-spacing: -2px;
color: #fff;
}
.error{
text-align: right;
color: red;
padding-right: 20px;
padding-bottom: 20px;
}
.submit{
margin-bottom: 27px;
text-align: right;
}
input[type="submit"]{
padding: 10px 43px 11px 19px;
font-size: 15px;
font-family : 'NanumMyeongjo';
color: #fff;
border: 1px solid #5b595f;
border-radius: 1px;
letter-spacing: -0.35px;
background: url(../images/pen.svg)no-repeat 89px 11px/16px;
cursor: pointer;
}
input[type="submit"]:hover{
color: #ccc;
transition: all 0.2s;
}
.editor{
background: #f9f9f9;
}
input[type="text"]{
display: block;
width: 100%;
padding: 35px 33px 0 30px;
border: none;
font-size: 19px;
font-family : 'NanumMyeongjo';
background-color: transparent;
box-sizing: border-box;
}
textarea{
width: 100%;
height: 500px;
padding: 33px 33px 30px 30px;
border: none;
font-size: 15px;
line-height: 1.73;
font-family : 'NanumMyeongjo';
background-color: transparent;
resize: none;
box-sizing: border-box;
}
textarea,input:focus{
outline: none;
}
textarea::placeholder{
color: #a9abb7;
letter-spacing: -0.44px;
}
input::placeholder{
color: #a9abb7;
letter-spacing: -0.44px;
}
.btn_back a{
display: inline-block;
margin-top: 30px;
padding: 10px 19px 11px;
color: #e38917;
font-size: 15px;
border: 1px solid #e38917;
}
.btn_back a:hover{
color: #f2ad56;
transition: all .2s;
}
.footer{
width: 65%;
min-width: 899px;
margin: 0 auto;
}
.footer p{
text-align: right;
font-size: 20px;
line-height: 75px;
font-weight: 800;
letter-spacing: -0.85px;
color: #53534f;
}
작성된 css 적용을 위해 html 템플릿을 수정한다.
<!--post_form.html-->
{% extends "./base.html" %}
{% load static %}
{% block css %}
<link rel="stylesheet" href="{% static "posts/css/post_form.css" %}"> <!--css 파일 이름 변경-->
{% endblock css %}
{% block post_header %} <!--글 쓰기 버튼을 post_header 블럭 영역에 추가-->
<div class="container"> <!--글 쓰기 버튼 div 태그로 감싸고 클래스로 지정-->
<h1>글쓰기</h1>
</div>
{% endblock post_header %}
{% block content %}
<form method="post">{% csrf_token %}
<div class="submit">
<input type="submit" value="작성 완료">
</div>
<div class="editor"> <!--작성 영역을 div 태그로 감싸고 editor 클래스로 지정 -->
<p>{{post_form.title}}</p>
{% for error in post_form.title.errors %}
<p class="error">{{error}}</p>
{% endfor %}
<p>{{post_form.content}}</p>
{% for error in post_form.content.errors %}
<p class="error">{{error}}</p>
{% endfor %}
</div>
</form>
<div class="btn_back"> <!--목록으로 이동하는 div 태그 생성 후, 클래스 추가-->
<a href="{% url "post-list" %}">목록으로</a> <!--a 태그 작성-->
</div>
{% endblock content %}
기존에 작성된 게시글을 수정하는 페이지를 만들어 본다. 수정 페이지는
폼 페이지에 기존에 작성된 내용
을 불러오는 형태로 만들면 될 것 같다.
urls.py
에서 주석 처리된 url 패턴을주석 해제
하고name 속성
에post-update
를 작성한다.
#urls.py
from django.urls import path
from . import views
urlpatterns = [
# path('', views.index),
path('posts/', views.post_list, name="post-list"),
path('posts/new/', views.post_create, name="post-create"),
path('posts/<int:post_id>/', views.post_detail, name="post-detail"),
path('posts/<int:post_id>/edit/', views.post_update, name="post-update"), #주석 해제 및 name 속성 작성
# path('posts/<int:post_id>/delete/', views.post_delete),
]
상세 페이지에서 게시글을 수정할 수 있는
<a>
태그를 추가한다.
css 적용
을 위해 두 개의<div>
태그로 감싸고 클래스를 지정한다.
포스트의
id
를 넘겨준다.
<!--post_detail.html > block content-->
{% block content %}
<div class="content">
<p class="text">{{post.content|linebreaksbr}}</p>
<p class="date_md">수정일 <span>{{post.dt_modified}}</span></p>
</div>
<div class="btn">
<div class="btn_list">
<a href="{% url "post-list" %}">스토리 목록으로</a>
</div>
<div class="right_btn"> <!--추가-->
<div class="btn_modify"> <!--추가-->
<a href="{% url "post-update" post.id %}">수정하기</a> <!--추가-->
</div> <!--추가-->
</div> <!--추가-->
</div>
{% endblock content %}
views.py
에서post_update
함수를 추가한다.
#views.py > post_update
def post_update(request, post_id):
post = Post.objects.get(id=post_id)
if request.method == "POST":
post_form = PostForm(request.POST, instance=post)
if post_form.is_valid():
post_form.save()
return redirect("post-detail", post_id=post.id)
else:
post_form = PostForm(instance=post)
context = {"post_form": post_form}
return render(request, "posts/post_form.html", context)
post_update 함수를 생성하는데, 요청과, post_id를 인자로 받는 파라미터를 추가한다.
def post_update(request, post_id):
Post 모델
에서post_id로 전달 받은 인자
와동일한 값의 id
를 지닌 데이터를 불러와변수 post
에 지정한다.post = Post.objects.get(id=post_id)
만약 요청 방식이
POST
라면if request.method == "POST":
폼 PostForm
에POST에 담긴 데이터
를바인딩
하고, 새 인스턴스가 아닌post로 지정된 인스턴스
에 매칭시킨 후변수 post_form
에 지정한다.post_form = PostForm(request.POST, instance=post)
만약
변수 post_form
의 데이터가 유효성 검증을 통과하면if post_form.is_valid():
post_form
을 데이터 베이스에 저장한다.post_form.save()
>상세 페이지로 이동하는데, 상세 페이지에서는 포스트의 id를 필요로 하기 때문에 `post_id`에 다시 `post.id`를 담아서 넘겨준다.
```python
return redirect("post-detail", post_id=post.id)
만약 요청 방식이
POST
가 아니라면,else:
변수 post_form
에폼 PostForm
을 지정하는데, 새 인스턴스가 아닌,post로 지정된 인스턴스
에 매칭시킨다.post_form = PostForm(instance=post)
변수 context
에post_form
을 지정한다.context = {"post_form": post_form}
요청
과context
를 담아서post_form.html
템플릿을 렌더한다.return render(request, "posts/post_form.html", context)
포스트를 삭제하는 기능을 구현한다.
url 패턴의 주석을 해제
하고name 속성
을 작성한다.
#urls.py
from django.urls import path
from . import views
urlpatterns = [
# path('', views.index),
path('posts/', views.post_list, name="post-list"),
path('posts/new/', views.post_create, name="post-create"),
path('posts/<int:post_id>/', views.post_detail, name="post-detail"),
path('posts/<int:post_id>/edit/', views.post_update, name="post-update"),
path('posts/<int:post_id>/delete/', views.post_delete, name="post-delete), # 주석 해제
]
상세 페이지에
<a>
태그를 작성해 삭제하기 버튼을 추가한다.
<!--post_detail.html > block content-->
{% block content %}
<div class="content">
<p class="text">{{post.content|linebreaksbr}}</p>
<p class="date_md">수정일 <span>{{post.dt_modified}}</span></p>
</div>
<div class="btn">
<div class="btn_list">
<a href="{% url "post-list" %}">스토리 목록으로</a>
</div>
<div class="right_btn">
<div class="btn_modify">
<a href="{% url "post-update" post.id %}">수정하기</a>
</div>
<div class="btn_delete"> <!--추가-->
<a href="{% url "post-delete" post.id %}">삭제하기</a>
</div>
</div>
</div>
{% endblock content %}
포스트를 삭제하는 뷰를 작성한다.
#views.py
def post_delete(request, post_id):
post = Post.objects.get(id=post_id)
post.delete()
return redirect("post-list")
url로부터 전달받은 id에 해당하는
데이터를 삭제하는 코드
다. 삭제 후에는 목록 페이지로 리다이렉트 한다.
삭제하기 버튼 클릭 시, 컨펌 없이 바로 삭제가 된다면 유저 경험 측면에서 좋지 않다. 따라서
삭제하기 컨펌 페이지
를 만드는 방법을 알아본다.
삭제하기 버튼을 클릭하면 이동되는 템플릿을 작성한다.
<!--post_confirm_delete.html-->
{% extends './base.html' %}
{% load static %}
{% block css %}
<link rel="stylesheet" href="{% static 'posts/css/post_confirm_delete.css' %}">
{% endblock css %}
{% block content %}
<div class="confirm">
<p class="title">[{{post.title}}]</p>
<p>삭제하시겠습니까?</p>
<form method="POST">{% csrf_token %}
<div class="confirm_btn">
<input type="submit" value="삭제하기">
<a href="{% url "post-detail" post.id %}">돌아가기</a>
</div>
</form>
</div>
{% endblock content %}
css는
강의에서 전달받은 css
파일에 개인적으로 적용하고 싶은 부분을 추가했다.
/*post_confirm_delete.css*/
h1,h2,h3,p,a,ul,li{
margin: 0;
padding: 0;
font-family : 'NanumMyeongjo';
font-weight: 200;
list-style: none;
text-decoration: none;
outline: none;
color: black;
}
a:hover,a:active,button:hover {
text-decoration: none;
}
html{
position: relative;
min-height: 100%;
margin: 0;
}
body {
min-height: 100%;
margin: 0;
padding: 0;
background: #2b2a2e;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: auto;
}
img {
margin: 0;
padding: 0;
border: 0;
}
#nav{
height: 60px;
}
#content{
margin-top: 160px;
margin-bottom: 300px;
}
#footer{
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 75px;
background: rgba(41, 41, 44, 0.7);
}
.container{
width: 800px;
margin: 0 auto;
}
.logo{
width: 65%;
min-width: 899px;
margin: 0 auto;
padding-top: 17px;
}
.logo img{
width: 88px;
}
.confirm{
padding: 108px 0 124px;
text-align: center;
background: #fff;
}
p{
font-size: 17px;
color: #575362;
letter-spacing: -0.35px;
}
p.title{
padding-bottom: 8px;
font-size: 20px;
font-weight: bold;
color: #333236;
letter-spacing: -0.45px;
}
.confirm_btn{
padding-top: 46px;
}
.confirm_btn ul li{
display: inline-block;
}
.confirm_btn input{
display: inline-block;
padding: 10px 19px 11px;
font-size: 15px;
color: #f6f6f6;
background: red;
border: none;
}
.confirm_btn a{
display: inline-block;
margin-left: 18px;
padding: 10px 19px 11px;
font-size: 15px;
color: #69666d;
background: #f6f6f6;
border: none;
}
.confirm_btn a:hover{
color: #95929a;
transition: all .2s;
}
.confirm_btn input:hover{
color: red;
background: #f6f6f6;
transition: all .2s;
}
.footer{
width: 65%;
min-width: 899px;
margin: 0 auto;
}
.footer p{
text-align: right;
font-size: 20px;
line-height: 75px;
font-weight: 800;
letter-spacing: -0.85px;
color: #53534f;
}
기존에 작성한
post_delete
함수를 수정한다.
#views.py > post_delete
def post_delete(request, post_id):
post = Post.objects.get(id=post_id)
if request.method == "POST":
post.delete()
return redirect("post-list")
else:
context = {"post": post}
return render(request, "posts/post_confirm_delete.html", context=context)
요청 방식이
POST
인 경우 게시글을 삭제하고 포스트 목록 페이지로 리다이렉트 한다.
요청 방식이
GET
인 경우 삭제하기 컨펌 템플릿을 렌더한다.