books 앱 구현
이번 시간에는 도서관 관리 시스템(Library Management System, LMS)의 books 앱을 구현해보겠습니다.
먼저, books 앱의 CRUD(Create, Read, Update, Delete) 기능을 구현해보겠습니다.
1. 도서 관련 뷰 구현
1.1 도서 목록 조회
def book_list(request):
"""도서 목록 및 검색"""
query = request.GET.get('query', '') # 검색어
if query:
books = Book.objects.filter(
models.Q(title__icontains=query) | # 제목 검색
models.Q(author__icontains=query) | # 저자 검색
models.Q(isbn__icontains=query) # ISBN 검색
)
else:
books = Book.objects.all() # 전체 도서 목록
return render(request, 'books/book_list.html', {'books': books, 'query': query}) # 도서 목록 템플릿 렌더링
def book_list(request):
"""도서 목록 및 검색"""
query = request.GET.get('query', '') # 검색어
if query:
books = Book.objects.filter(
models.Q(title__icontains=query) | # 제목 검색
models.Q(author__icontains=query) | # 저자 검색
models.Q(isbn__icontains=query) # ISBN 검색
)
else:
books = Book.objects.all() # 전체 도서 목록
return render(request, 'books/book_list.html', {'books': books, 'query': query}) # 도서 목록 템플릿 렌더링
1.2 도서 상세 조회
def book_detail(request, pk):
"""도서 상세 정보"""
book = get_object_or_404(Book, pk=pk) # 도서 조회
return render(request, 'books/book_detail.html', {'book': book}) # 도서 상세 정보 템플릿 렌더링
def book_detail(request, pk):
"""도서 상세 정보"""
book = get_object_or_404(Book, pk=pk) # 도서 조회
return render(request, 'books/book_detail.html', {'book': book}) # 도서 상세 정보 템플릿 렌더링
1.3 도서 등록
@librarian_required # 사서 권한 데코레이터
def book_create(request):
"""도서 등록 (사서 전용)"""
if request.method == 'POST': # POST 요청인 경우
form = BookForm(request.POST) # 폼 데이터 바인딩
if form.is_valid(): # 폼 데이터 유효성 검사
book = form.save(commit=False) # 도서 정보 저장
book.available_quantity = book.total_quantity # 대출 가능 수량 초기화
book.save() # 도서 정보 저장
messages.success(request, '도서가 등록되었습니다.') # 성공 메시지
return redirect('books:book-detail', pk=book.pk) # 도서 상세 페이지로 이동
else:
form = BookForm() # 폼 초기화
return render(request, 'books/book_form.html', {'form': form, 'title': '도서 등록'}) # 도서 등록 폼 렌더링
@librarian_required # 사서 권한 데코레이터
def book_create(request):
"""도서 등록 (사서 전용)"""
if request.method == 'POST': # POST 요청인 경우
form = BookForm(request.POST) # 폼 데이터 바인딩
if form.is_valid(): # 폼 데이터 유효성 검사
book = form.save(commit=False) # 도서 정보 저장
book.available_quantity = book.total_quantity # 대출 가능 수량 초기화
book.save() # 도서 정보 저장
messages.success(request, '도서가 등록되었습니다.') # 성공 메시지
return redirect('books:book-detail', pk=book.pk) # 도서 상세 페이지로 이동
else:
form = BookForm() # 폼 초기화
return render(request, 'books/book_form.html', {'form': form, 'title': '도서 등록'}) # 도서 등록 폼 렌더링
1.4 도서 수정
@librarian_required # 사서 권한 데코레이터
def book_update(request, pk):
"""도서 수정 (사서 전용)"""
book = get_object_or_404(Book, pk=pk) # 도서 조회
if request.method == 'POST': # POST 요청인 경우
form = BookForm(request.POST, instance=book) # 폼 데이터 바인딩
if form.is_valid(): # 폼 데이터 유효성 검사
book = form.save() # 도서 정보 저장
messages.success(request, '도서가 수정되었습니다.') # 성공 메시지
return redirect('books:book-detail', pk=book.pk) # 도서 상세 페이지로 이동
else:
form = BookForm(instance=book) # 폼 초기화
return render(request, 'books/book_form.html', {'form': form, 'title': '도서 수정'}) # 도서 수정 폼 렌더링
@librarian_required # 사서 권한 데코레이터
def book_update(request, pk):
"""도서 수정 (사서 전용)"""
book = get_object_or_404(Book, pk=pk) # 도서 조회
if request.method == 'POST': # POST 요청인 경우
form = BookForm(request.POST, instance=book) # 폼 데이터 바인딩
if form.is_valid(): # 폼 데이터 유효성 검사
book = form.save() # 도서 정보 저장
messages.success(request, '도서가 수정되었습니다.') # 성공 메시지
return redirect('books:book-detail', pk=book.pk) # 도서 상세 페이지로 이동
else:
form = BookForm(instance=book) # 폼 초기화
return render(request, 'books/book_form.html', {'form': form, 'title': '도서 수정'}) # 도서 수정 폼 렌더링
1.5 도서 삭제
@librarian_required # 사서 권한 데코레이터
def book_delete(request, pk):
"""도서 삭제 (사서 전용)"""
book = get_object_or_404(Book, pk=pk) # 도서 조회
if request.method == 'POST': # POST 요청인 경우
book.delete() # 도서 삭제
messages.success(request, '도서가 삭제되었습니다.')
return redirect('books:book-list') # 도서 목록 페이지로 이동
return render(request, 'books/book_confirm_delete.html', {'book': book}) # 도서 삭제 확인 페이지 렌더링
@librarian_required # 사서 권한 데코레이터
def book_delete(request, pk):
"""도서 삭제 (사서 전용)"""
book = get_object_or_404(Book, pk=pk) # 도서 조회
if request.method == 'POST': # POST 요청인 경우
book.delete() # 도서 삭제
messages.success(request, '도서가 삭제되었습니다.')
return redirect('books:book-list') # 도서 목록 페이지로 이동
return render(request, 'books/book_confirm_delete.html', {'book': book}) # 도서 삭제 확인 페이지 렌더링
2.도서 폼 구현
# books/forms.py
from django import forms
from .models import Book
class BookForm(forms.ModelForm):
class Meta:
model = Book
fields = ['title', 'author', 'isbn', 'publisher', 'total_quantity']
# 각 필드별 위젯 설정
widgets = {
'title': forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': '도서명을 입력하세요'
}
),
'author': forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': '저자명을 입력하세요'
}
),
'isbn': forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': 'ISBN을 입력하세요'
}
),
'publisher': forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': '출판사명을 입력하세요'
}
),
'total_quantity': forms.NumberInput(
attrs={
'class': 'form-control',
'min': '1', # 최소값 설정
}
)
}
# 각 필드별 레이블 설정
labels = {
'title': '도서명',
'author': '저자',
'isbn': 'ISBN',
'publisher': '출판사',
'total_quantity': '총 수량'
}
# 각 필드별 도움말 설정
help_texts = {
'isbn': 'ISBN 13자리를 입력하세요',
'total_quantity': '최소 1권 이상 입력하세요'
}
def clean_isbn(self):
"""ISBN 유효성 검사"""
isbn = self.cleaned_data.get('isbn')
if len(isbn) != 13: # ISBN은 13자리여야 함
raise forms.ValidationError('ISBN은 13자리여야 합니다.')
# ISBN이 이미 존재하는지 확인 (수정 시에는 자기 자신은 제외)
exists = Book.objects.filter(isbn=isbn)
if self.instance: # 수정 시
exists = exists.exclude(pk=self.instance.pk)
if exists.exists():
raise forms.ValidationError('이미 등록된 ISBN입니다.')
return isbn
def clean_total_quantity(self):
"""수량 유효성 검사"""
quantity = self.cleaned_data.get('total_quantity')
if quantity < 1: # 최소 1권
raise forms.ValidationError('수량은 1권 이상이어야 합니다.')
return quantity
def save(self, commit=True):
"""저장 시 available_quantity도 함께 설정"""
instance = super().save(commit=False)
if not self.instance.pk: # 새로운 도서 등록 시
instance.available_quantity = instance.total_quantity
elif instance.total_quantity < self.instance.available_quantity: # 수정 시 총 수량을 줄이는 경우
instance.available_quantity = instance.total_quantity
if commit:
instance.save()
return instance
# books/forms.py
from django import forms
from .models import Book
class BookForm(forms.ModelForm):
class Meta:
model = Book
fields = ['title', 'author', 'isbn', 'publisher', 'total_quantity']
# 각 필드별 위젯 설정
widgets = {
'title': forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': '도서명을 입력하세요'
}
),
'author': forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': '저자명을 입력하세요'
}
),
'isbn': forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': 'ISBN을 입력하세요'
}
),
'publisher': forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': '출판사명을 입력하세요'
}
),
'total_quantity': forms.NumberInput(
attrs={
'class': 'form-control',
'min': '1', # 최소값 설정
}
)
}
# 각 필드별 레이블 설정
labels = {
'title': '도서명',
'author': '저자',
'isbn': 'ISBN',
'publisher': '출판사',
'total_quantity': '총 수량'
}
# 각 필드별 도움말 설정
help_texts = {
'isbn': 'ISBN 13자리를 입력하세요',
'total_quantity': '최소 1권 이상 입력하세요'
}
def clean_isbn(self):
"""ISBN 유효성 검사"""
isbn = self.cleaned_data.get('isbn')
if len(isbn) != 13: # ISBN은 13자리여야 함
raise forms.ValidationError('ISBN은 13자리여야 합니다.')
# ISBN이 이미 존재하는지 확인 (수정 시에는 자기 자신은 제외)
exists = Book.objects.filter(isbn=isbn)
if self.instance: # 수정 시
exists = exists.exclude(pk=self.instance.pk)
if exists.exists():
raise forms.ValidationError('이미 등록된 ISBN입니다.')
return isbn
def clean_total_quantity(self):
"""수량 유효성 검사"""
quantity = self.cleaned_data.get('total_quantity')
if quantity < 1: # 최소 1권
raise forms.ValidationError('수량은 1권 이상이어야 합니다.')
return quantity
def save(self, commit=True):
"""저장 시 available_quantity도 함께 설정"""
instance = super().save(commit=False)
if not self.instance.pk: # 새로운 도서 등록 시
instance.available_quantity = instance.total_quantity
elif instance.total_quantity < self.instance.available_quantity: # 수정 시 총 수량을 줄이는 경우
instance.available_quantity = instance.total_quantity
if commit:
instance.save()
return instance