Skip to content

Commit

Permalink
feat : 회원가입 페이지 개발 완료 (#136)
Browse files Browse the repository at this point in the history
* chore : 로그인 수정

* feat : email 체크 api 연동

* chore : 로그인 이미지 나오도록 수정

* feat : 라우팅 설정

* feat : 회원가입 api 연동

* feat : 유효성 검사

* feat : 헤더 라우팅

* chore : 로그인 수정

* feat : email 체크 api 연동

* chore : 로그인 이미지 나오도록 수정

* feat : 라우팅 설정

* feat : 회원가입 api 연동

* feat : 유효성 검사

* feat : 헤더 라우팅
  • Loading branch information
HelloWook authored Nov 4, 2024
1 parent 81b3e68 commit 04a9f17
Show file tree
Hide file tree
Showing 13 changed files with 239 additions and 25 deletions.
7 changes: 6 additions & 1 deletion src/app/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createBrowserRouter } from 'react-router-dom';
import Layout from './layout/Layout';
import MyDiaryListPage from '@/pages/MyDiaryListPage';
import LoginPage from '@/pages/LoginPage/LoginPage';
import SignPage from '@/pages/SignPage/SignPage';

const router = createBrowserRouter([
{
Expand All @@ -18,8 +19,12 @@ const router = createBrowserRouter([
element: <LoginPage />
},
{
path: 'diary',
path: '/diary',
element: <MyDiaryListPage />
},
{
path: '/join',
element: <SignPage />
}
]
}
Expand Down
36 changes: 36 additions & 0 deletions src/features/join/hook/useJoin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useMutation, UseMutationResult } from '@tanstack/react-query';
import { join } from '@/shared/api/join';
import { JoinParam } from '../model/joinParams';
import { JoinType } from '../model/JoinType';
import { AxiosError } from 'axios';
import { useToastStore } from '@/features/Toast/hooks/useToastStore';
import { useNavigate } from 'react-router-dom';
import useLocalStorage from '@/shared/hooks/useLocalStorage';

/**
* 회원가입 API 호출을 위한 커스텀 훅
*/
const useJoin = (): UseMutationResult<JoinType, AxiosError, JoinParam> => {
const navigate = useNavigate();
const { setValue: setToken } = useLocalStorage<string>('token', '');

const { addToast } = useToastStore();
const mutation = useMutation<JoinType, AxiosError, JoinParam>({
mutationFn: (params: JoinParam) => join(params),
onSuccess: (data) => {
if (data.token) {
addToast(data.message, 'success');
setToken(data.token);
navigate('/login');
} else {
addToast(data.message, 'error');
}
},
onError: (error: Error) => {
addToast(error.message, 'error');
}
});
return mutation;
};

export default useJoin;
44 changes: 44 additions & 0 deletions src/features/join/hook/usecheckEmail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Waring } from '../../Toast/ui/Toast.stories';
import { useMutation } from '@tanstack/react-query';
import { checkEmailExists } from '@/shared/api/checkEmail';
import { useToastStore } from '@/features/Toast/hooks/useToastStore';
import { useState } from 'react';

interface CheckEmailResponse {
exists: boolean;
message: string;
}

/**
* 이메일 중복 여부를 확인하는 커스텀 훅
*/
const useCheckEmail = () => {
const { addToast } = useToastStore();
const [isEmailAvailable, setIsEmailAvailable] = useState<boolean | null>(
null
);

const {
mutate: checkEmail,
data,
error
} = useMutation<CheckEmailResponse, Error, string>({
mutationFn: (email: string) => checkEmailExists(email),
onSuccess: (res) => {
addToast(res.message, res.exists === false ? 'success' : 'warning');
setIsEmailAvailable(!res.exists);
},
onError: () => {
addToast('서버와 응답에 실패했습니다', 'error');
}
});

return {
checkEmail,
isEmailAvailable,
data,
error
};
};

export default useCheckEmail;
4 changes: 4 additions & 0 deletions src/features/join/model/JoinType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface JoinType {
message: string;
token: string;
}
7 changes: 7 additions & 0 deletions src/features/join/model/joinParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface JoinParam {
email: string;
username: string;
password: string;
gender: string;
phoneNumber: string;
}
6 changes: 4 additions & 2 deletions src/features/login/hooks/useLogin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ const useLogin = (): UseMutationResult<LoginType, Error, LoginParams> => {
addToast('로그인 성공했습니다.', 'success');
setToken(res.token);
const payload = decodeJwtPayload<userType>(res.token);
setUserInfo(email, userName, true);
if (payload) {
setUserInfo(payload.username, payload.email, true);
}
navigate('/');
},
onError: (err: Error) => {
addToast(err.message, 'warning');
addToast(err.message, 'error');
}
});

Expand Down
12 changes: 12 additions & 0 deletions src/pages/SignPage/SignPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Sign from '@/widgets/Sign/Sign';
import React from 'react';

const SignPage = () => {
return (
<div>
<Sign />
</div>
);
};

export default SignPage;
27 changes: 27 additions & 0 deletions src/shared/api/checkEmail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { AxiosError } from 'axios';
import defaultApi from '@/shared/api/api';

const api = defaultApi();

/**
* 이메일 중복 여부를 확인하는 유틸리티 함수
* @param {string} email - 확인할 이메일 주소
* @returns {Promise<{ exists: boolean, message: string }>} - 이메일 중복 여부와 메시지
* @throws {Error} - 서버 에러 또는 네트워크 오류 발생 시 에러를 던짐
*/
export const checkEmailExists = async (
email: string
): Promise<{ exists: boolean; message: string }> => {
try {
const response = await api.post('/check-username', { email });
const { exists, message } = response.data;
return { exists, message };
} catch (error: unknown) {
if (error instanceof AxiosError && error.response) {
const serverMessage =
error.response.data.message || '서버 에러가 발생했습니다.';
throw new Error(serverMessage);
}
throw new Error('네트워크 오류가 발생했습니다.');
}
};
26 changes: 26 additions & 0 deletions src/shared/api/join.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { JoinType } from '../../features/join/model/JoinType';
import { JoinParam } from '../../features/join/model/joinParams';
import defaultApi from '@/shared/api/api';
import { AxiosError } from 'axios';

const api = defaultApi();

export const join = async (params: JoinParam): Promise<JoinType> => {
try {
const { phoneNumber, ...restParams } = params;
const modifiedParams = {
...restParams,
phone_number: phoneNumber
};

const response = await api.post('/signup', modifiedParams);
return response.data;
} catch (error: unknown) {
if (error instanceof AxiosError && error.response) {
const serverMessage =
error.response.data.message || '서버 에러가 발생했습니다.';
throw new Error(serverMessage);
}
throw new Error('네트워크 오류가 발생했습니다.');
}
};
8 changes: 3 additions & 5 deletions src/shared/ui/InputForm/InputForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React, { useState } from 'react';
import { InputFormProps } from './InputForm.types';
import visbilty from '@/shared/assets/visibility.svg';
import visbiltyOff from '@/shared/assets/visibility_off.svg';
import {
InputContainer,
StyledLabel,
Expand Down Expand Up @@ -96,11 +98,7 @@ const InputForm = ({
}
>
<StyledImg
src={
isPasswordVisible
? '/visibility_off.svg'
: '/visibility.svg'
}
src={isPasswordVisible ? visbilty : visbiltyOff}
alt={
isPasswordVisible
? '비밀번호 보이기'
Expand Down
9 changes: 5 additions & 4 deletions src/widgets/Login/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ import { useToastStore } from '@/features/Toast/hooks/useToastStore';
import { ButtonStyled, LoginStyled } from './Login.styled';
import Span from '@/shared/ui/Span/Span';
import useLogin from '@/features/login/hooks/useLogin';
import { useNavigate } from 'react-router-dom';

const Login = () => {
const { addToast } = useToastStore();
const [email, setEmail] = useState<string>('');
const [password, setPassword] = useState<string>('');
const { mutate, isError, isSuccess, data, error } = useLogin();

const navigate = useNavigate();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
mutate({ email, password });
Expand All @@ -28,12 +29,12 @@ const Login = () => {
<Margin bottom={70} />
<form onSubmit={handleSubmit}>
<InputForm
label="아이디 입력 (영문, 숫자 6~20자)"
label="이메일 입력"
value={email}
width="500px"
height="52px"
onChange={setEmail}
placeholder="아이디 입력 (영문, 숫자 6~20자)"
placeholder="이메일 입력"
/>
<Margin bottom={25} />
<InputForm
Expand All @@ -55,7 +56,7 @@ const Login = () => {
fontSize="16px"
type="button"
onClick={() => {
addToast('회원가입', 'success');
navigate('/join');
}}
>
회원가입
Expand Down
70 changes: 57 additions & 13 deletions src/widgets/Sign/Sign.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,28 @@ import Margin from '@/shared/ui/Margin/Margin';
import InputForm from '@/shared/ui/InputForm/InputForm';
import Button from '@/shared/ui/Button/Button';
import { useToastStore } from '@/features/Toast/hooks/useToastStore';
import Span from '@/shared/ui/Span/Span';
import { IdContainerStyled, SignStyled } from './Sign.styled';
import useCheckEmail from '@/features/join/hook/usecheckEmail';
import {
EMAIL_REG_EXP,
PHONE_NUMBER_REG_EXP,
PASSWORD_REG_EXP
} from './util/RegExp';
import useJoin from '@/features/join/hook/useJoin';

const Sign = () => {
const [id, setId] = useState<string>('');
const [email, setEmail] = useState<string>('');
const [password, setPassword] = useState<string>('');
const [checkPassword, setCheckPassword] = useState<string>('');
const [name, setName] = useState<string>('');
const [phoneNumber, setPhoneNumber] = useState<string>('');
const [gender, setGender] = useState<string>('');
const { addToast } = useToastStore();

const { checkEmail, isEmailAvailable } = useCheckEmail();
const { mutate } = useJoin();
return (
<SignStyled>
<Margin top={120} />
<Title isLoading={false}>아이디 / 비밀번호 찾기</Title>
<Info isLoading={false}>무디와 일기쓰고 노래 추천 받기</Info>
<Margin bottom={93} />
Expand Down Expand Up @@ -51,20 +60,28 @@ const Sign = () => {
<Margin bottom={25} />
<IdContainerStyled>
<InputForm
label="아이디"
value={id}
label="이메일"
value={email}
width="383px"
height="52px"
onChange={setId}
placeholder="아이디 입력 (영문, 숫자 6~20자)"
onChange={setEmail}
placeholder="이메일 입력"
/>
<Button
height="52px"
width="110px"
fontSize="16px"
hasBorder
onClick={() => {
addToast('조회하기', 'success');
if (email === '') {
addToast('이메일을 입력해주세요', 'warning');
return;
}
if (EMAIL_REG_EXP.test(email)) {
checkEmail(email);
} else {
addToast('잘못된 이메일 형식입니다.', 'warning');
}
}}
>
중복확인
Expand All @@ -83,12 +100,10 @@ const Sign = () => {
<Margin bottom={25} />
<InputForm
label="비밀번호 확인"
value={phoneNumber}
value={checkPassword}
width="500px"
height="52px"
onChange={() => {
addToast('조회하기', 'success');
}}
onChange={setCheckPassword}
placeholder="비밀번호 확인"
errorMessage="잘못된 비밀번호 입니다."
/>
Expand All @@ -98,7 +113,36 @@ const Sign = () => {
width="500px"
fontSize="16px"
onClick={() => {
addToast('조회하기', 'success');
if (!isEmailAvailable) {
addToast('이메일 중복확인을 해주세요.', 'warning');
return;
}
if (!gender) {
addToast('성별을 골라주세요.', 'warning');
return;
}
if (!EMAIL_REG_EXP.test(email)) {
addToast('잘못된 이메일 형식입니다.', 'warning');
return;
}
if (!PASSWORD_REG_EXP.test(password)) {
addToast('잘못된 비밀번호 형식입니다.', 'warning');
return;
}
if (!PHONE_NUMBER_REG_EXP.test(phoneNumber)) {
addToast('잘못된 전화번호 형식입니다.', 'warning');
return;
}
if (password !== checkPassword) {
addToast('비밀번호가 일치하지 않습니다.', 'warning');
}
mutate({
email,
username: name,
password,
gender: gender as '남성' | '여성',
phoneNumber
});
}}
>
회원가입
Expand Down
8 changes: 8 additions & 0 deletions src/widgets/Sign/util/RegExp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const EMAIL_REG_EXP: RegExp =
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;

export const PHONE_NUMBER_REG_EXP: RegExp =
/^(01[016789]-?\d{3,4}-?\d{4})$|^(0[2-8][0-5]?-?\d{3,4}-?\d{4})$/;

export const PASSWORD_REG_EXP: RegExp =
/^(?=.*[A-Za-z])(?=.*\d)(?=.*[!@#$%^&*()_+~`\-={}[\]:;"'<>,.?/]).{8,20}$/;

0 comments on commit 04a9f17

Please sign in to comment.