-
Notifications
You must be signed in to change notification settings - Fork 1
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
[TASK-30] feat: 로그인/회원가입 페이지 구현 #11
Changes from all commits
e5ec060
8c8d1d4
061b13c
3c668b7
ee39927
c28cde9
370320a
097b907
c307199
9005a42
f86bc2c
0a2dc8a
dfbfcff
8f44f34
0772ac6
106a1d3
358b655
a83bee1
8d0b4f3
c59ceb7
43b9078
ee0e66e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { USER } from '@/constants/API'; | ||
import { useMutation } from '@tanstack/react-query'; | ||
import { useRouter } from 'next/navigation'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
해당 파일에선 같은 상우님은 어떻게 생각하실까요? 🤔 |
||
|
||
import defaultClient from '..'; | ||
|
||
const postLogin = async (formData: LoginFormType) => { | ||
const { data } = await defaultClient.post<ResponseRootType<DefaultAuthType>>( | ||
USER.login, | ||
formData, | ||
); | ||
|
||
return data; | ||
}; | ||
|
||
const usePostLogin = () => { | ||
const router = useRouter(); | ||
|
||
return useMutation({ | ||
mutationKey: [USER.login], | ||
mutationFn: (formData: LoginFormType) => postLogin(formData), | ||
onSuccess: () => router.push('/'), | ||
}); | ||
}; | ||
|
||
export default usePostLogin; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { USER } from '@/constants/API'; | ||
import { useMutation } from '@tanstack/react-query'; | ||
import { useRouter } from 'next/navigation'; | ||
import defaultClient from '..'; | ||
|
||
const postSignUp = async (formData: SignUpFormType) => { | ||
const { data } = await defaultClient.post<ResponseRootType<DefaultAuthType>>( | ||
USER.signup, | ||
formData, | ||
); | ||
|
||
return data; | ||
}; | ||
|
||
const usePostSignUp = () => { | ||
const router = useRouter(); | ||
|
||
return useMutation({ | ||
mutationKey: [USER.signup], | ||
mutationFn: (formData: SignUpFormType) => postSignUp(formData), | ||
onSuccess: () => router.push('/'), | ||
}); | ||
}; | ||
|
||
export default usePostSignUp; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { USER } from '@/constants/API'; | ||
import { useQuery } from '@tanstack/react-query'; | ||
import defaultClient from '..'; | ||
|
||
const getSocialLogin = async (queryUrl: string) => { | ||
const { data } = | ||
await defaultClient.get<ResponseRootType<SocialLoginAuthType>>(queryUrl); | ||
|
||
return data; | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 현재 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 백엔드 구현이 완료된 이후에 작성되어있는 버전의 api 문서로 구체적인 에러케이스를 처리하는 것이 낫지 않을까 하는 생각에 우선 성궁 케이스에 대해서만 고려해두었습니다..! 에러 핸들링까지 추가해두겠습니다! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아아 그렇군요. 반영 감사해요! 이와 관련해서 백엔드 측과 얘기를 해봐야겠네요 |
||
|
||
const useGetSocialLogin = ({ | ||
provider, | ||
params, | ||
}: { | ||
provider: string; | ||
params: string; | ||
}) => { | ||
const queryUrl = `${USER.socialLogin}/${provider}?${params}`; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아래 코드와의 한 줄을 띄우고, |
||
const { data, isLoading } = useQuery({ | ||
queryKey: [`${USER.socialLogin}/${provider}`], | ||
queryFn: () => getSocialLogin(queryUrl), | ||
}); | ||
|
||
const hasData = !!data; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 데이터의 유무를 파악하기 위한 변수다 보니 '어떤 데이터의 유무를 파악하기 위함'을 내포하는 이름으로 명확히 작명하면 더 좋을 것 같아요! 예를 들면 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이부분에서는 다른 의견이 있습니다! 말씀해주신 피드백에 대해서 고민을 해봤을 때 현재 hasData의 용도가 단순히 데이터의 유무에 초점이 맞춰져 있고, 데이터의 종류에는 관계가 없다보니 어떤 데이터에 대한 정보가 네이밍에 포함되지 않아도 될 것 같다고 생각합니다! 만약 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아아 변수명이 명확하지 않으면 코드를 읽는 내내 파일명에 의존해서 상기하며 읽어야 되더라구요. 물론 지금은 한 파일 내에 코드가 많지 않기도 하고, 작은 프로젝트다 보니 크게 유의미하지 않을지 모르지만 유지보수나 프로젝트 디벨롭 측면을 염두에 두고 제안드렸어요! 상우님이 편하신 대로 해도 됩니다 ㅎㅎ |
||
|
||
const authorizedRes: SocialLoginAuthType = hasData | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
? data.value | ||
: { | ||
isRegistered: false, | ||
token: { | ||
accessToken: '', | ||
refreshToken: '', | ||
}, | ||
}; | ||
|
||
return { hasData, authorizedRes, isLoading }; | ||
}; | ||
|
||
export default useGetSocialLogin; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { USER } from '@/constants/API'; | ||
import { useQuery } from '@tanstack/react-query'; | ||
import defaultClient from '..'; | ||
|
||
const getValidateEmail = async (email: string): Promise<ValidateResponse> => { | ||
const { data } = await defaultClient.get<ResponseRootType<null>>( | ||
USER.validateEmail, | ||
{ | ||
params: { | ||
email, | ||
}, | ||
}, | ||
); | ||
|
||
const { code, message } = data; | ||
|
||
return { | ||
isValid: code === 204, | ||
message, | ||
}; | ||
}; | ||
|
||
const useGetValidateEmail = (email: string, mode: string) => { | ||
const { data, isLoading } = useQuery({ | ||
queryKey: [USER.validateEmail, email], | ||
queryFn: () => getValidateEmail(email), | ||
enabled: email.trim() !== '' && mode === 'sign-up', | ||
}); | ||
|
||
const hasData = !!data; | ||
|
||
const validInfo = hasData | ||
? { isValid: data.isValid, message: data.message } | ||
: { isValid: false, message: '' }; | ||
|
||
return { isLoading, ...validInfo }; | ||
}; | ||
|
||
export default useGetValidateEmail; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { USER } from '@/constants/API'; | ||
import { useQuery } from '@tanstack/react-query'; | ||
import defaultClient from '..'; | ||
|
||
const getValidateNickname = async ( | ||
nickname: string, | ||
): Promise<ValidateResponse> => { | ||
const { data } = await defaultClient.get<ResponseRootType<null>>( | ||
USER.validateNickname, | ||
{ | ||
params: { | ||
nickname, | ||
}, | ||
}, | ||
); | ||
|
||
return { | ||
isValid: data.code === 204, | ||
message: data.message, | ||
}; | ||
}; | ||
|
||
const useGetValidateNickName = (nickName: string) => { | ||
const { data, isLoading } = useQuery({ | ||
queryKey: [USER.validateNickname, nickName], | ||
queryFn: () => getValidateNickname(nickName), | ||
enabled: nickName.trim() !== '', | ||
}); | ||
|
||
const hasData = !!data; | ||
|
||
const validInfo = hasData | ||
? { isValid: data.isValid, message: data.message } | ||
: { isValid: false, message: '' }; | ||
|
||
return { isLoading, ...validInfo }; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
}; | ||
|
||
export default useGetValidateNickName; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import Image from 'next/image'; | ||
import { ReactNode } from 'react'; | ||
|
||
import logo from '@/../public/images/momentiaLogoSymbol.png'; | ||
|
||
interface LayoutProps { | ||
readonly children: ReactNode; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. children 하나만 Prop으로 사용되고 있는데, 따로 선언하신 이유가 있을까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 다른 컴포넌트와 형식을 통일시키는 의도로 따로 뺐었습니다! 다현님께서는 보통 몇 개 이상 props가 있으면 분리하시나요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그러셨군요 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아 그랬었군요;; 2개 이상인 경우 Props 타입을 별도로 정의 해서 사용하겠습니다! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 땀 ㅋㅋㅋㅋㅋ 그럼 2개 이상이 2개 포함인 걸까요? 저도 타입 정의할 때 맞추려구요 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2개부터 분리하는걸로 하시죠! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 헉 그럼 지금 prop 타입 2개 이상인데 분리 안 한 게 많아서 지금부터 분리 적용하되, 이전에 한 건 추후에 분리해도 될까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이거는 추후에 분리하시죠..! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ㅋㅋㅋㅋㅋㅋㅋ 넵넵 |
||
|
||
export default function layout({ children }: LayoutProps) { | ||
return ( | ||
<div className='m-auto h-full w-[420px] flex flex-col justify-center items-center gap-[25px]'> | ||
<Image src={logo} alt='모멘티아 로고' width={45} priority /> | ||
{children} | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
'use client'; | ||
|
||
import usePostLogin from '@/apis/user/login'; | ||
import SquareButtonL from '@/components/Button/SquareButtonL'; | ||
import EmailInput from '@/components/Input/EmailInput'; | ||
import PasswordInput from '@/components/Input/PasswordInput'; | ||
import SocialLoginSection from '@/components/SocialLoginSection'; | ||
|
||
import Link from 'next/link'; | ||
|
||
import { FormProvider, useForm } from 'react-hook-form'; | ||
|
||
export default function LogInPage() { | ||
const { mutate } = usePostLogin(); | ||
|
||
const methods = useForm<LoginFormType>({ | ||
defaultValues: { | ||
email: '', | ||
password: '', | ||
}, | ||
}); | ||
|
||
const onValid = (data: LoginFormType) => { | ||
console.log(data); | ||
mutate(data); | ||
}; | ||
|
||
const allFieldsFilled = Object.values(methods.watch()).every( | ||
(value) => value !== '', | ||
); | ||
|
||
return ( | ||
<div className='flex flex-col justify-center items-center gap-[60px]'> | ||
<h4>로그인</h4> | ||
|
||
<FormProvider {...methods}> | ||
<form | ||
name='login-form' | ||
onSubmit={methods.handleSubmit(onValid)} | ||
className='w-full flex flex-col gap-[60px]' | ||
> | ||
<div className='flex flex-col gap-[30px]'> | ||
<EmailInput mode={'sign-in'} /> | ||
<PasswordInput mode={'sign-in'} /> | ||
</div> | ||
<SquareButtonL | ||
type='submit' | ||
textSize={''} | ||
backgroundColor={allFieldsFilled ? 'bg-main' : 'bg-gray-800'} | ||
> | ||
<p>로그인</p> | ||
</SquareButtonL> | ||
</form> | ||
</FormProvider> | ||
<div className='w-full flex flex-col gap-[30px] justify-center text-center'> | ||
<SocialLoginSection /> | ||
<div className='flex gap-2.5 justify-center items-center mt-[13px]'> | ||
<p className='text-gray-600'>아직 회원이 아니신가요?</p> | ||
<Link href='/auth/signup'>회원가입하기</Link> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
'use client'; | ||
|
||
import usePostSignUp from '@/apis/user/signUp'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
import SquareButtonL from '@/components/Button/SquareButtonL'; | ||
import EmailInput from '@/components/Input/EmailInput'; | ||
import NicknameInput from '@/components/Input/NicknameInput'; | ||
import PasswordInput from '@/components/Input/PasswordInput'; | ||
|
||
import Link from 'next/link'; | ||
import { FormProvider, useForm } from 'react-hook-form'; | ||
|
||
export default function SignUpPage() { | ||
const { mutate } = usePostSignUp(); | ||
const methods = useForm<SignUpFormType>({ | ||
defaultValues: { | ||
email: '', | ||
password: '', | ||
nickname: '', | ||
}, | ||
mode: 'onChange', | ||
}); | ||
|
||
const onValidForm = (data: SignUpFormType) => { | ||
mutate(data); | ||
}; | ||
|
||
const { isValid } = methods.formState; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
물론 해당 파일 내에서 |
||
|
||
return ( | ||
<div className='flex flex-col justify-center items-center gap-[60px]'> | ||
<h4>회원가입</h4> | ||
<FormProvider {...methods}> | ||
<form | ||
onSubmit={methods.handleSubmit(onValidForm)} | ||
className='w-full flex flex-col gap-[60px]' | ||
> | ||
<div className='flex flex-col gap-[30px]'> | ||
<EmailInput mode={'sign-up'} /> | ||
<PasswordInput mode={'sign-up'} /> | ||
<NicknameInput /> | ||
</div> | ||
<SquareButtonL | ||
type='submit' | ||
textSize={''} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 타이포그래피가 들어가면 되는 prop인데 빈칸으로 남겨두신 이유가 있을까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 부분은 default 사이즈와 같은 크기인 것 같아서 비워두었습니다! default 옵션으로 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아 그렇군요. 네 알겠습니당 |
||
backgroundColor={isValid ? 'bg-main' : 'bg-gray-800'} | ||
disabled={!isValid} | ||
> | ||
<p>회원가입</p> | ||
</SquareButtonL> | ||
</form> | ||
</FormProvider> | ||
|
||
<div className='flex gap-2.5 justify-center items-center mt-[13px]'> | ||
<p className='text-gray-600'>이미 가입된 계정이 있으신가요?</p> | ||
<Link href='/auth/login'>로그인하러가기</Link> | ||
</div> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
'use client'; | ||
|
||
import useGetSocialLogin from '@/apis/user/socialLogIn'; | ||
|
||
import { useRouter, useSearchParams } from 'next/navigation'; | ||
import { useEffect } from 'react'; | ||
|
||
interface ProviderPageProps { | ||
provider: string; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이것도 동일하게 다른 컴포넌트와 형식을 통일시키는 의도로 따로 뺐었습니다! |
||
|
||
function ProviderPage({ provider }: ProviderPageProps) { | ||
const router = useRouter(); | ||
const searchParams = useSearchParams(); | ||
const { hasData, isLoading } = useGetSocialLogin({ | ||
provider, | ||
params: searchParams.toString(), | ||
}); | ||
|
||
useEffect(() => { | ||
if (hasData) router.replace('/'); | ||
}, [hasData, isLoading]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hasData만 의존성 배열에 있을때 리다이렉트가 잘 안되던 현상이 있어서 isLoading을 함께 넣었던 거였는데 빼고 테스트 해보니 문제가 없군요;; 제가 착각한 부분이었던 것 같습니다! |
||
|
||
return <></>; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 빈 JSX보다는, 로딩 상태를 사용자에게 명확히 표시하기 위해 로딩 JSX를 추가하는 건 어떨까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 빈 페이지 보다 로딩요소가 보이는 게 좋을 것 같다는 점은 찬성입니다. 이부분은 다른 리뷰까지 확인 후에 예림님께 요청드려서 Loading UI를 추가하겠습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 네 감사해요! |
||
} | ||
|
||
export default ProviderPage; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
'use client'; | ||
|
||
import Script from 'next/script'; | ||
|
||
declare global { | ||
interface Window { | ||
Kakao: any; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Kakao 인스턴스에 대한 타입 정의가 카카오 측에서 제공되고 있지 않은 걸로 확인됩니다..! npm에 임의로 타입 정의를 제공하는 라이브러리가 있긴 한데, 이걸 활용하거나, 제가 필요한 함수만 타입 정의를 하거나 해야할 것 같습니다..! 이 부분은 조금더 확인해보고 타입을 활용할 수 있도록 수정하겠습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 네 알겠습니다 :) |
||
} | ||
} | ||
|
||
export default function KakaoProvider() { | ||
const initKakao = () => { | ||
window.Kakao.init(process.env.NEXT_PUBLIC_KAKAO_API_KEY); | ||
}; | ||
|
||
return ( | ||
<Script | ||
src='https://t1.kakaocdn.net/kakao_js_sdk/2.7.4/kakao.min.js' | ||
integrity='sha384-DKYJZ8NLiK8MN4/C5P2dtSmLQ4KwPaoqAfyA/DfmEc1VDxu4yyC7wy6K1Hs90nka' | ||
crossOrigin='anonymous' | ||
onLoad={initKakao} | ||
/> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
로그인 관련 파일과 코드는 파일명과 함수명을 'login' 으로 작성하고, 회원가입은 'sign up' 으로 나누신 이유가 따로 있을까요?
로그인은 sign in, 회원가입은 sign up으로 이름 일관성을 맞추는 게 좋을 것 같아서요 :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
처음에 signin, signup으로 진행했었는데, 백엔드 API 쪽이 login / sign up을 활용하고 있는 걸 확인하고 통일하기 위해 그렇게 네이밍을 변경했었습니다. front 쪽은 sign in / sing up으로 통일하겠습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아하 그러셨군요
백엔드와도 네이밍 컨벤션을 동일하게 해야 하는 부분이 있네요. sign in 통일 확인했습니다!