팀프로젝트: Wingle(1.4) API 연결 - Auth: 회원가입 input 데이터 관리

윤뿔소·2023년 4월 24일
0

팀프로젝트: Wingle

목록 보기
7/16

이제 사진을 담고 리코일 아톰에 보냈으니 input을 관리해보자.

디자인/기능 기획

구현할 구역은 다음과 같다.

위에 맞게 기획해보자면
1. 이메일 전송 및 인증번호 확인 input/API 연결 및 유효성 검사
2. 비밀번호 및 확인 input 및 유효성 검사
3. 실명-이름 input 및 유효성 검사
4. 닉네임 input/API 연결 및 유효성 검사

참고: 디자인만 된 코드

진짜 겁나 길다; 300줄 이상이라 그렇다. 보기 싫다면 넘어가는걸 추천한다.
벨로그는 드롭다운이 왜 없나몰라

import React, { useCallback, useEffect, useState } from "react";
import { Text, Margin } from "@/src/components/ui";
import styled from "styled-components";
import Image from "next/image";

interface StyledInputProps {
  small: boolean;
  error: boolean;
}

interface InputData {
  email: string;
  emailCertificaion: string;
  password: string;
  name: string;
  nickname: string;
}

function ErrorMent({ error, errorMent, ment }: any) {
  return (
    <>
      {error ? (
        <>
          <S.ErrorWrapper>
            <Image src="/auth/error.svg" alt="error" />
            <Margin direction="row" size={8} />
            <Text.Caption3 color="red500">{errorMent}</Text.Caption3>
          </S.ErrorWrapper>
        </>
      ) : (
        <Text.Caption3 color="gray900">{ment}</Text.Caption3>
      )}
    </>
  );
}

export default function InputBox({ getError }: { getError: (error: boolean) => void }) {
  const [buttonMessage, setButtonMessage] = useState("인증 전송");
  const [emailMent, setEmailMent] = useState("");
  const [inputData, setInputData] = useState<InputData>({
    email: "",
    emailCertificaion: "",
    password: "",
    name: "",
    nickname: "",
  });
  const { email, emailCertificaion, password, name, nickname } = inputData;
  const [isError, setError] = useState(true);
  const [isErrorEmailCertify, setErrorEmailCertify] = useState(false);
  const [isErrorPassword, setErrorPassword] = useState(false);
  const [isErrorPasswordCheck, setErrorPasswordCheck] = useState(false);
  const [isErrorName, setErrorName] = useState(false);
  const [isErrorNickName, setErrorNickName] = useState(false);

  const handleInputData = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    setInputData((prevData) => ({ ...prevData, [e.target.name]: e.target.value }));
  }, []);

  const handleInputError = useCallback(() => {
    if (
      !isErrorEmailCertify &&
      !isErrorPassword &&
      !isErrorPasswordCheck &&
      !isErrorName &&
      !isErrorNickName
    ) {
      setError(false);
    } else {
      setError(true);
    }
  }, [isErrorEmailCertify, isErrorPassword, isErrorPasswordCheck, isErrorName, isErrorNickName]);

  const handleEmail = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    const passwordRegex = /^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{6,15}$/;
    if (!passwordRegex.test(e.target.value)) {
      setErrorPassword(true);
    } else {
      setErrorPassword(false);
    }
  }, []);

  const handlePassword = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    const passwordRegex = /^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{6,15}$/;
    if (!passwordRegex.test(e.target.value)) {
      setErrorPassword(true);
    } else {
      setErrorPassword(false);
    }
  }, []);

  const handleName = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    const special_pattern = /[`~!@#$%^&*|\\\'\";:\/?]/gi;
    if (special_pattern.test(e.target.value)) {
      setErrorName(true);
    } else {
      setErrorName(false);
    }
  }, []);

  const handleNickName = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    const pattern = /[`~!@#$%^&*|\\\'\";:\/?]/;
    const pattern2 = /[0-9]/;
    if (
      pattern.test(e.target.value) ||
      pattern2.test(e.target.value) ||
      e.target.value.length < 2 ||
      e.target.value.length > 10
    ) {
      setErrorNickName(true);
    } else {
      setErrorNickName(false);
    }
  }, []);

  useEffect(() => {
    handleInputError();
    getError(isError);
  }, [isError, handleInputError, getError]);

  return (
    <>
      <Text.Body1 color="gray700">이메일</Text.Body1>
      <Margin direction="column" size={8} />
      <S.ContentWrapper>
        <S.Content>
          <S.InputField small={true} error={false}>
            <input
              name="Email"
              value={email}
              type="email"
              placeholder="abc@naver.com"
              onChange={(e) => {
                handleInputData(e);
                handleEmail(e);
              }}
            />
          </S.InputField>
          <S.ButtonWrapper small={true} error={false}>
            <S.Button
              onClick={() => {
                setButtonMessage("재전송");
                setEmailMent("인증메일을 전송했습니다.");
              }}
            >
              {buttonMessage}
            </S.Button>
          </S.ButtonWrapper>
        </S.Content>
        <ErrorMent error={false} errorMent="" ment={emailMent} />
      </S.ContentWrapper>

      <Text.Body1 color="gray700">인증번호 입력</Text.Body1>
      <Margin direction="column" size={8} />
      <S.ContentWrapper>
        <S.Content>
          <S.InputField small={true} error={isErrorEmailCertify}>
            <input
              name="EmailCertificaion"
              value={emailCertificaion}
              type="string"
              placeholder="인증번호"
              onChange={handleInputData}
            />
          </S.InputField>
          <S.ButtonWrapper small={true} error={isErrorEmailCertify}>
            <S.Button
              onClick={() => {
                inputData.emailCertificaion === "123"
                  ? setErrorEmailCertify(false)
                  : setErrorEmailCertify(true);
              }}
            >
              인증 확인
            </S.Button>
          </S.ButtonWrapper>
        </S.Content>
        <ErrorMent error={isErrorEmailCertify} errorMent="인증정보가 일치하지 않습니다." ment=" " />
      </S.ContentWrapper>

      <Text.Body1 color="gray700">비밀번호</Text.Body1>
      <Margin direction="column" size={8} />
      <S.ContentWrapper>
        <S.Content>
          <S.InputField small={false} error={isErrorPassword}>
            <input
              name="Password"
              value={password}
              type="string"
              placeholder="비밀번호"
              onChange={(e) => {
                handleInputData(e);
                handlePassword(e);
              }}
            />
          </S.InputField>
          <S.ButtonWrapper small={false} error={false}></S.ButtonWrapper>
        </S.Content>
        <ErrorMent
          error={isErrorPassword}
          errorMent="영문자/숫자/특수기호 포함 최소 8, 최대 15자 "
          ment="영문자/숫자/특수기호 포함 최소 8, 최대 15자"
        />
      </S.ContentWrapper>

      <Text.Body1 color="gray700">비밀번호 확인</Text.Body1>
      <Margin direction="column" size={8} />
      <S.ContentWrapper>
        <S.Content>
          <S.InputField small={false} error={false}>
            <input
              type="string"
              placeholder="비밀번호"
              onChange={(e) => {
                e.target.value === inputData.password
                  ? setErrorPasswordCheck(false)
                  : setErrorPasswordCheck(true);
              }}
            />
          </S.InputField>
          <S.ButtonWrapper small={false} error={false}></S.ButtonWrapper>
        </S.Content>
        <ErrorMent error={isErrorPasswordCheck} errorMent="정보를 정확히 입력해주세요." ment=" " />
      </S.ContentWrapper>

      <Text.Body1 color="gray700">이름</Text.Body1>
      <Margin direction="column" size={8} />
      <S.ContentWrapper>
        <S.Content>
          <S.InputField small={false} error={false}>
            <input
              name="Name"
              value={name}
              type="string"
              placeholder="김윙글"
              onChange={(e) => {
                handleInputData(e);
                handleName(e);
              }}
            />
          </S.InputField>
          <S.ButtonWrapper small={false} error={false}></S.ButtonWrapper>
        </S.Content>
        <ErrorMent
          error={isErrorName}
          errorMent="실명을 입력하세요 (한글, 영어 대/소문자 사용 가능) "
          ment=" 실명을 입력하세요 (한글, 영어 대/소문자 사용 가능) "
        />
      </S.ContentWrapper>

      <Text.Body1 color="gray700">닉네임</Text.Body1>
      <Margin direction="column" size={8} />
      <S.ContentWrapper>
        <S.Content>
          <S.InputField small={true} error={false}>
            <input
              name="NickName"
              value={nickname}
              type="string"
              placeholder="희망찬윙그리"
              onChange={(e) => {
                handleInputData(e);
                handleNickName(e);
              }}
            />
          </S.InputField>
          <S.ButtonWrapper small={true} error={false}>
            <S.Button>중복 확인</S.Button>
          </S.ButtonWrapper>
        </S.Content>
        <ErrorMent
          error={isErrorNickName}
          errorMent="한글/영어 두글자 이상 10글자 이하 "
          ment="  "
        />
      </S.ContentWrapper>
    </>
  );
}

const S = {
  ContentWrapper: styled.div`
    padding-bottom: 24px;
  `,
  Content: styled.div`
    display: flex;
  `,
  InputField: styled.div<StyledInputProps>`
    width: ${(props) => (props.small ? "345px" : "452px")};
    height: 50px;
    border: ${(props) => (props.error ? "1px solid #FF7070" : "1px solid #dcdce0;")};
    border-radius: 8px;
    margin-bottom: 8px;

    & > input {
      width: 300px;
      border: none;

      padding: 14px;
      border-radius: 8px;
      height: 22px;

      &::placeholder {
        font-weight: 400;
        font-size: 16px;
        line-height: 140%;
        color: #959599;
      }
    }
  `,
  Button: styled.button`
    font-size: 16px;
    font-weight: 700;
    color: #49494d;
    width: 99px;
  `,
  ButtonWrapper: styled.div<StyledInputProps>`
    display: ${(props) => (props.small ? "flex" : "none")};
    height: 50px;
    width: 99px;
    border: 1px solid #959599;
    border-radius: 8px;
    margin-left: 8px;
  `,
  ErrorWrapper: styled.div`
    display: flex;
  `,
};

buttonMessage, emailMent 등으로 에러 시 나올 메세지나 완료시 메시지를 넣으려고 했고, inputData로 데이터를 따로 관리했다.

불린데이터인 isError~를 넣어 에러가 일어나면 멘트가 나오는 등의 조건을 만들었다.

나머지 handle~는 거의 유효성 검사를 해서 isError~의 set에 영향을 줘 에러처리를 했다.

위에 적혀있는 ErrorMent는 따로 컴포넌트로 옮겼다. 옮기기만 한 것이기에 따로 적진 않았다.

회원가입 input 기능 개발 시작

위처럼 구획을 나눠서 기능 개발을 할 것이다. 이제 스타트 해보자!

이메일 보내기 및 검증

윙글은 이메일을 아이디로 쓰일 것이기 때문에 이메일이 중복되는가 안되는가를 확인하는 동시에 입력한 이메일에 메일을 보내 인증번호를 받아오고, API 통신 코드를 구현하여 인증번호를 입력해 서버에서 인증을 받아오는 식이다.

먼저 API 코드를 구현해보자.

API 통신 코드

import instance from "../axiosModul";

interface EmailAuthResponse {
  status: number;
  message: string;
  data: {
    certificationKey?: string;
  };
}

export const sendEmailAuth = async (email: string): Promise<EmailAuthResponse> => {
  const response = await instance.post<EmailAuthResponse>("/auth/email", { email: email });
  return response.data;
};

되게 쉽다.

  1. sendEmailAuth 함수를 선언하고 문자열인 email을 파라미터로 받는다.
  2. 전편에서 만든 instance를 가져와 email을 바디로 보내며 POST한다.

이게 끝이다! EmailAuthResponse타입도 선언해줘 안전하게 데이터를 받고 보냈다.

리액트쿼리 사용하기

다 끝났다! sendEmailAuth 함수를 가져와 인자로 email을 넣어준다. 그다음 mutate하는 함수 sendEmail을 가져와 호출을 하면 된다.

리액트쿼리 부분

handleSendEmail을 만들어 sendEmail를 호출해준다. email 상태가 "", 즉 입력하지 않았다면 안되게 설정도 해놨다!

// 이메일 인증메일 보내기
  const { mutate: sendEmail } = useMutation(() => sendEmailAuth(email));

  const handleSendEmail = useCallback(() => {
    if (email === "") {
      alert("이메일을 입력해주세요.");
      return;
    }
    sendEmail();
  }, [email, sendEmail]);

이제 여기에 리액트 쿼리의 진행 상태에 따라 실행되는 함수도 넣어줄 수 있다.

const { mutate: sendEmail } = useMutation(() => sendEmailAuth(email), {
  onMutate: () => {
    setButtonMessage("전송 중");
  },
  onSuccess: () => {
    setButtonMessage("재전송");
    setEmailMent("인증메일을 전송했습니다.");
  },
  onError: (error) => {
    setErrorEmailCertify(true);
    alert(error);
    throw error;
  },
});
  1. onMutate : sendEmail이 실행 전 보내기 버튼의 value인 메세지에 '전송 중'을 보냈다.
  2. onSuccess : sendEmail이 실행 후 응답이 성공했다면(200) 버튼 메세지를 '재전송'으로 바꾸고, setEmailMent을 만들어 input 아래 멘트에 나오도록 했다.
  3. onError : sendEmail이 실행 후 실패했다면 setErrorEmailCertifytrue, 즉 에러가 떴다고 상태를 바꾼다.

TSX 부분

먼저 알아둬야할게 handleEmail이라는 유효성 검사맡는 함수가 사라지고 그냥 handleInputData를 넣었다. 나중에 인증번호가 오고, 맞게 입력한다면 그때 리코일 아톰에 들어가기로 했다. 걍 유효성 검사가 필요 없어서 뺐다.

<Text.Body1 color="gray700">이메일</Text.Body1>
<Margin direction="column" size={8} />
<S.ContentWrapper>
  <S.Content>
    <S.InputField small={true} error={false}>
      <input
        name="email"
        value={email}
        type="email"
        placeholder="abc@naver.com"
        onChange={(e) => {
          handleInputData(e);
        }}
      />
    </S.InputField>
    <S.ButtonWrapper small={true} error={false}>
      <S.Button onClick={() => handleSendEmail()}>{buttonMessage}</S.Button>
    </S.ButtonWrapper>
  </S.Content>
  <ErrorMent error={false} errorMent="" ment={emailMent} />
</S.ContentWrapper>

입력할 때마다 handleInputData이 실행, 입력 후 버튼을 누르면 handleSendEmail를 실행한다. 그 후로 API가 실행되어 이메일이 받아와진다.


다음편엔 이어서 이메일 검증, 비밀번호, 닉네임을 후술하겠다. 이게 은근 노가다성이라서 빡셌다.

profile
코뿔소처럼 저돌적으로

7개의 댓글

comment-user-thumbnail
2023년 4월 24일

고생하셨네요.. 디자인은 300줄 이상이라 스킵했습니다.. ㅎㅎ
React Query를 잘활용하시는거 같네요. 화이팅입니다!

1개의 답글
comment-user-thumbnail
2023년 4월 30일

Wow.... 고생이 많으십니다

답글 달기
comment-user-thumbnail
2023년 4월 30일

호오... 고생하셨습니다 !

답글 달기
comment-user-thumbnail
2023년 4월 30일

와우... 벨로그에 드롭다운 추가해주길 바라는 사람 여기도 있어요.. 고생하셨습니다!

1개의 답글
comment-user-thumbnail
2023년 5월 1일

스크롤 압박이 와우.. 대단합니다

답글 달기