24.06.13_TIL ( 팀 프로젝트 : AI NOST Django ) _ 15. 이메일 확인 모달창(백엔드), 회원가입시 에러메세지창, 이메일 재발송
[ 세번째 프로젝트 ]
AI를 이용한 소설 사이트를 만들어 보자.
++ 팀 스로젝트로 팀과의 협업이 중요하다.
++ 장고 공식 문서는 항상 확인하기
https://docs.djangoproject.com/en/4.2/
++ 랭체인 공식 문서
https://www.langchain.com/
++ 리액트 공식문서
https://ko.legacy.reactjs.org/ # 한국어
https://ko.react.dev/
https://github.com/1489ehdghks/NOST.git
이메일로 회원가입 이메일 인증 참조 : https://velog.io/@nayu1105/%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EC%8B%9C-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EC%9D%B8%EC%A6%9D-%EA%B5%AC%ED%98%84
1. 회원가입시 이메일 확인 모달창 만들기
- 회원가입 후 이메일 인증 안내 메시지 표시:
- 회원가입이 성공하면 signupSuccess 상태를 true로 설정하여 이메일 인증 안내 메시지를 표시합니다.
- 이메일 인증 완료 후 로그인 화면 전환:
- 이메일 인증 API 호출 후 성공하면 emailVerified 상태를 true로 설정하여 로그인 화면으로 전환합니다.
- 프론트엔드에서 이메일 인증 성공/실패를 처리:
- 이메일 인증 링크를 클릭하면 백엔드의 ConfirmEmailView가 호출됩니다.
- 성공 시 프론트엔드의 특정 경로로 리디렉션합니다 (/login/success/).
- 실패 시 프론트엔드의 다른 경로로 리디렉션합니다 (/login/failure/).
accounts / urls.py
from django.urls import path, include, re_path
from dj_rest_auth.registration.views import VerifyEmailView
from .views import ProfileAPIView, ConfirmEmailView
urlpatterns = [
path("", include("dj_rest_auth.urls")),
# 유효한 이메일이 유저에게 전달
re_path(
r"^account-confirm-email/$",
VerifyEmailView.as_view(),
name="account_email_verification_sent",
),
# 유저가 클릭한 이메일(=링크) 확인
re_path(
r"^account-confirm-email/(?P<key>[-:\w]+)/$",
ConfirmEmailView.as_view(),
name="account_confirm_email",
),
path("", include("dj_rest_auth.registration.urls")),
path("profile/", ProfileAPIView.as_view()),
]
views.py
class ConfirmEmailView(APIView):
permission_classes = [AllowAny]
def get(self, *args, **kwargs):
self.object = confirmation = self.get_object()
confirmation.confirm(self.request)
# A React Router Route will handle the failure scenario
return HttpResponseRedirect("/login/success/")
def get_object(self, queryset=None):
key = self.kwargs["key"]
email_confirmation = EmailConfirmationHMAC.from_key(key)
if not email_confirmation:
if queryset is None:
queryset = self.get_queryset()
try:
email_confirmation = queryset.get(key=key.lower())
except EmailConfirmation.DoesNotExist:
# A React Router Route will handle the failure scenario
return HttpResponseRedirect("/login/failure/")
return email_confirmation
def get_queryset(self):
qs = EmailConfirmation.objects.all_valid()
qs = qs.select_related("email_address__user")
return qs
백엔드 코드가 이럴 때 프론트엔드와 연결하기.
import React, { useState, useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { login } from '../../features/auth/LoginInstance';
import { signup } from '../../features/auth/SignupInstance';
import useGlobalStore from '../../shared/store/GlobalStore';
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import './LoginModal.scss';
const LoginModal = ({ onClose }) => {
const [isLoginFormActive, setLoginFormActive] = useState(true);
const [loginInputs, setLoginInputs] = useState({ email: '', password: '' });
const [loginErrors, setLoginErrors] = useState({});
const [signupInputs, setSignupInputs] = useState({ nickname: '', email: '', password1: '', password2: '' });
const [signupErrors, setSignupErrors] = useState({});
const isLoading = useGlobalStore(state => state.isLoading);
const globalError = useGlobalStore(state => state.error);
const [signupSuccess, setSignupSuccess] = useState(false);
const navigate = useNavigate();
const [emailVerified, setEmailVerified] = useState(false);
const location = useLocation();
useEffect(() => {
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
onClose();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [onClose]);
useEffect(() => {
if (signupSuccess) {
toast.success("이메일 인증메일이 발송되었습니다. 확인해주세요.");
}
}, [signupSuccess]);
const handleLoginInputChange = (event) => {
const { name, value } = event.target;
setLoginInputs({ ...loginInputs, [name]: value });
};
const handleSignupInputChange = (event) => {
const { name, value } = event.target;
setSignupInputs({ ...signupInputs, [name]: value });
};
const handleLoginSubmit = async (event) => {
event.preventDefault();
setLoginErrors({});
await login(loginInputs.email, loginInputs.password);
if (globalError) {
const errors = {};
if (globalError.includes('email')) {
errors.email = globalError;
} else if (globalError.includes('password')) {
errors.password = globalError;
} else {
errors.non_field_errors = globalError;
}
setLoginErrors(errors);
} else {
navigate('/');
}
};
const handleSignupSubmit = async (event) => {
event.preventDefault();
setSignupErrors({});
if (signupInputs.password1 !== signupInputs.password2) {
setSignupErrors({ password2: ['Passwords do not match'] });
return;
}
const response = await signup(signupInputs.email, signupInputs.password1, signupInputs.password2, signupInputs.nickname);
setSignupSuccess(true);
setLoginFormActive(true);
};
return (
<div className="modalOverlay">
<ToastContainer />
<div className="modalContent" onClick={(e) => e.stopPropagation()}>
<div className="user_options-container">
<div className={`user_options-text ${isLoginFormActive ? '' : 'slide-out'}`}>
{/* 로그인 왼쪽 */}
<div className="user_options-unregistered">
<h2 className="user_unregistered-title">Don't have an account?</h2>
<p className="user_unregistered-text">Sign up to join our community!</p>
<button className="user_unregistered-signup" onClick={() => setLoginFormActive(false)}>SIGN UP</button>
</div>
{/* 로그인 오른쪽 */}
<div className="user_options-registered">
<h2 className="user_registered-title">Have an account?</h2>
<p className="user_registered-text">Log in to continue.</p>
<button className="user_registered-login" onClick={() => setLoginFormActive(true)}>LOGIN</button>
</div>
</div>
</div>
{/* Forms */}
<div className={`forms-container ${isLoginFormActive ? 'show-login' : 'show-signup'}`}>
{/* 로그인폼 */}
<div className={`user_forms-login ${isLoginFormActive ? 'active' : 'inactive'}`}>
<button className="closeButton" onClick={onClose}>×</button>
<h2 className="forms_title">Login</h2>
<form className="forms_form" onSubmit={handleLoginSubmit}>
<fieldset className="forms_fieldset">
<div className="forms_field">
<input
type="email"
placeholder="Email"
className="forms_field-input"
required
autoFocus
name="email"
value={loginInputs.email}
onChange={handleLoginInputChange}
disabled={isLoading}
/>
{loginErrors.email && <div className="error-message">{loginErrors.email}</div>}
</div>
<div className="forms_field">
<input
type="password"
placeholder="Password"
className="forms_field-input"
required
name="password"
value={loginInputs.password}
onChange={handleLoginInputChange}
disabled={isLoading}
/>
{loginErrors.password && <div className="error-message">{loginErrors.password}</div>}
</div>
</fieldset>
<div className="forms_buttons">
<button type="button" className="forms_buttons-forgot" disabled={isLoading}>Forgot password?</button>
<input type="submit" value="Log In" className="forms_buttons-action" disabled={isLoading} />
</div>
{loginErrors.non_field_errors && <div className="error-message">{loginErrors.non_field_errors}</div>}
</form>
<div className="social-login-buttons">
<button className="social-button google-login">
<img src="/path/to/google-icon.png" alt="Google Icon" />
Login with Google
</button>
<button className="social-button naver-login">
<img src="/path/to/naver-icon.png" alt="Naver Icon" />
Login with Naver
</button>
</div>
</div>
{/* 회원가입폼 */}
<div className={`user_forms-signup ${!isLoginFormActive ? 'active' : 'inactive'}`}>
<button className="closeButton" onClick={onClose}>×</button>
<h2 className="forms_title">Sign Up</h2>
<form className="forms_form" onSubmit={handleSignupSubmit}>
<fieldset className="forms_fieldset">
<div className="forms_field">
<input
type="text"
placeholder="Nickname"
className="forms_field-input"
required
name="nickname"
value={signupInputs.nickname}
onChange={handleSignupInputChange}
disabled={isLoading}
/>
{signupErrors.nickname && <div className="error-message">{signupErrors.nickname}</div>}
</div>
<div className="forms_field">
<input
type="email"
placeholder="Email"
className="forms_field-input"
required
name="email"
value={signupInputs.email}
onChange={handleSignupInputChange}
disabled={isLoading}
/>
{signupErrors.email && <div className="error-message">{signupErrors.email}</div>}
</div>
<div className="forms_field">
<input
type="password"
placeholder="Password"
className="forms_field-input"
required
name="password1"
value={signupInputs.password1}
onChange={handleSignupInputChange}
disabled={isLoading}
/>
{signupErrors.password1 && <div className="error-message">{signupErrors.password1}</div>}
</div>
<div className="forms_field">
<input
type="password"
placeholder="Confirm Password"
className="forms_field-input"
required
name="password2"
value={signupInputs.password2}
onChange={handleSignupInputChange}
disabled={isLoading}
/>
{signupErrors.password2 && <div className="error-message">{signupErrors.password2}</div>}
</div>
</fieldset>
<div className="forms_buttons">
<input type="submit" value="Sign Up" className="forms_buttons-action" disabled={isLoading} />
</div>
{signupErrors.non_field_errors && <div className="error-message">{signupErrors.non_field_errors}</div>}
</form>
</div>
</div>
</div>
</div>
);
};
export default LoginModal;
모달 확인만 하게 해주었다.. 다시 주고 받는 유알엘을 넣으려고 하니까 백엔드에서와의 호환문제가 있어서 그냥 백엔드에서 바로 신호를 받아서 html 로 창하나 띄워주는 것으로 바꿈
백엔드에서 get으로는 잘 받는데 유알엘을 찾을 수 없다고 화면을 못보여주는 것으로 404 에러를 띄운다.
메일을 클릭해도 페이지가 없어서 에러가 뜨는 모양 그래서 백엔드에서 바로 html 로 해주려는 것
"GET /api/accounts/account-confirm-email/Mjk:1sGTrq:bD7ALutvU_Cey-qndJRTBBz3_zzuiEFfH_8fsrP82F8/ HTTP/1.1" 302 0
Not Found: /login/success/
[10/Jun/2024 10:30:51] "GET /login/success/ HTTP/1.1" 404 2735
Page not found (404)
Request Method: GET
Request URL: http://localhost:8000/login/success/
Using the URLconf defined in config.urls, Django tried these URL patterns, in this order:
admin/
api/accounts/
api/books/
api/schema/ [name='schema']
api/schema/swagger-ui/ [name='swagger-ui']
^media/(?P<path>.*)$
The current path, login/success/, didn’t match any of these.
이렇게 에러 메세지가 떠서 유알엘 설정을 해주고 보내주어야 함
config / urls.py 에서 추가해줘야함
from django.views.generic import TemplateView
path(
"login/success/",
TemplateView.as_view(template_name="email_success.html"),
name="login_success"
),
path(
"login/failure/",
TemplateView.as_view(template_name="email_failure.html"),
name="login_failure"
),
config / settings.py 에서 base 부분 추가 수정
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / 'templates'],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
config 라인에 templates를 추가해서 html을 가져가야한다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>이메일 확인 성공</title>
</head>
<body>
<h1>이메일 확인이 성공적으로 완료되었습니다!</h1>
<p>계속 진행하려면 로그인 페이지로 이동하십시오.</p>
</body>
</html>
일단 기본으로 나오게 바꾸면
여기로 가면서 바로 완료되었다는 확인 메세지가 뜬다.
2. 에러 메세지창
이메일 인증을 받지 않았을 때, 로그인을 시도하면 에러메세지 창이 뜰 수 있도록 수정
import useAuthStore from '../../shared/store/AuthStore';
import useGlobalStore from '../../shared/store/GlobalStore';
import axiosInstance from './AuthInstance';
export const login = async (email, password) => {
useGlobalStore.getState().setIsLoading(true);
useGlobalStore.getState().setError(null);
try {
const response = await axiosInstance.post('/api/accounts/login/', {
email,
password,
});
const data = response.data;
console.log("data:", data)
useAuthStore.getState().setToken(data.access);
useAuthStore.getState().setRefreshToken(data.refresh);
useAuthStore.getState().setIsLoggedIn(true);
useAuthStore.getState().setUserId(data.user.id);
useAuthStore.getState().setNickname(data.user.nickname);
useAuthStore.getState().setEmail(data.user.email);
useAuthStore.getState().setUser({
id: data.user.id,
name: data.user.name,
nickname: data.user.nickname,
email: data.user.email,
profilePicture: data.user.profile_picture,
});
console.log("11111로그인 성공:")
} catch (err) {
if (err.response && err.response.data) {
const errorData = err.response.data;
console.log("로그인 에러:",err.response.data);
let errorMessage = 'Login failed';
if (errorData.detail === 'No active account found with the given credentials') {
errorMessage = 'Invalid email or password';
} else if (errorData.email) {
errorMessage = 'The email address is not registered';
alert('이메일 주소가 맞지 않습니다.');
} else if (errorData.password) {
errorMessage = 'Incorrect password';
alert('패스워드가 틀렸습니다.');
} else if (errorData.detail) {
errorMessage = errorData.detail;
} else if (errorData.non_field_errors) {
errorMessage = 'Email is not verified.';
alert('이메일 확인이 되지 않았습니다. 인증메일을 확인한 후 로그인 해 주세요.');
}
useGlobalStore.getState().setError(errorMessage);
} else {
useGlobalStore.getState().setError('Login failed');
}
} finally {
useGlobalStore.getState().setIsLoading(false);
}
};
3. 이메일 재발송
이메일 확인을 받아야 회원가입이 완료되고 로그인을 할 수 있는데 가끔 이메일이 발송되지 않거나 엉뚱한 곳으로 이메일이 갈 것을 대비해서 이메일 재전송 버튼을 만든다.
이메일이 확인되지 않았을 때만 '이메일 재전송' 버튼이 생긴다.
import React, { useState, useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { login } from '../../features/auth/LoginInstance';
import { signup } from '../../features/auth/SignupInstance';
import useGlobalStore from '../../shared/store/GlobalStore';
import { ToastContainer, toast } from 'react-toastify';
import ResendEmailModal from './ResendEmailModal'; //다시 메일 보내는 것 모달창 추가
import 'react-toastify/dist/ReactToastify.css';
import './LoginModal.scss';
const LoginModal = ({ onClose }) => {
const [isLoginFormActive, setLoginFormActive] = useState(true);
const [loginInputs, setLoginInputs] = useState({ email: '', password: '' });
const [loginErrors, setLoginErrors] = useState({});
const [signupInputs, setSignupInputs] = useState({ nickname: '', email: '', password1: '', password2: '' });
const [signupErrors, setSignupErrors] = useState({});
const {isLoading, error, setError } = useGlobalStore(state => state.isLoading);
const globalError = useGlobalStore(state => state.error);
const [signupSuccess, setSignupSuccess] = useState(false);
const navigate = useNavigate();
const [showResendEmailModal, setShowResendEmailModal] = useState(false); //다시 메일 보내는 것 추가
...
useEffect(() => {
if (signupSuccess) {
toast.success("이메일 인증메일이 발송되었습니다. 확인해주세요.");
}
}, [signupSuccess]);
// 이메일이 없다는 에러가 떴을 때 '이메일 재전송'버튼이 모이게
useEffect(() => {
if (globalError && globalError.includes('Email is not verified.')) {
setShowResendEmailModal(true);
}
}, [globalError]);
...
return (
<div className="modalOverlay">
<ToastContainer />
<div className="modalContent" onClick={(e) => e.stopPropagation()}>
...
{/* Forms */}
<div className={`forms-container ${isLoginFormActive ? 'show-login' : 'show-signup'}`}>
{/* 로그인폼 */}
<div className={`user_forms-login ${isLoginFormActive ? 'active' : 'inactive'}`}>
<button className="closeButton" onClick={onClose}>×</button>
<h2 className="forms_title">Login</h2>
<form className="forms_form" onSubmit={handleLoginSubmit}>
<fieldset className="forms_fieldset">
<div className="forms_field">
<input
type="email"
placeholder="Email"
className="forms_field-input"
required
autoFocus
name="email"
value={loginInputs.email}
onChange={handleLoginInputChange}
disabled={isLoading}
/>
{loginErrors.email && <div className="error-message">{loginErrors.email}</div>}
</div>
<div className="forms_field">
<input
type="password"
placeholder="Password"
className="forms_field-input"
required
name="password"
value={loginInputs.password}
onChange={handleLoginInputChange}
disabled={isLoading}
/>
{loginErrors.password && <div className="error-message">{loginErrors.password}</div>}
</div>
</fieldset>
//여기에 넣어줘야 모달창 안에 들어가서 보인다 <div>{showResendEmailModal && <ResendEmailModal onClose={() => setShowResendEmailModal(false)} />}</div>
<div className="forms_buttons">
<button type="button" className="forms_buttons-forgot" disabled={isLoading}>Forgot password?</button>
<input type="submit" value="Log In" className="forms_buttons-action" disabled={isLoading} />
</div>
{loginErrors.non_field_errors && <div className="error-message">{loginErrors.non_field_errors}</div>}
</form>
<div className="social-login-buttons">
<button className="social-button google-login">
<img src="/path/to/google-icon.png" alt="Google Icon" />
Login with Google
</button>
<button className="social-button naver-login">
<img src="/path/to/naver-icon.png" alt="Naver Icon" />
Login with Naver
</button>
</div>
</div>
...
</div>
</div>
</div>
</div>
);
};
export default LoginModal;
import React, { useState } from 'react';
import axiosInstance from '../../features/auth/AuthInstance';
import useThemeStore from '../../shared/store/Themestore';
import './ResendEmailModal.scss';
const ResendEmailModal = () => {
const { themes, currentSeason } = useThemeStore();
const currentTheme = themes[currentSeason];
const [email, setEmail] = useState('');
const [showModal, setShowModal] = useState(false);
const handleEmailChange = (event) => {
setEmail(event.target.value);
};
const handleResendEmail = async () => {
try {
const response = await axiosInstance.post('/api/accounts/resend-email/', { email });
console.log("이메일 재전송 성공:", response.data);
// 이메일 재전송 성공 시 필요한 로직 추가
} catch (error) {
console.error("이메일 재전송 에러:", error);
// 이메일 재전송 실패 시 필요한 로직 추가
}
};
const handleResend = () => {
setShowModal(false);
handleResendEmail();
};
return (
<div>
<button onClick={() => setShowModal(true)}>이메일 재전송</button>
{showModal && (
<div className="modal">
<div className="modal-content">
<span className="close" onClick={() => setShowModal(false)}>
×
</span>
<h1>Resend Verification Email</h1>
<input
type="email"
placeholder="Enter your email"
value={email}
onChange={handleEmailChange}
/>
<p>이메일을 재전송하시겠습니까?</p>
<button style={{ backgroundColor: currentTheme.buttonBackgroundColor, color: currentTheme.buttonTextColor }} onClick={handleResend}>재전송</button>
</div>
</div>
)}
</div>
);
};
export default ResendEmailModal;
.modal {
display: block;
position: fixed;
z-index: 20;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.4);
.modal-content {
background-color: hsl(0, 0%, 100%);
margin: 15% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
&:hover {
background-color: #f9f9f9 !important;
}
}
button {
background-color: inherit;
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
width: 100%;
&:hover {
background-color: #f9f9f9 !important;
}
}
input[type="email"] {
width: 100%;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 5px;
box-sizing: border-box;
}
}
}
로그인 할 때 이메일이 확인 안되면 다시 이메일을 재전송해서 회원 확인을 받기 위해서 재전송 버튼을 누르면 모달창이 떠서 재전송을 할 수 있게 만들었다.