Skip to content

Commit

Permalink
Merge pull request #284 from Jaymyong66/feat/signup_login
Browse files Browse the repository at this point in the history
회원가입 / 로그인 페이지 구현, 기능 구현
  • Loading branch information
Hain-tain authored Aug 6, 2024
2 parents 9f541d6 + 9024f33 commit 23bf890
Show file tree
Hide file tree
Showing 27 changed files with 712 additions and 48 deletions.
77 changes: 77 additions & 0 deletions frontend/src/api/authentication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { LoginRequest, SignupRequest } from '@/types/authentication';
import { customFetch } from './customFetch';

const API_URL = process.env.REACT_APP_API_URL;

export const SIGNUP_API_URL = `${API_URL}/signup`;
export const LOGIN_API_URL = `${API_URL}/login`;
export const LOGIN_STATE_API_URL = `${API_URL}/login/check`;
export const CHECK_USERNAME_API_URL = `${API_URL}/check-username`;
export const CHECK_EMAIL_API_URL = `${API_URL}/check-email`;

export const postSignup = async (signupInfo: SignupRequest) =>
await customFetch({
method: 'POST',
url: `${SIGNUP_API_URL}`,
body: JSON.stringify(signupInfo),
});

export const postLogin = async (loginInfo: LoginRequest) => {
const response = await customFetch({
method: 'POST',
url: `${LOGIN_API_URL}`,
body: JSON.stringify(loginInfo),
});

return response;
};

export const getLoginState = async () => {
const url = `${LOGIN_STATE_API_URL}`;

const response = await customFetch({ url });

if (response.status === 401) {
throw new Error('로그인을 해주세요.');
}

if (!response.ok) {
throw new Error('서버 에러가 발생했습니다.');
}

return {};
};

export const checkEmail = async (email: string) => {
const params = new URLSearchParams({ email });
const url = `${CHECK_EMAIL_API_URL}?${params}`;

const response = await customFetch({ url });

if (response.status === 409) {
throw new Error('중복된 이메일입니다.');
}

if (!response.ok) {
throw new Error('서버 에러가 발생했습니다.');
}

return {};
};

export const checkUsername = async (username: string) => {
const params = new URLSearchParams({ username });
const url = `${CHECK_USERNAME_API_URL}?${params}`;

const response = await customFetch({ url });

if (response.status === 409) {
throw new Error('중복된 닉네임입니다.');
}

if (!response.ok) {
throw new Error('서버 에러가 발생했습니다.');
}

return {};
};
15 changes: 3 additions & 12 deletions frontend/src/api/customFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,18 @@ interface Props {
errorMessage?: string;
}

export const customFetch = async ({
url,
headers,
method = 'GET',
body,
errorMessage = '[Error] response was not ok',
}: Props) => {
export const customFetch = async ({ url, headers, method = 'GET', body }: Props) => {
try {
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
...headers,
},
credentials: 'include',
body,
});

if (!response.ok) {
throw new Error(errorMessage);
}

if (method !== 'GET') {
return response;
}
Expand All @@ -35,6 +26,6 @@ export const customFetch = async ({

return data;
} catch (error) {
throw new Error(errorMessage);
throw new Error(String(error));
}
};
3 changes: 3 additions & 0 deletions frontend/src/api/queryKeys.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export const QUERY_KEY = {
TEMPLATE: 'template',
TEMPLATE_LIST: 'templateList',
LOGIN_STATE: 'loginState',
CHECK_EMAIL: 'checkEmail',
CHECK_USERNAME: 'userName',
};
1 change: 1 addition & 0 deletions frontend/src/assets/images/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { default as pencilIcon } from './pencil.png';
export { default as searchIcon } from './search.png';
export { default as trashcanIcon } from './trashcan.png';
export { default as userMenuIcon } from './userMenu_38x38.png';
export { default as passwordEyeIcon } from './passwordEye.png';
Binary file added frontend/src/assets/images/passwordEye.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
46 changes: 25 additions & 21 deletions frontend/src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,34 @@ import { Link } from 'react-router-dom';

import { logoIcon, newTemplateIcon, userMenuIcon } from '@/assets/images';
import { Button, Flex, Heading, Text } from '@/components';
import { useCheckLoginState } from '@/hooks/authentication';
import { theme } from '../../style/theme';
import * as S from './Header.style';

const Header = () => (
<S.HeaderContainer>
<S.HeaderContentContainer>
<Logo />

<Flex align='center' gap='2rem' flex='1'>
<NavOption route='/' name='내 템플릿' />
<NavOption route='/explore' name='구경가기' />
</Flex>

<Flex align='center' gap='2rem'>
<Link to={'/templates/upload'}>
<Button variant='outlined' size='medium' weight='bold' hoverStyle='none'>
<img src={newTemplateIcon} alt='' />새 템플릿
</Button>
</Link>
<UserMenuButton />
</Flex>
</S.HeaderContentContainer>
</S.HeaderContainer>
);
const Header = () => {
useCheckLoginState();

return (
<S.HeaderContainer>
<S.HeaderContentContainer>
<Logo />
<Flex align='center' gap='2rem' flex='1'>
<NavOption route='/' name='내 템플릿' />
<NavOption route='/explore' name='구경가기' />
</Flex>

<Flex align='center' gap='2rem'>
<Link to={'/templates/upload'}>
<Button variant='outlined' size='medium' weight='bold' hoverStyle='none'>
<img src={newTemplateIcon} alt='' />새 템플릿
</Button>
</Link>
<UserMenuButton />
</Flex>
</S.HeaderContentContainer>
</S.HeaderContainer>
);
};

const Logo = () => (
<Link to={'/'}>
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/hooks/authentication/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { useShowPassword } from './useShowPassword';
export { useSignupForm } from './useSignupForm';
export { useCheckLoginState } from './useCheckLoginState';
20 changes: 20 additions & 0 deletions frontend/src/hooks/authentication/useCheckLoginState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useCallback, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';

import { useLoginStateQuery } from '@/queries/authentication/useLoginStateQuery';

export const useCheckLoginState = () => {
const { error, isError } = useLoginStateQuery();
const navigate = useNavigate();

const handleLoginNavigate = useCallback(() => {
navigate('/login');
}, [navigate]);

useEffect(() => {
if (isError) {
alert(error.message);
handleLoginNavigate();
}
}, [error, isError, handleLoginNavigate]);
};
53 changes: 53 additions & 0 deletions frontend/src/hooks/authentication/useLoginForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { FormEvent } from 'react';
import { useNavigate } from 'react-router-dom';

import { postLogin } from '@/api/authentication';
import { useInputWithValidate } from '../useInputWithValidate';
import { validateEmail, validatePassword } from './validates';

export const useLoginForm = () => {
const navigate = useNavigate();

const {
value: email,
errorMessage: emailError,
handleChange: handleEmailChange,
} = useInputWithValidate('', validateEmail);

const {
value: password,
errorMessage: passwordError,
handleChange: handlePasswordChange,
} = useInputWithValidate('', validatePassword);

const isFormValid = () => !emailError && !passwordError && email && password;

const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();

if (isFormValid()) {
const response = await postLogin({ email, password });

if (!response.ok) {
console.error(response);

return;
}

navigate('/');
}
};

return {
email,
password,
errors: {
email: emailError,
password: passwordError,
},
handleEmailChange,
handlePasswordChange,
isFormValid,
handleSubmit,
};
};
14 changes: 14 additions & 0 deletions frontend/src/hooks/authentication/useShowPassword.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useState } from 'react';

export const useShowPassword = () => {
const [showPassword, setShowPassword] = useState(false);

const handlePasswordToggle = () => {
setShowPassword((prevShowPassword) => !prevShowPassword);
};

return {
showPassword,
handlePasswordToggle,
};
};
112 changes: 112 additions & 0 deletions frontend/src/hooks/authentication/useSignupForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { FormEvent, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';

import { postSignup } from '@/api/authentication';
import { useCheckEmailQuery } from '@/queries/authentication';
import { useCheckUsernameQuery } from '@/queries/authentication/useCheckUsernameQuery';
import { useInputWithValidate } from '../useInputWithValidate';
import { validateEmail, validateUsername, validatePassword, validateConfirmPassword } from './validates';

export const useSignupForm = () => {
const navigate = useNavigate();

const {
value: email,
errorMessage: emailError,
handleChange: handleEmailChange,
handleErrorMessage: handleEmailErrorMessage,
} = useInputWithValidate('', validateEmail);

const {
value: username,
errorMessage: usernameError,
handleChange: handleUsernameChange,
handleErrorMessage: handleUsernameErrorMessage,
} = useInputWithValidate('', validateUsername);

const {
value: password,
errorMessage: passwordError,
handleChange: handlePasswordChange,
} = useInputWithValidate('', validatePassword);

const {
value: confirmPassword,
errorMessage: confirmPasswordError,
handleChange: handleConfirmPasswordChange,
handleErrorMessage: handleConfirmPasswordErrorMessage,
} = useInputWithValidate('', (value, compareValue) => validateConfirmPassword(value, compareValue ?? ''));

const { refetch: checkEmailQuery } = useCheckEmailQuery(email);
const { refetch: checkUsernameQuery } = useCheckUsernameQuery(username);

const handleEmailCheck = async () => {
const { error } = await checkEmailQuery();

// refetch does not exist onError
if (error) {
handleEmailErrorMessage(error.message);
}
};

const handleUsernameCheck = async () => {
const { error } = await checkUsernameQuery();

// refetch does not exist onError
if (error) {
handleUsernameErrorMessage(error.message);
}
};

// only change password not confirmPassword
useEffect(() => {
handleConfirmPasswordErrorMessage(validateConfirmPassword(password, confirmPassword));
}, [password, confirmPassword, handleConfirmPasswordErrorMessage]);

const isFormValid = () =>
!emailError &&
!usernameError &&
!passwordError &&
!confirmPasswordError &&
email &&
username &&
password &&
confirmPassword;

const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();

if (isFormValid()) {
const response = await postSignup({ email, username, password });

if (!response.ok) {
console.error(response);

return;
}

navigate('/login');
}
};

return {
email,
username,
password,
confirmPassword,
errors: {
email: emailError,
username: usernameError,
password: passwordError,
confirmPassword: confirmPasswordError,
},
handleEmailChange,
handleUsernameChange,
handlePasswordChange,
handleConfirmPasswordChange,
isFormValid,
handleSubmit,
handleEmailCheck,
handleUsernameCheck,
};
};
Loading

0 comments on commit 23bf890

Please sign in to comment.