BEB

[BEB] 회원가입 페이지 기능 구현 / 이메일, 닉네임 중복검사

peach_h 2024. 12. 10. 17:30

현재 회원가입 페이지 상태

input에 입력한 데이터를 작성완료 버튼을 눌렀을 때 확인할 수 있도록 해놨다.

이제 가장 중요한 기능 연결을 할 차례

기존코드

import React, {useState} from 'react';
import '../styles/auth/SignupPage.scss';
import InputField from '../components/utils/InputField';
import Button from '../components/utils/Button';
import {useNavigate} from 'react-router-dom';

function SignupPage() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [age, setAge] = useState('');
  const [passwordConfirm, setPasswordConfirm] = useState('');
  const [passwordMatch, setPasswordMatch] = useState(true); // 비밀번호 확인 상태
  const [nickname, setNickname] = useState('');
  const [gender, setGender] = useState('');
  const [profileImage, setProfileImage] = useState(null); // 이미지 상태 추가
  const navigate = useNavigate();

  const handleSignup = () => {
    navigate('/'); // 회원가입 페이지로 이동
  };

  const handleImageUpload = (e) => {
    const file = e.target.files[0];
    setProfileImage(file); // 이미지 상태 저장
  };

  const handleGenderChange = (selectedGender) => {
    setGender(selectedGender); // 성별 상태 업데이트
  };

  // 비밀번호 확인 로직
  const handlePasswordConfirmChange = (value) => {
    setPasswordConfirm(value);
    setPasswordMatch(value === password); // 비밀번호와 비밀번호 확인 비교
  };

  const handleSubmit = (e) => {
    e.preventDefault();

    // 입력값을 JSON 형식으로 준비
    const userData = {
      email,
      password,
      nickname,
      age: Number(age), // 숫자로 변환
      gender
    };

    // JSON 데이터를 alert 창에 출력
    alert(`입력된 데이터:
    이메일: ${userData.email}
    비밀번호: ${userData.password}
    닉네임: ${userData.nickname}
    나이: ${userData.age}
    성별: ${userData.gender}`);
  };

  return (
    <div className="main-box">
      <img
        className="back"
        src="/icons/backicon.png"
        onClick={handleSignup}
      ></img>
      <h1 className="signup-title">회원가입</h1>
      <div className="total-box">
        <div className="left-box">
          <label>프로필 이미지는 선택사항입니다</label>
          {profileImage && (
            <div className="image-preview">
              <img
                src={URL.createObjectURL(profileImage)}
                alt="프로필 미리보기"
              />
            </div>
          )}
          <input type="file" accept="image/*" onChange={handleImageUpload} />
        </div>

        <div className="right-box">
          <form className="signup-form" onSubmit={handleSubmit}>
            {/* 이메일 입력 */}
            <InputField
              label="이메일(ID)"
              placeholder="이메일(ID)를 입력해주세요"
              type="email"
              value={email}
              onChange={setEmail}
              errorMessage="유효하지 않은 이메일 형식입니다."
            />

            {/* 비밀번호 입력 */}
            <InputField
              label="비밀번호"
              placeholder="8-20자 숫자, 영어, 특수문자(! @ # $ % ^ & ^) 사용가능"
              type="password"
              value={password}
              onChange={setPassword}
              errorMessage="비밀번호는 최소 8자 이상이어야 합니다."
            />

            {/* 비밀번호 확인 */}
            <InputField
              label="비밀번호 확인"
              placeholder="비밀번호를 다시 입력하세요"
              type="password"
              value={passwordConfirm}
              onChange={handlePasswordConfirmChange}
              errorMessage="비밀번호가 일치하지 않습니다."
              valid={passwordMatch} // 유효성 상태 전달
            />

            {/* 닉네임 입력 */}
            <InputField
              label="닉네임"
              placeholder="8자 이하로 한글, 숫자, 영어만 가능합니다"
              type="nickname"
              value={nickname}
              onChange={setNickname}
              errorMessage="8자 이하로 한글, 숫자, 영어만 가능합니다"
            />
            <div className="submit">
              <div className="agender-box">
                {/* 닉네임 입력 */}
                <InputField
                  label="나이"
                  placeholder="숫자만 입력가능합니다"
                  type="age"
                  value={age}
                  onChange={setAge}
                />

                {/* 성별 선택 */}

                <div className="gender-box">
                  <div className="text">성별</div>
                  <div className="gender-container">
                    <label className="custom-checkbox">
                      <input
                        type="checkbox"
                        checked={gender === 'male'}
                        onChange={() => handleGenderChange('male')}
                      />{' '}
                      <span className="checkbox-icon"></span>
                      남자
                    </label>
                    <label className="custom-checkbox">
                      <input
                        type="checkbox"
                        checked={gender === 'female'}
                        onChange={() => handleGenderChange('female')}
                      />{' '}
                      <span className="checkbox-icon"></span>
                      여자
                    </label>
                  </div>
                </div>
              </div>
              <Button
                variant="submit"
                label="작성 완료"
                className="submit-button"
              ></Button>
            </div>
          </form>
        </div>
      </div>
    </div>
  );
}

export default SignupPage;

 

우선 POST 요청을 보내야하기 때문에 axios를 설치한다

npm install axios

 

추가한 코드

import React, {useState} from 'react';
import '../styles/auth/SignupPage.scss';
import InputField from '../components/utils/InputField';
import Button from '../components/utils/Button';
import {useNavigate} from 'react-router-dom';
import axios from 'axios';

function SignupPage() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [age, setAge] = useState('');
  const [passwordConfirm, setPasswordConfirm] = useState('');
  const [passwordMatch, setPasswordMatch] = useState(true); // 비밀번호 확인 상태
  const [nickname, setNickname] = useState('');
  const [gender, setGender] = useState('');
  const [profileImage, setProfileImage] = useState(null); // 이미지 상태 추가
  const navigate = useNavigate();
  const [serverResponse, setServerResponse] = useState(null);

  const handleSignup = () => {
    navigate('/'); // 회원가입 페이지로 이동
  };

  const handleImageUpload = (e) => {
    const file = e.target.files[0];
    setProfileImage(file); // 이미지 상태 저장
  };

  const handleGenderChange = (selectedGender) => {
    setGender(selectedGender); // 성별 상태 업데이트
  };

  // 비밀번호 확인 로직
  const handlePasswordConfirmChange = (value) => {
    setPasswordConfirm(value);
    setPasswordMatch(value === password); // 비밀번호와 비밀번호 확인 비교
  };

  const handleSubmit = async (e) => {
    e.preventDefault();

    // 비밀번호와 비밀번호 확인이 일치하는지 확인
    if (!passwordMatch) {
      alert('비밀번호가 일치하지 않습니다.');
      return;
    }

    // 입력값을 JSON 형식으로 준비
    const userData = {
      email,
      password,
      nickname,
      age: Number(age), // 숫자로 변환
      gender,
      profileImgPath: profileImage ? URL.createObjectURL(profileImage) : null // 프로필 이미지 경로
    };

    try {
      // POST 요청 보내기
      const response = await axios.post('/api/v1/users/signup', userData, {
        headers: {
          'Content-Type': 'application/json' // JSON 형식으로 보냄
        }
      });
      // 서버 응답을 상태로 저장
      setServerResponse(response.data);
      alert('회원가입 성공!');
      navigate('/'); // 성공 후 홈 페이지로 리디렉션
    } catch (error) {
      console.error('회원가입 실패', error);
      alert('회원가입 실패, 다시 시도해주세요.');
    }
  };
  return (
    <div className="main-box">
      <img
        className="back"
        src="/icons/backicon.png"
        onClick={handleSignup}
      ></img>
      <h1 className="signup-title">회원가입</h1>
      <div className="total-box">
        <div className="left-box">
          <label>프로필 이미지는 선택사항입니다</label>
          {profileImage && (
            <div className="image-preview">
              <img
                src={URL.createObjectURL(profileImage)}
                alt="프로필 미리보기"
              />
            </div>
          )}
          <input type="file" accept="image/*" onChange={handleImageUpload} />
        </div>

        <div className="right-box">
          <form className="signup-form" onSubmit={handleSubmit}>
            {/* 이메일 입력 */}
            <InputField
              label="이메일(ID)"
              placeholder="이메일(ID)를 입력해주세요"
              type="email"
              value={email}
              onChange={setEmail}
              errorMessage="유효하지 않은 이메일 형식입니다."
            />

            {/* 비밀번호 입력 */}
            <InputField
              label="비밀번호"
              placeholder="8-20자 숫자, 영어, 특수문자(! @ # $ % ^ & ^) 사용가능"
              type="password"
              value={password}
              onChange={setPassword}
              errorMessage="비밀번호는 최소 8자 이상이어야 합니다."
            />

            {/* 비밀번호 확인 */}
            <InputField
              label="비밀번호 확인"
              placeholder="비밀번호를 다시 입력하세요"
              type="password"
              value={passwordConfirm}
              onChange={handlePasswordConfirmChange}
              errorMessage="비밀번호가 일치하지 않습니다."
              valid={passwordMatch} // 유효성 상태 전달
            />

            {/* 닉네임 입력 */}
            <InputField
              label="닉네임"
              placeholder="8자 이하로 한글, 숫자, 영어만 가능합니다"
              type="nickname"
              value={nickname}
              onChange={setNickname}
              errorMessage="8자 이하로 한글, 숫자, 영어만 가능합니다"
            />
            <div className="submit">
              <div className="agender-box">
                {/* 닉네임 입력 */}
                <InputField
                  label="나이"
                  placeholder="숫자만 입력가능합니다"
                  type="age"
                  value={age}
                  onChange={setAge}
                />

                {/* 성별 선택 */}

                <div className="gender-box">
                  <div className="text">성별</div>
                  <div className="gender-container">
                    <label className="custom-checkbox">
                      <input
                        type="checkbox"
                        checked={gender === 'M'}
                        onChange={() => handleGenderChange('M')}
                      />{' '}
                      <span className="checkbox-icon"></span>
                      남자
                    </label>
                    <label className="custom-checkbox">
                      <input
                        type="checkbox"
                        checked={gender === 'F'}
                        onChange={() => handleGenderChange('F')}
                      />{' '}
                      <span className="checkbox-icon"></span>
                      여자
                    </label>
                  </div>
                </div>
              </div>
              {serverResponse && (
                <div>
                  <h2>서버 응답:</h2>
                  <pre>{JSON.stringify(serverResponse, null, 2)}</pre>
                </div>
              )}
              <Button
                variant="submit"
                label="작성 완료"
                className="submit-button"
              ></Button>
            </div>
          </form>
        </div>
      </div>
    </div>
  );
}

export default SignupPage;

 

 

성공 !!

( 수많은 에러들의 원인은 1. 디비가 죽음 2. 배포중 이런 이유들이 있었음 )

 

 

하지만 이메일과 닉네임 중복검사 버튼을 만들지 않았다 .. !

뒤늦게 만들어주는데 문제가 발생함

 

기존의 작성완료 버튼은 모든 내용이 제대로 입력되었을 때 작동하도록 코드를 짰다.

근데 이제 .. 

1. 이메일 중복 검사를 했는가

2. 이메일이 중복이 아닌가

3. 이메일이 제대로 입력되었는가

이렇게 3가지 상태를 관리해야했음..

우선 노가다로 상태를 3개를 만들어서 관리했는데, 다른 문제가 발생했다.

  const [hasCheckedEmail, setHasCheckedEmail] = useState(false); // 이메일 중복검사 수행 여부
  const [isEmailDuplicate, setIsEmailDuplicate] = useState(false); // 이메일 중복 여부

 

 

중복검사 버튼을 눌렀는데 다른 input의 에러창이 같이 뜨는 것 !!

이유는 중복검사 버튼이 submit 영역 안에 있던 button 이였어서 submit이 같이 동작해 발생하는 문제였다.

그래서 email check 함수에서 기본동작을 방지하는 코드를 추가하고, 중복검사 버튼에 type을 정확히 명시해줬다.

 const handleCheckEmail = async (e) => {
    e.preventDefault(); // 기본 폼 제출 동작 방지

    try {
      const response = await axios.get(`/api/v1/users/email-availability`, {
        params: {email} // 쿼리 파라미터로 이메일 전달
      });

      const {result, data, meta} = response.data;

      setHasCheckedEmail(true); // 중복검사 수행 상태 업데이트

      if (result === 1 && data.isAvailable) {
        alert(meta.message); // "사용 가능한 이메일"
        setIsEmailDuplicate(false); // 중복 아님
      } else {
        alert('이미 사용 중인 이메일입니다.');
        setIsEmailDuplicate(true); // 중복
      }
    } catch (error) {
      console.error('이메일 중복 검사 실패:', error);
      alert('이메일 중복 검사 중 문제가 발생했습니다.');
      setHasCheckedEmail(false);
      setIsEmailDuplicate(true); // 실패 시 기본적으로 중복으로 처리
    }
  };
 <InputField
                label="이메일(ID)"
                placeholder="이메일(ID)를 입력해주세요"
                type="email"
                value={email}
                onChange={(value) => {
                  setEmail(value);
                  setHasCheckedEmail(false); // 중복검사 수행 상태 초기화
                  setIsEmailDuplicate(false); // 중복 여부 초기화
                }}
                errorMessage="유효하지 않은 이메일 형식입니다."
              />

이렇게하니 해결되었음 !! 이제 이걸 닉네임에도 똑같이 적용해주면 된다.

못생긴 alert은 나중에 꾸며주도록하자.

 

+ 추가로 모든항목에 대한 alert이 뜰수 있도록 submit 함수를 수정했음

오늘의 최종 코드

import React, {useState} from 'react';
import '../styles/auth/SignupPage.scss';
import InputField from '../components/utils/InputField';
import Button from '../components/utils/Button';
import {useNavigate} from 'react-router-dom';
import axios from 'axios';

function SignupPage() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [age, setAge] = useState('');
  const [passwordConfirm, setPasswordConfirm] = useState('');
  const [passwordMatch, setPasswordMatch] = useState(true); // 비밀번호 확인 상태
  const [nickname, setNickname] = useState('');
  const [gender, setGender] = useState('');
  const [profileImage, setProfileImage] = useState(null); // 이미지 상태 추가
  const navigate = useNavigate();
  const [serverResponse, setServerResponse] = useState(null);
  const [isNicknameValid, setIsNicknameValid] = useState(false);
  const [hasCheckedEmail, setHasCheckedEmail] = useState(false); // 이메일 중복검사 수행 여부
  const [isEmailDuplicate, setIsEmailDuplicate] = useState(false); // 이메일 중복 여부
  const [hasCheckedNickname, setHasCheckedNickname] = useState(false); // 이메일 중복검사 수행 여부
  const [isNicknameDuplicate, setIsNicknameDuplicate] = useState(false); // 이메일 중복 여부

  const handleSignup = () => {
    navigate('/'); // 회원가입 페이지로 이동
  };

  const handleImageUpload = (e) => {
    const file = e.target.files[0];
    setProfileImage(file); // 이미지 상태 저장
  };

  const handleGenderChange = (selectedGender) => {
    setGender(selectedGender); // 성별 상태 업데이트
  };

  // 비밀번호 확인 로직
  const handlePasswordConfirmChange = (value) => {
    setPasswordConfirm(value);
    setPasswordMatch(value === password); // 비밀번호와 비밀번호 확인 비교
  };

  const isFormValid =
    hasCheckedEmail &&
    isEmailDuplicate &&
    isNicknameDuplicate &&
    hasCheckedNickname &&
    passwordMatch &&
    password.length >= 8 &&
    nickname;

  const handleCheckEmail = async (e) => {
    e.preventDefault(); // 기본 폼 제출 동작 방지

    try {
      const response = await axios.get(`/api/v1/users/email-availability`, {
        params: {email} // 쿼리 파라미터로 이메일 전달
      });

      const {result, data, meta} = response.data;

      setHasCheckedEmail(false); // 중복검사 수행 상태 업데이트

      if (result === 1 && data.isAvailable) {
        alert(meta.message); // "사용 가능한 이메일"
        setIsEmailDuplicate(false); // 중복 아님
        setHasCheckedEmail(true);
      } else {
        alert('이미 사용 중인 이메일입니다.');
        setIsEmailDuplicate(true); // 중복
      }
    } catch (error) {
      console.error('이메일 중복 검사 실패:', error);
      alert('이메일 중복 검사 중 문제가 발생했습니다.');
      setHasCheckedEmail(false);
      setIsEmailDuplicate(true); // 실패 시 기본적으로 중복으로 처리
    }
  };

  const handleCheckNickname = async (e) => {
    e.preventDefault(); // 기본 폼 제출 동작 방지

    try {
      const response = await axios.get(`/api/v1/users/nickname-availability`, {
        params: {nickname} // 쿼리 파라미터로 닉네임 전달
      });

      const {result, data, meta} = response.data;

      setHasCheckedNickname(false); // 닉네임 검사가 수행되었음을 표시

      if (result === 1 && data.isAvailable) {
        alert(meta.message); // "사용 가능한 닉네임"
        setIsNicknameDuplicate(false); // 중복 아님
        setHasCheckedNickname(true);
      } else {
        alert('이미 사용 중인 닉네임입니다.');
        setIsNicknameDuplicate(true); // 닉네임 중복
      }
    } catch (error) {
      console.error('닉네임 중복 검사 실패:', error);
      alert('닉네임 중복 검사 중 문제가 발생했습니다.');
      setHasCheckedNickname(false);
      setIsNicknameDuplicate(true);
    }
  };

  const handleSubmit = async (e) => {
    e.preventDefault();

    // 중복검사 상태 확인
    if (!hasCheckedEmail) {
      alert('이메일 중복검사를 완료해주세요.');
      return;
    }

    if (isEmailDuplicate) {
      alert('이미 사용 중인 이메일입니다.');
      return;
    }

    if (!hasCheckedNickname) {
      alert('닉네임 중복검사를 완료해주세요.');
      return;
    }

    if (isNicknameDuplicate) {
      alert('이미 사용 중인 닉네임입니다.');
      return;
    }

    // 입력값 검증
    if (!email) {
      alert('이메일을 입력해주세요.');
      return;
    }

    if (!password) {
      alert('비밀번호를 입력해주세요.');
      return;
    }

    if (!passwordConfirm) {
      alert('비밀번호 확인을 입력해주세요.');
      return;
    }

    if (!passwordMatch) {
      alert('비밀번호가 일치하지 않습니다.');
      return;
    }

    if (!nickname) {
      alert('닉네임을 입력해주세요.');
      return;
    }

    if (!age) {
      alert('나이를 입력해주세요.');
      return;
    }

    if (!gender) {
      alert('성별을 선택해주세요.');
      return;
    }

    // 회원가입 데이터 준비
    const userData = {
      email,
      password,
      nickname,
      age: Number(age),
      gender,
      profileImgPath: profileImage ? URL.createObjectURL(profileImage) : null
    };

    try {
      // POST 요청 보내기
      const response = await axios.post('/api/v1/users/signup', userData, {
        headers: {
          'Content-Type': 'application/json'
        }
      });
      setServerResponse(response.data);
      alert('회원가입 성공!');
      navigate('/');
    } catch (error) {
      console.error('회원가입 실패', error);
      alert('회원가입 실패, 다시 시도해주세요.');
    }
  };

  return (
    <div className="main-box">
      <img
        className="back"
        src="/icons/backicon.png"
        onClick={handleSignup}
      ></img>
      <h1 className="signup-title">회원가입</h1>
      <div className="total-box">
        <div className="left-box">
          <label>프로필 이미지는 선택사항입니다</label>
          {profileImage && (
            <div className="image-preview">
              <img
                src={URL.createObjectURL(profileImage)}
                alt="프로필 미리보기"
              />
            </div>
          )}
          <input type="file" accept="image/*" onChange={handleImageUpload} />
        </div>

        <div className="right-box">
          <form className="signup-form" onSubmit={handleSubmit}>
            {/* 이메일 입력 */}
            <div className="plus-check">
              <InputField
                label="이메일(ID)"
                placeholder="이메일(ID)를 입력해주세요"
                type="email"
                value={email}
                onChange={(value) => {
                  setEmail(value);
                  setHasCheckedEmail(false); // 중복검사 수행 상태 초기화
                  setIsEmailDuplicate(false); // 중복 여부 초기화
                }}
                errorMessage="유효하지 않은 이메일 형식입니다."
              />

              <Button
                variant="check"
                label="중복검사"
                className="check-btn"
                type="button" // 버튼 타입 명시
                onClick={handleCheckEmail}
              />
            </div>

            {/* 비밀번호 입력 */}
            <InputField
              label="비밀번호"
              placeholder="8-20자 숫자, 영어, 특수문자(! @ # $ % ^ & ^) 사용가능"
              type="password"
              value={password}
              onChange={setPassword}
              errorMessage="비밀번호는 최소 8자 이상이어야 합니다."
            />

            {/* 비밀번호 확인 */}
            <InputField
              label="비밀번호 확인"
              placeholder="비밀번호를 다시 입력하세요"
              type="password"
              value={passwordConfirm}
              onChange={handlePasswordConfirmChange}
              errorMessage="비밀번호가 일치하지 않습니다."
              valid={passwordMatch} // 유효성 상태 전달
            />

            {/* 닉네임 입력 */}
            <div className="plus-check">
              <InputField
                label="닉네임"
                placeholder="8자 이하로 한글, 숫자, 영어만 가능합니다"
                type="nickname"
                value={nickname}
                onChange={(value) => {
                  setNickname(value);
                  setIsNicknameValid(false); // 닉네임 변경 시 상태 초기화
                }}
                errorMessage={
                  !isNicknameValid ? '이미 사용 중인 닉네임입니다.' : ''
                }
              />
              <Button
                variant="check"
                label="중복검사"
                className="check-btn"
                type="button" // 기본 제출 방지
                onClick={handleCheckNickname}
              />
            </div>
            <div className="submit">
              <div className="agender-box">
                {/* 닉네임 입력 */}
                <InputField
                  label="나이"
                  placeholder="숫자만 입력가능합니다"
                  type="age"
                  value={age}
                  onChange={setAge}
                />

                {/* 성별 선택 */}

                <div className="gender-box">
                  <div className="text">성별</div>
                  <div className="gender-container">
                    <label className="custom-checkbox">
                      <input
                        type="checkbox"
                        checked={gender === 'M'}
                        onChange={() => handleGenderChange('M')}
                      />{' '}
                      <span className="checkbox-icon"></span>
                      남자
                    </label>
                    <label className="custom-checkbox">
                      <input
                        type="checkbox"
                        checked={gender === 'F'}
                        onChange={() => handleGenderChange('F')}
                      />{' '}
                      <span className="checkbox-icon"></span>
                      여자
                    </label>
                  </div>
                </div>
              </div>
              {serverResponse && (
                <div>
                  <h2>서버 응답:</h2>
                  <pre>{JSON.stringify(serverResponse, null, 2)}</pre>
                </div>
              )}
              <Button
                variant="submit"
                label="작성 완료"
                className="submit-button"
                disabled={!isFormValid}
              ></Button>
            </div>
          </form>
        </div>
      </div>
    </div>
  );
}

export default SignupPage;

 

https://github.com/Book-Eating-Bunny/BEB-FE/commit/53379b7dae88c18f1be24b77f888aed6916662c3

 

Feat: 회원가입 페이지 기능 구현 · Book-Eating-Bunny/BEB-FE@53379b7

- 회원가입 API 연동 - 이메일, 닉네임 중복검사 버튼 추가 - 회원가입 폼 유효성 검사 통합 Resolves: #29, #30, #31, #32

github.com

 

회원가입 페이지 기능구현 끝 !!