WeniVooks

검색

Django 베이스캠프

URL 라우터, 모델 설계 및 구현

이전 시간에는 도서관 관리 시스템(Library Management System, LMS)의 요구사항 명세서를 작성하였습니다.

이번 시간에는, URL 라우터를 구현하고, 모델을 설계하고 구현해보겠습니다.

1. URL 라우터 구현

1.1 계정 관련 URL (accounts/urls.py)
  • 로그인/로그아웃
  • 회원가입
  • 프로필 페이지
# accounts/urls.py
from django.urls import path
from . import views
 
app_name = 'accounts' # 네임스페이스 설정
 
urlpatterns = [
    path('login/', views.login_view, name='login'),
    path('logout/', views.logout_view, name='logout'),
    path('register/', views.register_view, name='register'),
    path('profile/', views.profile_view, name='profile'),
]
# accounts/urls.py
from django.urls import path
from . import views
 
app_name = 'accounts' # 네임스페이스 설정
 
urlpatterns = [
    path('login/', views.login_view, name='login'),
    path('logout/', views.logout_view, name='logout'),
    path('register/', views.register_view, name='register'),
    path('profile/', views.profile_view, name='profile'),
]
1.2 도서 관련 URL (books/urls.py)
  • 도서 CRUD 작업
  • 대출/반납 관리
  • 예약 시스템
  • API 엔드포인트 (선택사항)
# books/urls.py  
from django.urls import path
from . import views
 
app_name = 'books'
 
urlpatterns = [
    # 도서 관련
    path('', views.book_list, name='book-list'),
    path('<int:pk>/', views.book_detail, name='book-detail'),
    path('create/', views.book_create, name='book-create'),
    path('<int:pk>/update/', views.book_update, name='book-update'),
    path('<int:pk>/delete/', views.book_delete, name='book-delete'),
    
    # 대출 관련
    path('loans/', views.loan_list, name='loan-list'),
    path('loans/user/', views.user_loan_list, name='user-loan-list'),
    path('loans/create/<int:book_id>/', views.loan_create, name='loan-create'),
    path('loans/return/<int:loan_id>/', views.loan_return, name='loan-return'),
    path('loans/overdue/', views.loan_overdue_list, name='loan-overdue'),
    
    # 예약 관련
    path('reservations/', views.reservation_list, name='reservation-list'),
    path('reservations/user/', views.user_reservation_list, name='user-reservation-list'),
    path('reservations/create/<int:book_id>/', views.reservation_create, name='reservation-create'),
    path('reservations/cancel/<int:reservation_id>/', views.reservation_cancel, name='reservation-cancel'),
    
    # API 엔드포인트 (선택 사항)
    path('search/', views.book_search, name='book-search'),
    path('status/<int:book_id>/', views.book_status, name='book-status'),
    path('loans/status/<int:loan_id>/', views.loan_status, name='loan-status'),
]
# books/urls.py  
from django.urls import path
from . import views
 
app_name = 'books'
 
urlpatterns = [
    # 도서 관련
    path('', views.book_list, name='book-list'),
    path('<int:pk>/', views.book_detail, name='book-detail'),
    path('create/', views.book_create, name='book-create'),
    path('<int:pk>/update/', views.book_update, name='book-update'),
    path('<int:pk>/delete/', views.book_delete, name='book-delete'),
    
    # 대출 관련
    path('loans/', views.loan_list, name='loan-list'),
    path('loans/user/', views.user_loan_list, name='user-loan-list'),
    path('loans/create/<int:book_id>/', views.loan_create, name='loan-create'),
    path('loans/return/<int:loan_id>/', views.loan_return, name='loan-return'),
    path('loans/overdue/', views.loan_overdue_list, name='loan-overdue'),
    
    # 예약 관련
    path('reservations/', views.reservation_list, name='reservation-list'),
    path('reservations/user/', views.user_reservation_list, name='user-reservation-list'),
    path('reservations/create/<int:book_id>/', views.reservation_create, name='reservation-create'),
    path('reservations/cancel/<int:reservation_id>/', views.reservation_cancel, name='reservation-cancel'),
    
    # API 엔드포인트 (선택 사항)
    path('search/', views.book_search, name='book-search'),
    path('status/<int:book_id>/', views.book_status, name='book-status'),
    path('loans/status/<int:loan_id>/', views.loan_status, name='loan-status'),
]
1.3 메인 페이지 URL (config/urls.py)
  • admin 페이지
  • accounts 앱 URL 포함
  • books 앱 URL 포함
# config/urls.py
from django.contrib import admin
from django.urls import path, include
 
urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('books.urls')),  # 메인 페이지는 도서 목록
    path('accounts/', include('accounts.urls')),
    path('books/', include('books.urls')), 
]
# config/urls.py
from django.contrib import admin
from django.urls import path, include
 
urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('books.urls')),  # 메인 페이지는 도서 목록
    path('accounts/', include('accounts.urls')),
    path('books/', include('books.urls')), 
]

각 패턴은 해당하는 뷰 함수와 연결되며, name 파라미터를 통해 템플릿에서 URL 역참조가 가능합니다. app_name을 지정하여 URL 패턴의 네임스페이스를 구분했습니다.

이제 각각의 view 함수를 구현한 후, 템플릿에서는 다음과 같이 URL을 참조할 수 있습니다.

<a href="{% url 'books:book-list' %}">도서 목록</a>
<a href="{% url 'accounts:login' %}">로그인</a>
<a href="{% url 'books:book-list' %}">도서 목록</a>
<a href="{% url 'accounts:login' %}">로그인</a>

2. 모델 설계 및 구현

2.1 사용자 모델 (accounts/models.py)
  • AbstractUser 상속으로 기본 인증 기능 활용
  • 사서/일반 사용자 역할 구분
  • 추가 정보(전화번호) 저장
# accounts/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
 
class User(AbstractUser):
   """
   사용자 모델: AbstractUser를 상속받아 기본 인증 시스템 활용
   기본 필드: username, password, email, first_name, last_name, is_staff, is_active, date_joined
   """
   ROLE_CHOICES = [
       ('LIBRARIAN', '사서'),
       ('USER', '일반 사용자'),
   ]
   
   role = models.CharField(
       max_length=10, 
       choices=ROLE_CHOICES,
       default='USER',
       verbose_name='역할'
   )
   phone = models.CharField(
       max_length=11,
       blank=True,
       verbose_name='전화번호'
   )
 
   class Meta: # Meta 클래스를 통해 모델의 메타데이터(옵션) 설정
       verbose_name = '사용자' # verbose_name 필드는 단수형 이름
       verbose_name_plural = '사용자 목록' # verbose_name_plural 필드는 복수형 이름
# accounts/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
 
class User(AbstractUser):
   """
   사용자 모델: AbstractUser를 상속받아 기본 인증 시스템 활용
   기본 필드: username, password, email, first_name, last_name, is_staff, is_active, date_joined
   """
   ROLE_CHOICES = [
       ('LIBRARIAN', '사서'),
       ('USER', '일반 사용자'),
   ]
   
   role = models.CharField(
       max_length=10, 
       choices=ROLE_CHOICES,
       default='USER',
       verbose_name='역할'
   )
   phone = models.CharField(
       max_length=11,
       blank=True,
       verbose_name='전화번호'
   )
 
   class Meta: # Meta 클래스를 통해 모델의 메타데이터(옵션) 설정
       verbose_name = '사용자' # verbose_name 필드는 단수형 이름
       verbose_name_plural = '사용자 목록' # verbose_name_plural 필드는 복수형 이름
2.2 도서, 대출, 예약 모델 (books/models.py)
2.2.1 도서 모델 (Book)
  • 기본적인 도서 정보 관리
  • 수량 관리 (전체/가용)
  • 생성/수정 시간 자동 기록
# books/models.py
from django.db import models
from django.conf import settings
 
class Book(models.Model):
   """
   도서 정보를 저장하는 모델
   """
   title = models.CharField(
       max_length=200, 
       verbose_name='도서명'
   )
   author = models.CharField(
       max_length=100, 
       verbose_name='저자'
   )
   isbn = models.CharField(
       max_length=13,
       unique=True,
       verbose_name='ISBN'
   )
   publisher = models.CharField(
       max_length=100, 
       verbose_name='출판사'
   )
   total_quantity = models.PositiveIntegerField( # PositiveIntegerField: 양수만 저장
       default=1,
       verbose_name='전체 수량'
   )
   available_quantity = models.PositiveIntegerField(
       default=1,
       verbose_name='대출 가능 수량'
   )
   created_at = models.DateTimeField(
       auto_now_add=True, # 생성 시간 자동 기록
       verbose_name='등록일'
   )
   updated_at = models.DateTimeField(
       auto_now=True, # 수정 시간 자동 기록
       verbose_name='수정일'
   )
 
   class Meta:
       verbose_name = '도서'
       verbose_name_plural = '도서 목록'
       ordering = ['-created_at'] # 기본 정렬 순서
 
   def __str__(self): # 객체를 문자열로 표현하는 메서드
       return self.title # 도서명으로 표현
 
   def is_available(self):
       """대출 가능 여부 확인"""
       return self.available_quantity > 0 # 대출 가능 수량이 0보다 크면 True 반환
       
   def decrease_quantity(self):
       """대출 시 수량 감소"""
       if self.is_available():
           self.available_quantity -= 1
           self.save()
           return True
       return False
 
   def increase_quantity(self):
       """반납 시 수량 증가"""
       if self.available_quantity < self.total_quantity:
           self.available_quantity += 1
           self.save()
           return True
       return False   
# books/models.py
from django.db import models
from django.conf import settings
 
class Book(models.Model):
   """
   도서 정보를 저장하는 모델
   """
   title = models.CharField(
       max_length=200, 
       verbose_name='도서명'
   )
   author = models.CharField(
       max_length=100, 
       verbose_name='저자'
   )
   isbn = models.CharField(
       max_length=13,
       unique=True,
       verbose_name='ISBN'
   )
   publisher = models.CharField(
       max_length=100, 
       verbose_name='출판사'
   )
   total_quantity = models.PositiveIntegerField( # PositiveIntegerField: 양수만 저장
       default=1,
       verbose_name='전체 수량'
   )
   available_quantity = models.PositiveIntegerField(
       default=1,
       verbose_name='대출 가능 수량'
   )
   created_at = models.DateTimeField(
       auto_now_add=True, # 생성 시간 자동 기록
       verbose_name='등록일'
   )
   updated_at = models.DateTimeField(
       auto_now=True, # 수정 시간 자동 기록
       verbose_name='수정일'
   )
 
   class Meta:
       verbose_name = '도서'
       verbose_name_plural = '도서 목록'
       ordering = ['-created_at'] # 기본 정렬 순서
 
   def __str__(self): # 객체를 문자열로 표현하는 메서드
       return self.title # 도서명으로 표현
 
   def is_available(self):
       """대출 가능 여부 확인"""
       return self.available_quantity > 0 # 대출 가능 수량이 0보다 크면 True 반환
       
   def decrease_quantity(self):
       """대출 시 수량 감소"""
       if self.is_available():
           self.available_quantity -= 1
           self.save()
           return True
       return False
 
   def increase_quantity(self):
       """반납 시 수량 증가"""
       if self.available_quantity < self.total_quantity:
           self.available_quantity += 1
           self.save()
           return True
       return False   
2.2.2 대출 모델 (Loan)
  • 대출 상태 관리 (대출중/연체/반납완료)
  • 대출일과 반납예정일 관리
  • 연체 여부 확인 메서드
 
# ... 위 코드 계속
 
class Loan(models.Model):
   """
   도서 대출 정보를 저장하는 모델
   """
   STATUS_CHOICES = [
       ('ACTIVE', '대출중'),
       ('OVERDUE', '연체'),
       ('RETURNED', '반납완료'),
   ]
 
   user = models.ForeignKey( # ForeignKey: 다른 모델과의 관계 설정
       settings.AUTH_USER_MODEL, # settings.py에 지정한 사용자 모델
       on_delete=models.CASCADE, # 연결된 사용자가 삭제되면 대출 정보도 삭제
       verbose_name='사용자' 
   )
   book = models.ForeignKey(
       Book,
       on_delete=models.CASCADE,
       verbose_name='도서'
   )
   status = models.CharField(
       max_length=10,
       choices=STATUS_CHOICES,
       default='ACTIVE',
       verbose_name='상태'
   )
   loan_date = models.DateTimeField(
       auto_now_add=True,
       verbose_name='대출일'
   )
   due_date = models.DateTimeField(
       verbose_name='반납예정일'
   )
   returned_date = models.DateTimeField(
       null=True,
       blank=True,
       verbose_name='반납일'
   )
 
   class Meta:
       verbose_name = '대출'
       verbose_name_plural = '대출 목록'
       ordering = ['-loan_date'] # 최신 대출 순으로 정렬
 
   def __str__(self):
       return f"{self.user.username} - {self.book.title}"
 
   def is_overdue(self):
       """연체 여부 확인"""
       from django.utils import timezone
       return self.status == 'ACTIVE' and timezone.now() > self.due_date # 대출중이고 반납예정일이 지났으면 True 반환
 
# ... 위 코드 계속
 
class Loan(models.Model):
   """
   도서 대출 정보를 저장하는 모델
   """
   STATUS_CHOICES = [
       ('ACTIVE', '대출중'),
       ('OVERDUE', '연체'),
       ('RETURNED', '반납완료'),
   ]
 
   user = models.ForeignKey( # ForeignKey: 다른 모델과의 관계 설정
       settings.AUTH_USER_MODEL, # settings.py에 지정한 사용자 모델
       on_delete=models.CASCADE, # 연결된 사용자가 삭제되면 대출 정보도 삭제
       verbose_name='사용자' 
   )
   book = models.ForeignKey(
       Book,
       on_delete=models.CASCADE,
       verbose_name='도서'
   )
   status = models.CharField(
       max_length=10,
       choices=STATUS_CHOICES,
       default='ACTIVE',
       verbose_name='상태'
   )
   loan_date = models.DateTimeField(
       auto_now_add=True,
       verbose_name='대출일'
   )
   due_date = models.DateTimeField(
       verbose_name='반납예정일'
   )
   returned_date = models.DateTimeField(
       null=True,
       blank=True,
       verbose_name='반납일'
   )
 
   class Meta:
       verbose_name = '대출'
       verbose_name_plural = '대출 목록'
       ordering = ['-loan_date'] # 최신 대출 순으로 정렬
 
   def __str__(self):
       return f"{self.user.username} - {self.book.title}"
 
   def is_overdue(self):
       """연체 여부 확인"""
       from django.utils import timezone
       return self.status == 'ACTIVE' and timezone.now() > self.due_date # 대출중이고 반납예정일이 지났으면 True 반환
2.2.3 예약 모델 (Reservation)
  • 예약 상태 관리 (대기/가능/취소)
  • 예약 만료일 설정
  • 중복 예약 방지
# ... 위 코드 계속
class Reservation(models.Model):
   """
   도서 예약 정보를 저장하는 모델
   """
   STATUS_CHOICES = [
       ('WAITING', '대기중'),
       ('AVAILABLE', '대출가능'),
       ('CANCELLED', '취소됨'),
   ]
 
   user = models.ForeignKey(
       settings.AUTH_USER_MODEL,
       on_delete=models.CASCADE,
       verbose_name='사용자'
   )
   book = models.ForeignKey(
       Book,
       on_delete=models.CASCADE,
       verbose_name='도서'
   )
   status = models.CharField(
       max_length=10,
       choices=STATUS_CHOICES,
       default='WAITING',
       verbose_name='상태'
   )
   reserved_date = models.DateTimeField(
       auto_now_add=True,
       verbose_name='예약일'
   )
   expiry_date = models.DateTimeField(
       verbose_name='예약만료일'
   )
 
   class Meta:
       verbose_name = '예약'
       verbose_name_plural = '예약 목록'
       ordering = ['reserved_date'] # 예약일 순으로 정렬
       # 한 사용자가 같은 도서를 중복 예약할 수 없도록 제약
       unique_together = ['user', 'book'] # 사용자, 도서 같은 경우 중복 예약 방지
 
   def __str__(self):
       return f"{self.user.username} - {self.book.title}"
 
   def is_expired(self):
       """예약 만료 여부 확인"""
       from django.utils import timezone
       return timezone.now() > self.expiry_date # 예약 만료일이 지났으면 True 반환
# ... 위 코드 계속
class Reservation(models.Model):
   """
   도서 예약 정보를 저장하는 모델
   """
   STATUS_CHOICES = [
       ('WAITING', '대기중'),
       ('AVAILABLE', '대출가능'),
       ('CANCELLED', '취소됨'),
   ]
 
   user = models.ForeignKey(
       settings.AUTH_USER_MODEL,
       on_delete=models.CASCADE,
       verbose_name='사용자'
   )
   book = models.ForeignKey(
       Book,
       on_delete=models.CASCADE,
       verbose_name='도서'
   )
   status = models.CharField(
       max_length=10,
       choices=STATUS_CHOICES,
       default='WAITING',
       verbose_name='상태'
   )
   reserved_date = models.DateTimeField(
       auto_now_add=True,
       verbose_name='예약일'
   )
   expiry_date = models.DateTimeField(
       verbose_name='예약만료일'
   )
 
   class Meta:
       verbose_name = '예약'
       verbose_name_plural = '예약 목록'
       ordering = ['reserved_date'] # 예약일 순으로 정렬
       # 한 사용자가 같은 도서를 중복 예약할 수 없도록 제약
       unique_together = ['user', 'book'] # 사용자, 도서 같은 경우 중복 예약 방지
 
   def __str__(self):
       return f"{self.user.username} - {self.book.title}"
 
   def is_expired(self):
       """예약 만료 여부 확인"""
       from django.utils import timezone
       return timezone.now() > self.expiry_date # 예약 만료일이 지났으면 True 반환

3. 어드민 등록

3.1 사용자 모델 등록 (accounts/admin.py)
# accounts/admin.py
from django.contrib import admin
from .models import User
 
admin.site.register(User)
# accounts/admin.py
from django.contrib import admin
from .models import User
 
admin.site.register(User)
3.2 도서, 대출, 예약 모델 등록 (books/admin.py)
 
# books/admin.py
 
from django.contrib import admin
from .models import Book, Loan, Reservation
 
admin.site.register(Book)
admin.site.register(Loan)
admin.site.register(Reservation)
 
# books/admin.py
 
from django.contrib import admin
from .models import Book, Loan, Reservation
 
admin.site.register(Book)
admin.site.register(Loan)
admin.site.register(Reservation)
7.2 User, AbstractUser, AbstractBaseUser 모델 설명7.4 템플릿 구현