1. 템플릿 설계
우리가 자주 본 웹사이트들을 보면 대부분의 페이지들이 항상 일정한 헤드, 메뉴, 푸터 등을 표시하는 것을 볼 수 있습니다. 현재 보고 있는 티스토리 블로그도 헤드(상단)과 사이드(좌측), 푸터 등이 항상 일정하게 나타나고 있습니다. 템플릿을 기능별로 구분한다면 재활용성이 높고 개발할 때 단순함을 더할 수 있습니다.
게시판의 모든 화면은 크게 두가지로 나눌 것입니다. 기본구조와 실제 화면 내용으로 구분됩니다. 기본구조는 HTML의 공통적인 head와 body에서 화면내용이 삽일 될 틀입니다. 화면내용은 뷰마다 제공하는 데이터를 사용자가 알아볼 수 있게 표현하는 부분입니다.
<!-- bbs/templates/base.html -->
<!DOCTYPE html>
<html lang="ko">
<head>
<!-- 페이지 별 타이틀 공간 -->
{% block title %}
<title>Django-Tutorial</title>
{% endblock title %}
<!-- 페이지 별 메타 데이터 공간 -->
{% block meta %}
{% endblock meta %}
<!-- 페이지 별 스크립트 공간 -->
{% block script %}
{% endblock script %}
<!-- 페이지 별 스크립트 공간 -->
{% block css %}
{% endblock css %}
</head>
<body>
{% block content %}
<!-- ctx['view'] -->
view: {{view}}
<br>
<!-- ctx['data'] -->
data: {{data}}
{% endblock content %}
</body>
</html>
템플릿 태그와 템플릿 변수
우선 기본구조를 먼저 생성합니다. 템플릿은 단순한 텍스트파일입니다. 템플릿엔진은 내부가 HTML인지 CSV인지 뭔지 아무런 관심도 없고 이해하지도 못합니다. 템플릿엔진이 알아볼 수 있는 건 딱 두 가지 템플릿태그와 템플릿변수입니다.
각 block태그들은 페이지마다 끼워 넣거나 대체해 넣을 수 있는 공간입니다. 해당 블록에 title bolck처럼 block의 시작과 종료사이에 값을 넣으면 해당하는 값의 기본값이 설정됩니다. 어떤 템플릿이든 이 템플릿을 상속받으면 해당 데이터를 덮어쓰거나 추가할 수 있습니다.
템플릿태그와 템플릿 변수
- 템플릿 태그는 for-in 반복문, if-elif-else 조건문 등의 템플릿 엔진이 이해하는 몇가지 기능들을 수행합니다. {% %}로 표현하고 for -in, if-elif-else처럼 처리해야할 텍스트가 2줄이상 될수 있는 경우 여는 태그 {{% %}}와 닫는 태그{{% %}}로 이루어집니다. 여는 태그와 닫는 태그 사이의 텍스트를 제어하는 것입니다. for-in태그는 endfor 로 종료되어야합니다.
- 템플릿 변수는 뷰로부터 전달받은 객체의 값을 인용할때 사용합니다. {{ }}로 표현하며 표현할 변수의 값이 딕셔너리일 경우에도 getattr연산자(.)으로 key에 접근할 수 있습니다. 리스트나 튜플일 때도 인덱스를 대괄호가 아니라 .으로 접근할 수 있습니다. 템플릿 엔진은 일부 문법에 대해 파이썬 문법보다 조금 더 유연함을 제공합니다.
템플릿은 수정 후 장고를 재시작할 필요 없습니다. 단순히 페이지를 새로고침 하는 것으로 변경사항을 확인할 수 있습니다.
이는 매 요청마다 템플릿 렌더링을 하고 있다는 것이며 성능적으로 별로 좋지 않습니다.
이 상태에서 뷰를 테스트했을 때와 같이 접속해보면 동일한 결과를 볼 수 있습니다.
- http://127.0.0.1/article/
- http://127.0.0.1/article/create/
- http://127.0.0.1/article/10/
- http://127.0.0.1/article/11/update/
2. 리스트 템플릿 구현
템플릿의 상속
정상적으로 출력이 된다면 각 화면별로 템플릿을 작성합니다. 반드시 base.html템플릿을 상속받도록 구현하는 것이 좋습니다. 그렇게 해야 중복된 코드를 줄이고 실수로 공통 코드를 빠트리지 않을 수 있습니다.
<!-- bbs/templates/article_list.html -->
{% extends 'base.html' %}
{% block title %}<title>게시글 목록</title>{% endblock title %}
{% block content %}
view: {{view}}
<br>
data: {{data}}
{% endblock content %}
article_list.html 템플릿은 extends의 인자인 'base.html' 파일을 상속받습니다. 템플릿에서 상속이란 기본 뼈대를 부모 템플릿으로 두고 각 block를 오버라이드 한다는 의미입니다. python의 클래스 상속과 마찬가지로 부모템플릿의 내용을 인용하고 싶다면 {{ block.super }} 변수를 사용하면 됩니다.
ArticleListView의 템플릿이 base.html에서 article_list.html로 변경되었으니 뷰의 template_name 클래스 변수를 변경합니다.
# bbs/views.py
class ArticleListView(TemplateView):
template_name = 'article_list.html' # 뷰 전용 템플릿 생성.
queryset = None
def get(self, request, *args, **kwargs):
print(request.GET)
ctx = {
'view': self.__class__.__name__,
'data': self.get_queryset()
}
return self.render_to_response(ctx)
def get_queryset(self):
if not self.queryset:
self.queryset = Article.objects.all()
return self.queryset
템플릿 파일명을 article_list.html로 지은 이유가 있습니다. 모델을 기반으로 하는 ListView나 DetailView 등은 클래스 변수 model을 정의할 경우 자동으로 모델명(소문자) + '_list.html' 또는 '_detail.html'로 템플릿파일을 자동으로 생성합니다. 파일명을 이런 식으로 작명한다면 나중에 더 복잡한 제네릭뷰를 사용할 때 편하고 오류를 줄일 수 있습니다.
템플릿 반복
리스트 템플릿은 0개 이상의 데이터를 표현해야 합니다. 0개의 경우를 따로 구현하지 않을 예정이지만 1개 이상인 경우 테이블 형태로 표현되도록 할 예정입니다.
<!-- bbs/templates/article_list.html -->
{% extends 'base.html' %}
{% block title %}<title>게시글 목록</title>{% endblock title %}
{% block content %}
<table>
<thead>
<th>게시글번호</th><th>제목</th><th>작성자</th>
</thead>
<tbody>
<tr>
<td>3</td><td>제목3</td><td>작성자</td>
</tr>
<tr>
<td>2</td><td>제목2</td><td>작성자</td>
</tr>
<tr>
<td>1</td><td>제목1</td><td>작성자</td>
</tr>
</tbody>
</table>
{% endblock content %}
코드를 제대로 입력하였다면 위 사진처럼 테이블 형태로 표현이 될 것입니다. 아직은 실제 데이터를 넣지 않았고 임의로 데이터를 만들어 틀만 구성하였습니다. 우선 table태그를 간단히 살펴보겠습니다.
table 태그
- thead와 tbody로 나뉩니다.
- thead는 칼럼의 제목들이 표시될 것입니다.
- hread 안에는 th태그들이 있는데 각 태그들은 칼럼들의 제목이 표시됩니다.
- tbody는 여러 개의 tr로 구성됩니다. tr은 데이터의 개수만큼 출력됩니다.
이 태그들 안에 적절한 속성값을 넣어주면 됩니다.
위 코드를 보면 tr태그 단위로 하나의 데이터라는 것을 알 수 있습니다. 즉 for 루프가 한 사이클 돌 때마다 tr태그를 생성해 주면 됩니다. 물론 tr태그 내부에 있는 td의 데이터도 채워서 생성해줘야 합니다.
<!-- bbs/templates/article_list.html -->
{% extends 'base.html' %}
{% block title %}<title>게시글 목록</title>{% endblock title %}
{% block content %}
<table>
<thead>
<th>번호</th><th>제목</th><th>작성자</th>
</thead>
<tbody>
{% for article in articles %} <! -- for tag 시작 -->
<tr>
<td>3</td><td>제목3</td><td>작성자</td>
</tr>
{% endfor %} <! -- for tag 종료 -->
</tbody>
</table>
{% endblock content %}
한 번 더 임의의 데이터를 가지고 출력을 하였습니다. 기존의 코드와 달라진 점은 for 태그가 사용되었다는 것입니다. python의 for - in 루프와 닮았습니다. 다른 점은 {% %}로 감싸져 있다는 것이고 {% endfor %}로 for-in 루프의 블록의 끝을 표시했다는 것입니다. for태그는 반복문을 실행하되 {% for ~ in ~ %} 에서부터 {% endfor %} 사이트 텍스트를 출력해 줍니다.
실제로 테스트를 하기 위해 뷰의 ctx를 수정합니다.
class ArticleListView(TemplateView):
template_name = 'article_list.html'
queryset = Article.objects.all()
def get(self, request, *args, **kwargs):
print(request.GET)
ctx = {
'articles': self.queryset
}
return self.render_to_response(ctx)
템플릿의 title 태그로 페이지 제목을 알 수 없으니 ctx의 view값을 제거했습니다. data라는 이름으로 모호했던 이름을 템플릿에서 사용하는 articles로 변경합니다. 현재 데이터베이스에 2개의 값이 저장된 상태여서 articles.count()는 2개입니다.
게시글이 총 2개이기 때문에 tr태그가 2번 반복해서 출력이 되었습니다. 그럼 마지막으로 td의 값을 실제 값으로 채워 넣어봅시다.
<!-- bbs/templates/article_list.html -->
{% extends 'base.html' %}
{% block title %}<title>게시글 목록</title>{% endblock title %}
{% block content %}
<table>
<thead>
<th>번호</th><th>제목</th><th>작성자</th>
</thead>
<tbody>
{% for article in articles %}
<tr>
<td>{{ article.pk }}</td><td>{{ article.title }}</td><td>{{ article.author }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock content %}
템플릿 변수를 사용하면 특정 값으로 치환할 수 있습니다. for 루프에서 선언한 변수 article의 값을 템플릿 변수에서 접근하는데 각각 pk, title, author 속성값으로 치환합니다. pk는 전에 설명한 대로 primarykey로 설정된 값인 id값이 반환됩니다.
bootstrap을 이용해서 디자인을 입혀보겠습니다. bootstrap의 한글 매뉴얼도 있으니 참고해서 보면 좋습니다.
{% extends 'base.html' %}
{% block title %}<title>게시글 목록</title>{% endblock title %}
{% block css %} <!-- bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
{% endblock css %}
{% block content %}
<table class="table table-hover table-responsive"> <!-- hover, responsive -->
<thead>
<th>번호</th><th>제목</th><th>작성자</th>
</thead>
<tbody>
{% for article in articles %}
<tr>
<td>{{ article.pk }}</td><td>{{ article.title }}</td><td>{{ article.author }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock content %}
부트스트랩의 자바스크립트는 아직까지 사용할 일이 없으니 추가하지 않습니다. 부트스트랩의 table은 css파일만 가져오면 됩니다. 공개 cdn으로부터 무료로 다운로드해서 사용하실 수 있습니다. table태그에. table. table-hover. table-responsive클래스를 추가합니다.. table은 부트스트랩의 테이블 디자인을 사용한다는 의미이고. table-hover는 각 줄에 마우스를 올리면 변화되는 효과입니다.. table-responsive는 모바일처럼 폭이 좁은 화면에서도 깨짐 없이 보일 수 있게 해주는 기능입니다.
다른 페이지로 링크
상세 페이지와 새 게시글 작성 페이지로 이동하는 링크를 추가하면 일단 완료됩니다.
<!-- bbs/templates/article_list.html -->
{% extends 'base.html' %}
{% block title %}<title>게시글 목록</title>{% endblock title %}
{% block css %}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
<style type="text/css">
tbody > tr {cursor: pointer;}
</style>
{% endblock css %}
{% block content %}
<table class="table table-hover table-responsive">
<thead>
<th>번호</th><th>제목</th><th>작성자</th>
</thead>
<tbody>
{% for article in articles %}
<tr onclick="location.href='/article/{{ article.pk }}/'"> <!-- 테이블 행 click 시 url 이동 -->
<td>{{ article.pk }}</td><td>{{ article.title }}</td><td>{{ article.author }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- 버튼 click 시 url 이동 -->
<a href="/article/create/"><button class="btn btn-primary" type="button">새 게시글 작성</button></a>
{% endblock content %}
테이블의 행을 클릭하면 그 행의 pk를 따라 이동하도록 하였습니다. tr, td태그에서는 a태그가 적용되지 않아서 tr태그에 onclick 이벤트를 등록하였습니다. 태그의 onclick 속성값을 정의하면 해당 태그를 클릭했을 때 정의된 값이 실행됩니다.
새 게시글 작성 버튼은 무조건 /article/create로 이동하도록 하였습니다.
3. 게시물 상세 보기 템플릿 구현
게시물 상세 보기는 게시물 목록화면보다 간단합니다. 모든 내용을 다 출력해주면 됩니다. 별 내용이 없으니 아예 수정하기 버튼까지 만들면 좋겠습니다.
<!-- bbs/templates/article_detail.html -->
{% extends 'base.html' %}
{% block title %}<title>게시글 상세 - {{ article.pk }}. {{ article.title }}</title>{% endblock title %}
{% block css %}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
{% endblock css %}
{% block content %}
<table class="table table-striped table-bordered">
<tr>
<th>번호</th>
<td>{{ article.pk }}</td>
</tr>
<tr>
<th>제목</th>
<td>{{ article.title }}</td>
</tr>
<tr>
<th>내용</th>
<td>{{ article.content }}</td>
</tr>
<tr>
<th>작성자</th>
<td>{{ article.author }}</td>
</tr>
<tr>
<th>작성자</th>
<td>{{ article.created_at }}</td>
</tr>
</table>
<a href="/article/{{ article.pk }}/update/"><button class="btn btn-primary" type="button">게시글 수정</button></a>
{% endblock content %}
게시글 목록 화면과 같이 뷰도 수정해 줍니다.
# bbs/views.py
class ArticleDetailView(TemplateView):
template_name = 'article_detail.html'
queryset = Article.objects.all()
pk_url_kwargs = 'article_id'
def get_object(self, queryset=None):
queryset = queryset or self.queryset
pk = self.kwargs.get(self.pk_url_kwargs)
article = queryset.filter(pk=pk).first()
if not article:
raise Http404('invalid pk')
return article
def get(self, request, *args, **kwargs):
article = self.get_object()
ctx = {
'article': article
}
return self.render_to_response(ctx)
게시글 목록과는 다르게 오직 하나의 데이터만 템플릿에 전달하기 때문에 게시글 객체 이름을 article이라고 정의했습니다.
table은 리스트와는 다르게 한 행에 한 속성씩 출력했습니다. 테이블에 마우스 클릭이 필요 없으니 hover효과를 빼고 가시성을 높이는. table-striped와. table-bordered 클래스를 추가했습니다.
게시글 수정 버튼을 클릭하면 해당 게시물의 업데이트 화면으로 이동할 수 있게 추가했습니다.
게시글 목록에서 아무행이나 클릭해서 상세페이지로 이동해 봅니다.
템플릿 필터
위 사진처럼 뜨면 정상적으로 출력된 것입니다. 여기서 조금 더 보기 편하게 하기 위해 두 가지 정도를 수정해 줄까 합니다.
- 내용의 데이터가 한 줄로 출력되었는데, 줄 바꿈이 제대로 적용되지 않았습니다.
- 작성일이 영어로 뜨는 부분이 조금 불편해 보입니다.
장고에서 제공해주는 필터라는 기능을 사용하여 수정을 해주겠습니다. 필터는 템플릿 변수 안에서 파이프( | )로 연결하여 값을 변경하는 함수를 말합니다. linebreaksbr이라는 필터를 사용하면 필터링할 문자열에서 모든 줄 바꿈 문자를 br태그로 변환해 줍니다.
날짜 시간의 포맷을 변경하는 것 역시 필터를 이용하여 수정해 줍니다. date라는 필터인데 이 필터는 인자를 넘겨줄 수도 있습니다. 인자를 넘겨주지 않으면 기본 포맷으로 출력되는데 원하는 데로 나온다는 보장이 없습니다 PHP의 시간 포맷과 비슷한데 "Y-m-d H:i"이라고 인자를 넘겨주면 익숙한 형태의 날짜와 시간이 출력됩니다.
<!-- bbs/templates/article_detail.html -->
{% extends 'base.html' %}
{% block title %}<title>게시글 상세 - {{ article.pk }}. {{ article.title }}</title>{% endblock title %}
{% block css %}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
{% endblock css %}
{% block content %}
<table class="table table-striped table-bordered">
<tr>
<th>번호</th>
<td>{{ article.pk }}</td>
</tr>
<tr>
<th>제목</th>
<td>{{ article.title }}</td>
</tr>
<tr>
<th>내용</th>
<td>{{ article.content | linebreaksbr }}</td>
</tr>
<tr>
<th>작성자</th>
<td>{{ article.author }}</td>
</tr>
<tr>
<th>작성일</th>
<td>{{ article.created_at | date:"Y-m-d H:i" }}</td>
</tr>
</table>
<a href="/article/{{ article.pk }}/update/"><button class="btn btn-primary" type="button">게시글 수정</button></a>
{% endblock content %}
4. 게시물 업데이트 템플릿 구현
게시물 상세화면을 조금 수정해서 템플릿을 구현할 것입니다. 게시물 업데이트 화면은 번호와 작성일은 수정할 수 없고 제목, 내용, 작성자만 변경할 수 있게 할 것입니다. 각 항목들은 서버에 저장되어 있는 값들을 기본으로 채워 넣은 상태로 보여줍니다. 그래야 사용자가 변경하기 쉬우니깐요.
<!-- bbs/templates/article_update.html -->
{% extends 'base.html' %}
{% block title %}<title>게시글 상세 - {{ article.pk }}. {{ article.title }}</title>{% endblock title %}
{% block css %}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
{% endblock css %}
{% block content %}
<!-- form -->
<form action="/article/{{ article.pk }}/update/" method="post" class="form-horizontal">
{% csrf_token %} <!-- csrftoken 태그 -->
<input type="hidden" name="action" value="update"> <!-- action -->
<table class="table table-striped table-bordered">
<tr>
<th>번호</th>
<td>{{ article.pk }}</td>
</tr>
<tr>
<th>제목</th> <!-- 제목 입력 -->
<td><input type="text" class="form-control" name="title" value="{{ article.title }}"></td>
</tr>
<tr>
<th>내용</th> <!-- 내용 입력 -->
<td><textarea rows="10" class="form-control" name="content">{{ article.content }}</textarea></td>
</tr>
<tr>
<th>작성자</th> <!-- 작성자 입력 -->
<td><input type="text" class="form-control" name="author" value="{{ article.author }}"></td>
</tr>
<tr>
<th>작성일</th>
<td>{{ article.created_at | date:"Y-m-d H:i" }}</td>
</tr>
</table>
<button class="btn btn-primary" type="submit">게시글 저장</button>
</form>
{% endblock content %}
테이블과 버튼을 form태그로 감싸서 테이블 안의 input, textarea의 데이터들을 전송할 수 있게 했습니다. form 태그부터 속성을 살펴보면, action은 update 액션의 url을 지정했고, method는 post를 지정했습니다. class를 form-horizontal로 지정했는데 html 엘리먼트가 수병으로 잘 정리되도록 하는 부트스트랩 속성입니다.
그 아래를 보면 csrf_token이라는 태그가 있습니다. 이 태그는 <input type="hidden" name="csrfmiddlewaretoke" value="kjxqvcTIDJ...... w2 RT9 HMHhdF"> 식으로 자동으로 태그를 만들어 줍니다. 장고는 이 요청은 csrfmiddlewaretoken이라는 값이 있어야만 정상적인 요청으로 인식합니다. 템플릿 엔진은 csrf_toke 태그를 만나면 자동으로 csrf_verification 프레임워크에서 생성한 csrfmiddlewaretoken의 값을 이용해서 html 태그로 변경해 줍니다. 그리고 post 요청을 했을 때 장고의 미들 웨어에서 이 값을 검증하고 비정상일 경우 오류를 반환합니다. 템플릿에 {% csrf_token %} 토큰만 넣으면 됩니다.
그 아래는 hidden 타입의 input태그를 추가하였습니다. action이라는 이름에 update라는 값을 지정했습니다. hidden 타입은 사용자에게는 보이지 않는 input태그입니다. action이라는 값을 사용자에게 보여주지도 않고 볼 수도 없으니 변경할 수도 없도록 한 것입니다.
값을 입력받을 때 1줄로 입력받아도 된다면 input, 2줄 이상의 값을 입력받아야 한다면 textarea를 사용합니다.
textarea는 엔터키를 줄 바꿈으로 인식합니다.
마지막으로 버튼을 둘러싸고 있던 a 태그를 제거하고 type을 submit으로 변경하였습니다. submit타입의 버튼은 클릭 시 해당 버튼을 둘러싸고 있는 가장 가까운 폼을 서버에 전송합니다. 물론 form 태그 내부에 있는 모든 데이터들을 가지고 전송이 됩니다. 각 태그들의 이름이 key가 되고 value가 값이 되어 서버에 전송이 됩니다. 이렇게 전송된 값들은 장고의 미들웨어에서 자동으로 딕셔너리 형태로 변환 후 request.POST 객체에 저장이 됩니다.
이제 처음 부분에 추가하였던 데코레이터를 삭제해주어야 합니다. csrf_exempt 데코레이터는 테스트용으로만 사용하고 가급적 사용하지 않아야 합니다.
위 사진처럼 정상적으로 출력이 됩니다. 하지만 내용을 수정하고 게시글 수정 버튼을 눌러도 깜박임이 있을 뿐 저장이 잘 되었는지 확인을 할 방법이 없습니다.
메시지창을 만들어서 사용자 요청이 성공하였는지 에러가 발생하였는지 알려주는 것이 좋을 것 같습니다.
장고의 messages 프레임 워크를 사용하면 사용자에게 messages를 보낼 수 있습니다. messages프레임 워크는 아무 때나 메시지 내용을 기록하면서 템플릿에서 데이터를 출력할 때까지 임시로 데이터를 저장해두는 프레임워크입니다. 로그처럼 레벨이 있기 때문에 오류인지, 단순한 정보인지 구분하기 좋습니다.
먼저 views에서 messages 프레임 워크에 메시지를 입력하도록 수정해 줍니다.
# bbs/views.py
from django.contrib import messages
# 생략
class ArticleCreateUpdateView(TemplateView):
template_name = 'article_update.html'
queryset = Article.objects.all()
pk_url_kwargs = 'article_id'
def get_object(self, queryset=None):
queryset = queryset or self.queryset
pk = self.kwargs.get(self.pk_url_kwargs)
article = queryset.filter(pk=pk).first()
if pk and not article:
raise Http404('invalid pk')
return article
def get(self, request, *args, **kwargs):
article = self.get_object()
ctx = {
'article': article,
}
return self.render_to_response(ctx)
def post(self, request, *args, **kwargs):
action = request.POST.get('action')
post_data = {key: request.POST.get(key) for key in ('title', 'content', 'author')}
for key in post_data:
if not post_data[key]:
messages.error(self.request, '{} 값이 존재하지 않습니다.'.format(key), extra_tags='danger')
if len(messages.get_messages(request)) == 0:
if action == 'create':
article = Article.objects.create(**post_data)
messages.success(self.request, '게시글이 저장되었습니다.')
elif action == 'update':
article = self.get_object()
for key, value in post_data.items():
setattr(article, key, value)
article.save()
messages.success(self.request, '게시글이 저장되었습니다.')
else:
messages.error(self.request, '알 수 없는 요청입니다.', extra_tags='danger')
return HttpResponseRedirect('/article/') # 정상적인 저장이 완료되면 '/articles/'로 이동됨
ctx = {
'article': self.get_object() if action == 'update' else None
}
return self.render_to_response(ctx)
messages프레임워크는 뷰에서 사용하는 방법이 간단합니다. messages모듈의 debug, info, success, warning, error의 5가지의 함수중 하나를 선택해서 request객체와 저장할 메시지를 전달해주면 됩니다.
messages.get_messages(request) 함수는 현재까지 저장된 메시지들을 반환합니다. 저장된 메시지들이 1개 이상이라면 현재 코드에서는 반드시 오류가 발생했다는 것이기 때문에 액션로직을 실행하지 않도록 했습니다. artice변수는 액션로직 안에서 정의하기 때문에 만약 오류가 발생한다면 action이 'update'인 경우 artice을 검색해 오고 'create'인 경우는 None를 저장하도록 했습니다.
장고에서 템플릿으로 messages라는 객체로 저장된 메시지들이 전달이 됩니다.
<!-- bbs/templates/article_update.html -->
{% extends 'base.html' %}
{% block title %}<title>게시글 상세 - {{ article.pk }}. {{ article.title }}</title>{% endblock title %}
{% block css %}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
{% endblock css %}
{% block content %}
{% if messages %} <!-- message 프레임워크 -->
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible" role="alert">
{{ message }}
</div>
{% endfor %}
{% endif %}
<!-- 생략 -->
{% endblock content %}
if 템플릿 태그로 messages 객체가 있는지 확인합니다. if태그는 반드시 endif태그로 종료되어야 합니다.
messages객체는 iterable 객체이기 때문에 for-in루프로 반복출력해야 합니다. for-in루프처럼 iterate를 진행해야 메시지가 사용된 것으로 변경됩니다. message 그 자체를 출력해도 되고 message.tags 또는 message.leve을 이용해도 됩니다.
message.level은 message를 저장할 때 사용하는 그 레벨이 출력이 되고 tags는 extra_tags와 message.level의 조합입니다.
여기에서는 message.tags를 이용했는데 extra_tags를 전달하지 않았기 때문에 level값만 출력이 됩니다. bootstrap의 alert class를 사용하면 쉽게 강조표시를 할 수 있습니다. alert-success, alert-info, alert-warning, alert-danger 등에 따라 색상이 달라지기 때문에 message의 레벨을 적절히 조합하면 손쉽게 일관성 있는 강조 표시를 할 수 있습니다. messages 에는 danger라는 레벨이 없기 때문에 error라는 레벨의 함수에는 extra_tags를 이용해서 error를 추가해 줬습니다. 그러면 message.tags 는 'danger error'를 출력합니다.
부트스트랩 네비게이션 바
업데이트까지 정상적으로 되는 것이 확인되었습니다. 목록보기 바로 갈 수 있는 버튼이 없어 살짝 불편하기 때문에 상단의 네비게이션바에서 홈 버튼을 구현해 주겠습니다.
<!-- bbs/templates/base.html -->
<!DOCTYPE html>
<html lang="ko">
<head>
{% block title %}
<title>bbs - minitutorial</title>
{% endblock title %}
{% block meta %}
{% endblock meta %}
{% block scripts %}
{% endblock scripts %}
{% block css %}
{% endblock css %}
</head>
<body>
{% block header %}
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="/article/">게시글 목록</a>
</div>
</div>
</nav>
{% endblock header %}
{% block content %}
{% endblock content %}
</body>
</html>
공통적으로 표시되어야 할 부분이기 때문에 base.html에 header 블럭을 추가하고 그 안에 코드를 넣었습니다. 각페이지에서 네비게이션 바를 변형시키고 싶다면 block 태그를 이용해서 변경하면 됩니다. base.html은 항상 특정 페이지에서 변경이 있을 수 있다는 점을 염두에 두고 각 부분마다 block으로 정의해두면 좋습니다. header 블럭도 여러 태그로 구성되어 있는데 각 태그마다 블럭을 정의해도 괜찮습니다.
5. 게시물 작성 템플릿 구현
게시물 작성 페이지는 이전의 update와 동일한 뷰와 템플릿을 사용해서 만들겠습니다. 아무것도 하지 않아도 게시글 목록 화면에서 게시글 작성 버튼을 누르면 게시글 작성 페이지로 잘 이동합니다. 하지만 제목이 게시글 수정으로 되어 있고, 게시글 저장 버튼을 클릭했을 때도 오류가 발생합니다. (action이 update로 되어 있기 때문입니다.)
create, update 분리
<!-- bbs/templates/article_update.html -->
{% extends 'base.html' %}
{% block title %}<title>게시글 수정 - {{ article.pk }}. {{ article.title }}</title>{% endblock title %}
{% block css %}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
{% endblock css %}
{% block content %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible" role="alert">
{{ message }}
</div>
{% endfor %}
{% endif %}
<form action="." method="post" class="form-horizontal"> # action 변경
{% csrf_token %}
<input type="hidden" name="action" value="{% if article %}update{% else %}create{% endif %}">
<table class="table table-striped table-bordered">
<tr>
<th>번호</th>
<td>{{ article.pk }}</td>
</tr>
<tr>
<th>제목</th>
<td><input type="text" class="form-control" name="title" value="{{ article.title }}"></td>
</tr>
<tr>
<th>내용</th>
<td><textarea rows="10" class="form-control" name="content">{{ article.content }}</textarea></td>
</tr>
<tr>
<th>작성자</th>
<td><input type="text" class="form-control" name="author" value="{{ article.author }}"></td>
</tr>
<tr>
<th>작성일</th>
<td>{{ article.created_at | date:"Y-m-d H:i" }}</td>
</tr>
</table>
<button class="btn btn-primary" type="submit">게시글 저장</button>
</form>
{% endblock content %}
update는 url에 article.pk값이 포함되기 때문에 아직 객체가 생성되지 않은 create 액션은 사용할 수 없는 url입니다. 그래서 현재 url을 의미하는. 을 이용하였습니다. 어차피 post나 get이나 모두 같은 뷰에서 처리하니 url이 같아도 상관없습니다.
action의 값은 뷰에서 article 객체가 전달이 되었으면 'update'그렇지 않으면 'create'가 되도록 수정했습니다. 게시글 생성화면에서 article객체가 전달되지 않지만 article.py, article.title 등의 변수는 python과는 달리 오류를 발생하지 않습니다. None 객체의 속성값에 접근하면 None이 출력됩니다.
이대로 테스트를 해보면 게시글 저장이 정상적으로 작동이 되지만, 저장된 내용으로 채워진 게시글 수정화면으로 이동합니다. 저장 전과 후의 화면이 혼동이 될 수 있으니 아예 새 게시글이 정상적으로 저장이 되면 게시글 목록화면으로 이동시킵니다.
# bbs/views.py
# 생략
class ArticleCreateUpdateView(TemplateView):
template_name = 'article_update.html'
queryset = Article.objects.all()
pk_url_kwargs = 'article_id'
def get_object(self, queryset=None):
queryset = queryset or self.queryset
pk = self.kwargs.get(self.pk_url_kwargs)
article = queryset.filter(pk=pk).first()
if pk and not article:
raise Http404('invalid pk')
return article
def get(self, request, *args, **kwargs):
article = self.get_object()
ctx = {
'article': article,
}
return self.render_to_response(ctx)
def post(self, request, *args, **kwargs):
action = request.POST.get('action')
post_data = {key: request.POST.get(key) for key in ('title', 'content', 'author')}
for key in post_data:
if not post_data[key]:
messages.error(self.request, '{} 값이 존재하지 않습니다.'.format(key), extra_tags='danger')
if len(messages.get_messages(request)) == 0:
if action == 'create':
article = Article.objects.create(**post_data)
messages.success(self.request, '게시글이 저장되었습니다.')
elif action == 'update':
article = self.get_object()
for key, value in post_data.items():
setattr(article, key, value)
article.save()
messages.success(self.request, '게시글이 저장되었습니다.')
else:
messages.error(self.request, '알 수 없는 요청입니다.', extra_tags='danger')
return HttpResponseRedirect('/article/') # 정상적인 저장이 완료되면 '/articles/'로 이동됨
ctx = {
'article': self.get_object() if action == 'update' else None
}
return self.render_to_response(ctx)
템플릿 내 message 중복
각 화면 템플릿마다 message 출력을 위한 코드가 동일한 모습으로 추가되어 있습니다. 모든 화면이 base.html 템플릿을 확장하고 있기 때문에 base.html 템플릿에서 처리하면 base.html 템플릿을 확장하는 곳에서는 따로 처리해줄 필요가 없어집니다.
<!-- bbs/templates/base.html -->
<!DOCTYPE html>
<html lang="ko">
<head>
{% block title %}
<title>bbs - minitutorial</title>
{% endblock title %}
{% block meta %}
{% endblock meta %}
{% block scripts %}
{% endblock scripts %}
{% block css %}
{% endblock css %}
</head>
<body>
{% block header %}
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="/article/">게시글 목록</a>
</div>
</div>
</nav>
{% if messages %} <!-- 추가된 부분 시작 -->
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible" role="alert">
{{ message }}
</div>
{% endfor %}
{% endif %} <!-- 추가된 부분 끝 -->
{% endblock header %}
{% block content %}
{% endblock content %}
</body>
</html>
'BackEnd > Django, DRF' 카테고리의 다른 글
[Django Tutorial] 사용자 인증 - 회원가입 (1) | 2023.01.12 |
---|---|
[Django Tutorial] 사용자 인증 - auth프레임워크, 커스텀 사용자 모델(User) (0) | 2023.01.11 |
[Django Tutorial] 뷰 만들기 (0) | 2023.01.09 |
[Django Tutorial] 모델 만들기 (0) | 2023.01.07 |
[Django Tutorial] 가상환경, 프로젝트 만들기 (1) | 2023.01.06 |