Skip to content

Commit bf32263

Browse files
nirii00wldnjs990AAminha
authored
feat: 로그인 기능구현 (#52)
* feat: 소셜로그인 및 회원가입 연결 - 소셜 로그인, 회원가입 시 리디렉션 페이지 연동 완료 - 유저정보 저장 진행 중 * feat:편지 작성 페이지 1차 기능구현 (#47) * feat:편지전송 답장 post api 연결완료 * feat:매칭된 편지 작성시 api 분기 구현 * feat:카테고리 페이지 페이지 뒤로가기 버튼 제거 + 이전단계 위치 조정 * feat:옵션 모달 외부 클릭시 슬라이드 닫히는 로직 구현 * feat: 편지작성 페이지 전역변수 편지전송 관련 request값들 객체형태로 통일 * fix:코드리뷰 사항 수정 * feat:랜덤 편지 + 편지 상세 1차 기능 구현 (#46) * feat:편지 상세 페이지 삭제 모달 추가 * chore:기존 파일들 컴포넌트파일로 이동 * feat:쿨타임 컴포넌트 생성 + 랜덤편지 타입 수정 * feat:차단된 편지 버튼 disabled처리 * feat:편지 상세보기 모달 생성(매칭편지는 상세페이지와 분리) * fix:쿨타임 페이지 letterWrapper 적용 * feat:편지 상세, 랜덤 편지 api throw error 예외처리 추가 * feat:편지 매칭 제한시간 구현 * feat:랜덤편지 페이지 매칭 제한시간, 쿨타임 로직 구현 * feat:랜덤편지 페이지 편지 매칭 제한시간, 쿨타임 시간 구현 * feat:매칭된 편지 전달시 location값 전달처리({randomMatched: true}) * feat:코드리뷰 사항 수정 * feat: 로그인 기능 임시 구현 - API 완성 되지 않음 : 논의된 부분을 바탕으로 임시로 제작함(작동 안함) - 토큰을 전달 받기 위해 권한 상태 페이지 추가(보여주기 위한 용도가 아니라 쿼리문으로 토큰 전달을 위한 목적) * feat: logout API 추가 * test: 로그인 테스트용 임시 커밋 * refactor: API 변경 사항 수정 반영, 토큰관련 interceptor 문 수정 - API 수정 반영하여 오류 사항들 수정 - 기존 interceptor 문에 오류가 있어서 수정 - AuthCallback Page에서 회원정보 설정 및 우편번호 발급 * test: 탈퇴 처리 테스트 * Perf: 내 편지함 탄스택 쿼리 적용 (#50) * fix: 모바일에서 gradient 보이지 않는 문제 해결 * refactor: 내 편지함 및 상세 querykey 적용 - useState을 삭제 하고 데이터 관리 및 성능 최적화를 위해 tanstack query 적용 * fix: 편지 상세 무한 스크롤 오류 수정 - 마지막 편지 보일 시 새로 페이징 하는 로직을 response data에 맞게 수정 - react-intersection-observer 라이브러리를 이용하여 마지막 요소가 view에 들어오는지 확인 * fix: 내 편지함 디자인 보이지 않는 문제 해결 - gradietn class에 !important를 적용 --------- Co-authored-by: nirii00 <[email protected]> * feat: 소셜로그인 및 회원가입 연결 - 소셜 로그인, 회원가입 시 리디렉션 페이지 연동 완료 - 유저정보 저장 진행 중 * Fix: intercepter 오류 해결 - intercepter retry 코드 빠진 부분 수정 - header에 잘못 설정 하는 부분 수정 * feat: 마이페이지 로그아웃 적용 - 로그아웃 테스트를 위해 마이페이지에 로그아웃 기능을 먼저 구현했습니다! * Update src/pages/Auth/index.tsx refac: 코드 개선 Co-authored-by: Minha Ahn <[email protected]> * refactor: 코드 리뷰 반영 - 로그인 타입 파일 만듦 - logout 중복 선언 정리 - myPage 탈퇴 추가, p 태그 -> 버튼 - useAuthState 상태 가져오는 코드 수정 * refactor: myPage 오타 수정 --------- Co-authored-by: nirii00 <[email protected]> Co-authored-by: wldnjs990 <[email protected]> Co-authored-by: Minha Ahn <[email protected]>
1 parent 7662866 commit bf32263

File tree

13 files changed

+307
-28
lines changed

13 files changed

+307
-28
lines changed

pnpm-lock.yaml

Lines changed: 9 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/App.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import AdminPage from './pages/Admin';
77
import FilteredLetterManage from './pages/Admin/FilteredLetter';
88
import FilteringManage from './pages/Admin/Filtering';
99
import ReportManage from './pages/Admin/Report';
10+
import AuthCallbackPage from './pages/Auth';
1011
import Home from './pages/Home';
1112
import Landing from './pages/Landing';
1213
import LetterBoardPage from './pages/LetterBoard';
@@ -54,7 +55,8 @@ const App = () => {
5455
<Route path="board" element={<LetterBoardPage />} />
5556
<Route path="notifications" element={<NotificationsPage />} />
5657
</Route>
57-
<Route path="*" element={<NotFoundPage />}></Route>
58+
<Route path="*" element={<NotFoundPage />} />
59+
<Route path="auth-callback" element={<AuthCallbackPage />} />
5860
</Route>
5961

6062
<Route path="admin" element={<AdminPage />}>

src/apis/auth.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import client from './client';
2+
3+
export const socialLogin = (loginType: LoginType) => {
4+
window.location.href = `${import.meta.env.VITE_API_URL}/oauth2/authorization/${loginType}`;
5+
};
6+
7+
export const getUserToken = async (stateToken: string) => {
8+
try {
9+
const response = await client.get(`/api/auth/token?state=${stateToken}`);
10+
if (!response) throw new Error('getUserToken: Error while fetching user token');
11+
const userInfo = response.data;
12+
if (userInfo) {
13+
return userInfo;
14+
}
15+
} catch (error) {
16+
console.error(error);
17+
}
18+
};
19+
20+
export const postZipCode = async () => {
21+
try {
22+
const response = await client.post(`/api/members/zipCode`);
23+
if (!response) throw new Error('fail to post ZipCode');
24+
return response;
25+
} catch (error) {
26+
console.error(error);
27+
}
28+
};
29+
30+
export const getNewToken = async () => {
31+
try {
32+
const response = await client.get('/api/reissue', { withCredentials: true });
33+
if (!response) throw new Error('getNewToken: no response data');
34+
return response;
35+
} catch (error) {
36+
console.error(error);
37+
}
38+
};
39+
40+
export const getMydata = async () => {
41+
try {
42+
const response = await client.get('/api/members/me');
43+
if (!response) throw new Error('getNewTOken: no response data');
44+
return response;
45+
} catch (error) {
46+
console.error(error);
47+
}
48+
};
49+
50+
export const deleteUserInfo = async () => {
51+
try {
52+
const response = await client.delete('/api/members/me', {
53+
withCredentials: true,
54+
});
55+
if (!response) throw new Error('deleteUserInfo: no response');
56+
return response;
57+
} catch (error) {
58+
console.error(error);
59+
}
60+
};
61+
62+
export const postLogout = async () => {
63+
try {
64+
const response = await client.post('/api/logout', { withCredentials: true });
65+
if (!response) throw new Error('postLogout: failed to logout');
66+
return response;
67+
} catch (error) {
68+
console.error(error);
69+
}
70+
};

src/apis/client.ts

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,70 @@
11
import axios from 'axios';
22

3+
import useAuthStore from '@/stores/authStore';
4+
5+
import { getNewToken } from './auth';
6+
37
const client = axios.create({
48
baseURL: import.meta.env.VITE_API_URL,
59
});
610

7-
// client.interceptors.request.use(
8-
// (config) => {
9-
// const token = localStorage.getItem('authToken');
10-
// if (token) {
11-
// config.headers['Authorization'] = `Bearer ${token}`;
12-
// }
13-
// return config;
14-
// },
15-
// (error) => {
16-
// //TODO: 에러처리
17-
// return Promise.reject(error);
18-
// },
19-
// );
11+
client.interceptors.request.use(
12+
(config) => {
13+
const accessToken = useAuthStore((state) => state.accessToken);
14+
console.log(config.url);
15+
console.log(accessToken);
16+
if (config.url !== '/auth/reissue' && accessToken) {
17+
config.headers.Authorization = `Bearer ${accessToken}`;
18+
console.log('intercepter', config.headers);
19+
}
20+
return config;
21+
},
22+
(error) => {
23+
const logout = useAuthStore((state) => state.logout);
24+
logout();
25+
window.location.replace('/login');
26+
return Promise.reject(error);
27+
},
28+
);
29+
30+
client.interceptors.response.use(
31+
(response) => response,
32+
async (error) => {
33+
const setAccessToken = useAuthStore((state) => state.setAccessToken);
34+
const logout = useAuthStore((state) => state.logout);
35+
36+
const originalRequest = error.config;
37+
38+
if (!originalRequest) {
39+
return Promise.reject(error);
40+
}
41+
42+
if (
43+
(error.response.status === 401 ||
44+
error.response.status === 403 ||
45+
error.response.data.message === 'Unauthorized') &&
46+
!originalRequest._retry
47+
) {
48+
originalRequest._retry = true;
49+
50+
try {
51+
const response = await getNewToken();
52+
const newToken = response?.data.accessToken;
53+
54+
if (!newToken) throw new Error('Failed to Refresh Token');
55+
56+
setAccessToken(newToken);
57+
originalRequest.headers.Authorization = `Bearer ${newToken}`;
58+
59+
return client(originalRequest);
60+
} catch (e) {
61+
logout();
62+
window.location.replace('/login');
63+
return Promise.reject(e);
64+
}
65+
}
66+
return Promise.reject(error);
67+
},
68+
);
2069

2170
export default client;

src/main.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ queryClient.setDefaultOptions({
1515
});
1616

1717
createRoot(document.getElementById('root')!).render(
18-
<StrictMode>
19-
<QueryClientProvider client={queryClient}>
20-
<BrowserRouter>
21-
<App />
22-
</BrowserRouter>
23-
</QueryClientProvider>
24-
</StrictMode>,
18+
// <StrictMode>
19+
<QueryClientProvider client={queryClient}>
20+
<BrowserRouter>
21+
<App />
22+
</BrowserRouter>
23+
</QueryClientProvider>,
24+
// </StrictMode>,
2525
);

src/pages/Auth/index.tsx

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/* eslint-disable @typescript-eslint/no-unused-expressions */
2+
import { useEffect } from 'react';
3+
import { useNavigate } from 'react-router';
4+
5+
import { getUserToken, getMydata, postZipCode } from '@/apis/auth';
6+
import useAuthStore from '@/stores/authStore';
7+
8+
const AuthCallbackPage = () => {
9+
const stateToken = new URLSearchParams(window.location.search).get('state');
10+
const redirectURL = new URLSearchParams(window.location.search).get('redirect');
11+
12+
const login = useAuthStore((state) => state.login);
13+
const setAccessToken = useAuthStore((state) => state.setAccessToken);
14+
const setZipCode = useAuthStore((state) => state.setZipCode);
15+
16+
const navigate = useNavigate();
17+
18+
const setUserInfo = async (stateToken: string) => {
19+
try {
20+
const response = await getUserToken(stateToken);
21+
if (!response) throw new Error('Error Fetching userInfo');
22+
23+
const userInfo = response.data;
24+
if (userInfo) {
25+
login();
26+
userInfo.accessToken && setAccessToken(userInfo.accessToken);
27+
28+
if (redirectURL == 'home') {
29+
const zipCodeResponse = await getMydata();
30+
if (!zipCodeResponse) throw new Error('Error Fetching userInfo');
31+
const zipCode = zipCodeResponse.data.data.zipCode;
32+
zipCode && setZipCode(zipCode);
33+
34+
console.log(
35+
'isLoggedIn',
36+
useAuthStore.getState().isLoggedIn,
37+
'access',
38+
useAuthStore.getState().accessToken,
39+
'zipCode',
40+
useAuthStore.getState().zipCode,
41+
);
42+
} else if (redirectURL === 'onboarding') {
43+
const createZipCodeResponse = await postZipCode();
44+
if (!createZipCodeResponse) throw new Error('Error creating ZipCode');
45+
const zipCode = createZipCodeResponse.data.data.zipCode;
46+
console.log(createZipCodeResponse);
47+
const newAccessToken = createZipCodeResponse.headers['Authorization'];
48+
setZipCode(zipCode);
49+
setAccessToken(newAccessToken);
50+
console.log(
51+
'isLoggedIn',
52+
useAuthStore.getState().isLoggedIn,
53+
'access',
54+
useAuthStore.getState().accessToken,
55+
'zipCode',
56+
useAuthStore.getState().zipCode,
57+
);
58+
}
59+
} else {
60+
navigate('/login');
61+
}
62+
} catch (error) {
63+
console.error(error);
64+
}
65+
};
66+
67+
const redirection = () => {
68+
if (redirectURL === 'onboarding') navigate('/onboarding');
69+
else if (redirectURL === 'home') navigate('/');
70+
else navigate('/notFound');
71+
};
72+
73+
useEffect(() => {
74+
if (stateToken) {
75+
setUserInfo(stateToken as string);
76+
redirection();
77+
} else navigate('/notFound');
78+
}, []);
79+
return <></>;
80+
};
81+
82+
export default AuthCallbackPage;

src/pages/Login/index.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import { socialLogin } from '@/apis/auth';
12
import { GoogleIcon, KakaoIcon, NaverIcon, StampIcon } from '@/assets/icons';
23

34
import Background from './components/Background';
45

56
const LoginPage = () => {
7+
const handleLogin = (loginType: LoginType) => {
8+
socialLogin(loginType);
9+
};
610
return (
711
<>
812
<main className="mt-10 flex grow flex-col items-center justify-between">
@@ -22,20 +26,23 @@ const LoginPage = () => {
2226
type="button"
2327
className="rounded-full bg-[#03C75A] p-3.5"
2428
aria-label="네이버 로그인"
29+
onClick={() => handleLogin('naver')}
2530
>
2631
<NaverIcon />
2732
</button>
2833
<button
2934
type="button"
3035
className="rounded-full bg-[#FEE500] p-3.5"
3136
aria-label="카카오 로그인"
37+
onClick={() => handleLogin('kakao')}
3238
>
3339
<KakaoIcon />
3440
</button>
3541
<button
3642
type="button"
3743
className="border-gray-5 rounded-full border bg-white p-3.5"
3844
aria-label="구글 로그인"
45+
onClick={() => handleLogin('google')}
3946
>
4047
<GoogleIcon />
4148
</button>

0 commit comments

Comments
 (0)