본문 바로가기
프로젝트/로그인, 회원가입

7. 인증코드 제한 시간

by 갱생angel 2024. 6. 6.

<추가/수정 사항>

※인증코드 제한 시간을 3분으로 지정해서 제한 시간이 지날 시 재발급 요청

※인증 코드 페이지를 authInput.js 컴포넌트로 분리

※localStorage에 사용자 정보를 저장해 서버로 보내는 코드 삭제

   -하위 컴포넌트로 state를 전송하는 식으로 변경

※회원가입 기능에도 이메일 인증코드 기능 추가

    -이메일 오타 입력 시 가입되는 것을 방지

※커뮤니티 페이지 삭제, 학습하기 페이지 추가

※서버 메일 전송 코드를 util 파일로 분리

 

-백엔드-

 

util - <email.js> : 메일 전송 코드 util 파일로 분류

const nodemailer = require("nodemailer");
require("dotenv").config();

module.exports = async (email, title, code) => {
  const mailPoster = nodemailer.createTransport({
    service: "naver",
    host: "smtp.naver.com",
    port: 587,
    auth: { user: process.env.MAIL_USER, pass: process.env.MAIL_PWD },
  });

  const mailOption = {
    from: process.env.MAIL_USER,
    to: email,
    subject: `${title} 인증코드`,
    text: `인증 코드는 ${code}입니다.`,
  };

  try {
    await mailPoster.sendMail(mailOption);
    return "success";
  } catch (error) {
    return error;
  }
};

 

controller - <userController.js> : 이메일 util 파일을 불러서 아이디 찾기, 비밀번호 찾기, 회원가입 메일 인증에 적용

(...)
const mailer = require("../util/email"); //메일 전송 util 불러오기

(...)

//Post Find User_id, /find_id : 아이디 찾기
const findId = asynchHandler(async (req, res) => {
  const { email } = req.body;
  const user = await User.findOne({ email });
  if (!user)
    return res.status(401).json({ message: "존재하지 않는 이메일입니다." });
  else {
    const code = Math.floor(100000 + Math.random() * 900000); //6자리 랜덤 숫자 생성
    await Authcode.create({ code }); //인증코드 document 생성
    const mailRes = await mailer(email, "아이디 찾기", code); //아이디 찾기 코드 전송
    if (mailRes === "success") //메일 전송 성공
      res.status(201).json({ message: "인증코드가 전송되었습니다." });
    else res.status(400).json({ message: "인증코드 전송에 실패했습니다." }); //메일 전송 실패
  }
});

//Post Find User_pwd, /find_pwd : 비밀번호 찾기
const findPwd = asynchHandler(async (req, res) => {
  const { username, email } = req.body;
  const user = await User.findOne({ username });
  if (!user)
    return res.status(401).json({ nameMessage: "일치하는 아이디가 없습니다." });
  else if (user.email !== email)
    return res.status(401).json({ emailMessage: "이메일이 틀립니다." });
  else {
    const code = Math.floor(100000 + Math.random() * 900000); //6자리 랜덤 숫자 생성
    await Authcode.create({ code }); //인증코드 document 생성
    const mailRes = await mailer(email, "비밀번호 찾기", code); //비밀번호 찾기 코드 전송
    if (mailRes === "success") //메일 전송 성공
      res.status(201).json({ message: "인증코드가 전송되었습니다." });
    else res.status(400).json({ message: "인증코드 전송에 실패했습니다." }); //메일 전송 실패
  }
});

//Pose AuthCode send to Mail, /mailsend : 회원가입 이메일 인증 확인
const mailCode = asynchHandler(async (req, res) => {
  const { email } = req.body;
  const emailRegEx =
    /^[A-Za-z0-9]([-_.]?[A-Za-z0-9])*@[A-Za-z0-9]([-_.]?[A-Za-z0-9])*\.[A-Za-z]{2,3}$/i;
  if (!emailRegEx.test(email))
    return res.status(401).json({
      emailMessage: "이메일 형식이 올바르지 않습니다. ex) admin@aaa.com",
    });
  const code = Math.floor(100000 + Math.random() * 900000); //6자리 랜덤 숫자 생성
  await Authcode.create({ code }); //인증코드 document 생성
  const mailRes = await mailer(email, "이메일 인증", code); //이메일 인증 코드 전송
  if (mailRes === "success") //메일 전송 성공
    res.status(201).json({ message: "인증코드가 전송되었습니다." });
  else res.status(400).json({ message: "인증코드 전송에 실패했습니다." }); //메일 전송 실패
});

module.exports = { findId, findPwd, mailCode };

 

route - <userRoute.js> : 회원가입 이메일 인증 라우터 추가

const express = require("express");
const router = express.Router();
const {
  getUserData,
  loginUser,
  findId,
  getUsername,
  findPwd,
  changePwd,
  mailCode, //회원가입 이메일 인증
  checkAuthCode,
  registerUser,
} = require("../controller/userController");
const { authUser } = require("../middleware/authMiddleware");

router.route("/login").get(authUser, getUserData).post(loginUser);
router.route("/find_id").post(findId);
router.route("/check_id").post(getUsername);
router.route("/find_pwd").post(findPwd);
router.route("/change_pwd").post(changePwd);
router.route("/mailsend").post(mailCode); //회원가입 이메일 인증 라우터 추가
router.route("/authcode").post(checkAuthCode);
router.route("/register").post(registerUser);

module.exports = router;

-프론트엔드-

 

component - <Timer.js> : 인증코드 타이머 3분 기능

import React, { useCallback, useEffect, useState } from "react";

function Timer({ setIsAuth, setIsFind }) {
  const [second, setSecond] = useState(0); //초
  const [minuts, setMinuts] = useState(3); //분

  const reduceSecond = () => setSecond((second) => Math.max(second - 1, 0)); //초 1초 씩 감소
  const reduceMinuts = () => setMinuts((minuts) => Math.max(minuts - 1, 0)); //분 1초 씩 감소

  const secondTimer = String(second).padStart(2, 0); //00 : 00 형식으로 변환
  const minutsTimer = String(minuts).padStart(2, 0); //00 : 00 형식으로 변환

  const timeDelay = useCallback(() => {
    setTimeout(() => { //00 -> 59초 변환 시 함수 실행을 1초 지연
      reduceMinuts(); //1분 감소
      setSecond(59); //59초로 변환
    }, 1000);
  }, []);

  useEffect(() => {
    const reduce = setInterval(reduceSecond, 1000); //1초 간격으로 시간 갱신
    return () => clearInterval(reduce); //매초 재 렌더링하여 호출하는 성능저하 문제 막기
  });

  useEffect(() => {
    if (minuts === 0 && second === 0) { //00:00이 되었을 시
      alert("인증코드를 다시 발급해주세요.");
      setIsAuth(false); 
      setIsFind(true); //ID, 이메일 입력 컴포넌트로 변환
    } else if (second === 0) { //초가 0일 시
      timeDelay(); //1분 감소 함수 실행
    }
  }, [minuts, second, timeDelay, setIsAuth, setIsFind]);

  return <span>{`${minutsTimer} : ${secondTimer}`}</span>;
}

export default Timer;

 

User - <authInput.js> : 인증코드 컴포넌트

import React, { useState } from "react";
import Timer from "../../component/Timer";
import "../../css/user.css";
import axios from "axios";

function AuthInput({ setIsAuth, setIsFind, setIsCheck }) {
  const [authCode, setAuthCode] = useState(""); //인증코드 state
  const [authCodeError, setAuthCodeError] = useState(""); //인증코드 에러 state

  const submitAuthCode = async (e) => { //인증코드 확인 함수 
    e.preventDefault();
    if (authCode === "") alert("빈칸을 입력해주세요."); //인증코드 입력 안 했을 시
    else { //인증코드 입력 했을 시
      try {
        await axios
          .post("http://localhost:5000/authcode", { code: authCode })
          .then((res) => alert(res.data.message));
        setIsAuth(false); 
        setIsCheck(true); //인증 완료 컴포넌트로 변환
      } catch (err) {
        setAuthCodeError(err.response.data.message); //인증코드 에러
      }
    }
  };

  return (
    <div>
      <form onSubmit={submitAuthCode}>
        <div>
          <label>
            인증코드(6자리)
            {<Timer setIsAuth={setIsAuth} setIsFind={setIsFind} />} <!--타이머-->
          </label>
          <input
            type="text"
            value={authCode}
            onChange={(e) => setAuthCode(e.target.value)}
          />
          <h4>{authCodeError}</h4>
        </div>
        <button type="submit" className="submitBtn">
          인증
        </button>
      </form>
    </div>
  );
}

export default AuthInput;

 

page - <IdFind.js> : ID 찾기 페이지

import React, { useEffect, useState } from "react";
import "../../../css/user.css";
import axios from "axios";
import { useNavigate } from "react-router-dom";
import AuthInput from "../authInput";
import IdCheck from "./IdCheck";

function IdFind() {
  const navigate = useNavigate();

  const [isAuth, setIsAuth] = useState(false); //인증코드 입력 컴포넌트 state
  const [isFind, setIsFind] = useState(true); //이메일 입력 컴포넌트 state
  const [isCheck, setIsCheck] = useState(false); //인증 완료 컴포넌트 state
  const [email, setEmail] = useState(""); //이메일 state
  const [emailError, setEmailError] = useState(""); //이메일 에러 state

  const IdFindSubmit = async (e) => { //이메일 인증 후 인증코드 전송 함수
    e.preventDefault();
    const idData = { email: email }; //서버로 이메일 보내기
    if (email === "") alert("빈칸을 입력해주세요."); //이메일 입력을 안 했을 시
    else { //이메일 입력을 했을 시
      try {
        await axios
          .post("http://localhost:5000/find_id", idData)
          .then((res) => alert(res.data.message));
        setIsAuth(true); //인증코드 입력 컴포넌트로 변환
        setIsFind(false); 
      } catch (err) {
        setEmailError(err.response.data.message); //이메일 오류
      }
    }
  };

  useEffect(() => {
    if (localStorage.getItem("token") !== null) navigate("/");
  }, [navigate]);

  return (
    <div className="userContainer">
      <h1>ID 찾기</h1>
      {isAuth && (
        <AuthInput
          setIsAuth={setIsAuth}
          setIsFind={setIsFind}
          setIsCheck={setIsCheck}
        />
      )} <!--인증코드 입력 컴포넌트-->
      {isCheck && <IdCheck emailer={email} />} <!--인증 완료 컴포넌트로 email state 전송-->
      {isFind && (
        <div>
          <form onSubmit={IdFindSubmit}>
            <div>
              <label>이메일</label>
              <input
                type="text"
                value={email}
                onChange={(e) => setEmail(e.target.value)}
                placeholder="ex) admin@aaa.com"
              />
              <h4>{emailError}</h4>
            </div>
            <button className="submitBtn">ID 찾기</button>
          </form>
          <p onClick={() => navigate("/login")}>로그인</p>
        </div>
      )} <!--이메일 입력 컴포넌트-->
    </div>
  );
}

export default IdFind;

 

page - <IdCheck.js> : ID 확인 페이지

import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";
import "../../../css/user.css";

function IdCheck({ emailer }) {
  const navigate = useNavigate();

  const [username, setUsername] = useState(""); //ID state

  const fetchUsername = async () => { //사용자 ID 가져오는 함수
    const emailData = { email: emailer }; //<IdFind.js>에서 받은 email state
    try {
      await axios
        .post("http://localhost:5000/check_id", emailData)
        .then((res) => setUsername(res.data.username));
    } catch (err) {
      console.error(err);
    }
  };

  useEffect(() => {
    fetchUsername();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <div className="userContainer">
      <h2>ID : {username}</h2>
      <button className="submitBtn" onClick={() => navigate("/login")}>
        로그인
      </button>
    </div>
  );
}

export default IdCheck;

page - <PwdFind.js> : 비밀번호 찾기 페이지

import axios from "axios";
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import "../../../css/user.css";
import AuthInput from "../authInput";
import PwdChange from "./PwdChange";

function PwdFind() {
  const navigate = useNavigate();

  const [isAuth, setIsAuth] = useState(false); //인증코드 입력 컴포넌트 state
  const [isFind, setIsFind] = useState(true); //이메일 입력 컴포넌트 state
  const [isCheck, setIsCheck] = useState(false); //인증 완료 컴포넌트 state
  const [username, setUsername] = useState(""); //ID state
  const [usernameError, setUsernameError] = useState(""); //ID 에러 state
  const [email, setEmail] = useState(""); //이메일 state
  const [emailError, setEmailError] = useState(""); //이메일 오류 state

  const pwdFindSubmit = async (e) => { //ID, 이메일 인증 후 인증코드 전송 함수
    e.preventDefault();
    const pwdData = { username: username, email: email }; //서버로 보낼 ID, 이메일
    if (username === "" || email === "") alert("빈칸을 입력해주세요."); //입력 안 했을 시
    else { //입력 했을 시
      try {
        await axios
          .post("http://localhost:5000/find_pwd", pwdData)
          .then((res) => alert(res.data.message));
        setIsAuth(true); //인증코드 입력 컴포넌트로 변환
        setIsFind(false);
      } catch (err) {
        setUsernameError(err.response.data.nameMessage); //ID 입력 에러
        setEmailError(err.response.data.emailMessage); //이메일 입력 에러
      }
    }
  };

  useEffect(() => {
    if (localStorage.getItem("token") !== null) navigate("/");
  }, [navigate]);

  return (
    <div className="userContainer">
      <h1>비밀번호 찾기</h1>
      {isAuth && (
        <AuthInput
          setIsFind={setIsFind}
          setIsCheck={setIsCheck}
          setIsAuth={setIsAuth}
        />
      )} <!--인증코드 입력 컴포넌트-->
      {isCheck && <PwdChange name={username} />} <!--PwdChange.js 컴포넌트로 ID state 전송-->
      {isFind && (
        <div>
          <form onSubmit={pwdFindSubmit}>
            <div>
              <label>ID</label>
              <input
                type="text"
                value={username}
                onChange={(e) => setUsername(e.target.value)}
              />
              <h4>{usernameError}</h4>
            </div>
            <div>
              <label>이메일</label>
              <input
                type="text"
                value={email}
                onChange={(e) => setEmail(e.target.value)}
                placeholder="ex) admin@aaa.com"
              />
              <h4>{emailError}</h4>
            </div>
            <button type="submit" className="submitBtn">
              다음
            </button>
          </form>
          <p onClick={() => navigate("/login")}>로그인</p>
        </div>
      )} <!--ID, 이메일 입력 컴포넌트-->
    </div>
  );
}

export default PwdFind;

 

page - <PwdChange.js> : 비밀번호 변경 페이지

import React, { useState } from "react";
import "../../../css/user.css";
import axios from "axios";
import { useNavigate } from "react-router-dom";

function PwdChange({ name }) {
  const navigate = useNavigate();

  const [password, setPassword] = useState(""); //새 비밀번호 state
  const [password2, setPassword2] = useState(""); //새 비밀번호 확인 state
  const [passwordError, setPasswordError] = useState(""); //비밀번호 오류 state

  const pwdChangeSubmit = async (e) => {
    e.preventDefault();
    const inputs = [password, password2]; 
    const pwdData = {
      username: name,
      password: password,
      checkPassword: password2,
    }; //서버로 보낼 비밀번호 state
    if (inputs.some((input) => input === "")) alert("빈칸을 입력해주세요."); //입력 안 했을 시
    else { //입력 했을 시
      try {
        await axios
          .post("http://localhost:5000/change_pwd", pwdData)
          .then((res) => alert(res.data.message));
        navigate("/login"); //로그인 페이지로 이동
      } catch (err) {
        setPasswordError(err.response.data.message); //비밀번호 에러
      }
    }
  };

  return (
    <div className="userContainer">
      <form onSubmit={pwdChangeSubmit}>
        <div>
          <label>새 비밀번호</label>
          <input
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>
        <div>
          <label>새 비밀번호 확인</label>
          <input
            type="password"
            value={password2}
            onChange={(e) => setPassword2(e.target.value)}
          />
          <h4>{passwordError}</h4>
        </div>
        <button type="submit" className="submitBtn">
          비밀번호 변경
        </button>
      </form>
    </div>
  );
}

export default PwdChange;

page - <Register.js> : 회원가입 페이지

import React, { useEffect, useState } from "react";
import axios from "axios";
import "../../css/user.css";
import { useNavigate } from "react-router-dom";
import Timer from "../../component/Timer";

function Register() {
  const navigate = useNavigate();

  const [isTimer, setIsTimer] = useState(true); //타이머 컴포넌트 state
  const [isSubmit, setIsSubmit] = useState(false); //화원가입 가능 여부 state
  const [username, setUsername] = useState(""); //ID state
  const [usernameError, setUsernameError] = useState(""); //ID 에러 state
  const [password, setPassword] = useState(""); //비밀번호 state
  const [password2, setPassword2] = useState(""); //비밀번호 확인 state
  const [passwordError, setPasswordError] = useState(""); //비밀번호 에러 state
  const [email, setEmail] = useState(""); //이메일 state
  const [emailError, setEmailError] = useState(""); //이메일 에러 state
  const [authCode, setAuthCode] = useState(""); //인증코드 state
  const [authCodeError, setAuthCodeError] = useState(""); //인증코드 에러 state

  //이메일 인증 확인 함수
  const mailSendCode = async (e) => {
    e.preventDefault();
    if (email === "") alert("빈칸을 입력해주세요.");
    else {
      try {
        await axios
          .post("http://localhost:5000/mailsend", { email: email })
          .then((res) => alert(res.data.message));
        setIsTimer(false); //인증코드 타이머 작동
      } catch (err) {
        alert(err.response.data.message);
        setEmailError(err.response.data.emailMessage); //이메일 에러
      }
    }
  };

  //인증코드 확인 함수
  const submitAuthCode = async (e) => {
    e.preventDefault();
    if (authCode === "") alert("빈칸을 입력해주세요.");
    else {
      try {
        await axios
          .post("http://localhost:5000/authcode", { code: authCode })
          .then((res) => alert(res.data.message));
        setAuthCodeError(""); //인증코드 에러 state ''로 변환
        setIsSubmit(true); //회원가입 가능 
        setIsTimer(true); //타이머 종료
      } catch (err) {
        setAuthCodeError(err.response.data.message); //인증코드 에러
      }
    }
  };

  //회원가입 함수
  const registerSubmit = async (e) => {
    e.preventDefault();
    const inputs = [username, password, password2, email];
    const registerData = {
      username: username,
      password: password,
      checkPassword: password2,
      email: email,
    };
    if (inputs.some((input) => input === "")) alert("빈칸을 입력해주세요."); //입력 안 했을 시
    else if (isSubmit === false) alert("이메일 인증을 해주세요."); //이메일 인증을 안했을 시
    else { //이메일 인증을 했을 시
      try {
        await axios
          .post("http://localhost:5000/register", registerData)
          .then((res) => alert(res.data.message));
        navigate("/login"); //로그인 페이지로 이동
      } catch (err) {
        setUsernameError(err.response.data.nameMessage); //ID 에러
        setPasswordError(err.response.data.pwdMessage); //비밀번호 에러
        setEmailError(err.response.data.emailMessage); //이메일 에러
      }
    }
  };

  useEffect(() => {
    if (localStorage.getItem("accessToken") !== null) navigate("/");
  }, [navigate]);

  return (
    <div className="userContainer">
      <h3>회원가입</h3>
      <form onSubmit={registerSubmit} className="formContainer">
        <div>
          <label>ID</label>
          <input
            type="text"
            value={username}
            onChange={(e) => setUsername(e.target.value)}
          />
          <h4>{usernameError}</h4>
        </div>
        <div>
          <label>비밀번호</label>
          <input
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
          <h4>{null}</h4>
        </div>
        <div>
          <label>비밀번호 확인</label>
          <input
            type="password"
            value={password2}
            onChange={(e) => setPassword2(e.target.value)}
          />
          <h4>{passwordError}</h4>
        </div>
        <div>
          <label>이메일</label>
          <div className="checkUser">
            <input
              type="text"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              placeholder="ex) admin@aaa.com"
            />
            <button onClick={mailSendCode}>인증코드 전송</button>
          </div>
          <h4>{emailError}</h4>
        </div>
        <div>
          <label>
            인증코드(6자리)
            <span>{!isTimer && <Timer setIsFind={setIsTimer} />}</span> <!--타이머-->
          </label>
          <div className="checkUser">
            <input
              type="text"
              value={authCode}
              onChange={(e) => setAuthCode(e.target.value)}
            />
            <button onClick={submitAuthCode}>인증</button>
          </div>
          <h4>{authCodeError}</h4>
        </div>
        <button className="submitBtn" type="submit">
          회원가입
        </button>
      </form>
      <p onClick={() => navigate("/login")}>로그인</p>
    </div>
  );
}

export default Register;

<App.js> : 커뮤니티 페이지 삭제, 학습하기 페이지 추가

import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Nav from "./component/Nav";
import Login from "./page/User/Login";
import Register from "./page/User/Register";
import Home from "./page/Home";
import ImageRegist from "./page/Game/ImageRegist";
import ImageGame from "./page/Game/ImageGame";
import CombineGame from "./page/Game/CombineGame";
import IdFind from "./page/User/ID/IdFind";
import PwdFind from "./page/User/PWD/PwdFind";
import MyPage from "./page/User/MyPage";
import LearningPage from "./page/Learn/LearningPage"; //학습하기 페이지
import "bootstrap/dist/css/bootstrap.min.css";

function App() {
  return (
    <div>
      <BrowserRouter>
        <Nav />
        <Routes>
          <Route index element={<Home />} />
          <Route path="/login" element={<Login />} />
          <Route path="/find_id" element={<IdFind />} />
          <Route path="/find_pwd" element={<PwdFind />} />
          <Route path="/register" element={<Register />} />
          <Route path="/mypage" element={<MyPage />} />
          <Route path="/image/add" element={<ImageRegist />} />
          <Route path="/imageGame" element={<ImageGame />} />
          <Route path="/combineGame" element={<CombineGame />} />
          <Route path="/learn" element={<LearningPage />} /> <!--학습하기 페이지 라우터-->
        </Routes>
      </BrowserRouter>
    </div>
  );
}

export default App;

 

'프로젝트 > 로그인, 회원가입' 카테고리의 다른 글

6. 비밀번호 변경  (0) 2024.05.27
5. nodemailer, 인증코드  (0) 2024.05.27
4. 아이디 찾기  (0) 2024.05.24
3. 이메일, 정규 표현식  (0) 2024.05.23
2. JWT 토큰 검증  (0) 2024.05.20