🌉 점프 투 장고 사이트를 참고하여 일정 단위의
chapter
에 대해 실습 후새롭게 알게된 내용
, 혹은복습이 꼭 필요한 내용
에 대해 정리!!
2-07. 스태틱
템플릿에 스타일 적용
html
문서 상단에 하단의 코드 추가- 템플릿에 스타일시트와 같은 스태틱 파일을 사용하기 위해서는 템플릿 최상단에
{% load static %}
태그를 먼저 삽입해야 함.- 그래야만
{% static ... %}
와 같은 템플릿 태그를 사용 가능.
{% load static %}
<link rel="stylesheet" type="text/css" href="{% static 'style.css' %}">
2-08. 부트스트랩
- 부트스트랩(
Bootstrap
)은 디자이너의 도움 없이도 개발자 혼자서 상당히 괜찮은 수준의 웹 페이지를 만들수 있게 도와주는 프레임워크.- 부트스트랩은 트위터(
오픈소스
프로젝트
부트스트랩 설치
부트스트랩 주의사항
3.x
, 4.x
, 5.x
등의 버전이 존재하고 메이저 번호(3
, 4
, 5
)에 따라 그 사용방법이 다름.bootstrap.min.css
파일과 bootstrap.min.js
파일을 카피하여 static
디렉터리에 저장부트스트랩 적용
질문 목록 template
에 부트스트랩을 다음처럼 적용📄 /templates/pybo/question_list.html
{% load static %} # 추가
<link rel="stylesheet" type="text/css" href="{% static bootstrap.min.css' %}"> # 추가
{% if question_list %}
(... 생략 ...)
style
을 적용 했으니 template
도 부트스트랩을 사용하도록 다음과 같이 수정
- 기존에는
<ul>
태그로 심플하게 작성했던 질문 목록을테이블 구조
로 변경.- 그리고 번호와 작성일시 항목도 추가함.
- 번호는
for
문의 현재 순서를 의미하는{{ forloop.counter }}
를 이용.- 여기서 사용한
class="container my-3"
,class="table"
,class="table-dark"
등은 부트스트랩 스타일에 정의되어 있는 클래스들.
📄 /templates/pybo/question_list.html
{% load static %}
<link rel="stylesheet" type="text/css" href="{% static 'bootstrap.min.css' %}">
<div class="container my-3">
<table class="table">
<thead>
<tr class="table-dark">
<th>번호</th>
<th>제목</th>
<th>작성일시</th>
</tr>
</thead>
<tbody>
{% if question_list %}
{% for question in question_list %}
<tr>
<td>{{ forloop.counter }}</td>
<td>
<a href="{% url 'pybo:detail' question.id %}">{{ question.subject }}</a>
</td>
<td>{{ question.create_date }}</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="3">질문이 없습니다.</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
📌 부트스트랩에 대한 자세한 내용은 다음 URL을 참조!!!
- 앞으로 템플릿 작성시에 계속 부트스트랩 스타일들을 사용할 것
https://getbootstrap.com/docs/5.1/getting-started/introduction/
이제 다음처럼 부트스트랩이 적용된 질문 목록을 볼 수 있을
이어서 질문 상세 template
에도 다음처럼 부트스트랩을 적용
- 이번에는 수정사항이 좀 많음.
- 부트스트랩으로 화면을 구성하다 보면 가끔은 이렇게 많은 양의
HTML
코드를 작성해야 함.질문
이나답변
은하나의 뭉치
에 해당하므로 부트스트랩의card 컴포넌트
를 사용.
📄 /templates/pybo/question_detail.html
{% load static %}
<link rel="stylesheet" type="text/css" href="{% static 'bootstrap.min.css' %}">
<div class="container my-3">
<!-- 질문 -->
<h2 class="border-bottom py-2">{{ question.subject }}</h2>
<div class="card my-3">
<div class="card-body">
<div class="card-text" style="white-space: pre-line;">{{ question.content }}</div>
<div class="d-flex justify-content-end">
<div class="badge bg-light text-dark p-2">
{{ question.create_date }}
</div>
</div>
</div>
</div>
<!-- 답변 -->
<h5 class="border-bottom my-3 py-2">{{question.answer_set.count}}개의 답변이 있습니다.</h5>
{% for answer in question.answer_set.all %}
<div class="card my-3">
<div class="card-body">
<div class="card-text" style="white-space: pre-line;">{{ answer.content }}</div>
<div class="d-flex justify-content-end">
<div class="badge bg-light text-dark p-2">
{{ answer.create_date }}
</div>
</div>
</div>
</div>
{% endfor %}
<!-- 답변 등록 -->
<form action="{% url 'pybo:answer_create' question.id %}" method="post" class="my-3">
{% csrf_token %}
<div class="mb-3">
<label for="content" class="form-label">답변내용</label>
<textarea name="content" id="content" class="form-control" rows="10"></textarea>
</div>
<input type="submit" value="답변등록" class="btn btn-primary">
</form>
</div>
부트스트랩 card 컴포넌트 :
질문 상세 템플릿에 사용한 부트스트랩 클래스
- 그리고 질문 내용과 답변 내용에는
style="white-space: pre-line;"
과 같은 스타일을 지정해 줌.- 글 내용의 줄 바꿈을 정상적으로 표시하기위해 적용한 스타일임.
부트스트랩을 적용한 질문 상세 화면
2-09. 템플릿 상속
표준 HTML 구조
- 표준
HTML
문서의 구조는 위의 예처럼html
,head
,body
엘리먼트가 있어야 하며,CSS
파일 링크는head
엘리먼트 안에 있어야 함.
또한head
엘리먼트 안에는meta
,title
엘리먼트 등이 포함되어야 함.
📄 [표준 HTML 구조의 예]
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" type="text/css" href="/static/bootstrap.min.css">
<title>Hello, pybo!</title>
</head>
<body>
(... 생략 ...)
</body>
</html>
<table> (... 생략 ...) </table> <!-- table 엘리먼트 -->
위에서 <table>
은 table 태그
이고 <table> ~ </table>
처럼 table
태그로 시작해서 table
태그로 닫힌 구간(Block
)은 table 엘리먼트
이다.
템플릿 상속
HTML
구조가 되도록 수정하는 방법은 무엇일까?
- 그런데 템플릿 파일들을 모두 표준
HTML
구조로 변경하면body
엘리먼트 바깥 부분(head
엘리먼트 등)은 모두 같은 내용으로 중복될 것임.- 그러면
CSS
파일 이름이 변경되거나 새로운CSS
파일이 추가될 때마다 모든 템플릿 파일을 일일이 수정해야 함.
템플릿 상속(extend)
기능을 제공함.템플릿 상속
: 기본 틀이 되는 템플릿을 먼저 작성하고 다른 템플릿에서 그 템플릿을 상속해 사용하는 방법base.html
템플릿
- 모든 템플릿이 상속해야 하는 템플릿
- 표준
HTML
문서의 기본 틀body
엘리먼트 안의{% block content %}
와{% endblock %}
템플릿 태그는base.html을
상속한 템플릿에서 개별적으로 구현해야 하는 영역이 됨.
📄 /templates/base.html
{% load static %}
<!doctype html>
<html lang="ko">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" type="text/css" href="{% static 'bootstrap.min.css' %}">
<!-- pybo CSS -->
<link rel="stylesheet" type="text/css" href="{% static 'style.css' %}">
<title>Hello, pybo!</title>
</head>
<body>
<!-- 기본 템플릿 안에 삽입될 내용 Start -->
{% block content %}
{% endblock %}
<!-- 기본 템플릿 안에 삽입될 내용 End -->
</body>
</html>
question_list
템플릿question_list.html
템플릿을 다음과 같이 변경
base.html
템플릿을 상속하기 위해{% extends 'base.html' %}
처럼extends
템플릿 문법을 사용.- 상단의 두 줄은
base.html
에 이미 있는 내용이므로 삭제.- 그리고
{% block content %}
와{% endblock %}
사이에question_list.html
에서만 쓰이는 내용을 작성.- 이렇게 하면 이제
question_list.html
은base.html
템플릿을 상속받아 표준HTML
문서로 바뀌게 됨.
📄 /templates/pybo/question_list.html
{% extends 'base.html' %}
{% block content %}
<div class="container my-3">
<table class="table">
(... 생략 ...)
</table>
</div>
{% endblock %}
question_detail
템플릿question_detail.html
템플릿을 다음과 같이 변경
{% extends 'base.html' %}
템플릿 태그를 맨 위에 추가하고 기존 내용 위 아래로{% block content %}
와{% endblock %}
를 작성.- 템플릿 상속을 적용한 후 질문 목록, 질문 상세를 조회해보면 화면에 보여지는 것은 동일하지만 표준 HTML 구조로 변경된 것을 확인할 수 있음.
📄 /templates/pybo/question_list.html
{% extends 'base.html' %}
{% block content %}
<div class="container my-3">
<h2 class="border-bottom py-2">{{ question.subject }}</h2>
(... 생략 ...)
</form>
</div>
{% endblock %}
2-10. 폼
질문 등록
html
파일에 질문 등록하기
버튼 만들기
<a href="...">
과 같은 링크이지만 부트스트랩의btn btn-primary
클래스를 적용하면 버튼으로 보이게 됨.- 버튼을 클릭하면
pybo:question_create
별칭에 해당되는URL
이 호출될 것
📄 /templates/pybo/question_list.html
(... 생략 ...)
</table>
<a href="{% url 'pybo:question_create' %}" class="btn btn-primary">질문 등록하기</a> # 추가
</div>
{% endblock %}
URL 매핑
pybo:question_create
별칭에 해당되는 URL 매핑 규칙
을 추가
question_create
함수를 호출하도록 매핑
📄 /pybo/urls.py
from pybo.views import index, detail, question_create, answer_create # 추가
(... 생략 ...)
urlpatterns = [
(... 생략 ...)
path('question/create/', question_create, name='question_create'), # 추가
]
폼
views.py
에서 question_create
함수를 작성하기 전에 폼(Form
)을 작성
- 폼(
Form
)은 페이지 요청시 전달되는 파라미터들을 쉽게 관리하기 위해 사용하는 클래스- 폼은 필수 파라미터의 값이 누락되지 않았는지, 파라미터의 형식은 적절한지 등을 검증할 목적으로 사용
- 이 외에도 HTML을 자동으로 생성하거나 폼에 연결된 모델을 이용하여 데이터를 저장하는 기능도 있음.
question_create
함수를 호출하도록 매핑
질문 등록시 사용할 QuestionForm
을 forms.py
파일에 작성
QuestionForm
은 모델 폼(forms.ModelForm
)을 상속- 장고의 폼은 일반 폼(
forms.Form
)과 모델 폼(forms.ModelForm
)이 있음.
- 모델 폼(
forms.ModelForm
) : 모델(Model
)과 연결된 폼으로 폼을 저장하면 연결된 모델의 데이터를 저장할수 있는 폼
👉 모델 폼은 이너 클래스인Meta
클래스가 반드시 필요
👉Meta
클래스에는 사용할 모델과 모델의 속성을 기재해야 함.
즉, QuestionForm
은 Question
모델과 연결된 폼이고 속성으로 Question
모델의 subject
와 content
를 사용한다고 정의한 것
📄 /pybo/forms.py
from django import forms
from pybo.models import Question
class QuestionForm(forms.ModelForm):
class Meta:
model = Question # 사용할 모델
fields = ['subject', 'content'] # QuestionForm에서 사용할 Question 모델의 속성
view 함수
views.py
에서 question_create
함수를 작성
question_create
함수는 위에서 작성한QuestionForm
을 사용render
함수에 전달한{'form': form}
은 템플릿에서 질문 등록시 사용할 폼 엘리먼트를 생성할 때 사용
📄 /pybo/forms.py
from .forms import QuestionForm
(... 생략 ...)
def question_create(request):
form = QuestionForm()
return render(request, 'pybo/question_form.html', {'form': form})
template
pybo/question_form.html
템플릿을 작성
- 템플릿에서 사용한
{{ form.as_p }}
의form
은question_create
함수에서 전달한QuestionForm
의 객체{{ form.as_p }}
는 폼에 정의한subject
,content
속성에 해당하는HTML
코드를 자동으로 생성
📄 /template/pybo/question_form.html
{% extends 'base.html' %}
{% block content %}
<div class="container">
<h5 class="my-3 border-bottom pb-2">질문등록</h5>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">저장하기</button>
</form>
</div>
{% endblock %}
action
속성보통 form
태그에는 항상 action
속성을 지정하여 submit
실행시 action
에 정의된 URL
로 폼을 전송해야 함.
- 하지만 여기서는 특별하게
action
속성을 지정하지 않았음.form
태그에action
속성을 지정하지 않으면 현재 페이지의URL
이 디폴트action
으로 설정됨.
물론 action
속성을 다음처럼 명확하게 지정해도 됨.
<form method="post" action="{% url 'pybo:question_create' %}">
question_form.html
템플릿은 질문 등록
에서만 사용 가능
- 이후에 진행할
질문 수정
에서는 이 템플릿을 활용할 수가 없음.- 왜냐하면
질문 수정
일 경우에는action
값을 달리해야 하기 때문- 동일한 템플릿을 여러 기능에서 함께 사용할 경우에는 이처럼
form
의action
속성을 비워두는 트릭을 종종 사용- 우리는 이후에
질문 수정
기능을 구현할 때도question_form.html
템플릿을 사용할 것이므로action
속성은 비워둘 것!!
GET과 POST
서버 재시작 후 브라우저에서 정상 작동 여부 확인
- forms.py와 같은 신규 파일 작성시에는 로컬 서버 재시작이 필요
질문 목록 화면 하단에 "질문 등록하기" 버튼이 추가됨.
question_create
함수에 데이터를 저장하는 코드를 작성
📄 /pybo/views.py
def question_create(request):
if request.method == 'POST':
form = QuestionForm(request.POST)
if form.is_valid(): # 폼이 유효하다면
question = form.save(commit=False) # 임시 저장하여 question 객체를 리턴받고
question.create_date = timezone.now() # 실제 저장을 위해 작성일시를 설정.
question.save() # 데이터를 실제로 저장.
return redirect('pybo:index')
else:
form = QuestionForm()
context = {'form': form}
return render(request, 'pybo/question_form.html', context)
POST
, GET
동일한 URL
요청을 POST
, GET
요청 방식에 따라 다르게 처리
- 질문 목록 화면에서 "질문 등록하기" 버튼을 클릭한 경우에는
/pybo/question/create/
페이지가GET
방식으로 요청되어question_create
함수가 실행됨.
- 왜냐하면
<a href="{% url 'pybo:question_create' %}" class="btn btn-primary">질문 등록하기</a>
와 같이 링크를 통해 페이지를 요청할 경우에는 무조건GET
방식이 사용되기 때문.
따라서 이 경우에는request.method
값이GET
이 되어if .. else ..
구문에서else
구문을 타게 되어 질문을 등록하는 화면을 렌더링
- 그리고 질문 등록 화면에서
subject
,content
항목에 값을 기입하고저장하기
버튼을 누르면 이번에는/pybo/question/create/
페이지를POST
방식으로 요청함.
- 왜냐하면 앞서 설명했듯이
form
태그에action
속성이 지정되지 않으면 현재 페이지가 디폴트action
으로 설정되기 때문.- 따라서 질문 등록 화면에서
저장하기
버튼을 클릭하면question_create
함수가 실행되고request.method
값은POST
가 되어 해당 코드 블럭이 실행됨.
GET
방식에서는 form = QuestionForm()
처럼 QuestionForm
을 인수 없이 생성.
POST
방식에서는 form = QuestionForm(request.POST)
처럼 request.POST
를 인수로 생성.
request.POST
를 인수로QuestionForm
을 생성할 경우에는request.POST
에 담긴subject
,content
값이QuestionForm
의subject
,content
속성에 자동으로 저장되어 객체가 생성됨.
request.POST
에는 화면에서 사용자가 입력한 내용들이 담겨있음.- 그리고
form.is_valid()
는form
이 유효한지를 검사
- 만약
form
에 저장된subject
,content
의 값이 올바르지 않다면form
에는 오류 메시지가 저장되고form.is_valid()
가 실패하여 다시 질문 등록 화면을 렌더링.- 이 때 form에는 오류 메시지가 저장되므로 화면에 오류를 표시할 수 있음.
if form.is_valid()
: form
이 유효하다면 이후의 문장이 수행되어 질문 데이터가 생됨.
question = form.save(commit=False)
: form
에 저장된 데이터로 Question
데이터를 저장하기 위한 코드
QuestionForm
이Question
모델과 연결된 모델 폼이기 때문에 이와 같이 사용 가능.- 여기서
commit=False
는 임시 저장을 의미.
- 즉, 실제 데이터는 아직 데이터베이스에 저장되지 않은 상태를 뜻함.
- 여기서
form.save(commit=False)
대신form.save()
를 수행하면Question
모델의create_date
에 값이 없다는 오류가 발생.- 왜냐하면
QuestionForm
에는 현재subject
,content
속성만 정의되어 있고create_date
속성은 없기 때문.- 이러한 이유로 임시 저장을 하여 question 객체를 리턴받고 create_date에 값을 설정한 후 question.save()로 실제 데이터를 저장하는 것
create_date
속성은 데이터 저장 시점에 생성해야 하는 값이므로 QuestionForm에 등록하여 사용하지 않음.
이제 브라우저에서 질문 등록이 잘 되는지 확인
form widget
{{ form.as_p }}
태그는 HTML
코드를 자동으로 생성하기 때문에 부트스트랩을 적용할 수가 없음.
- 완벽하지는 않지만 다음처럼
QuestionForm
을 조금 수정하면 어느정도 해결이 가능widgets
속성을 지정하면subject
,content
입력 필드에form-control
과 같은 부트스트랩 클래스를 추가 가능
📄 /pybo/forms.py
from django import forms
from pybo.models import Question
class QuestionForm(forms.ModelForm):
class Meta:
model = Question
fields = ['subject', 'content']
widgets = {
'subject': forms.TextInput(attrs={'class': 'form-control'}),
'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}),
}
form label
Subject
, Content
를 영문이 아니라 한글로 표시하고 싶다면 다음처럼 labels
속성을 지정📄 /pybo/forms.py
from django import forms
from pybo.models import Question
class QuestionForm(forms.ModelForm):
class Meta:
model = Question
fields = ['subject', 'content']
widgets = {
'subject': forms.TextInput(attrs={'class': 'form-control'}),
'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}),
}
labels = { # 추가
'subject': '제목', # 추가
'content': '내용', # 추가
} # 추가
수동 폼 작성
{{ form.as_p }}
{{ form.as_p }}
를 사용하면 빠르게 템플릿을 만들 수 있지만HTML
코드가 자동으로 생성되므로 디자인 측면에서 많은 제한이 생김.- 예를 들어 폼 엘리먼트 내에 특정 태그를 추가하거나 필요한 클래스를 추가하는 작업에 제한이 생김.
- 또 디자인 영역과 서버 프로그램 영역이 혼재되어 웹 디자이너와 개발자의 역할을 분리하기도 모호해짐.
이용하여 자동으로 HTML
코드를 생성하지 않고 직접 HTML
코드를 작성하는 방법을 사용하기 위해 수작업시 필요없는 widget 속성을 제거
📄 /pybo/forms.py
from django import forms
from pybo.models import Question
class QuestionForm(forms.ModelForm):
class Meta:
model = Question # 사용할 모델
fields = ['subject', 'content'] # QuestionForm에서 사용할 Question 모델의 속성
widgets = {
'subject': forms.TextInput(attrs={'class': 'form-control'}),
'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}),
}
{{ form.as_p }}
로 자동으로 생성되는HTML
대신 제목과 내용에 해당되는 HTML코드를 직접 작성- 그리고
question_create
함수에서form.is_valid()
가 실패할 경우 발생하는 오류의 내용을 표시하기 위해오류를 표시하는 영역
을 추가
- 제목(
subject
) 항목의value
에는{{ form.subject.value|default_if_none:'' }}
처럼 값을 대입해 주었는데 이것은 오류가 발생했을 경우 기존에 입력했던 값을 유지하기 위함.|default_if_none:''
의 의미 : 폼 데이터(form.subject.value
)에 값이 없을 경우None
이라는 문자열이 표시되는데None
대신공백
으로 표시하라는 의미의 템플릿 필터
- 장고의 템플릿 필터는
|default_if_none:''
처럼|
기호와 함께 사용됨.
📄 /templates/pybo/question_form.html
{% extends 'base.html' %}
{% block content %}
<div class="container">
<h5 class="my-3 border-bottom pb-2">질문등록</h5>
<form method="post">
{% csrf_token %}
<!-- 오류표시 Start --> # 추가 시작
{% if form.errors %}
<div class="alert alert-danger" role="alert">
{% for field in form %}
{% if field.errors %}
<div>
<strong>{{ field.label }}</strong>
{{ field.errors }}
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
<!-- 오류표시 End -->
<div class="mb-3">
<label for="subject" class="form-label">제목</label>
<input type="text" class="form-control" name="subject" id="subject"
value="{{ form.subject.value|default_if_none:'' }}">
</div>
<div class="mb-3">
<label for="content" class="form-label">내용</label>
<textarea class="form-control" name="content"
id="content" rows="10">{{ form.content.value|default_if_none:'' }}</textarea>
</div> # 추가 끝
<button type="submit" class="btn btn-primary">저장하기</button>
</form>
</div>
{% endblock %}
- "내용"에 아무런 값도 입력하지 않았기 때문에 "내용"을 입력하라는 오류메시지 확인 가능
- 그리고 "제목"에 입력했던 "TEST"는 사라지지 않고 계속 유지되는 것도 확인 가능.
답변 등록
AnswerForm
AnswerForm
을 pybo/forms.py
파일에 다음과 같이 작성📄 /pybo/forms.py
from django import forms
from pybo.models import Question, Answer
(... 생략 ...)
class AnswerForm(forms.ModelForm):
class Meta:
model = Answer
fields = ['content']
labels = {
'content': '답변내용',
}
answer_create
함수answer_create
함수를 다음과 같이 수정
question_create
와 같은 방법으로AnswerForm
을 이용하도록 변경.- 하지만 답변 등록은
POST
방식만 사용되기 때문에GET
방식으로 요청할 경우에는HttpResponseNotAllowed
오류가 발생하도록 설정.
📄 /pybo/views.py
(... 생략 ...)
from django.http import HttpResponseNotAllowed
from .forms import QuestionForm, AnswerForm
(... 생략 ...)
def answer_create(request, question_id):
"""
pybo 답변등록
"""
question = get_object_or_404(Question, pk=question_id)
if request.method == "POST":
form = AnswerForm(request.POST)
if form.is_valid():
answer = form.save(commit=False)
answer.create_date = timezone.now()
answer.question = question
answer.save()
return redirect('pybo:detail', question_id=question.id)
else:
return HttpResponseNotAllowed('Only POST is possible.')
context = {'question': question, 'form': form}
return render(request, 'pybo/question_detail.html', context)
question_detail.html
📄 /templates/pybo/question_detail.html
{% extends 'base.html' %}
{% block content %}
<div class="container my-3">
(... 생략 ...)
<form action="{% url 'pybo:answer_create' question.id %}" method="post" class="my-3">
{% csrf_token %}
<!-- 오류표시 Start --> # 추가 시작
{% if form.errors %}
<div class="alert alert-danger" role="alert">
{% for field in form %}
{% if field.errors %}
<div>
<strong>{{ field.label }}</strong>
{{ field.errors }}
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
<!-- 오류표시 End --> # 추가 끝
<div class="form-group">
<textarea name="content" id="content" class="form-control" rows="10"></textarea>
</div>
<input type="submit" value="답변등록" class="btn btn-primary">
</form>
</div>
{% endblock %}