Skip to content

Commit 04b40e1

Browse files
authored
Merge pull request #166 from kakao-tech-campus-3rd-step3/feat/oauth-login
[FEAT] 관리자 Oauth 로그인 관련 Redirect 추가
2 parents 1e8c89f + 1459ac3 commit 04b40e1

File tree

11 files changed

+320
-6
lines changed

11 files changed

+320
-6
lines changed

src/constants/routerPath.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,7 @@ export const ROUTE_PATH = {
1313
MAIN: '/',
1414
CLUBDETAIL: 'clubs/:clubId',
1515
LOGIN: 'login',
16+
CALLBACK: 'login/redirect',
17+
SIGNUP: 'signup',
1618
},
1719
};

src/pages/Routes.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import { DashboardPage } from '@/pages/admin/Dashboard/Page';
77
import { ClubDetailPage } from '@/pages/user/ClubDetail/Page';
88
import { MainPage } from '@/pages/user/Main/Page.tsx';
99
import { ApplicationDetailPage } from './admin/ApplicationDetail/Page';
10+
import { KakaoCallback } from './admin/Login/KakaoCallback';
1011
import { LoginPage } from './admin/Login/Page';
12+
import { AdminSignupPage } from './admin/Signup/Page';
1113
import { ClubApplicationPage } from './user/Apply/Page';
1214

1315
const { USER, ADMIN, COMMON } = ROUTE_PATH;
@@ -29,6 +31,14 @@ export const router = createBrowserRouter([
2931
path: USER.APPLICATION,
3032
element: <ClubApplicationPage />,
3133
},
34+
{
35+
path: COMMON.CALLBACK,
36+
element: <KakaoCallback />,
37+
},
38+
{
39+
path: COMMON.SIGNUP,
40+
element: <AdminSignupPage />,
41+
},
3242
{
3343
path: '/admin',
3444
children: [
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import axios from 'axios';
2+
import { useEffect } from 'react';
3+
import { useNavigate } from 'react-router-dom';
4+
import { LoadingSpinner } from '@/shared/components/LoadingSpinner';
5+
6+
export const KakaoCallback = () => {
7+
const navigate = useNavigate();
8+
9+
useEffect(() => {
10+
const code = new URL(window.location.href).searchParams.get('code');
11+
if (!code) return;
12+
13+
console.log(code);
14+
const fetchToken = async () => {
15+
try {
16+
const res = axios.post(`${import.meta.env.VITE_API_BASE_URL}/auth/kakao/login`, {
17+
authorizationCode: code,
18+
});
19+
console.log('응답res ', res);
20+
21+
// CASE 1) 기존 회원
22+
23+
// 1-1. accessToken, refreshToken 발급
24+
// localStorage.setItem('accessToken', res.data.accessToken);
25+
// localStorage.setItem('refreshToken ', res.data.refreshToken)- (수정전)
26+
// refreshToken은 httpOnly 관리(수정후)
27+
// ------------------------------------------------------------
28+
// 2-2 main 페이지 이동
29+
// navigate('/'); // 로그인 후 홈으로 이동
30+
31+
// CASE 2) 기존 회원
32+
// 2-1. 임시 토큰
33+
// 2-2. navigate('/signup')
34+
} catch (error) {
35+
console.log('error:', error);
36+
}
37+
};
38+
39+
fetchToken();
40+
}, [navigate]);
41+
42+
return <LoadingSpinner />;
43+
};

src/pages/admin/Login/Page.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import styled from '@emotion/styled';
22
import { LoginButton } from './component/LoginButton';
3-
import { LoginInput } from './component/LoginInput';
43
import { Logo } from './component/Logo';
54

65
export const LoginPage = () => {
76
return (
87
<Container>
98
<Logo />
10-
<LoginInput />
119
<LoginButton />
1210
</Container>
1311
);

src/pages/admin/Login/component/LoginButton/index.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
import styled from '@emotion/styled';
22
import { Button } from '@/shared/components/Button';
33

4+
const REST_API_KEY = import.meta.env.VITE_KAKAO_REST_API_KEY;
5+
const REDIRECT_URI = import.meta.env.VITE_KAKAO_REDIRECT_URI;
6+
47
export const LoginButton = () => {
8+
const handleKakaoLogin = () => {
9+
const kakaoAuthUrl = `https://kauth.kakao.com/oauth/authorize?client_id=${REST_API_KEY}&redirect_uri=${REDIRECT_URI}&response_type=code`;
10+
window.location.href = kakaoAuthUrl;
11+
};
12+
513
return (
614
<Container>
7-
<Button width='100%' type='submit'>
8-
{'로그인'}
15+
<Button to='/signup' width='100%' type='button'>
16+
{'회원가입'}
917
</Button>
1018
<KakaoButtonWrapper>
11-
<Button width='100%' type='submit'>
19+
<Button width='100%' type='button' onClick={handleKakaoLogin}>
1220
<ButtonContent>
1321
<Icon src='/assets/kakao-icon.png' width={24} height={24} />
14-
<span>카카오 로그인</span>
22+
{'카카오 로그인'}
1523
</ButtonContent>
1624
</Button>
1725
</KakaoButtonWrapper>

src/pages/admin/Signup/Page.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import styled from '@emotion/styled';
2+
import { HeaderTitle } from './components/HeaderTitle';
3+
import { SignupForm } from './components/SignupForm';
4+
5+
export const AdminSignupPage = () => {
6+
return (
7+
<Layout>
8+
<HeaderTitle />
9+
<SignupForm />
10+
</Layout>
11+
);
12+
};
13+
14+
export const Layout = styled.main(({ theme }) => ({
15+
minHeight: '100vh',
16+
display: 'flex',
17+
flexDirection: 'column',
18+
alignItems: 'center',
19+
gap: '1.5rem',
20+
maxWidth: '1200px',
21+
width: '100%',
22+
margin: '0 auto 4rem auto',
23+
padding: '0 1.5rem',
24+
boxSizing: 'border-box',
25+
26+
[`@media (max-width: ${theme.breakpoints.mobile})`]: {
27+
padding: '1rem',
28+
},
29+
}));
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { SignupFormInputs } from '../type/signup';
2+
3+
export const postSignupForm = async (formData: SignupFormInputs): Promise<SignupFormInputs> => {
4+
const url = `${import.meta.env.VITE_API_BASE_URL}/auth/register`;
5+
const response = await fetch(url, {
6+
method: 'POST',
7+
headers: {
8+
'Content-Type': 'application/json',
9+
},
10+
body: JSON.stringify(formData),
11+
});
12+
13+
if (!response.ok) throw new Error('회원 가입 양식을 제출하지 못했습니다.');
14+
return await response.json();
15+
};
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import styled from '@emotion/styled';
2+
3+
export const HeaderTitle = () => {
4+
return (
5+
<TextWrapper>
6+
<Title>관리자 등록</Title>
7+
</TextWrapper>
8+
);
9+
};
10+
const TextWrapper = styled.div({
11+
display: 'flex',
12+
flexDirection: 'column',
13+
gap: '1rem',
14+
padding: '2rem 0 1rem 0',
15+
});
16+
17+
const Title = styled.h1(({ theme }) => ({
18+
fontSize: '2rem',
19+
fontWeight: theme.font.weight.medium,
20+
lineHeight: 1.3,
21+
color: theme.colors.gray900,
22+
}));
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import styled from '@emotion/styled';
2+
3+
export const UserInfoWrapper = styled.div(({ theme }) => ({
4+
boxSizing: 'border-box',
5+
width: '48rem',
6+
display: 'flex',
7+
flexDirection: 'column',
8+
gap: '30px',
9+
borderBottom: `1px solid ${theme.colors.gray200}`,
10+
paddingBottom: '3rem',
11+
12+
'@media (max-width: 48rem)': {
13+
width: '100%',
14+
},
15+
}));
16+
17+
export const FormField = styled.div({
18+
display: 'flex',
19+
flexDirection: 'column',
20+
gap: '10px',
21+
});
22+
23+
export const FormRow = styled.div({
24+
display: 'flex',
25+
flexDirection: 'row',
26+
gap: '2.5rem',
27+
width: '100%',
28+
'& > *': {
29+
flex: '1 1 0',
30+
minWidth: 0,
31+
},
32+
});
33+
34+
export const Label = styled.label(({ theme }) => ({
35+
display: 'flex',
36+
alignItems: 'center',
37+
gap: '4px',
38+
fontWeight: theme.font.weight.medium,
39+
}));
40+
41+
export const FormContainer = styled.main({
42+
display: 'flex',
43+
flexDirection: 'column',
44+
alignItems: 'center',
45+
gap: '3rem',
46+
});
47+
48+
export const ErrorMessage = styled.span(({ theme }) => ({
49+
color: theme.colors.warning,
50+
fontSize: theme.font.size.xs,
51+
padding: 0,
52+
}));
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { useForm, FormProvider } from 'react-hook-form';
2+
import { useNavigate } from 'react-router-dom';
3+
import { toast } from 'sonner';
4+
import { postSignupForm } from '@/pages/admin/Signup/api/signup';
5+
import * as S from '@/pages/admin/Signup/components/SignupForm/index.styled';
6+
import { Button } from '@/shared/components/Button';
7+
import { OutlineInputField } from '@/shared/components/Form/InputField/OutlineInputField';
8+
import { theme } from '@/styles/theme';
9+
import type { SignupFormInputs } from '@/pages/admin/Signup/type/signup';
10+
11+
export const SignupForm = () => {
12+
const navigate = useNavigate();
13+
const methods = useForm<SignupFormInputs>({
14+
mode: 'onTouched',
15+
defaultValues: {
16+
name: '',
17+
email: '',
18+
studentId: '',
19+
department: '',
20+
phoneNumber: '',
21+
},
22+
});
23+
24+
const { errors, isSubmitting } = methods.formState;
25+
26+
const onSubmit = async (signupFormValue: SignupFormInputs) => {
27+
try {
28+
await postSignupForm(signupFormValue);
29+
toast.success('회원가입 완료!', {
30+
style: { backgroundColor: theme.colors.primary, color: 'white' },
31+
duration: 1000,
32+
});
33+
setTimeout(() => {
34+
navigate(`/login`);
35+
}, 1000);
36+
} catch (e) {
37+
console.error(e);
38+
toast.error('회원가입 실패!', {
39+
duration: 1000,
40+
style: {
41+
backgroundColor: 'white',
42+
color: theme.colors.error,
43+
},
44+
});
45+
}
46+
};
47+
48+
return (
49+
<FormProvider {...methods}>
50+
<form onSubmit={methods.handleSubmit(onSubmit)}>
51+
<S.FormContainer>
52+
<S.UserInfoWrapper>
53+
<S.FormField>
54+
<S.Label>이름</S.Label>
55+
<OutlineInputField
56+
placeholder='이름을 입력하세요.'
57+
{...methods.register('name', { required: '이름을 입력하세요.' })}
58+
invalid={!!errors.name}
59+
message={errors.name?.message}
60+
/>
61+
</S.FormField>
62+
63+
<S.FormField>
64+
<S.Label>이메일</S.Label>
65+
<OutlineInputField
66+
placeholder='이메일을 입력하세요.'
67+
{...methods.register('email', {
68+
required: '이메일을 입력하세요.',
69+
pattern: {
70+
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
71+
message: '올바른 이메일 형식이 아닙니다.',
72+
},
73+
})}
74+
invalid={!!errors.email}
75+
message={errors.email?.message}
76+
/>
77+
</S.FormField>
78+
79+
<S.FormRow>
80+
<S.FormField>
81+
<S.Label>학번</S.Label>
82+
<OutlineInputField
83+
placeholder='학번을 입력하세요.'
84+
{...methods.register('studentId', {
85+
required: '학번을 입력하세요.',
86+
pattern: {
87+
value: /^[0-9]{6}$/,
88+
message: '학번은 숫자 6자리여야 합니다.',
89+
},
90+
})}
91+
invalid={!!errors.studentId}
92+
message={errors.studentId?.message}
93+
/>
94+
</S.FormField>
95+
96+
<S.FormField>
97+
<S.Label>학과</S.Label>
98+
<OutlineInputField
99+
placeholder='학과를 입력하세요.'
100+
{...methods.register('department', { required: '학과를 입력하세요.' })}
101+
invalid={!!errors.department}
102+
message={errors.department?.message}
103+
/>
104+
</S.FormField>
105+
</S.FormRow>
106+
107+
<S.FormField>
108+
<S.Label>전화번호</S.Label>
109+
<OutlineInputField
110+
placeholder='010-0000-0000'
111+
{...methods.register('phoneNumber', {
112+
required: '전화번호를 입력하세요.',
113+
pattern: {
114+
value: /^\d{2,3}-\d{3,4}-\d{4}$/,
115+
message: '올바른 전화번호 형식이 아닙니다.',
116+
},
117+
})}
118+
invalid={!!errors.phoneNumber}
119+
message={errors.phoneNumber?.message}
120+
/>
121+
</S.FormField>
122+
</S.UserInfoWrapper>
123+
<Button type='submit'>{isSubmitting ? '제출중...' : '회원가입'}</Button>
124+
</S.FormContainer>
125+
</form>
126+
</FormProvider>
127+
);
128+
};

0 commit comments

Comments
 (0)