WeniVooks

검색

Django 베이스캠프

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
7.5 accounts 앱 구현7.7 Loans 관련 로직 구현