html 은 기본적으로 절차 지향 언어
그래서 브라우저가 이해할 수 있도록 html 이나 xml 문서를 객체 구조로 변환한 것이 dom
즉, DOM 은 웹 페이지를 스크립트 언어와 연결하는 중요한 인터페이스 역할을 한다
트리 구조의 객체 모델
브라우저가 DOM 을 생성하면 자바스크립트는 이 DOM 객체에 접근하여 요소를 찾고, 내용을 바꾸거나, 새 요소를 추가/삭제하는 등의 작업을 수행할 수 있다
점프투장고 교재 p.125
부트스트랩에 필요한 파일 추가
static\bootstrap.min.js
static\jquery-3.7.1.min.js
→ jquery.com/download
temlates/base.html 에 파일 추가
templates\base.html
<script src="{% static 'jquery-3.7.1.min.js' %}"></script>
<script src="{% static 'bootstrap.min.js' %}"></script>
jquery 가 무조건 먼저!
navbar.html 생성 후 코드 작성
templates\navbar.html
<!-- 내비게이션바 -->
<nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
<a class="navbar-brand" href="{% url 'pybo:index' %}">Pybo</a>
<button class="navbar-toggler ml-auto" type="button" data-toggle="collapse"
data-target="#navbarNav" aria-controls="navbarNav"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse flex-grow-0" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="#">로그인</a>
</li>
</ul>
</div>
</nav>
data-toggle="collapse" : 버튼 클릭시 collapse 를 트리거data-target="#navbarNav" : id 가 navbarNav 인 애 열고 닫힘 기능aria-* : 스크린 속성 지정base.html에 include 적용
templates\base.html
<body>
<!-- 내비게이션바 -->
{% include 'navbar.html' %}
<!-- 기본 템플릿 안에 삽입될 내용 -->
{% block content %}
{% endblock %}
</body>
includeblock/endblockpython manage.py shell
>>> from pybo.models import Question
>>> from django.utils import timezone
>>> for i in range(300):
... q = Question(subject="테스트 데이터입니다: [%03d]" % i, content="내용 무", create_date=timezone.now())
... q.save()
넣을 때 indentation 주의!

pybo\views.py
def index(request):
"""
pybo 목록 출력
"""
# 입력 인자
page = request.GET.get('page', '1') #페이지
# 조회
question_list = Question.objects.order_by("-create_date")
# 페이징 처리
paginator = Paginator(question_list, 10)
page_obj = paginator.get_page(page)
context = {"question_list" : page_obj}
return render(request, 'pybo/question_list.html', context)
Paginator : 페이징 기능을 담당하는 클래스 : 쿼리셋(혹은 리스트)을 받아서 페이지 단위로 나누는 역할request.GET : URL 의 쿼리스트링(? 뒤에 있는 값)을 가져오는 부분page_obj 속성
| 항목 | 설명 |
|---|---|
| paginator.count | 전체 게시물 개수 |
| paginator.per_page | 페이지당 보여줄 게시물 개수 |
| paginator.page_range | 페이지 범위 |
| number | 현재 페이지 번호 |
| previous_page_number | 이전 페이지 번호 |
| next_page_number | 다음 페이지 번호 |
| has_previous | 이전 페이지 유무 |
| has_next | 다음 페이지 유무 |
| start_index | 현재 페이지 시작 인덱스(1부터 시작) |
| end_index | 현재 페이지의 끝 인덱스(1부터 시작) |
get_page()templates\pybo\question_list.html
<!-- 페이징 처리 시작 -->
<ul class="pagination justify-content-center">
<!-- 이전 페이지 -->
{% if question_list.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ question_list.previous_page_number }}">이전</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" tabindex="-1" aria-disabled="true" href="#">이전</a>
</li>
{% endif %}
<!-- 페이지 리스트 -->
{% for page_number in question_list.paginator.page_range %}
{% if page_number >= question_list.number|add:-5 and page_number <= question_list.number|add:5 %}
{% if page_number == question_list.number %}
<li class="page-item active" aria-current="page">
<a class="page-link" href="?page={{ page_number }}">{{ page_number }}</a>
</li>
{% else %}
<li class="page_item">
<a class="page-link" href="?page={{ page_number }}">{{ page_number }}</a>
</li>
{% endif %}
{% endif %}
{% endfor %}
<!-- 다음 페이지 -->
{% if question_list.has_next %}
<li class="page-item">
<a class="page-link" href="?page{{ question_list.next_page_number }}">다음</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" tabindex="-1" aria-disabled="true" href="#">다음</a>
</li>
{% endif %}
</ul>
<!-- 페이징 처리 끝 -->
django-admin startapp common
config\settings.py
INSTALLED_APPS = [
(... 생략 ...)
'django.contrib.auth',
(... 생략 ...)
]
django.contrib.auth 이다. 이 앱은 장고 프로젝트 생성시 자동으로 추가됨django.contrib.auth 앱을 이용하면 로그인과 로그아웃 기능을 정말 쉽게 구현할 수 있음config\settings.py
(... 생략 ...)
INSTALLED_APPS = [
**'common.apps.CommonConfig',**
'pybo.apps.PyboConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
(... 생략 ...)
config\urls.py
app_name = 'common'
urlpatterns = [
]
common\urls.py 생성
app_name = 'common'
urlpatterns = [
]
templates\navbar.html
(... 생략 ...)
<ul class="navbar-nav">
<li class="nav-item ">
<a class="nav-link" href="{% url 'common:login' %}">로그인</a>
</li>
</ul>
(... 생략 ...)
common\urls.py
from django.urls import path
from django.contrib.auth import views as auth_views
app_name = 'common'
urlpatterns = [
path('login/', auth_views.LoginView.as_view(), name='login'),
]
로그인 뷰는 따로 만들 필요없이 위 코드처럼 django.contrib.auth 앱의 LoginView를 사용하도록 설정
여기까지 수정하고 브라우저에서 내비게이션바의 '로그인' 링크를 눌러 보자. 그러면 아마도 다음과 같은 오류 페이지가 나타날 것이다.

→ 이 오류를 해결하려면 registration/login.html 템플릿 파일을 작성해야 함
→ 하지만 로그인은 common 앱에 구현할 것이므로 오류 메시지에 표시한 것처럼 registration 디렉터리에 템플릿 파일을 생성하기보다는 common 디렉터리에 템플릿을 생성하는 것이 좋음
LoginView가 common 디렉터리의 템플릿을 참조할 수 있도록 common\urls.py 수정
common\urls.py
from django.urls import path
from django.contrib.auth import views as auth_views
app_name = 'common'
urlpatterns = [
path('login/', auth_views.LoginView.as_view(**template_name='common/login.html'**), name='login'),
]
templates\common\login.html 생성
{% extends "base.html" %}
{% block content %}
<div class="container my-3">
<form method="post" action="{% url 'common:login' %}">
{% csrf_token %}
{% include "form_errors.html" %}
<div class="mb-3">
<label for="username">사용자ID</label>
<input type="text" class="form-control" name="username" id="username"
value="{{ form.username.value|default_if_none:'' }}">
</div>
<div class="mb-3">
<label for="password">비밀번호</label>
<input type="password" class="form-control" name="password" id="password"
value="{{ form.password.value|default_if_none:'' }}">
</div>
<button type="submit" class="btn btn-primary">로그인</button>
</form>
</div>
{% endblock %}
templates\common\form_errors.html
{% extends "base.html" %}
{% block content %}
<div class="container my-3">
<form method="post" action="{% url 'common:login' %}">
{% csrf_token %}
{% include "form_errors.html" %}
<div class="mb-3">
<label for="username">사용자ID</label>
<input type="text" class="form-control" name="username" id="username"
value="{{ form.username.value|default_if_none:'' }}">
</div>
<div class="mb-3">
<label for="password">비밀번호</label>
<input type="password" class="form-control" name="password" id="password"
value="{{ form.password.value|default_if_none:'' }}">
</div>
<button type="submit" class="btn btn-primary">로그인</button>
</form>
</div>
{% endblock %}
로그인 성공 시 이동할 페이지 등록
config\settings.py
LOGIN_REDIRECT_URL = '/'
내비게이션바 수정
templates\navbar.html
<li class="nav-item">
{% if user.is_authenticated %}
<form method="POST" action="{% url 'common:logout' %}">
{% csrf_token %}
<input style="background: none; border: none;" class="nav-link" type="submit" value="{{ user.username }} (로그아웃)"/>
</form>
{% else %}
<a class="nav-link" href="{% url 'common:login' %}">로그인</a>
{% endif %}
</li>
장고 5.0 버전 이후부터는 로그아웃뷰가 post 메소드 외에는 동작하지 않음
그러므로 POST 메소드를 사용할 수 있도록 form 태그 이용
URL 매핑
common\urls.py
urlpatterns = [
path('login/', auth_views.LoginView.as_view(template_name='common/login.html'), name='login'),
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
]
로그아웃 성공 시 이동할 페이지 등록
config\settings.py
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'