AI웹 개발자 과정 공부 (팀스파르타)/프로젝트

24.06.13_TIL ( 팀 프로젝트 : AI NOST Django ) _ 15. 이메일 확인 모달창(백엔드), 회원가입시 에러메세지창, 이메일 재발송

티아(tia) 2024. 6. 13. 22:51
728x90


[ 세번째 프로젝트 ] 

 
 

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

 

GitHub - 1489ehdghks/NOST

Contribute to 1489ehdghks/NOST development by creating an account on GitHub.

github.com

 

 

 

 

 


이메일로 회원가입 이메일 인증 참조 : 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 

 

회원가입 시 이메일 인증 구현

최근 프로젝트하며 회원가입 시 인증코드를 확인하는 절차를 만들었다. 이메일 정보를 입력하고 인증코드 보내기 버튼이 있으면 호출되는 api, 인증코드를 확인하는 api 이렇게 두가지를 구현했

velog.io

 

 

 

 

 

 

 

 

 

 

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}>&times;</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}>&times;</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}>&times;</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)}>
                            &times;
                        </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;
    }
  }
}

 

 

 

로그인 할 때 이메일이 확인 안되면 다시 이메일을 재전송해서 회원 확인을 받기 위해서 재전송 버튼을 누르면 모달창이 떠서 재전송을 할 수 있게 만들었다.

 

 

 

 

 

 

 

 

 

 

 

반응형