환경
OS : Windows 10
python : 3.10.11
Django : 5.0.7
주요 파일 경로
mysite/
|- manage.py
|- config/
| |- urls.py
| |- views.py
| |- settings/
| |- settings.py
|- member/
| |- models.py
| |- urls.py
| |- views.py
|- templates/
|- base.html
읽기 전에
이 글은 이전에 작성한 내용을 토대로 합니다.
본문 내용을 따라할 예정이라면 이전 글을 먼저 읽고 동일한 환경에서 진행해주세요.
이 글에서는 장고로 이메일 보내기 설정을 사용합니다.
장고로 이메일 보내는 방법을 먼저 적용하지 않았다면 이 글을 확인하세요.
member app생성하기
사이트를 내용을 구성하기에 앞서, 사이트를 이용하는 사람들의 데이터를 저장할 수 있어야 합니다.
사이트 이용자 정보를 관리할 "member" app을 생성합니다.
명령어는 "django-admin startapp member"입니다.
(django) C:\django\mysite>django-admin startapp member
(django) C:\django\mysite>
models.py 작성하기
member 폴더 안의 models.py를 찾아 다음과 같이 작성해줍니다.
# member/models.py
# code by 설표의장고(django.seolpyo.com)
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.validators import UnicodeUsernameValidator
from django.db import models
class Member(AbstractUser):
first_name = None # AbstractUser에서 내장하고 있는 Field, None으로 override하여 비활성화
last_name = None # AbstractUser에서 내장하고 있는 Field, None으로 override하여 비활성화
email = models.EmailField( # 로그인에 사용할 이메일
verbose_name='로그인 이메일', # 회원가입 페이지에 노출되는 명칭
unique=True, # 중복값 비허용
blank=False, # 이 값은 반드시 입력되어야함
null=False, # data가 반드시 유효해야함
)
username = models.CharField( # 로그인시 표시할 닉네임
verbose_name='닉네임', # 회원가입 페이지에 노출되는 명칭
max_length=9, # 최대 9글자
unique=True, # 중복값 비허용
blank=False, # 이 값은 반드시 입력되어야함
null=False, # data가 반드시 유효해야함
validators=[UnicodeUsernameValidator],
)
token_email = models.CharField( # 이메일 인증토큰
max_length=99,
null=True, # null 허용
)
USERNAME_FIELD = 'email' # email field를 로그인에 사용
REQUIRED_FIELDS = ['username',] # createsuperuser 명령시 요구하는 field 목록
# object의 기본 표시명을 username으로 설정
def __str__(self): return self.username
app 추가하기
settings.py의 "INSTALLED_APPS"에 "member"를 추가해 member app을 등록하고, INSTALLED_APPS 하단에 다음 4개 변수를 추가해줍니다.
# config/settings/settings.py
...
INSTALLED_APPS = [
'member',
...
]
AUTH_USER_MODEL = 'member.Member'
LOGIN_URL = '/'
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'
...
마이그레이션
장고는 모델이 추가되거나 변경된 내용이 있을 때마다 마이그레이션을 진행해주어야 합니다.
"python manage.py makemigrations" 명령으로 마이그레이션을 생성하고,
"python manage.py migrate" 명령으로 마이그레이션을 database에 적용합니다.
(django) C:\django\mysite>python manage.py makemigrations
Migrations for 'member':
member\migrations\0001_initial.py
- Create model Member
(django) C:\django\mysite>python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, member, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0001_initial... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying member.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying sessions.0001_initial... OK
(django) C:\django\mysite>
veiws.py 작성하기
회원가입과 이메일 인증을 위한 views.py를 작성합니다.
# member/veiws.py
# code by 설표의장고(django.seolpyo.com)
from urllib.parse import unquote
from django.core.mail import send_mail
from django.contrib import messages
from django.contrib.auth.hashers import make_password
from django.contrib.auth import forms as auth_forms
from django.forms.models import modelform_factory
from django.core.exceptions import BadRequest
from django.shortcuts import redirect, resolve_url, get_object_or_404
from django.utils import timezone
from django.views.generic import CreateView, UpdateView
from .models import Member
class Signup(CreateView):
"회원가입에 사용하는 class"
model = Member
fields = ['email']
template_name = 'post.html'
def send_email(self, email, token):
url = f'{self.request.scheme}://{self.request.get_host()}' + resolve_url('member:verify') # 이메일 인증에 사용하는 url
send_mail(
'회원가입 정보를 입력해주세요.',
message='',
from_email=None, # 이메일 발송인, None인 경우 setting에 설정된 값을 사용
recipient_list=[email], # 이메일 수신인 list
auth_user=None, # SMTP 유저, None인 경우 setting에 설정된 값을 사용
auth_password=None, # SMTP 비밀번호, None인 경우 setting에 설정된 값을 사용
html_message=f'<p>다음 버튼을 눌러 회원가입을 완료해주세요.</p><br><form target="_blank" action="{url}"><input type="hidden" name="email" value="{email}"><input type="hidden" name="token_email" value="{token}"><input type="submit" value="회원가입"></form>',
)
messages.info(self.request, f'"{email}"으로 회원가입 이메일이 발송되었습니다.<br>만약 5분이 지나도 이메일이 도착하지 않는다면 다시 회원가입을 신청해주세요.')
return redirect('member:login')
def form_valid(self, form):
token = timezone.now() + timezone.timedelta(minutes=10)
email = form.cleaned_data['email']
obj = self.model( # object 임시 생성
email=email,
username=email,
password='1', # 비밀번호 임의생성
token_email=token # 토큰 정보 저장
)
obj.save() # pk를 생성하기 위한 저장
obj.username = f'USER-{obj.pk:0{self.model.username.field.max_length}}' # pk를 이용해 username 임의생성
obj.save() # username 갱신을 위해 다시 저장
return self.send_email(email, token) # 회원가입 이메일 보내기
def form_invalid(self, form):
errors = form.errors
if errors.get('email') and '이미 존재' in errors['email'][0]: # 이미 가입한 이메일로 회원가입을 신청한 경우
email = form.data['email']
token = timezone.now() + timezone.timedelta(minutes=10)
obj = self.model.objects.get(email=email) # object 가져오기
if not obj.token_email: messages.info(self.request, '이미 가입한 이메일입니다.') # 회원가입을 완료한 이메일인 경우
else: # 이메일 인증이 아직 되지 않은 이메일인 경우
self.model.objects.filter(email=email).update(token_email=token) # 토큰 변경
return self.send_email(email, token)
return super().form_invalid(form)
class Verify(UpdateView):
"이메일 인증에 사용하는 class"
model = Member
form_class = modelform_factory(Member, auth_forms.UserCreationForm, fields=['username'],)
template_name = 'post.html'
def get_object(self, queryset=None):
email = unquote(self.request.GET.get('email', ''))
token = unquote(self.request.GET.get('token_email', ''))
obj = get_object_or_404(self.model, email=email,) # object 가져오기
if not obj.token_email: raise BadRequest('이미 회원가입이 완료된 이메일입니다.') # 이메일 인증은 마친 계정인 경우
elif obj.token_email != token: raise BadRequest('잘못된 요청입니다.') # 잘못된 토큰을 전달받은 경우
return obj
def form_valid(self, form):
self.object.username = form.cleaned_data['username'] # 닉네임 변경
self.object.password = make_password(form.cleaned_data['password1']) # 비밀번호 갱신
self.object.token_email = None # 이메일 토큰 제거
self.object.save() # 저장
return redirect(resolve_url('member:login')) # 로그인 페이지로 redirect
class RequestPasswordChange(CreateView):
"비밀번호 변경 요청에 사용하는 class"
model = Member
fields = ['email']
template_name = 'post.html'
def send_email(self, email, token):
url = f'{self.request.scheme}://{self.request.get_host()}' + resolve_url('member:passwordchange') # 비밀번호 변경에 사용하는 url
send_mail(
'비밀번호를 변경해주세요.',
message='',
from_email=None, # 이메일 발송인, None인 경우 setting에 설정된 값을 사용
recipient_list=[email], # 이메일 수신인 list
auth_user=None, # SMTP 유저, None인 경우 setting에 설정된 값을 사용
auth_password=None, # SMTP 비밀번호, None인 경우 setting에 설정된 값을 사용
html_message=f'<p>다음 버튼을 눌러 비밀번호를 변경해주세요.</p><br><form target="_blank" action="{url}"><input type="hidden" name="email" value="{email}"><input type="hidden" name="token_email" value="{token}"><input type="submit" value="비밀번호 변경"></form>',
)
messages.info(self.request, f'"{email}"으로 비밀번호 변경 이메일이 발송되었습니다.<br>만약 5분이 지나도 이메일이 도착하지 않는다면 다시 요청해주세요.')
return redirect('member:login')
def form_valid(self, form):
messages.info(self.request, f'미가입 이메일입니다.<br>회원가입을 진행해주세요.')
return redirect('member:login')
def form_invalid(self, form):
email = form.data['email']
try: obj = self.model.objects.get(email=email)
except self.model.DoesNotExist:
messages.info(self.request, '가입되지 않은 이메일입니다.')
return redirect(resolve_url('member:login'))
else:
if obj.password == '1': # 회원가입시 설정되는 초기 비밀번호가 적용 중인지 확인
messages.info(self.request, '회원가입이 진행 중이 이메일입니다.<br>이메일이 오지 않았다면 "회원가입"을 다시 신청해주세요.')
return redirect('member:login')
token = timezone.now()
obj.token_email = token # 토큰 생성
obj.save() # 변경내용 저장
return self.send_email(email, token,)
class PasswordChange(UpdateView):
"비밀번호 변경에 사용하는 class"
model = Member
form_class = modelform_factory(Member, auth_forms.UserCreationForm, fields=[],)
template_name = 'post.html'
def get_object(self, queryset=None):
email = unquote(self.request.GET.get('email', ''))
token = unquote(self.request.GET.get('token_email', ''))
obj = get_object_or_404(self.model, email=email,)
if not obj.token_email or obj.token_email != token: raise BadRequest('잘못된 요청입니다.')
return obj
def form_valid(self, form):
self.object.password = make_password(form.cleaned_data['password1'])
self.object.token_email = None
self.object.save()
return redirect(resolve_url('member:login'))
urls.py 작성하기
urls.py를 다음과 같이 작성합니다.
# member/urls.py
# code by 설표의장고(django.seolpyo.com)
from django.contrib.auth import views as auth_views
from django.urls import path
from . import views
app_name = 'member' # url 탐색을 위한 app 명칭, 여기에 속한 url은 "member:{name}" 형식의 이름을 갖게 된다.
urlpatterns = [
path(
'login/', # 로그인에 사용할 url
auth_views.LoginView.as_view( # 수행할 작업
template_name='login.html', # 로그인 페이지에서 사용하는 template
redirect_authenticated_user=True # 로그인한 이용자가 접근한 경우 redirect
),
name='login', # url name, app_name과 조합되므로 호출하는 이름은 "memeber:login"이 된다.
),
path(
'logout/', # 로그아웃에 사용할 url
auth_views.LogoutView.as_view(), # 수행할 작업
name='logout', # url name, app_name과 조합되므로 호출하는 이름은 "memeber:logout"이 된다.
),
path('signup/', views.Signup.as_view(), name='signup',), # 회원가입
path('verify/', views.Verify.as_view(), name='verify',), # 이메일 인증
path('resetpassword/', views.RequestPasswordChange.as_view(), name='passwordreset',), # 비밀번호 초기화
path('changepassword/', views.PasswordChange.as_view(), name='passwordchange',), # 비밀번호 변경
]
url 할당하기
member app의 url을 작성했으니, 이번에는 config 폴더에 있는 urls.py에서 member app의 url을 사용할 수 있도록 만들어야 합니다.
다음과 같이 "member/" path를 추가합니다.
from django.contrib import admin
from django.urls import path, include
from . import views
urlpatterns = [
path('admin/', admin.site.urls),
path('member/', include('member.urls')), # member/ 디렉토리 접근시 member app의 urls.py를 사용
path('', views.home),
]
home function 수정하기
이전에 작성한 config 폴더의 views.py를 다음과 같이 수정합니다.
# config/views.py
from django.shortcuts import render
def home(request):
return render(request, 'base.html')
템플릿 작성하기
base.html
templates 폴더에 base.html을 다음과 같이 작성합니다.
# templates/base.html
{% load static %}
<html>
<head>
<link rel="stylesheet" href="{% static 'style.css' %}">
</head>
<body>
<header>
<a href="/">설표의 장고</a>
</header>
<div style="padding-top: 40px;">
<nav>
{% if request.user.is_anonymous %}
{# 로그인하지 않은 경우 #}
<form action="{% url 'member:login' %}?next={{ request.path_info }}">
<input type="submit" value="로그인">
</form>
{% else %}
{# 로그인한 경우 #}
<div class="member" style="display: flex;">
{% if request.user.is_staff %}[관리자]{% endif %} "{{ request.user }}"님
<form method="post" action="{% url 'member:logout' %}" style="display: inline-block; margin-left: auto;">
{% csrf_token %}
<input type="submit" value="로그아웃">
</form>
</div>
{% endif %}
</nav>
<hr>
<br>
<div class="message">
{# 알림 메세지가 있는 경우 메세지 표시 #}
{% if messages %}
{% for msg in messages %}
{{ msg|safe }}
{% endfor %}
{% endif %}
</div>
<main>
{# base.html을 상속받는 템플릿에서 사용 가능한 block #}
{% block main %}{% endblock %}
</main>
</div>
</body>
<footer style="border-top: #000 1px solid; text-align: center; padding-top: 20px;">
<div>
<a target="_blank" href="https://www.djangoproject.com/">Powered by Django</a>
</div>
<div>
<a target="_blank" href="https://django.seolpyo.com/">Designed by 하얀설표</a>
</div>
</footer>
</html>
login.html
로그인 페이지에 사용되는 login.html을 다음과 같이 작성합니다.
# templates/login.html
{# base.html 을 상속받아 사용하는 명령어 #}
{% extends "base.html" %}
{# base.html에 선언된 {% block main %}에 삽입되는 내용 #}
{% block main %}
// code by 설표의장고(django.seolpyo.com)
<form method="post">
{% csrf_token %}
{{ form }}
<input type="submit">
</form>
<a href="{% url 'member:signup' %}">회원가입</a>
<a href="{% url 'member:passwordreset' %}">비밀번호 변경</a>
{% endblock %}
post.html
회원가입에 사용할 post.html을 다음과 같이 작성합니다.
# templates/post.html
{# base.html 을 상속받아 사용하는 명령어 #}
{% extends "base.html" %}
{# base.html에 선언된 {% block main %}에 삽입되는 내용 #}
{% block main %}
// code by 설표의장고(django.seolpyo.com)
{{ form.media }}
<form method="post">
{% csrf_token %}
{{ form }}
<input type="submit">
</form>
{% endblock %}
장고 프로젝트 실행하기
이제 장고 프로젝트로 운영되는 사이트에서 회원가입과 로그인, 로그아웃을 수행할 수 있습니다.
모든 템플릿이 base.html을 상속받고 있으므로 모든 페이지에서 로그인과 로그아웃 작업을 수행할 수 있습니다.
이메일 주소로 이메일을 발송하고, 거기서 다시 회원가입 페이지를 구현하는 과정이 정말 번거롭습니다.
그러나 이렇게 하지 않으면 "aaa@aaa.com"과 같이 이메일 형식만 맞춰주면 회원가입이 이루어져 최소한의 검증 절차를 진행하는 것이 좋습니다.
이 글의 댓글 기능은 일부러 막아놓았습니다. 궁금한 내용이 있다면 게시판을 이용해주세요!