Skip to content

Commit

Permalink
Merge pull request #11 from JECT-Study/feature/auth
Browse files Browse the repository at this point in the history
[TASK-30] 로그인/회원가입 페이지 구현
  • Loading branch information
SangWoo9734 authored Dec 25, 2024
2 parents 8da7cff + ee0e66e commit e5de3a1
Show file tree
Hide file tree
Showing 29 changed files with 1,738 additions and 1,026 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
"dependencies": {
"@nextui-org/react": "^2.6.8",
"@tanstack/react-query": "^5.62.3",
"axios": "^1.7.9",
"framer-motion": "^11.14.4",
"next": "15.0.4",
"next-themes": "^0.4.4",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-hook-form": "^7.54.1",
"zustand": "^5.0.2"
},
"devDependencies": {
Expand Down
2,041 changes: 1,074 additions & 967 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions public/images/google.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions public/images/kakako.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file removed src/apis/.gitkeep
Empty file.
26 changes: 26 additions & 0 deletions src/apis/user/login.ts
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';

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;
25 changes: 25 additions & 0 deletions src/apis/user/signUp.ts
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;
40 changes: 40 additions & 0 deletions src/apis/user/socialLogIn.ts
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;
};

const useGetSocialLogin = ({
provider,
params,
}: {
provider: string;
params: string;
}) => {
const queryUrl = `${USER.socialLogin}/${provider}?${params}`;
const { data, isLoading } = useQuery({
queryKey: [`${USER.socialLogin}/${provider}`],
queryFn: () => getSocialLogin(queryUrl),
});

const hasData = !!data;

const authorizedRes: SocialLoginAuthType = hasData
? data.value
: {
isRegistered: false,
token: {
accessToken: '',
refreshToken: '',
},
};

return { hasData, authorizedRes, isLoading };
};

export default useGetSocialLogin;
39 changes: 39 additions & 0 deletions src/apis/user/validateEmail.ts
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;
39 changes: 39 additions & 0 deletions src/apis/user/validateNickname.ts
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 };
};

export default useGetValidateNickName;
17 changes: 17 additions & 0 deletions src/app/auth/(authWithLayout)/layout.tsx
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;
}

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>
);
}
64 changes: 64 additions & 0 deletions src/app/auth/(authWithLayout)/login/page.tsx
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>
);
}
59 changes: 59 additions & 0 deletions src/app/auth/(authWithLayout)/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
'use client';

import usePostSignUp from '@/apis/user/signUp';
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;

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={''}
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>
);
}
27 changes: 27 additions & 0 deletions src/app/auth/redirect/[provider]/page.tsx
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;
}

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]);

return <></>;
}

export default ProviderPage;
2 changes: 2 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ReactNode } from 'react';

import '../styles/globals.css';

import KakaoProvider from './providers/KakaoProvider';
import MSWProvider from './providers/MSWProvider';
import ReactQueryProvider from './providers/ReactQueryProvider';

Expand All @@ -28,6 +29,7 @@ const RootLayout = ({
<AppShell>{children}</AppShell>
</ReactQueryProvider>
</MSWProvider>
<KakaoProvider />
</body>
</html>
);
Expand Down
24 changes: 24 additions & 0 deletions src/app/providers/KakaoProvider.tsx
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;
}
}

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}
/>
);
}
Loading

0 comments on commit e5de3a1

Please sign in to comment.