
웹 페이지를 구성하는데 기능 구현만큼 어려운게 디자인이다. 물론 웹 디자이너와 함께 협업하면 좋겠지만 함께 작업할 수 없다면 과학실 인체모형같은 흉측한 나의 웹 페이지를 그냥 보여주던가 눈물을 머금고 프론트엔드를 들여다 봐야한다. 한편으로 풀스택 개발자님들이 존경스럽다...
하지만 그런 디자인을 쉽게 받아서 사용할 수 있는 프레임워크가 바로 Bootstrap이다.
웹 개발에 있어 CSS와 JS 프레임워크를 지원하고 웹 뿐만 아니라 모바일, 태블잇에도 적용할 수 있도록 해준다.

해당 페이지를 통해서 예제를 쉽게 받아서 적용할 수 있다.

적용하는 방법은 패키지 관리자로 설치하는 방법도 있고 CDN으로 적용하는 방법도 있다.
📑 djangoProject/templates/base.html
... <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> ... <!-- bootstrap / css --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9" crossorigin="anonymous"> </head> <body> ... <!-- bootstrap / js --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-HwwvtgBNo3bZJJLYd8oVXjrBZt8cqVSpeBNS5n7C8IVInixGAoxmnlMuBnhbgrkm" crossorigin="anonymous"></script> </body> </html> ...
두 라인을 기본 베이스 html에 추가해주면 설치는 간단하다.
중요한 점은 css는 head에 js는 body의 마지막줄에 추가해주어야 한다는 점이다.

문서에서는 다양한 디자인에 대해 사용할 수 있다.
필요할때 보는 것도 좋지만 심심할 때 찾아보면 적용할 수 있는 디자인에 대해 식견이 넓어진다.

메인 페이지에 적용할 내비게이션 바를 찾아서 불러온 다음 적용하면 된다.
해당 offcanvas dark navbar를 불러와서 적용하면 되지만 당연히 그냥 넣으면 안되고 어느 부분에 기능이 작동하는지, 코드 내부에 기능을 추가할 곳은 어딘지 확인할 필요가 있다.
📑 djangoProject/templates/base.html
{% load static %} <!doctype html> <html lang="en"> <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- jquery --> <script src="https://kit.fontawesome.com/f9a809ddea.js" crossorigin="anonymous"></script> <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> <!-- bootstrap / css --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9" crossorigin="anonymous"> <!-- ajax--> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script> <!-- 페이지 타이틀 설정 --> <title>DjangoBooks</title> <!-- 스타일 블록 --> {% block style %}{% endblock %} <!-- 내비게이션 바 --> <nav class="navbar navbar-dark bg-dark fixed-top"> <div class="container-fluid"> <a class="navbar-brand" href="{% url 'main' %}"> <img src="{% static 'img/django.png' %}" alt="Logo" width="80" height="30" class="d-inline-block align-text-top"> DjangoBooks </a> <button class="navbar-toggler" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasDarkNavbar" aria-controls="offcanvasDarkNavbar" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="offcanvas offcanvas-end text-bg-dark" tabindex="-1" id="offcanvasDarkNavbar" aria-labelledby="offcanvasDarkNavbarLabel"> <div class="offcanvas-header"> <h5 class="offcanvas-title" id="offcanvasDarkNavbarLabel">DjangoBooks</h5> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" aria-label="Close"></button> </div> <div class="offcanvas-body"> <ul class="navbar-nav justify-content-end flex-grow-1 pe-3"> <li class="nav-item"> <a class="nav-link active" aria-current="page" href="#">Home</a> </li> <li class="nav-item"> <a class="nav-link" href="{% url 'djangoBooks:list' %}">Book List</a> </li> <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false"> Dropdown </a> <ul class="dropdown-menu dropdown-menu-dark"> <li> <a class="dropdown-item" href="#">Action</a> </li> <li> <a class="dropdown-item" href="#">Another action</a> </li> <li> <hr class="dropdown-divider"> </li> <li> <a class="dropdown-item" href="#">Something else here</a> </li> </ul> </li> </ul> <form class="d-flex mt-3" role="search"> <input class="form-control me-2" type="search" placeholder="Search" aria-label="Search"> <button class="btn btn-success" type="submit">Search</button> </form> </div> </div> </div> </nav> </head> <body style="margin-top: 60px; padding: 15px;"> <!-- 내용 --> {% block content %}{% endblock %} <footer></footer> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-HwwvtgBNo3bZJJLYd8oVXjrBZt8cqVSpeBNS5n7C8IVInixGAoxmnlMuBnhbgrkm" crossorigin="anonymous"></script> </body> </html>
전체 코드는 다음과 같고 각 아이템에 대해 경로를 지정하거나 이미지를 붙여 넣었다.
조금 추가한 부분은 상단 고정 메뉴바이기 때문에 body에 상단으로부터 공간을 넣어주었다는 점이다.

메뉴바가 잘 작동한다.

Bootstrap List Item
다양한 리스트 아이템에 대해 참고할만한 데이터 또한 해당 페이지에서 구할 수 있다.
참고해서 리스트 아이템에 변화를 주면 다음과 같이 된다.
📑 djangoBooks/templates/books_table.html
{% load static %} <div id="table"> <div class="container"> <!-- Book Item : START--> <ul class="list-group"> {% if books_list %} {% for book in books_list %} <li class="list-group-item clearfix"> <!-- 도서 이미지 --> {% if book.img_url %} <img class="img-responsive img-book" src="{{book.img_url}}" alt=""/> {% else %} <img class="img-responsive img-book" src="{% static 'img/no_book_image.png' %}" alt=""/> {% endif %} <!-- 도서 제목 --> <h3 class="list-group-item-heading"> {{book.title}} </h3> <!-- 도서 작가 --> <p class="list-group-item-text lead"> {{book.author}} <br/> </p> <!-- 하단 메뉴 --> <div class="btn-toolbar pull-right" role="toolbar" aria-label=""> <a href="#" class="btn btn-default m-1">Add to Bookshelf</a> <a href="#" class="btn btn-primary m-1">More</a> </div> </li> {% endfor %} <!-- Book Item : END--> <!-- Pagination : START --> <div class="text-center mt-3 mt-sm-3"> <ul class="pagination justify-content-center mb-3"> {% if books_list.has_previous %} <li class="page-item"> {% else %} <li class="page-item disabled"> {% endif %} <a class="page-link" href="#">First</a> </li> {% for page in books_list.paginator.page_range %} {% if page >= books_list.number|add:-2 and page <= books_list.number|add:2 %} {% if page == books_list.number%} <li class="page-item d-none d-md-inline-block"> <a class="page-link active" href="#">{{page}}</a> </li> {%else%} <li class="page-item"> <a class="page-link" href="#">{{page}}</a> </li> {% endif %} {% endif %} {%endfor %} {% if books_list.has_next %} <li class="page-item" value="{{books_list.paginator.num_pages}}"> {% else %} <li class="page-item disabled"> {% endif %} <a class="page-link" href="#">Last</a> </li> </ul> </div> <!-- Pagination : END --> <!-- 도서 목록이 없는 경우 --> {% else %} <p><b>{{search_input}}</b>도서가 없습니다</p> {% endif %} </ul> </div> </div> <script> //페이지네이션 버튼 클릭에 따른 결과 전달 $(".page-item").click(function () { if ($(this).text().trim() == 'First') { //첫 페이지인 경우 : 1 num_pages = 1; } else if ($(this).text().trim() == 'Last') { //마지막 페이지인 경우 : 페이지 끝번 num_pages = parseInt($(this).attr("value")); } else { //해당 페이지 : 페이지 번호 num_pages = parseInt($(this).text()); } page_click(); }); //페이지 클릭 시 작동방식 : AJAX function page_click() { $.ajax({ type: "get", //GET 방식 사용 url: "{% url 'djangoBooks:list' %}", //djangoBooks의 list url을 불러옴 data: { page: num_pages, //GET 방식에 페이지 번호를 넣어서 전송 }, success: function (data) { $("#table").html(data); //전송 성공시에 전송받는 데이터를 table class에 변경을 추가 window.scrollTo(0, 0); // 스크롤을 위로 다시 올려준다. } }); } </script>
리스트 아이템을 잘 파악하고 반복문, 조건문을 삽입해준다. 눈알이 핑핑돈다...
JavaScript에서는 변화된 class 이름에 따라 버튼 클릭에 대한 기능에 수정이 바뀌었음을 확인할 수 있다.
📑 djangoBooks/templates/books_list.html
{% extends 'base.html' %} {% block style %} <style> body { b ackground-color: #163a63; color: #163a63; padding-top: 15px; } .list-group { box-shadow: 0 11px 23px 5px rgba(0, 0, 0, 0.34); } .list-group-item { background-color: rgba(255, 255, 255, 0.7); border: 1; } .btn-toolbar { margin-top: 10px; } .img-book { float: left; margin-right: 15px; height: 128px; width: 128px; } /* ==== SOME BOOTSTRAP MODS/STYLING ==== */ .btn-default { background-color: rgba(255, 255, 255, 0.3); border-color: rgba(0, 0, 0, 0.2); } .btn, .img-rounded, .label { border-radius: 6px; } .btn { padding: 6px 18px; } .dropdown-menu > li > a { color: #777; } .dropdown-menu { background-color: rgba(0, 0, 0, 0.8); .divider { background-color: #555; } } /* ==== FONTS ==== */ @import url(https://fonts.googleapis.com/css?family=Montserrat:400,700); @import url(https://fonts.googleapis.com/css?family=Open+Sans:300); h1, h2, h3, h4, h5, h6 { font-family: 'Montserrat', sans-serif; text-transform: uppercase; font-weight: 700; } body, html { font-family: 'Open Sans', sans-serif; -webkit-font-smoothing: antialiased !important; } .active a { color: #c7ddef; } </style> {% endblock %} {% block content %} {% load static %} <h1 class="mt-3">Book List</h1> {% include 'books_table.html' %} {% endblock %}
해당 디자인이 포함된 css를 적용해주어야하는데 해당 부분은 style block으로 포함시켜 넣어주면 된다.
도서 목록의 각 아이템이 상세 내용에 대한 페이지로 연결할 수도 있지만 그러면 재미없으니 Modal을 통해서 작은 창이 뜨도록 해볼수 있다.

해당 페이지에서는 다양한 Modal 디자인을 확인하고 선택할 수 있다.
📑 djangoBooks/templates/books_table.html
... <!-- 도서 아이템 --> <li class="list-group-item clearfix "> {% if book.img_url %} <img class="img-responsive img-book" src="{{book.img_url}}" alt=""/> {% else %} <img class="img-responsive img-book" src="{% static 'img/no_book_image.png' %}" alt=""/> {% endif %} <h3 class="list-group-item-heading"> {{book.title}} </h3> <p class="list-group-item-text lead"> {{book.author}} <br/> </p> <div class="btn-toolbar pull-right" role="toolbar" aria-label=""> <a href="#" class="btn btn-default m-1">Add to Bookshelf</a> <!-- modal 창이 뜨도록 버튼 배치 --> <a href="#" class="btn btn-primary m-1" data-bs-toggle="modal" data-bs-target="#staticBackdrop" data-url="{% url 'djangoBooks:detail'%}?isbn13={{book.isbn13}}">More</a> </div> </li> ... <div class="modal fade" id="staticBackdrop" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true"> <div class="modal-dialog "> <div class="modal-content"></div> </div> </div>
도서 아이템의 버튼에 modal을 열 수 있는 기능을 연결하고 modal에 대한 정의를 html 하단, js 정의 위에 배치한다. 중요한 점은 해당 modal이 각 아이템에 대한 상세 페이지라는 점이다. 당연히 아이템별 데이터를 보내야 하고 이에 대한 정보를 반환해야한다.
data-url="{% url 'djangoBooks:detail'%}?isbn13={{book.isbn13}}
해당 부분이 어디로 요청을 보낼지와 도서에 대한 isbn13을 의미한다.
요청으로 받은 내용은 modal-content로 들어갈 것이다.
<script> $(document).on("click", ".btn-primary", function (e) { e.preventDefault(); var url = $(this).data("url"); var $staticBackdrop = $("#staticBackdrop"); $(".modal-content", $staticBackdrop).load(url, function () { $popup.modal("show"); }); }); ... </script>
그리고 버튼클릭에 따른 기능을 다음과 같이 정의한다.
내부 url을 받아 이를 요청하고 받은 내용을 modal-content로 돌려주게 된다.
요청은 urls.py에서 정의했던 views로 가게 된다.
📑 djangoBooks/views.py
# 도서 상세 페이지 class books_detail(View): context={} template_name = 'books_detail_modal.html' def get(self,request): # 도서 isbn13 받아오기 isbn13 = request.GET.get('isbn13', '') if Book.objects.filter(isbn13 = isbn13).exists(): book = Book.objects.get(isbn13 = isbn13) # 도서 객채 추가 self.context["book"] = book return render(request, self.template_name ,self.context) else: # 도서를 찾을 수 없습니다! return redirect("books:list") def post(self,request): return redirect("books:list")
views에 정의된 클래스로는 isbn13에 대한 값을 get으로 받게 된다.
해당 isbn13에 해당하는 도서를 검색하고 일치하는 데이터를 담아서 html로 보내게 된다.
📑 djangoBooks/templates/books_detail_modal.html
{% load static %} <div class="modal-header"> <h5 class="modal-title" id="staticBackdropLabel">{{book.title}}</h5> </div> <div class="modal-body"> {% if book.img_url %} <img src="{{book.img_url}}"> {% else %} <img src="{% static 'img/no_book_image.png' %}"> {% endif %} <table class="bibil"> <tr> <th>저자</th> <td>{{book.author}}</td> <th>출판사</th> <td>{{book.publisher}}</td> <th>출판년도</th> <td>{{book.pub_date}}</td> </tr> <tr> <th>주제</th> <td>{{book.kdc_class_no}}</td> <th>ISBN</th> <td>{{book.isbn13}}</td> <th></th> <td></td> </tr> </table> <div class="content-container"> <div class="content-title"> 소개글 </div> <div class="content-desc"> {% if book.description %} {{book.description}} {% else %} 도서 설명이 없습니다. {% endif %} </div> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-primary">Understood</button> </div>
modal 내부에서 어떻게 나타낼지에 대해 정의하되 넘겨받은 book 변수로부터 도서에 대한 정보를 받을 수 있다.
