Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

회원가입 / 로그인 페이지 구현, 기능 구현 #284

Merged
merged 68 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
39f4cdc
feat(SignupPage): 회원가입 페이지 생성, 라우트 설정
Jaymyong66 Jul 31, 2024
9d77a01
feat(LoginPage): 로그인 페이지 생성, 라우트 설정
Jaymyong66 Jul 31, 2024
ce63c28
refactor(style): 오타로 스타일 적용 안된 것 수정
Jaymyong66 Jul 31, 2024
e9a704e
refactor(Input): Wrapper -> Container 변수명 변경
Jaymyong66 Jul 31, 2024
b737873
feat(Input): Input 컴포넌트에 Label 추가
Jaymyong66 Jul 31, 2024
5021580
feat(Images): passwordEye 이미지 생성
Jaymyong66 Jul 31, 2024
7c9a36b
feat(SignupPage): 회원가입 페이지 UI
Jaymyong66 Jul 31, 2024
f6d7a1e
feat(Login): 로그인 페이지 UI
Jaymyong66 Jul 31, 2024
b057b4d
feat(authentication): 비밀번호 보기 버튼 훅 생성
Jaymyong66 Jul 31, 2024
a5682e7
feat(LoginPage): 비밀번호 보기 기능 적용
Jaymyong66 Jul 31, 2024
3d1b049
feat(SignupPage): 비밀번호와 비밀번호 확인 각각에 보기 기능 적용
Jaymyong66 Jul 31, 2024
239c865
feat(authentication): useSignupForm 생성, http 요청 제외
Jaymyong66 Jul 31, 2024
166bf64
feat(SignupPage): 회원가입 유효성 검증 hook 적용
Jaymyong66 Jul 31, 2024
af1ff3b
feat(api): 회원가입 POST 요청 함수 생성
Jaymyong66 Jul 31, 2024
809800c
feat(mocks): 회원가입 POST 요청에 대한 MSW 모킹 추가
Jaymyong66 Jul 31, 2024
ff71d12
feat(authentication): 회원가입 hook에 submit 액션에 API 요청 추가
Jaymyong66 Jul 31, 2024
31cbd52
feat(SignupPage): 로그인 페이지로 가는 버튼 기능 추가
Jaymyong66 Jul 31, 2024
eef1dce
feat(LoginPage): 회원가입 페이지로 가는 버튼 기능 추가
Jaymyong66 Jul 31, 2024
ab67ca5
refactor(SignupPage): HelperText의 높이 고정, 유효하지 않을 시 Input의 isValid 변경 처리
Jaymyong66 Aug 1, 2024
3d23bce
feat(api): 중복 이메일, 중복 닉네임 확인 API 요청 함수 생성
Jaymyong66 Aug 1, 2024
2cd4b9f
refactor(mocks): MSW handler의 회원가입 요청 URL 상수화
Jaymyong66 Aug 1, 2024
4caf5ce
feat(mocks): MSW handler 추가 - 이메일 중복 검사, 닉네임 중복 검사 URL
Jaymyong66 Aug 1, 2024
d151b66
refactor(mocks): templates 관련 MSW handler 코드 스타일 변경
Jaymyong66 Aug 1, 2024
e7cda45
feat(authentication): 이메일 중복 검사 Query 훅 생성
Jaymyong66 Aug 1, 2024
a6670bc
feat(mocks): 이메일 중복 검사, 닉네임 중복 검사 MSW handler 생성
Jaymyong66 Aug 1, 2024
002db5a
feat(SignupPage): 이메일 입력 후 Blur 이벤트 발생 시, 이메일 중복 검사 요청 기능
Jaymyong66 Aug 1, 2024
2eb2d13
refactor(authentication): 회원가입 요청의 데이터 타입에서 confirmPassword 제거,
Jaymyong66 Aug 1, 2024
f37863f
refactor(authentication): queries/authentication 폴더에 index 파일 생성
Jaymyong66 Aug 1, 2024
dd54fde
feat(api): 로그인 API 요청 함수 생성
Jaymyong66 Aug 1, 2024
25e52fe
feat(mocks): 로그인 요청 MSW handler 추가
Jaymyong66 Aug 1, 2024
274289b
feat(authentication): 회원가입 후 로그인 페이지로 라우팅
Jaymyong66 Aug 1, 2024
b25e29b
feat(authentication): 로그인 기능 훅 생성, LoginPage에 적용
Jaymyong66 Aug 1, 2024
c9f151c
refactor(mocks): 멤버별 템플릿 목록 모킹 데이터 분리
Jaymyong66 Aug 2, 2024
8cc6a9f
refactor(authentication): 로그인 훅에서 로컬스토리지 로직 분리 -> 임시로 API 레이어로
Jaymyong66 Aug 2, 2024
246ab5c
feat(api): customFetch - localStorage에 token이 있다면 요청 header에 추가
Jaymyong66 Aug 2, 2024
0fb93ba
feat(mocks): handler - 두 명의 유저를 가정, token에 따라 응답을 다르게 주는 login 모킹
Jaymyong66 Aug 2, 2024
7675ce2
refactor(SignupPage): nickname -> username 으로 변경
Jaymyong66 Aug 3, 2024
fd9a757
feat(authentication): username 중복 검사 요청 훅 생성
Jaymyong66 Aug 3, 2024
2dae680
refactor(authentication): useSignupForm - 입력값의 전후 공백 제거, nickname -> …
Jaymyong66 Aug 3, 2024
d9a15b1
feat(SignupPage): 중복 사용자이름 검사 추가
Jaymyong66 Aug 3, 2024
c8b046f
refactor(SignupPage): form 태그 추가, gap 변경
Jaymyong66 Aug 3, 2024
3390522
refactor(SignupPage): h1태그를 Heading 컴포넌트로 변경
Jaymyong66 Aug 3, 2024
7b93a74
refactor(LoginPage): Input 상위에 form 태그 추가
Jaymyong66 Aug 3, 2024
880d4c2
refactor(LoginPage): input에 autoComplate 옵션 추가
Jaymyong66 Aug 3, 2024
6351a4c
refactor(SignupPage): input에 autoComplate 옵션 추가
Jaymyong66 Aug 3, 2024
2ba0dc7
refactor(authentication): handleSubmit에 event의 preventDefault 추가
Jaymyong66 Aug 3, 2024
b7ac892
refactor(authentication): 로그인 입력값에 전후 공백 제거
Jaymyong66 Aug 3, 2024
2d7b4ac
refactor(pages): LoginPage, SignupPage - Text 컴포넌트 적용
Jaymyong66 Aug 3, 2024
48d3d8a
feat(api): customFetch - fetch함수에 httponly 쿠키를 위한 credentials 옵션 추가
Jaymyong66 Aug 3, 2024
733a9a3
refactor(authenication): validates - 유효성 검사 로직 분리
Jaymyong66 Aug 3, 2024
1b1208f
feat(hooks): useInput - 훅 생성
Jaymyong66 Aug 4, 2024
8968ad3
refactor(authentication): useSignupForm - useInput 훅 사용, handle 함수 구현…
Jaymyong66 Aug 4, 2024
2c2daa1
refactor(hooks): useInput - handle 함수를 useCallback으로 최적화(useEffect의 의…
Jaymyong66 Aug 4, 2024
fc17de2
refactor(authentication): useLoginForm - useInput 훅 사용
Jaymyong66 Aug 4, 2024
b67ffae
refactor(template): useTemplateListQuery - mock 데이터 변경에 따른 테스트 스펙 변경
Jaymyong66 Aug 4, 2024
b4983d4
refactor(authentication): API 변경에 따른 response 반환값 제거
Jaymyong66 Aug 5, 2024
4950c57
refactor(api): customFetch - 내부 throw error 처리 로직 제거
Jaymyong66 Aug 5, 2024
0e0cb60
refactor(authentication): 이메일 중복 handling 함수
Jaymyong66 Aug 5, 2024
8b68a40
refactor(mocks): 이메일이 중복된 경우의 mocking 추가
Jaymyong66 Aug 5, 2024
6f4adbf
refactor(hooks): useInput -> useInputWithValidate 훅 이름 변경
Jaymyong66 Aug 6, 2024
735ced5
refactor(pages): 변경된 Button 컴포넌트 적용
Jaymyong66 Aug 6, 2024
e3795bf
feat(api): getLoginState 생성
Jaymyong66 Aug 6, 2024
c648c7a
feat(mocks): login/check 에 대한 실패 모킹 handler 추가
Jaymyong66 Aug 6, 2024
41090fb
feat(authentication): useLoginStateQuery 생성
Jaymyong66 Aug 6, 2024
b7a90df
feat(authentication): useCheckLoginState 생성
Jaymyong66 Aug 6, 2024
1ab9ab4
feat(Header): Header 마운트시 useCheckLoginState 훅 실행
Jaymyong66 Aug 6, 2024
7da0574
refactor(authentication): 사용자 이름 중복 확인 요청 API 변경
Jaymyong66 Aug 6, 2024
9024f33
refactor(api): 인증/인가 토큰 변경에 따른 localstorage token 관리 제거
Jaymyong66 Aug 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]);
};
Comment on lines +1 to +20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

접근 권한을 제어하는 다른 방법도 있더라구요! 링크 첨부해요!
이와 같이 router.ts에서 설정하면 각 페이지에서 인가 여부를 판단하지 않아도 됩니다!

https://github.com/noveogroup-amorgunov/nukeapp/blob/main/src/app/appRouter.tsx

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