Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…roject/WEB1_1_ZeroOne_FE into feat/#82-gathering-api
  • Loading branch information
joarthvr committed Dec 4, 2024
2 parents 9793a78 + e58f258 commit 94ea861
Show file tree
Hide file tree
Showing 17 changed files with 463 additions and 105 deletions.
13 changes: 5 additions & 8 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import { RouterProvider } from 'react-router-dom';

import AppRouter from './appRouter';
import AuthProvider from './AuthProvider';
import ModalProvider from './ModalProvider';
import QueryProvider from './QueryProvider';

function App() {
return (
<AuthProvider>
<QueryProvider>
<ModalProvider>
<RouterProvider router={AppRouter()} />
</ModalProvider>
</QueryProvider>
</AuthProvider>
<QueryProvider>
<ModalProvider>
<RouterProvider router={AppRouter()} />
</ModalProvider>
</QueryProvider>
);
}

Expand Down
121 changes: 49 additions & 72 deletions src/app/AuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,83 +1,60 @@
import axios from 'axios';
import type { ReactNode } from 'react';
import { createContext, useContext, useRef, useState } from 'react';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';

import api from '@/shared/api/baseApi';
import { getLocalAccessToken, removeLocalAccessToken } from '@/features/auth/auth.api';
import { useUserStore } from '@/features/user/model/user.store';
import { getMyProfile } from '@/features/user/user.api';
import { setInterceptorEvents } from '@/shared/api/baseApi';
import { customConfirm } from '@/shared/ui';

interface AuthContextType {
accessToken: string | null;
setAccessToken: (token: string | null) => void;
reissueToken: () => Promise<void>;
logout: () => Promise<void>;
removeToken: () => void;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

const AuthProvider = ({ children }: { children: ReactNode }) => {
const [accessToken, setAccessToken] = useState<string | null>(
localStorage.getItem('accessToken'),
);
const isRefreshing = useRef(false);

const reissueToken = async () => {
if (isRefreshing.current) return;

isRefreshing.current = true;

try {
const response = await axios.post('/token/reissue', {}, { withCredentials: true });

const newAccessToken = (response.headers['authorization'] as string)?.split(' ')[1];

if (newAccessToken) {
setAccessToken(newAccessToken);
localStorage.setItem('accessToken', newAccessToken);
} else {
throw new Error('accessToken이 헤더에 포함되어 있지 않습니다.');
const navigate = useNavigate();
const accessToken = getLocalAccessToken();
const { setUserData, clearUserData } = useUserStore(state => state.actions);

useEffect(() => {
const getUserData = async () => {
try {
if (accessToken) {
const userData = await getMyProfile().then(res => res.data);
if (!userData) throw new Error('유저 정보를 찾을 수가 없습니다.');
setUserData(userData);
return userData;
}
clearUserData();
} catch (error) {
console.error('유저 데이터를 불러오는 중 오류가 발생했습니다.', error);
}
} catch (error) {
console.error('토큰 재발급 실패:', error);
removeToken();
} finally {
// eslint-disable-next-line require-atomic-updates
isRefreshing.current = false;
}
};

const removeToken = () => {
setAccessToken(null);
localStorage.removeItem('accessToken');
customConfirm({ title: '로그인', text: '로그인 페이지로 이동합니다.', icon: 'info' });
};

const logout = async () => {
try {
await api.post('/user/logout');

setAccessToken(null);
localStorage.removeItem('accessToken');
} catch (error) {
console.error('로그아웃 실패:', error);
}
};
};

void getUserData();
}, []);

//인터셉터 함수 등록 effects
useEffect(() => {
const logout = async (text: string) => {
try {
await customConfirm({
title: '로그아웃',
text,
icon: 'error',
showCancelButton: false,
});
removeLocalAccessToken();
clearUserData();
navigate('/');
} catch (error) {
console.error('로그아웃 실패:', error);
}
};

return (
<AuthContext.Provider
value={{ accessToken, setAccessToken, reissueToken, logout, removeToken }}
>
{children}
</AuthContext.Provider>
);
};
setInterceptorEvents('logout', (text: string) => {
void logout(text);
});
}, []);

export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('AuthProvider Error');
}
return context;
return <>{children}</>;
};

export default AuthProvider;
2 changes: 1 addition & 1 deletion src/app/QueryProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const QueryProvider = ({ children }: { children: React.ReactNode }) => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
staleTime: 0,
refetchOnWindowFocus: false,
retry: 1,
},
Expand Down
5 changes: 5 additions & 0 deletions src/app/appRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createBrowserRouter } from 'react-router-dom';

import { LoginLoading } from '@/features/auth/ui/LoginLoading';
import {
ArchiveListPage,
DetailArchivePage,
Expand Down Expand Up @@ -73,6 +74,10 @@ const AppRouter = () => {
path: '/like',
element: <LikeListPage />,
},
{
path: '/login',
element: <LoginLoading />,
},
],
},
]);
Expand Down
56 changes: 56 additions & 0 deletions src/features/auth/auth.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { AxiosError } from 'axios';

import type { TokenApiResponse } from './auth.dto';
import type { PostUserResponseDTO } from '../user/user.dto';

import api from '@/shared/api/baseApi';

export const loginWithToken = async (token: string) => {
const response = await api.get<TokenApiResponse>(`/token/issue?token=${token}`);
const newAccessToken = response.headers['authorization'];
if (newAccessToken) {
setLocalAccessToken(newAccessToken);
}

return newAccessToken as string;
};

export const logout = async () => {
try {
await api.post<PostUserResponseDTO>('/user/logout');
removeLocalAccessToken();
} catch (error) {
console.error('로그아웃 실패:', error);
}
};

export const getLocalAccessToken = () => {
return localStorage.getItem('accessToken');
};

export const setLocalAccessToken = (token: string) => {
localStorage.setItem('accessToken', token);
};

export const removeLocalAccessToken = () => {
localStorage.removeItem('accessToken');
};

export const reissueToken = async () => {
try {
const response = await api.post('/token/reissue');

const newAccessToken = response.headers['authorization'];

if (newAccessToken) {
setLocalAccessToken(newAccessToken);
return newAccessToken as string;
} else {
throw new Error('accessToken이 헤더에 포함되어 있지 않습니다.');
}
} catch (error) {
if (error instanceof AxiosError) {
throw new Error(`토큰 재발행 중 오류가 발생했습니다. ${error.message}`);
}
}
};
8 changes: 8 additions & 0 deletions src/features/auth/auth.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { ApiResponse } from '@/shared/api';

export interface TokenResponse {
message: string;
expiresIn: number;
}

export type TokenApiResponse = ApiResponse<TokenResponse>;
8 changes: 7 additions & 1 deletion src/features/auth/ui/GoogleLogin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ import { Button } from '@/shared/ui';

export const GoogleLogin = () => {
return (
<Button className={styles.loginBtn} skin='invert'>
<Button
className={styles.loginBtn}
onClick={() => {
window.location.href = 'https://api.palettee.site/oauth2/authorization/google?';
}}
skin='invert'
>
<div>
<GoogleLogo width={64} />
Log In
Expand Down
57 changes: 57 additions & 0 deletions src/features/auth/ui/LoginLoading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';

import { loginWithToken, removeLocalAccessToken, setLocalAccessToken } from '../auth.api';

import type { UserDataState } from '@/features/user/model/user.store';
import { useUserStore } from '@/features/user/model/user.store';
import { getMyProfile } from '@/features/user/user.api';
import { customConfirm } from '@/shared/ui';

export const LoginLoading = () => {
const [params] = useSearchParams();
const navigate = useNavigate();
const { setUserData } = useUserStore(state => state.actions);
const token = params.get('token') ?? '';

useEffect(() => {
const login = async () => {
try {
if (!token) {
await customConfirm({
title: '잘못된 접근',
text: '메인 페이지로 이동합니다.',
icon: 'error',
showCancelButton: false,
});
return;
}

const newAccessToken = await loginWithToken(token);

if (newAccessToken) {
setLocalAccessToken(newAccessToken);
}

const userData = (await getMyProfile().then(res => res.data)) as UserDataState;
setUserData(userData);

navigate('/');
} catch (error) {
console.error(error);
await customConfirm({
title: '로그인 실패',
text: '메인 페이지로 이동합니다.',
icon: 'error',
showCancelButton: false,
});
removeLocalAccessToken();
navigate('/');
}
};

void login();
}, []);

return <div>Redirect...</div>;
};
6 changes: 6 additions & 0 deletions src/features/portfolio/portfolio.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { PostPortfolioApiResponse, PostPortfolioDTO } from './portfolio.dto';

import api from '@/shared/api/baseApi';

export const postCreatePortfolio = (data: PostPortfolioDTO) =>
api.post<PostPortfolioApiResponse>('/portfolio', data).then(res => res.data);
9 changes: 9 additions & 0 deletions src/features/portfolio/portfolio.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { PostUserResponseDTO } from '../user/user.dto';

import type { ApiResponse } from '@/shared/api';

export interface PostPortfolioDTO {
portfolioURL: string;
}

export type PostPortfolioApiResponse = ApiResponse<PostUserResponseDTO>;
49 changes: 49 additions & 0 deletions src/features/user/model/user.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';

import type { UserRole } from '../user.dto';

export interface UserDataState {
userId: number;
name: string;
imageUrl: string;
role: UserRole;
}

interface UserState {
userData: UserDataState | null;
}

interface UserActions {
actions: {
setUserData: (data: UserDataState | null) => void;
updateUserData: (updatedData: Partial<UserDataState>) => void;
clearUserData: () => void;
};
}

const initialState = {
userId: -1,
userData: null,
};

export const useUserStore = create(
immer<UserState & UserActions>(set => ({
userData: null,
actions: {
setUserData: data => {
set({ userData: data });
},
updateUserData: updatedData => {
set(state => {
if (state.userData) {
Object.assign(state.userData, updatedData);
}
});
},
clearUserData: () => {
set(initialState);
},
},
})),
);
Loading

0 comments on commit 94ea861

Please sign in to comment.