WeniVooks

검색

Django 베이스캠프

템플릿 구현

이전 시간에는 도서관 관리 시스템(Library Management System, LMS)의 URL 라우터 및 모델을 구현했습니다.

이번 시간에는 요구사항에 맞게 템플릿을 구현해보겠습니다.

1. 핵심 기능별 필요 템플릿

1.1 공통 템플릿
templates/
    ├── base.html          # 기본 레이아웃
    └── navbar.html        # 공통 네비게이션
templates/
    ├── base.html          # 기본 레이아웃
    └── navbar.html        # 공통 네비게이션
1.2 사용자 관리 템플릿
accounts/templates/accounts/
    ├── login.html        # 로그인
    ├── register.html     # 회원가입
    └── profile.html      # 프로필(대출/예약 현황)
accounts/templates/accounts/
    ├── login.html        # 로그인
    ├── register.html     # 회원가입
    └── profile.html      # 프로필(대출/예약 현황)
1.3 도서 관리 템플릿
books/templates/books/
    ├── book_list.html     # 도서 목록
    ├── book_detail.html   # 도서 상세
    ├── book_confirm_delete.html  # 도서 삭제 확인
    ├── book_form.html     # 도서 등록/수정 (사서용)
    ├── loan_list.html     # 대출 현황
    ├── loan_form.html     # 대출 신청
    ├── reservation_form.html  # 도서 예약
    └── reservation_list.html  # 예약 현황
books/templates/books/
    ├── book_list.html     # 도서 목록
    ├── book_detail.html   # 도서 상세
    ├── book_confirm_delete.html  # 도서 삭제 확인
    ├── book_form.html     # 도서 등록/수정 (사서용)
    ├── loan_list.html     # 대출 현황
    ├── loan_form.html     # 대출 신청
    ├── reservation_form.html  # 도서 예약
    └── reservation_list.html  # 예약 현황

2. 기본 템플릿 구조 예시

2.1 base.html
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>도서관 관리 시스템</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    {% include 'navbar.html' %}
    <div class="container mt-4">
        {% if messages %}
            {% for message in messages %}
                <div class="alert alert-{{ message.tags }}">
                    {{ message }}
                </div>
            {% endfor %}
        {% endif %}
        {% block content %}
        {% endblock %}
    </div>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>도서관 관리 시스템</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    {% include 'navbar.html' %}
    <div class="container mt-4">
        {% if messages %}
            {% for message in messages %}
                <div class="alert alert-{{ message.tags }}">
                    {{ message }}
                </div>
            {% endfor %}
        {% endif %}
        {% block content %}
        {% endblock %}
    </div>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
2.1.1 base.html 핵심 요소
  • {% block content %}: 다른 템플릿들이 내용을 채워넣을 영역 정의
  • {% include 'navbar.html' %}: 네비게이션 바 포함
  • {% if messages %}: Django messages framework로 전달된 알림 표시
  • Bootstrap 5 CSS/JS 포함
2.2 navbar.html
<!-- templates/navbar.html -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
  <div class="container">
    <a class="navbar-brand" href="/">도서관</a>
    <div class="navbar-nav">
      {% if user.is_authenticated %}
      {% if user.role == 'LIBRARIAN' %}
      <a class="nav-link" href="{% url 'books:book-create' %}">도서등록</a>
      <a class="nav-link" href="{% url 'books:loan-list' %}">대출관리</a>
      {% endif %}
      <a class="nav-link" href="{% url 'accounts:profile' %}">내정보</a>
      <form class="d-inline" method="post" action="{% url 'accounts:logout' %}">
        {% csrf_token %}
        <button class="btn btn-link nav-link">로그아웃</button>
      </form>
      {% else %}
      <a class="nav-link" href="{% url 'accounts:login' %}">로그인</a>
      <a class="nav-link" href="{% url 'accounts:register' %}">회원가입</a>
      {% endif %}
    </div>
  </div>
</nav>
<!-- templates/navbar.html -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
  <div class="container">
    <a class="navbar-brand" href="/">도서관</a>
    <div class="navbar-nav">
      {% if user.is_authenticated %}
      {% if user.role == 'LIBRARIAN' %}
      <a class="nav-link" href="{% url 'books:book-create' %}">도서등록</a>
      <a class="nav-link" href="{% url 'books:loan-list' %}">대출관리</a>
      {% endif %}
      <a class="nav-link" href="{% url 'accounts:profile' %}">내정보</a>
      <form class="d-inline" method="post" action="{% url 'accounts:logout' %}">
        {% csrf_token %}
        <button class="btn btn-link nav-link">로그아웃</button>
      </form>
      {% else %}
      <a class="nav-link" href="{% url 'accounts:login' %}">로그인</a>
      <a class="nav-link" href="{% url 'accounts:register' %}">회원가입</a>
      {% endif %}
    </div>
  </div>
</nav>
2.2.1 navbar.html 핵심 기능
  • 사용자 인증 상태에 따른 메뉴 분기({% if user.is_authenticated %})
  • 사서 권한 확인 및 관리자 메뉴 표시({% if user.role == 'LIBRARIAN' %})
  • 로그아웃은 POST 방식으로 처리(CSRF 보호)

3. 도서 관리 템플릿 예시

3.1 book_list.html
<!-- books/templates/books/book_list.html -->
{% extends 'base.html' %}
 
{% block content %}
<h3>도서 목록</h3>
 
<table class="table">
  <thead>
  <tr>
    <th>도서명</th>
    <th>저자</th>
    <th>ISBN</th>
    <th>출판사</th>
    <th>잔여/전체</th>
    <th>액션</th>
  </tr>
  </thead>
  <tbody>
  {% for book in books %}
  <tr>
    <td><a href="{% url 'books:book-detail' book.pk %}">{{ book.title }}</a></td>
    <td>{{ book.author }}</td>
    <td>{{ book.isbn }}</td>
    <td>{{ book.publisher }}</td>
    <td>{{ book.available_quantity }}/{{ book.total_quantity }}</td>
    <td>
      {% if user.is_authenticated %}
      {% if book.available_quantity > 0 %}
      <form method="post" action="{% url 'books:loan-create' book.id %}" class="d-inline">
        {% csrf_token %}
        <button type="submit" class="btn btn-sm btn-primary">대출</button>
      </form>
      {% else %}
      <form method="post" action="{% url 'books:reservation-create' book.id %}" class="d-inline">
        {% csrf_token %}
        <button type="submit" class="btn btn-sm btn-warning">예약</button>
      </form>
      {% endif %}
 
      {% if user.role == 'LIBRARIAN' %}
      <a href="{% url 'books:book-update' book.pk %}" class="btn btn-sm btn-secondary">수정</a>
      <a href="{% url 'books:book-delete' book.pk %}" class="btn btn-sm btn-danger">삭제</a>
      {% endif %}
      {% endif %}
    </td>
  </tr>
  {% empty %}
  <tr>
    <td colspan="6" class="text-center">도서가 없습니다.</td>
  </tr>
  {% endfor %}
  </tbody>
</table>
{% endblock %}
<!-- books/templates/books/book_list.html -->
{% extends 'base.html' %}
 
{% block content %}
<h3>도서 목록</h3>
 
<table class="table">
  <thead>
  <tr>
    <th>도서명</th>
    <th>저자</th>
    <th>ISBN</th>
    <th>출판사</th>
    <th>잔여/전체</th>
    <th>액션</th>
  </tr>
  </thead>
  <tbody>
  {% for book in books %}
  <tr>
    <td><a href="{% url 'books:book-detail' book.pk %}">{{ book.title }}</a></td>
    <td>{{ book.author }}</td>
    <td>{{ book.isbn }}</td>
    <td>{{ book.publisher }}</td>
    <td>{{ book.available_quantity }}/{{ book.total_quantity }}</td>
    <td>
      {% if user.is_authenticated %}
      {% if book.available_quantity > 0 %}
      <form method="post" action="{% url 'books:loan-create' book.id %}" class="d-inline">
        {% csrf_token %}
        <button type="submit" class="btn btn-sm btn-primary">대출</button>
      </form>
      {% else %}
      <form method="post" action="{% url 'books:reservation-create' book.id %}" class="d-inline">
        {% csrf_token %}
        <button type="submit" class="btn btn-sm btn-warning">예약</button>
      </form>
      {% endif %}
 
      {% if user.role == 'LIBRARIAN' %}
      <a href="{% url 'books:book-update' book.pk %}" class="btn btn-sm btn-secondary">수정</a>
      <a href="{% url 'books:book-delete' book.pk %}" class="btn btn-sm btn-danger">삭제</a>
      {% endif %}
      {% endif %}
    </td>
  </tr>
  {% empty %}
  <tr>
    <td colspan="6" class="text-center">도서가 없습니다.</td>
  </tr>
  {% endfor %}
  </tbody>
</table>
{% endblock %}
3.1.1 book_list.html 핵심 기능
  • 도서 목록을 테이블로 표시
  • 각 도서의 상세 페이지 링크({% url 'books:book-detail' book.pk %})
  • 대출 가능 여부에 따른 버튼 분기:
    • 재고 있음: 대출 버튼
    • 재고 없음: 예약 버튼
  • 사서 권한자만 수정/삭제 버튼 표시
3.2 book_detail.html
<!-- books/templates/books/book_detail.html -->
{% extends 'base.html' %}
 
{% block content %}
<div class="card">
    <div class="card-body">
        <h3>{{ book.title }}</h3>
        <p>저자: {{ book.author }}</p>
        <p>ISBN: {{ book.isbn }}</p>
        <p>출판사: {{ book.publisher }}</p>
        <p>잔여 수량: {{ book.available_quantity }}/{{ book.total_quantity }}</p>
        
        {% if user.is_authenticated and book.available_quantity > 0 %}
            <form method="post" action="{% url 'books:loan-create' book.id %}">
                {% csrf_token %}
                <button class="btn btn-primary">대출신청</button>
            </form>
        {% endif %}
    </div>
</div>
{% endblock %}
<!-- books/templates/books/book_detail.html -->
{% extends 'base.html' %}
 
{% block content %}
<div class="card">
    <div class="card-body">
        <h3>{{ book.title }}</h3>
        <p>저자: {{ book.author }}</p>
        <p>ISBN: {{ book.isbn }}</p>
        <p>출판사: {{ book.publisher }}</p>
        <p>잔여 수량: {{ book.available_quantity }}/{{ book.total_quantity }}</p>
        
        {% if user.is_authenticated and book.available_quantity > 0 %}
            <form method="post" action="{% url 'books:loan-create' book.id %}">
                {% csrf_token %}
                <button class="btn btn-primary">대출신청</button>
            </form>
        {% endif %}
    </div>
</div>
{% endblock %}
3.2.1 book_detail.html 핵심 기능
  • 도서 정보(제목, 저자, ISBN 등) 표시
  • 재고 상태 표시
  • 인증된 사용자 + 재고 있을 때만 대출 버튼 표시
3.3 book_form.html
<!-- books/templates/books/book_form.html -->
{% extends 'base.html' %}
 
{% block content %}
<div class="card">
    <div class="card-body">
        <h3>{% if book %}도서 수정{% else %}도서 등록{% endif %}</h3>
        <form method="post">
            {% csrf_token %}
            <div class="mb-3">
                <label for="title" class="form-label">도서명</label>
                <input type="text" class="form-control" id="title" name="title" value="{{ book.title|default:'' }}" required>
            </div>
            <div class="mb-3">
                <label for="author" class="form-label">저자</label>
                <input type="text" class="form-control" id="author" name="author" value="{{ book.author|default:'' }}" required>
            </div>
            <div class="mb-3">
                <label for="isbn" class="form-label">ISBN</label>
                <input type="text" class="form-control" id="isbn" name="isbn" value="{{ book.isbn|default:'' }}" required>
            </div>
            <div class="mb-3">
                <label for="publisher" class="form-label">출판사</label>
                <input type="text" class="form-control" id="publisher" name="publisher" value="{{ book.publisher|default:'' }}" required>
            </div>
            <div class="mb-3">
                <label for="total_quantity" class="form-label">총 수량</label>
                <input type="number" class="form-control" id="total_quantity" name="total_quantity" value="{{ book.total_quantity|default:'1' }}" required>
            </div>
            <button type="submit" class="btn btn-primary">저장</button>
            <a href="{% url 'books:book-list' %}" class="btn btn-secondary">취소</a>
        </form>
    </div>
</div>
{% endblock %}
<!-- books/templates/books/book_form.html -->
{% extends 'base.html' %}
 
{% block content %}
<div class="card">
    <div class="card-body">
        <h3>{% if book %}도서 수정{% else %}도서 등록{% endif %}</h3>
        <form method="post">
            {% csrf_token %}
            <div class="mb-3">
                <label for="title" class="form-label">도서명</label>
                <input type="text" class="form-control" id="title" name="title" value="{{ book.title|default:'' }}" required>
            </div>
            <div class="mb-3">
                <label for="author" class="form-label">저자</label>
                <input type="text" class="form-control" id="author" name="author" value="{{ book.author|default:'' }}" required>
            </div>
            <div class="mb-3">
                <label for="isbn" class="form-label">ISBN</label>
                <input type="text" class="form-control" id="isbn" name="isbn" value="{{ book.isbn|default:'' }}" required>
            </div>
            <div class="mb-3">
                <label for="publisher" class="form-label">출판사</label>
                <input type="text" class="form-control" id="publisher" name="publisher" value="{{ book.publisher|default:'' }}" required>
            </div>
            <div class="mb-3">
                <label for="total_quantity" class="form-label">총 수량</label>
                <input type="number" class="form-control" id="total_quantity" name="total_quantity" value="{{ book.total_quantity|default:'1' }}" required>
            </div>
            <button type="submit" class="btn btn-primary">저장</button>
            <a href="{% url 'books:book-list' %}" class="btn btn-secondary">취소</a>
        </form>
    </div>
</div>
{% endblock %}
3.3.1 book_form.html 핵심 기능
  • 등록/수정 구분({% if book %})
  • 필수 입력 필드 구성
  • 기존 데이터 표시({{ book.field|default:'' }})
3.4 book_confirm_delete.html
{% extends 'base.html' %}
 
{% block content %}
<div class="card">
    <div class="card-body">
        <h3 class="card-title">도서 삭제</h3>
        <p>정말 "{{ book.title }}" 도서를 삭제하시겠습니까?</p>
        <form method="post">
            {% csrf_token %}
            <button type="submit" class="btn btn-danger">삭제</button>
            <a href="{% url 'books:book-detail' book.pk %}" class="btn btn-secondary">취소</a>
        </form>
    </div>
</div>
{% endblock %}
{% extends 'base.html' %}
 
{% block content %}
<div class="card">
    <div class="card-body">
        <h3 class="card-title">도서 삭제</h3>
        <p>정말 "{{ book.title }}" 도서를 삭제하시겠습니까?</p>
        <form method="post">
            {% csrf_token %}
            <button type="submit" class="btn btn-danger">삭제</button>
            <a href="{% url 'books:book-detail' book.pk %}" class="btn btn-secondary">취소</a>
        </form>
    </div>
</div>
{% endblock %}
3.4.1 book_confirm_delete.html 핵심 기능
  • 삭제 확인 메시지
  • 취소 시 상세 페이지로 복귀
  • POST 방식으로 삭제 처리
3.5 loan_list.html
<<!-- books/templates/books/loan_list.html -->
{% extends 'base.html' %}
 
{% block content %}
<h3>대출 현황</h3>
<table class="table">
  <thead>
  <tr>
    <th>도서명</th>
    <th>대출자</th>
    <th>대출일</th>
    <th>반납예정일</th>
    <th>상태</th>
    <th>액션</th>
  </tr>
  </thead>
  <tbody>
  {% for loan in loans %}
  <tr>
    <td>{{ loan.book.title }}</td>
    <td>{{ loan.user.username }}</td>
    <td>{{ loan.loan_date|date:"Y-m-d" }}</td>
    <td>{{ loan.due_date|date:"Y-m-d" }}</td>
    <td>
      {% if loan.status == 'RETURNED' %}
      <span class="badge bg-success">반납완료</span>
      {% elif loan.is_overdue %}
      <span class="badge bg-danger">연체</span>
      {% else %}
      <span class="badge bg-primary">대출중</span>
      {% endif %}
    </td>
    <td>
      {% if user.role == 'LIBRARIAN' or user == loan.user %}
      <form method="post" action="{% url 'books:loan-return' loan.id %}" class="d-inline">
        {% csrf_token %}
        <button type="submit" class="btn btn-sm btn-success">반납</button>
      </form>
      {% endif %}
    </td>
  </tr>
  {% empty %}
  <tr>
    <td colspan="6" class="text-center">대출 내역이 없습니다.</td>
  </tr>
  {% endfor %}
  </tbody>
</table>
{% endblock %}
<<!-- books/templates/books/loan_list.html -->
{% extends 'base.html' %}
 
{% block content %}
<h3>대출 현황</h3>
<table class="table">
  <thead>
  <tr>
    <th>도서명</th>
    <th>대출자</th>
    <th>대출일</th>
    <th>반납예정일</th>
    <th>상태</th>
    <th>액션</th>
  </tr>
  </thead>
  <tbody>
  {% for loan in loans %}
  <tr>
    <td>{{ loan.book.title }}</td>
    <td>{{ loan.user.username }}</td>
    <td>{{ loan.loan_date|date:"Y-m-d" }}</td>
    <td>{{ loan.due_date|date:"Y-m-d" }}</td>
    <td>
      {% if loan.status == 'RETURNED' %}
      <span class="badge bg-success">반납완료</span>
      {% elif loan.is_overdue %}
      <span class="badge bg-danger">연체</span>
      {% else %}
      <span class="badge bg-primary">대출중</span>
      {% endif %}
    </td>
    <td>
      {% if user.role == 'LIBRARIAN' or user == loan.user %}
      <form method="post" action="{% url 'books:loan-return' loan.id %}" class="d-inline">
        {% csrf_token %}
        <button type="submit" class="btn btn-sm btn-success">반납</button>
      </form>
      {% endif %}
    </td>
  </tr>
  {% empty %}
  <tr>
    <td colspan="6" class="text-center">대출 내역이 없습니다.</td>
  </tr>
  {% endfor %}
  </tbody>
</table>
{% endblock %}
3.5.1 loan_list.html (대출 현황) 핵심 기능
  1. 대출 상태 표시 시스템

    • RETURNED: 녹색 배지로 "반납완료" 표시
    • is_overdue: 빨간색 배지로 "연체" 표시
    • 그 외: 파란색 배지로 "대출중" 표시
  2. 권한 기반 반납 기능

    • 사서 또는 해당 대출자만 반납 버튼 표시
    {% if user.role == 'LIBRARIAN' or user == loan.user %}
    {% if user.role == 'LIBRARIAN' or user == loan.user %}
  3. 날짜 포맷팅

    • |date:"Y-m-d" 필터로 날짜 형식 통일
3.6 loan_form.html
<!-- books/templates/books/loan_form.html -->
{% extends 'base.html' %}
 
{% block content %}
<div class="container">
  <div class="row justify-content-center">
    <div class="col-md-8">
      <div class="card">
        <div class="card-header">
          <h3 class="mb-0">도서 대출</h3>
        </div>
        <div class="card-body">
          <h5 class="card-title">{{ book.title }}</h5>
          <p class="card-text">
            <strong>저자:</strong> {{ book.author }}<br>
            <strong>출판사:</strong> {{ book.publisher }}<br>
            <strong>ISBN:</strong> {{ book.isbn }}<br>
            <strong>잔여 수량:</strong> {{ book.available_quantity }}/{{ book.total_quantity }}
          </p>
 
          {% if form.errors %}
          <div class="alert alert-danger">
            {% for error in form.non_field_errors %}
            <p>{{ error }}</p>
            {% endfor %}
          </div>
          {% endif %}
 
          <form method="post">
            {% csrf_token %}
            {{ form.as_p }}
            <button type="submit" class="btn btn-primary">대출하기</button>
            <a href="{% url 'books:book-detail' book.pk %}" class="btn btn-secondary">취소</a>
          </form>
        </div>
      </div>
    </div>
  </div>
</div>
{% endblock %}
<!-- books/templates/books/loan_form.html -->
{% extends 'base.html' %}
 
{% block content %}
<div class="container">
  <div class="row justify-content-center">
    <div class="col-md-8">
      <div class="card">
        <div class="card-header">
          <h3 class="mb-0">도서 대출</h3>
        </div>
        <div class="card-body">
          <h5 class="card-title">{{ book.title }}</h5>
          <p class="card-text">
            <strong>저자:</strong> {{ book.author }}<br>
            <strong>출판사:</strong> {{ book.publisher }}<br>
            <strong>ISBN:</strong> {{ book.isbn }}<br>
            <strong>잔여 수량:</strong> {{ book.available_quantity }}/{{ book.total_quantity }}
          </p>
 
          {% if form.errors %}
          <div class="alert alert-danger">
            {% for error in form.non_field_errors %}
            <p>{{ error }}</p>
            {% endfor %}
          </div>
          {% endif %}
 
          <form method="post">
            {% csrf_token %}
            {{ form.as_p }}
            <button type="submit" class="btn btn-primary">대출하기</button>
            <a href="{% url 'books:book-detail' book.pk %}" class="btn btn-secondary">취소</a>
          </form>
        </div>
      </div>
    </div>
  </div>
</div>
{% endblock %}
3.6.1 loan_form.html (대출 신청) 주요 구성
  1. 도서 정보 표시 섹션

    • 제목, 저자, 출판사, ISBN
    • 현재 재고 상태 표시
  2. 폼 오류 처리

    {% if form.errors %}
    {% for error in form.non_field_errors %}
    {% if form.errors %}
    {% for error in form.non_field_errors %}
  3. 폼 레이아웃

    • Bootstrap 그리드 시스템 활용
    • 반응형 디자인(col-md-8)
3.7 reservation_list.html
<!-- books/templates/books/reservation_list.html -->
{% extends 'base.html' %}
 
{% block content %}
<h3>예약 현황</h3>
<table class="table">
    <thead>
        <tr>
            <th>도서명</th>
            <th>예약자</th>
            <th>예약일</th>
            <th>만료일</th>
            <th>상태</th>
            <th>액션</th>
        </tr>
    </thead>
    <tbody>
        {% for reservation in reservations %}
        <tr>
            <td>{{ reservation.book.title }}</td>
            <td>{{ reservation.user.username }}</td>
            <td>{{ reservation.reserved_date|date:"Y-m-d" }}</td>
            <td>{{ reservation.expiry_date|date:"Y-m-d" }}</td>
            <td>
                {% if reservation.status == 'WAITING' %}
                    <span class="badge bg-warning">대기중</span>
                {% elif reservation.status == 'AVAILABLE' %}
                    <span class="badge bg-success">대출가능</span>
                {% else %}
                    <span class="badge bg-secondary">취소됨</span>
                {% endif %}
            </td>
            <td>
                {% if reservation.status == 'WAITING' %}
                    <form method="post" action="{% url 'books:reservation-cancel' reservation.id %}" class="d-inline">
                        {% csrf_token %}
                        <button type="submit" class="btn btn-sm btn-danger">취소</button>
                    </form>
                {% elif reservation.status == 'AVAILABLE' %}
                    <form method="post" action="{% url 'books:loan-create' reservation.book.id %}" class="d-inline">
                        {% csrf_token %}
                        <button type="submit" class="btn btn-sm btn-primary">대출</button>
                    </form>
                {% endif %}
            </td>
        </tr>
        {% empty %}
        <tr>
            <td colspan="6" class="text-center">예약 내역이 없습니다.</td>
        </tr>
        {% endfor %}
    </tbody>
</table>
{% endblock %}
<!-- books/templates/books/reservation_list.html -->
{% extends 'base.html' %}
 
{% block content %}
<h3>예약 현황</h3>
<table class="table">
    <thead>
        <tr>
            <th>도서명</th>
            <th>예약자</th>
            <th>예약일</th>
            <th>만료일</th>
            <th>상태</th>
            <th>액션</th>
        </tr>
    </thead>
    <tbody>
        {% for reservation in reservations %}
        <tr>
            <td>{{ reservation.book.title }}</td>
            <td>{{ reservation.user.username }}</td>
            <td>{{ reservation.reserved_date|date:"Y-m-d" }}</td>
            <td>{{ reservation.expiry_date|date:"Y-m-d" }}</td>
            <td>
                {% if reservation.status == 'WAITING' %}
                    <span class="badge bg-warning">대기중</span>
                {% elif reservation.status == 'AVAILABLE' %}
                    <span class="badge bg-success">대출가능</span>
                {% else %}
                    <span class="badge bg-secondary">취소됨</span>
                {% endif %}
            </td>
            <td>
                {% if reservation.status == 'WAITING' %}
                    <form method="post" action="{% url 'books:reservation-cancel' reservation.id %}" class="d-inline">
                        {% csrf_token %}
                        <button type="submit" class="btn btn-sm btn-danger">취소</button>
                    </form>
                {% elif reservation.status == 'AVAILABLE' %}
                    <form method="post" action="{% url 'books:loan-create' reservation.book.id %}" class="d-inline">
                        {% csrf_token %}
                        <button type="submit" class="btn btn-sm btn-primary">대출</button>
                    </form>
                {% endif %}
            </td>
        </tr>
        {% empty %}
        <tr>
            <td colspan="6" class="text-center">예약 내역이 없습니다.</td>
        </tr>
        {% endfor %}
    </tbody>
</table>
{% endblock %}
3.7.1 reservation_list.html (예약 현황) 핵심 요소
  1. 예약 상태 시스템

    • WAITING: 노란색 배지
    • AVAILABLE: 녹색 배지
    • CANCELLED: 회색 배지
  2. 상태별 액션 버튼

    • 대기중: 예약 취소 버튼
    • 대출가능: 대출 신청 버튼
    • 취소됨: 액션 없음
  3. 날짜 정보

    • 예약일(reserved_date)
    • 만료일(expiry_date)
3.8 reservation_form.html
<!-- books/templates/books/reservation_form.html -->
{% extends 'base.html' %}
 
{% block content %}
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">
                    <h3 class="mb-0">도서 예약</h3>
                </div>
                <div class="card-body">
                    <h5 class="card-title">{{ book.title }}</h5>
                    <p class="card-text">
                        <strong>저자:</strong> {{ book.author }}<br>
                        <strong>출판사:</strong> {{ book.publisher }}<br>
                        <strong>ISBN:</strong> {{ book.isbn }}<br>
                        <strong>잔여 수량:</strong> {{ book.available_quantity }}/{{ book.total_quantity }}
                    </p>
                    
                    {% if form.errors %}
                    <div class="alert alert-danger">
                        {% for error in form.non_field_errors %}
                        <p>{{ error }}</p>
                        {% endfor %}
                    </div>
                    {% endif %}
                    
                    <form method="post">
                        {% csrf_token %}
                        {{ form.as_p }}
                        <button type="submit" class="btn btn-primary">예약하기</button>
                        <a href="{% url 'books:book-detail' book.pk %}" class="btn btn-secondary">취소</a>
                    </form>
                    
                    <div class="mt-3">
                        <small class="text-muted">
                            * 예약은 1일간 유효하며, 도서가 반납되면 우선적으로 대출하실 수 있습니다.
                        </small>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}
 
<!-- books/templates/books/reservation_form.html -->
{% extends 'base.html' %}
 
{% block content %}
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">
                    <h3 class="mb-0">도서 예약</h3>
                </div>
                <div class="card-body">
                    <h5 class="card-title">{{ book.title }}</h5>
                    <p class="card-text">
                        <strong>저자:</strong> {{ book.author }}<br>
                        <strong>출판사:</strong> {{ book.publisher }}<br>
                        <strong>ISBN:</strong> {{ book.isbn }}<br>
                        <strong>잔여 수량:</strong> {{ book.available_quantity }}/{{ book.total_quantity }}
                    </p>
                    
                    {% if form.errors %}
                    <div class="alert alert-danger">
                        {% for error in form.non_field_errors %}
                        <p>{{ error }}</p>
                        {% endfor %}
                    </div>
                    {% endif %}
                    
                    <form method="post">
                        {% csrf_token %}
                        {{ form.as_p }}
                        <button type="submit" class="btn btn-primary">예약하기</button>
                        <a href="{% url 'books:book-detail' book.pk %}" class="btn btn-secondary">취소</a>
                    </form>
                    
                    <div class="mt-3">
                        <small class="text-muted">
                            * 예약은 1일간 유효하며, 도서가 반납되면 우선적으로 대출하실 수 있습니다.
                        </small>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}
 
3.9.1 reservation_form.html (예약 신청) 상세 구성
  1. 도서 상세 정보 섹션

    • 기본 정보 표시
    • 현재 재고 상태
  2. 폼 검증

    • 서버 측 오류 메시지 표시
    • 필드별 오류 처리
  3. 추가 정보

    • 예약 유효 기간 안내
    • 우선 대출 권한 설명
  4. 레이아웃

    • Bootstrap 카드 컴포넌트
    • 반응형 그리드(justify-content-center)
  5. 네비게이션

    • 취소 시 도서 상세 페이지로 복귀
    • 폼 제출은 POST 방식

4. 사용자 관리 템플릿 예시

4.1 login.html
<!-- accounts/templates/accounts/login.html -->
{% extends 'base.html' %}
 
{% block content %}
<div class="row justify-content-center">
   <div class="col-md-6">
       <div class="card">
           <div class="card-body">
               <h3 class="card-title text-center mb-4">로그인</h3>
               <form method="post">
                   {% csrf_token %}
                   <div class="mb-3">
                       <label for="username" class="form-label">아이디</label>
                       <input type="text" class="form-control" id="username" name="username" required>
                   </div>
                   <div class="mb-3">
                       <label for="password" class="form-label">비밀번호</label>
                       <input type="password" class="form-control" id="password" name="password" required>
                   </div>
                   <button type="submit" class="btn btn-primary w-100">로그인</button>
               </form>
               <p class="text-center mt-3">
                   계정이 없으신가요? <a href="{% url 'accounts:register' %}">회원가입</a>
               </p>
           </div>
       </div>
   </div>
</div>
{% endblock %}
<!-- accounts/templates/accounts/login.html -->
{% extends 'base.html' %}
 
{% block content %}
<div class="row justify-content-center">
   <div class="col-md-6">
       <div class="card">
           <div class="card-body">
               <h3 class="card-title text-center mb-4">로그인</h3>
               <form method="post">
                   {% csrf_token %}
                   <div class="mb-3">
                       <label for="username" class="form-label">아이디</label>
                       <input type="text" class="form-control" id="username" name="username" required>
                   </div>
                   <div class="mb-3">
                       <label for="password" class="form-label">비밀번호</label>
                       <input type="password" class="form-control" id="password" name="password" required>
                   </div>
                   <button type="submit" class="btn btn-primary w-100">로그인</button>
               </form>
               <p class="text-center mt-3">
                   계정이 없으신가요? <a href="{% url 'accounts:register' %}">회원가입</a>
               </p>
           </div>
       </div>
   </div>
</div>
{% endblock %}
4.1.1 login.html (로그인) 핵심 구성
  1. 폼 구조

    • 아이디/비밀번호 필드
    • CSRF 토큰 보호
    • Bootstrap form-control 클래스 적용
  2. 레이아웃

    • 중앙 정렬(col-md-6)
    • 카드 컴포넌트 활용
    • 회원가입 링크 포함
4.2 register.html
<!-- accounts/templates/accounts/register.html -->
{% extends 'base.html' %}
 
{% block content %}
<div class="row justify-content-center">
   <div class="col-md-6">
       <div class="card">
           <div class="card-body">
               <h3 class="card-title text-center mb-4">회원가입</h3>
               <form method="post">
                   {% csrf_token %}
                   <div class="mb-3">
                       <label for="username" class="form-label">아이디</label>
                       <input type="text" class="form-control" id="username" name="username" required>
                   </div>
                   <div class="mb-3">
                       <label for="email" class="form-label">이메일</label>
                       <input type="email" class="form-control" id="email" name="email" required>
                   </div>
                   <div class="mb-3">
                       <label for="password1" class="form-label">비밀번호</label>
                       <input type="password" class="form-control" id="password1" name="password1" required>
                   </div>
                   <div class="mb-3">
                       <label for="password2" class="form-label">비밀번호 확인</label>
                       <input type="password" class="form-control" id="password2" name="password2" required>
                   </div>
                   <button type="submit" class="btn btn-primary w-100">회원가입</button>
               </form>
               <p class="text-center mt-3">
                   이미 계정이 있으신가요? <a href="{% url 'accounts:login' %}">로그인</a>
               </p>
           </div>
       </div>
   </div>
</div>
{% endblock %}
<!-- accounts/templates/accounts/register.html -->
{% extends 'base.html' %}
 
{% block content %}
<div class="row justify-content-center">
   <div class="col-md-6">
       <div class="card">
           <div class="card-body">
               <h3 class="card-title text-center mb-4">회원가입</h3>
               <form method="post">
                   {% csrf_token %}
                   <div class="mb-3">
                       <label for="username" class="form-label">아이디</label>
                       <input type="text" class="form-control" id="username" name="username" required>
                   </div>
                   <div class="mb-3">
                       <label for="email" class="form-label">이메일</label>
                       <input type="email" class="form-control" id="email" name="email" required>
                   </div>
                   <div class="mb-3">
                       <label for="password1" class="form-label">비밀번호</label>
                       <input type="password" class="form-control" id="password1" name="password1" required>
                   </div>
                   <div class="mb-3">
                       <label for="password2" class="form-label">비밀번호 확인</label>
                       <input type="password" class="form-control" id="password2" name="password2" required>
                   </div>
                   <button type="submit" class="btn btn-primary w-100">회원가입</button>
               </form>
               <p class="text-center mt-3">
                   이미 계정이 있으신가요? <a href="{% url 'accounts:login' %}">로그인</a>
               </p>
           </div>
       </div>
   </div>
</div>
{% endblock %}
4.2.1 register.html (회원가입) 주요 요소
  1. 필수 입력 필드

    • 아이디
    • 이메일
    • 비밀번호
    • 비밀번호 확인
  2. 보안 기능

    • CSRF 보호
    • 비밀번호 이중 입력 검증
    • required 속성으로 필수 입력 강제
  3. 네비게이션

    • 로그인 페이지 링크 제공
    • 폼 레이아웃은 로그인과 동일
4.3 profile.html
<!-- accounts/templates/accounts/profile.html -->
{% extends 'base.html' %}
 
{% block content %}
<div class="card mb-4">
   <div class="card-body">
       <h3 class="card-title">내 정보</h3>
       <p><strong>아이디:</strong> {{ user.username }}</p>
       <p><strong>이메일:</strong> {{ user.email }}</p>
       <p><strong>권한:</strong> {{ user.get_role_display }}</p>
   </div>
</div>
 
<div class="card mb-4">
   <div class="card-body">
       <h4>현재 대출 현황</h4>
       <table class="table">
           <thead>
               <tr>
                   <th>도서명</th>
                   <th>대출일</th>
                   <th>반납예정일</th>
                   <th>상태</th>
               </tr>
           </thead>
           <tbody>
               {% for loan in user.loan_set.all %}
               <tr>
                   <td>{{ loan.book.title }}</td>
                   <td>{{ loan.loan_date|date:"Y-m-d" }}</td>
                   <td>{{ loan.due_date|date:"Y-m-d" }}</td>
                   <td>
                       {% if loan.is_overdue %}
                           <span class="badge bg-danger">연체</span>
                       {% else %}
                           <span class="badge bg-primary">대출중</span>
                       {% endif %}
                   </td>
               </tr>
               {% empty %}
               <tr>
                   <td colspan="4" class="text-center">대출 중인 도서가 없습니다.</td>
               </tr>
               {% endfor %}
           </tbody>
       </table>
   </div>
</div>
 
<div class="card">
   <div class="card-body">
       <h4>예약 현황</h4>
       <table class="table">
           <thead>
               <tr>
                   <th>도서명</th>
                   <th>예약일</th>
                   <th>상태</th>
                   <th>액션</th>
               </tr>
           </thead>
           <tbody>
               {% for reservation in user.reservation_set.all %}
               <tr>
                   <td>{{ reservation.book.title }}</td>
                   <td>{{ reservation.reserved_date|date:"Y-m-d" }}</td>
                   <td>
                       {% if reservation.status == 'WAITING' %}
                           <span class="badge bg-warning">대기중</span>
                       {% elif reservation.status == 'AVAILABLE' %}
                           <span class="badge bg-success">대출가능</span>
                       {% endif %}
                   </td>
                   <td>
                       {% if reservation.status == 'WAITING' %}
                           <form method="post" action="{% url 'books:reservation-cancel' reservation.id %}" class="d-inline">
                               {% csrf_token %}
                               <button class="btn btn-sm btn-danger">취소</button>
                           </form>
                       {% endif %}
                   </td>
               </tr>
               {% empty %}
               <tr>
                   <td colspan="4" class="text-center">예약 중인 도서가 없습니다.</td>
               </tr>
               {% endfor %}
           </tbody>
       </table>
   </div>
</div>
{% endblock %}
<!-- accounts/templates/accounts/profile.html -->
{% extends 'base.html' %}
 
{% block content %}
<div class="card mb-4">
   <div class="card-body">
       <h3 class="card-title">내 정보</h3>
       <p><strong>아이디:</strong> {{ user.username }}</p>
       <p><strong>이메일:</strong> {{ user.email }}</p>
       <p><strong>권한:</strong> {{ user.get_role_display }}</p>
   </div>
</div>
 
<div class="card mb-4">
   <div class="card-body">
       <h4>현재 대출 현황</h4>
       <table class="table">
           <thead>
               <tr>
                   <th>도서명</th>
                   <th>대출일</th>
                   <th>반납예정일</th>
                   <th>상태</th>
               </tr>
           </thead>
           <tbody>
               {% for loan in user.loan_set.all %}
               <tr>
                   <td>{{ loan.book.title }}</td>
                   <td>{{ loan.loan_date|date:"Y-m-d" }}</td>
                   <td>{{ loan.due_date|date:"Y-m-d" }}</td>
                   <td>
                       {% if loan.is_overdue %}
                           <span class="badge bg-danger">연체</span>
                       {% else %}
                           <span class="badge bg-primary">대출중</span>
                       {% endif %}
                   </td>
               </tr>
               {% empty %}
               <tr>
                   <td colspan="4" class="text-center">대출 중인 도서가 없습니다.</td>
               </tr>
               {% endfor %}
           </tbody>
       </table>
   </div>
</div>
 
<div class="card">
   <div class="card-body">
       <h4>예약 현황</h4>
       <table class="table">
           <thead>
               <tr>
                   <th>도서명</th>
                   <th>예약일</th>
                   <th>상태</th>
                   <th>액션</th>
               </tr>
           </thead>
           <tbody>
               {% for reservation in user.reservation_set.all %}
               <tr>
                   <td>{{ reservation.book.title }}</td>
                   <td>{{ reservation.reserved_date|date:"Y-m-d" }}</td>
                   <td>
                       {% if reservation.status == 'WAITING' %}
                           <span class="badge bg-warning">대기중</span>
                       {% elif reservation.status == 'AVAILABLE' %}
                           <span class="badge bg-success">대출가능</span>
                       {% endif %}
                   </td>
                   <td>
                       {% if reservation.status == 'WAITING' %}
                           <form method="post" action="{% url 'books:reservation-cancel' reservation.id %}" class="d-inline">
                               {% csrf_token %}
                               <button class="btn btn-sm btn-danger">취소</button>
                           </form>
                       {% endif %}
                   </td>
               </tr>
               {% empty %}
               <tr>
                   <td colspan="4" class="text-center">예약 중인 도서가 없습니다.</td>
               </tr>
               {% endfor %}
           </tbody>
       </table>
   </div>
</div>
{% endblock %}
4.3.1 profile.html (프로필) 상세 구조
  1. 사용자 정보 섹션

    • 아이디/이메일 표시
    • 사용자 권한 표시(get_role_display)
  2. 대출 현황 테이블

    • 현재 대출 도서 목록
    • 연체 상태 표시(badge 사용)
    • 반납 예정일 정보
  3. 예약 현황 섹션

    • 예약 상태별 표시(대기중/대출가능)
    • 예약 취소 기능(WAITING 상태만)
    • 날짜 포맷팅 적용
  4. 데이터 연결

    • user.loan_set.all로 대출 정보 접근
    • user.reservation_set.all로 예약 정보 접근
  5. 빈 데이터 처리

    • {% empty %} 태그로 데이터 없음 메시지
    • 테이블 전체 너비 사용(colspan)
7.3 URL 라우터, 모델 설계 및 구현7.5 accounts 앱 구현