accounts 앱 구현
이전 시간에는 도서관 관리 시스템(Library Management System, LMS)의 템플릿을 구현하였습니다.
이번 시간부터는 각 도메인(앱) 별로 CRUD(Create, Read, Update, Delete) 기능을 구현해보겠습니다.
먼저, accounts 앱의 기능을 구현해보겠습니다.
1. accounts 모델 내 메서드 추가
accounts 앱의 모델에 메서드를 추가하여, 비지니스 로직을 수행할 수 있도록 합니다.
#... 생략
def is_librarian(self):
"""사서 여부 확인"""
return self.role == 'LIBRARIAN'
def can_borrow(self):
"""대출 가능 여부 확인"""
from books.models import Loan
active_loans = Loan.objects.filter( # 대출 중인 도서 수
user=self, # 현재 사용자
status='ACTIVE' # 대출 중인 도서
).count() # 개수
return active_loans < 3 # 3권 이하인 경우 대출 가능
def has_overdue_books(self):
"""연체 도서 여부 확인"""
from books.models import Loan
return Loan.objects.filter( # 연체 중인 도서 확인
user=self, # 현재 사용자
status='ACTIVE', # 대출 중인 도서
due_date__lt=timezone.now() # 현재 시간보다 이전
).exists() # 존재 여부
def get_active_loans(self):
"""현재 대출 중인 도서 목록"""
return self.loan_set.filter(status='ACTIVE')
def get_active_reservations(self):
"""현재 예약 중인 도서 목록"""
return self.reservation_set.filter(status='WAITING')
#... 생략
def is_librarian(self):
"""사서 여부 확인"""
return self.role == 'LIBRARIAN'
def can_borrow(self):
"""대출 가능 여부 확인"""
from books.models import Loan
active_loans = Loan.objects.filter( # 대출 중인 도서 수
user=self, # 현재 사용자
status='ACTIVE' # 대출 중인 도서
).count() # 개수
return active_loans < 3 # 3권 이하인 경우 대출 가능
def has_overdue_books(self):
"""연체 도서 여부 확인"""
from books.models import Loan
return Loan.objects.filter( # 연체 중인 도서 확인
user=self, # 현재 사용자
status='ACTIVE', # 대출 중인 도서
due_date__lt=timezone.now() # 현재 시간보다 이전
).exists() # 존재 여부
def get_active_loans(self):
"""현재 대출 중인 도서 목록"""
return self.loan_set.filter(status='ACTIVE')
def get_active_reservations(self):
"""현재 예약 중인 도서 목록"""
return self.reservation_set.filter(status='WAITING')
is_librarian()
: 사서 권한 확인 메서드can_borrow()
: 추가 대출 가능 여부 확인 (3권 제한)has_overdue_books()
: 연체 도서 여부 확인get_active_loans()
: 현재 대출 목록 조회get_active_reservations()
: 현재 예약 목록 조회
1.1 사서 권한을 체크하는 데코레이터 추가 작성
# accounts/decorators.py
from django.shortcuts import redirect
from django.contrib import messages
from functools import wraps # wraps 데코레이터를 사용하여 데코레이터를 작성
def librarian_required(view_func): # 뷰 함수를 인자로 받는 데코레이터 이뜻은 뷰 함수를 인자로 받아서 뷰 함수를 반환한다는 뜻
"""사서 권한 필요한 뷰의 데코레이터"""
@wraps(view_func) # wraps 데코레이터를 사용하여 데코레이터를 작성 즉, view_func 함수를 래핑하여 사용 -> 결국 view_func 함수를 반환
def _wrapped_view(request, *args, **kwargs): # 뷰 함수의 인자를 받아서 처리
if not request.user.is_authenticated: # 로그인이 되어 있지 않은 경우
messages.error(request, '로그인이 필요합니다.') # 에러 메시지 출력
return redirect('accounts:login') # 로그인 페이지로 이동
if not request.user.is_librarian(): # 사서 권한이 없는 경우
messages.error(request, '사서 권한이 필요합니다.') # 에러 메시지 출력
return redirect('accounts:profile') # 프로필 페이지로 이동
return view_func(request, *args, **kwargs) # 뷰 함수 실행
return _wrapped_view # 래핑된 뷰 함수 반환
# accounts/decorators.py
from django.shortcuts import redirect
from django.contrib import messages
from functools import wraps # wraps 데코레이터를 사용하여 데코레이터를 작성
def librarian_required(view_func): # 뷰 함수를 인자로 받는 데코레이터 이뜻은 뷰 함수를 인자로 받아서 뷰 함수를 반환한다는 뜻
"""사서 권한 필요한 뷰의 데코레이터"""
@wraps(view_func) # wraps 데코레이터를 사용하여 데코레이터를 작성 즉, view_func 함수를 래핑하여 사용 -> 결국 view_func 함수를 반환
def _wrapped_view(request, *args, **kwargs): # 뷰 함수의 인자를 받아서 처리
if not request.user.is_authenticated: # 로그인이 되어 있지 않은 경우
messages.error(request, '로그인이 필요합니다.') # 에러 메시지 출력
return redirect('accounts:login') # 로그인 페이지로 이동
if not request.user.is_librarian(): # 사서 권한이 없는 경우
messages.error(request, '사서 권한이 필요합니다.') # 에러 메시지 출력
return redirect('accounts:profile') # 프로필 페이지로 이동
return view_func(request, *args, **kwargs) # 뷰 함수 실행
return _wrapped_view # 래핑된 뷰 함수 반환
이 데코레이터의 흐름을 자세히 살펴보겠습니다.
librarian_required
데코레이터는view_func
함수를 인자로 받습니다.view_func
: 뷰 함수 (예:book_create
,book_update
,book_delete
)
@wraps(view_func)
데코레이터를 사용하여 데코레이터를 작성합니다.wraps
데코레이터는 데코레이터를 작성할 때 사용합니다.
_wrapped_view
함수는 뷰 함수의 인자를 받아서 처리합니다.request
: 요청 객체*args
: 가변 인자**kwargs
: 키워드 인자
if not request.user.is_authenticated:
: 로그인이 되어 있지 않은 경우messages.error(request, '로그인이 필요합니다.')
: 에러 메시지 출력return redirect('accounts:login')
: 로그인 페이지로 이동
if not request.user.is_librarian():
: 사서 권한이 없는 경우messages.error(request, '사서 권한이 필요합니다.')
: 에러 메시지 출력return redirect('accounts:profile')
: 프로필 페이지로 이동
return view_func(request, *args, **kwargs)
: 뷰 함수 실행 (예:book_create(request)
,book_update(request, pk=1)
,book_delete(request, pk=1)
)_wrapped_view
함수를 반환합니다.
1.2 데코레이터 적용 예시
이제 우리는 librarian_required
데코레이터를 사용하여 사서 권한이 필요한 뷰에 적용할 수 있습니다.
@librarian_required
def some_librarian_view(request):
# 사서만 접근 가능한 뷰 로직
pass
@librarian_required
def some_librarian_view(request):
# 사서만 접근 가능한 뷰 로직
pass
2. forms.py 작성
accounts 앱의 폼을 작성하여, 사용자 입력을 처리할 수 있도록 합니다.
# accounts/forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from .models import User
class UserRegistrationForm(UserCreationForm): # UserCreationForm은 기본적으로 제공되는 회원가입 폼
"""회원가입 폼"""
phone = forms.CharField(
max_length=11,
required=False, # 필수 입력이 아님
widget=forms.TextInput(attrs={'class': 'form-control'}) # Bootstrap 스타일 적용
)
class Meta:
model = User
fields = ('username', 'email', 'phone', 'password1', 'password2') # 필드 순서 지정
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Bootstrap 스타일 적용
for field in self.fields:
self.fields[field].widget.attrs['class'] = 'form-control'
class LoginForm(AuthenticationForm): # AuthenticationForm은 기본적으로 제공되는 로그인 폼
"""로그인 폼"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Bootstrap 스타일 적용
for field in self.fields:
self.fields[field].widget.attrs['class'] = 'form-control'
class UserUpdateForm(forms.ModelForm):
"""사용자 정보 수정 폼"""
class Meta:
model = User
fields = ['email', 'phone']
widgets = {
'email': forms.EmailInput(attrs={'class': 'form-control'}),
'phone': forms.TextInput(attrs={'class': 'form-control'})
}
# accounts/forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from .models import User
class UserRegistrationForm(UserCreationForm): # UserCreationForm은 기본적으로 제공되는 회원가입 폼
"""회원가입 폼"""
phone = forms.CharField(
max_length=11,
required=False, # 필수 입력이 아님
widget=forms.TextInput(attrs={'class': 'form-control'}) # Bootstrap 스타일 적용
)
class Meta:
model = User
fields = ('username', 'email', 'phone', 'password1', 'password2') # 필드 순서 지정
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Bootstrap 스타일 적용
for field in self.fields:
self.fields[field].widget.attrs['class'] = 'form-control'
class LoginForm(AuthenticationForm): # AuthenticationForm은 기본적으로 제공되는 로그인 폼
"""로그인 폼"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Bootstrap 스타일 적용
for field in self.fields:
self.fields[field].widget.attrs['class'] = 'form-control'
class UserUpdateForm(forms.ModelForm):
"""사용자 정보 수정 폼"""
class Meta:
model = User
fields = ['email', 'phone']
widgets = {
'email': forms.EmailInput(attrs={'class': 'form-control'}),
'phone': forms.TextInput(attrs={'class': 'form-control'})
}
3. views.py 작성
# accounts/views.py
from django.shortcuts import render, redirect
from django.contrib.auth import login, logout
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from .forms import UserRegistrationForm, LoginForm
def register_view(request): # rqeuest: 요청 객체
"""회원가입 뷰"""
if request.user.is_authenticated: # 로그인 상태인 경우
return redirect('accounts:profile') # 프로필 페이지로 이동
if request.method == 'POST': # POST 요청인 경우
form = UserRegistrationForm(request.POST) # 회원가입 폼 생성
if form.is_valid(): # 유효성 검사
user = form.save() # 회원가입
login(request, user) # 로그인
messages.success(request, '회원가입이 완료되었습니다.') # 메시지 출력
return redirect('accounts:profile') # 프로필 페이지로 이동
else: # GET 요청인 경우
form = UserRegistrationForm() # 회원가입 폼 생성
return render(request, 'accounts/register.html', {'form': form}) # 회원가입 페이지 렌더링
def login_view(request):
"""로그인 뷰"""
if request.user.is_authenticated: # 로그인 상태인 경우
return redirect(reverse_lazy('accounts:profile')) # 프로필 페이지로 이동
if request.method == 'POST':
form = LoginForm(request, request.POST) # 로그인 폼 생성
if form.is_valid(): # 유효성 검사
login(request, form.get_user()) # 로그인
# next 파라미터가 있으면 해당 URL로 리다이렉트 아니면 프로필 페이지로 이동
next_url = request.GET.get('next', reverse_lazy('accounts:profile'))
return redirect(next_url)
else: # GET 요청인 경우
form = LoginForm() # 로그인 폼 생성
return render(request, 'accounts/login.html', {'form': form}) # 로그인 페이지 렌더링
def logout_view(request):
"""로그아웃 뷰"""
if request.method == 'POST':
logout(request) # 로그아웃
messages.success(request, '로그아웃되었습니다.') # 메시지 출력
return redirect(reverse_lazy('accounts:login')) # 로그인 페이지로 이동
@login_required # 로그인 필요 이 데코레이터는 로그인이 필요한 경우에만 접근 가능하도록 설정
def profile_view(request):
"""프로필 뷰"""
return render(request, 'accounts/profile.html', { # 프로필 페이지 렌더링
'active_loans': request.user.get_active_loans(), # 현재 대출 중인 도서 목록
'active_reservations': request.user.get_active_reservations() # 현재 예약 중인 도서 목록
})
# accounts/views.py
from django.shortcuts import render, redirect
from django.contrib.auth import login, logout
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from .forms import UserRegistrationForm, LoginForm
def register_view(request): # rqeuest: 요청 객체
"""회원가입 뷰"""
if request.user.is_authenticated: # 로그인 상태인 경우
return redirect('accounts:profile') # 프로필 페이지로 이동
if request.method == 'POST': # POST 요청인 경우
form = UserRegistrationForm(request.POST) # 회원가입 폼 생성
if form.is_valid(): # 유효성 검사
user = form.save() # 회원가입
login(request, user) # 로그인
messages.success(request, '회원가입이 완료되었습니다.') # 메시지 출력
return redirect('accounts:profile') # 프로필 페이지로 이동
else: # GET 요청인 경우
form = UserRegistrationForm() # 회원가입 폼 생성
return render(request, 'accounts/register.html', {'form': form}) # 회원가입 페이지 렌더링
def login_view(request):
"""로그인 뷰"""
if request.user.is_authenticated: # 로그인 상태인 경우
return redirect(reverse_lazy('accounts:profile')) # 프로필 페이지로 이동
if request.method == 'POST':
form = LoginForm(request, request.POST) # 로그인 폼 생성
if form.is_valid(): # 유효성 검사
login(request, form.get_user()) # 로그인
# next 파라미터가 있으면 해당 URL로 리다이렉트 아니면 프로필 페이지로 이동
next_url = request.GET.get('next', reverse_lazy('accounts:profile'))
return redirect(next_url)
else: # GET 요청인 경우
form = LoginForm() # 로그인 폼 생성
return render(request, 'accounts/login.html', {'form': form}) # 로그인 페이지 렌더링
def logout_view(request):
"""로그아웃 뷰"""
if request.method == 'POST':
logout(request) # 로그아웃
messages.success(request, '로그아웃되었습니다.') # 메시지 출력
return redirect(reverse_lazy('accounts:login')) # 로그인 페이지로 이동
@login_required # 로그인 필요 이 데코레이터는 로그인이 필요한 경우에만 접근 가능하도록 설정
def profile_view(request):
"""프로필 뷰"""
return render(request, 'accounts/profile.html', { # 프로필 페이지 렌더링
'active_loans': request.user.get_active_loans(), # 현재 대출 중인 도서 목록
'active_reservations': request.user.get_active_reservations() # 현재 예약 중인 도서 목록
})
4. 로그인/ 로그아웃 URL 설정
settings.py에 로그인/로그아웃 관련 설정을 추가합니다.
# config/settings.py
# 로그인/로그아웃 관련 설정
LOGIN_URL = 'accounts:login' # 로그인이 필요할 때 리다이렉트할 URL
LOGIN_REDIRECT_URL = 'accounts:profile' # 로그인 성공 후 리다이렉트할 URL
LOGOUT_REDIRECT_URL = 'accounts:login' # 로그아웃 후 리다이렉트할 URL
# config/settings.py
# 로그인/로그아웃 관련 설정
LOGIN_URL = 'accounts:login' # 로그인이 필요할 때 리다이렉트할 URL
LOGIN_REDIRECT_URL = 'accounts:profile' # 로그인 성공 후 리다이렉트할 URL
LOGOUT_REDIRECT_URL = 'accounts:login' # 로그아웃 후 리다이렉트할 URL
결국 이 설정은 로그인이 필요한 경우에만 접근 가능하도록 설정합니다. 리다이렉트 URL은 accounts
특히, @login_required 데코레이터를 사용하여 로그인이 필요한 경우에만 접근 가능하도록 설정합니다. 리다이렉트 URL은 accounts
예시를 보여드리면,
# settings.py 설정이 없는 경우
@login_required # 기본값으로 '/accounts/login/'으로 리다이렉트
def profile_view(request):
...
# settings.py 설정이 있는 경우
LOGIN_URL = 'accounts:login' # 우리가 만든 커스텀 로그인 URL로 리다이렉트
# settings.py 설정이 없는 경우
@login_required # 기본값으로 '/accounts/login/'으로 리다이렉트
def profile_view(request):
...
# settings.py 설정이 있는 경우
LOGIN_URL = 'accounts:login' # 우리가 만든 커스텀 로그인 URL로 리다이렉트
settings.py 설정이 없는 경우 그리고 있는 경우의 차이는 결국 리다이렉트 URL이 다르다는 것입니다.
예시를 들면, settings.py 설정이 없는 경우에는 기본값인 '/accounts/login/'으로 리다이렉트되지만, (Django의 기본 로그인 URL)
설정이 있는 경우에는 우리가 만든 커스텀 로그인 URL로 리다이렉트됩니다.