모두의 이력서

[모두의 이력서_15일차] 회원가입기능 구현

개발조각 2023. 4. 11. 17:12
728x90
반응형

로그인 기능 구현할 때 cors, react-query, axios, recoil을 세팅했으므로 로그인 기능과 똑같이 구현해 주시면 됩니다.

 

회원가입 기능 구성


이메일, 닉네임, 비밀번호, 비밀번호 확인 → 회원가입 버튼

 

  • 알맞은 일메일을 작성할 경우 : 알맞은 이메일 형식입니다.
  • 알맞은 닉네임을 작성할 경우 : 알맞은 닉네임 형식입니다.
  • 알맞은 비밀번호를 작성할 경우 : 알맞은 비밀번호 형식입니다.
  • 비밀번호, 비밀번호 확인이 일치할 경우: 비밀번호가 일치합니다.

라는 문구를 띄우게 했습니다.

 

api > user.ts

 

import instance from ".";

interface User {
  email: string;
  nickName: string;
  password: string;
}

// 회원가입
export const register = async ({ email, nickName, password }: User) => {
  const result = await instance.post("/users/register", {
    email,
    nickName,
    password,
  });

  return result;
};

axios로 회원가입 api 정의해 주었습니다.

 

utils > regExp.ts

입력한 값(이메일, 닉네임, 비밀번호)이 알맞은 값인지 확인하기 위해 정규식을 사용했습니다.

이메일, 닉네임, 비밀번호는 회원 관련 기능을 구현할 때 자주 사용되므로 utils폴더 안에 넣어주었습니다.

로그인 기능 구현할 때도 사용했습니다.

// 이메일: (영어대소문자+숫자) + @ + (영어소문자) + (.com)
export const validateEmail = (email: string) => {
  return /([\d\w])+@{1}([a-z])+(.com$)/.test(email);
};

// 닉네임: 특수문자제외, 공백제외, 2~8글자
export const validateNickname = (nickname: string) => {
  return /^[가-힣\d\w]{2,8}$/.test(nickname);
};

// 비밀번호: 4~10글자
export const validatePassword = (value: string) => {
  return value.length >= 4 && value.length <= 10;
};

vaidateEmail(입력한 이메일 값)을 넣어주면 true, false 값이 나옵니다.

 

Register.tsx

import { ChangeEvent, FormEvent, useContext, useState } from "react";
import { useRouter } from "next/router";
import { useMutation } from "@tanstack/react-query";

import Logo from "../components/common/Logo";
import { ThemeContext } from "../context/themeContext";

import { IsDark } from "../context/type";
import {
  validateEmail,
  validateNickname,
  validatePassword,
} from "../utils/regExp";
import { register } from "../api/user";

import * as authStyled from "../styles/components/Auth";
import * as styled from "../styles/pages/Register";

// 이메일, 닉네임, 비밀번호, 비밀번호 확인 -> 회원가입
const Register = () => {
  const router = useRouter();

  const { isDark } = useContext(ThemeContext) as IsDark;
  const mode = isDark ? "darkTheme" : "lightTheme";

  const [email, setEmail] = useState("");
  const [nickName, setNickName] = useState("");
  const [password, setPassword] = useState("");
  const [checkPassword, setCechkPassword] = useState("");

  const onEmailHandler = (e: ChangeEvent<HTMLInputElement>) => {
    const { value } = e.target as any;
    setEmail(value);
  };

  const onNickNameHandler = (e: ChangeEvent<HTMLInputElement>) => {
    const { value } = e.target as any;
    setNickName(value);
  };

  const onPasswordHandler = (e: ChangeEvent<HTMLInputElement>) => {
    const { value } = e.target as any;
    setPassword(value);
  };

  const onCheckPasswordHandler = (e: ChangeEvent<HTMLInputElement>) => {
    const { value } = e.target as any;
    setCechkPassword(value);
  };

  const mutation = useMutation({
    mutationFn: register,
    onSuccess: (data) => {
      router.replace("/Login", "/auth/register");
    },
  });

  const onSubmitHandler = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault(); // 리프레시 막기

    if (
      validateEmail(email) &&
      validateNickname(nickName) &&
      validatePassword(password) &&
      password === checkPassword
    ) {
      mutation.mutate({ email: email, nickName: nickName, password: password });
    } else {
      alert("회원가입 실패했습니다. 다시 수정해주세요.");
    }
  };

  return (
    <authStyled.AuthWrap mode={mode}>
      <authStyled.AuthCon mode={mode}>
        <styled.TitleWrap>
          <Logo />
          <h2>이메일로 가입하기</h2>
        </styled.TitleWrap>

        <authStyled.LoginformWrap>
          <form onSubmit={onSubmitHandler}>
            <authStyled.InputBox mode={mode}>
              <p>이메일</p>
              <input
                autoFocus
                type="text"
                value={email}
                onChange={onEmailHandler}
              />
            </authStyled.InputBox>

            {validateEmail(email) && (
              <authStyled.Msg state={"success"}>
                알맞은 이메일 형식입니다.
              </authStyled.Msg>
            )}

            <authStyled.InputBox mode={mode}>
              <p>닉네임</p>
              <input type="text" onChange={onNickNameHandler} />
            </authStyled.InputBox>

            {validateNickname(nickName) && (
              <authStyled.Msg state={"success"}>
                알맞은 닉네임 형식입니다.
              </authStyled.Msg>
            )}

            <authStyled.InputBox mode={mode}>
              <p>비밀번호</p>
              <input type="password" onChange={onPasswordHandler} />
            </authStyled.InputBox>

            {validatePassword(password) && (
              <authStyled.Msg state={"success"}>
                알맞은 비밀번호 형식입니다.
              </authStyled.Msg>
            )}

            <authStyled.InputBox mode={mode}>
              <p>비밀번호 확인</p>
              <input type="password" onChange={onCheckPasswordHandler} />
            </authStyled.InputBox>

            {validatePassword(password) && password === checkPassword && (
              <authStyled.Msg state={"success"}>
                비밀번호가 일치합니다.
              </authStyled.Msg>
            )}

            <authStyled.btn type="submit">회원가입</authStyled.btn>
          </form>
        </authStyled.LoginformWrap>
      </authStyled.AuthCon>
    </authStyled.AuthWrap>
  );
};

export default Register;

 

Auth.ts

Login.ts css와 유사해서 공통으로 사용되는 부분은 Auth에 넣어주었고

Register에서만 사용되는 css는 Register.ts에 넣어주었습니다.

import styled from "styled-components";

export const AuthWrap = styled.div<{ mode: "darkTheme" | "lightTheme" }>`
  ${({ theme }) => theme.common.flexCenter}
  height: 100vh;
  background: ${({ theme, mode }) => theme.colors[mode].lightBg};
`;

export const AuthCon = styled.div<{ mode: "darkTheme" | "lightTheme" }>`
  ${({ theme }) => theme.common.flexCenter}
  flex-direction: column;
  padding: 88px 0;
  width: 640px;
  border-radius: 10px;
  background: ${({ theme, mode }) => theme.colors[mode].bg};

  ${({ theme }) => theme.device.mobile} {
    width: 100%;
    height: 100vh;
    padding: 28px 24px;
  }
`;

export const LoginformWrap = styled.div`
  margin-top: 48px;
  width: 320px;

  form {
    width: 100%;
  }
`;

export const InputBox = styled.div<{ mode: "darkTheme" | "lightTheme" }>`
  margin-top: 16px;

  p {
    font-weight: 700;
    font-size: ${({ theme }) => theme.fonts.size.small};
  }

  input {
    margin-top: 8px;
    padding: 10px 12px;
    width: 100%;
    border: 1px solid ${({ theme, mode }) => theme.colors[mode].border};
    border-radius: 4px;
    background: ${({ theme, mode }) => theme.colors[mode].lightBg};

    &:focus {
      border: 1px solid ${({ theme }) => theme.colors.lightMain};
    }
  }
`;

export const btn = styled.button`
  display: block;
  margin-top: 36px;
  width: 100%;
  height: 48px;
  border-radius: 4px;
  background: ${({ theme }) => theme.colors.main};
  color: ${({ theme }) => theme.colors.lightTheme.bg};
  transition: all 0.1s;

  &:hover {
    background: ${({ theme }) => theme.colors.lightMain};
  }
`;

export const Msg = styled.span<{ state: "warning" | "success" }>`
  display: block;
  margin-top: 12px;
  font-weight: 300;
  font-size: ${({ theme }) => theme.fonts.size.small};
  line-height: ${({ theme }) => theme.fonts.lineHeight.base};
  color: ${({ theme, state }) => theme.colors[state]};
`;

 

Register.ts

import styled from "styled-components";

export const TitleWrap = styled.div`
  text-align: center;

  h1 a img {
    width: auto;
    height: 16px;
  }

  h2 {
    margin-top: 16px;
    font-weight: 700;
    font-size: ${({ theme }) => theme.fonts.size.title};
  }
`;

 

728x90
반응형