Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@tanstack/react-query": "^5.66.0",
"axios": "^1.7.9",
"gsap": "^3.12.7",
"immer": "^10.1.1",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

immer는 어디에 사용되는지 알 수 있을까요?

"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-intersection-observer": "^9.15.1",
Expand Down
13 changes: 11 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import AdminPage from './pages/Admin';
import FilteredLetterManage from './pages/Admin/FilteredLetter';
import FilteringManage from './pages/Admin/Filtering';
import ReportManage from './pages/Admin/Report';
import AuthCallbackPage from './pages/Auth';
import Home from './pages/Home';
import Landing from './pages/Landing';
import LetterBoardPage from './pages/LetterBoard';
Expand Down Expand Up @@ -54,7 +55,8 @@ const App = () => {
<Route path="board" element={<LetterBoardPage />} />
<Route path="notifications" element={<NotificationsPage />} />
</Route>
<Route path="*" element={<NotFoundPage />}></Route>
<Route path="*" element={<NotFoundPage />} />
<Route path="auth-callback" element={<AuthCallbackPage />} />
</Route>

<Route path="admin" element={<AdminPage />}>
Expand Down
88 changes: 88 additions & 0 deletions src/apis/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import useAuthStore from '@/stores/authStore';

import client from './client';

type LoginType = 'kakao' | 'naver' | 'google';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 아까 어디서 봤는데..! 다른 곳에서도 사용된다면 전역 타입으로 빼둬도 좋을 것 같습니다

export const socialLogin = (loginType: LoginType) => {
window.location.href = `http://13.209.132.150:8081/oauth2/authorization/${loginType}`;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요 주소는 env파일에서 관리할 필요 없을까용?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하! 그러게요! env로 넘기겠습니다!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 주소 환경변수로 숨기면 좋을 것 같아요.

};

export const logout = async () => {
const { accessToken } = useAuthStore.getState();

try {
const response = await client.post(`/api/logout`, {
Authorization: { token: `Bearer ${accessToken}` },
withCredentials: true,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 client.ts에서 미리 설정해두면 좋을 것 같아요!

});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로그아웃은 axios의 intercept로 토큰값을 자동으로 담을수 없나용?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오호 이것도 수정하겠습니다!

if (!response) throw new Error('logout fail');
return response;
} catch (error) {
console.error(error);
}
};

export const getUserToken = async (stateToken: string) => {
try {
const response = await client.get(`/api/auth/token?state=${stateToken}`);
if (!response) throw new Error('getUserToken: Error while fetching user token');
const userInfo = response.data;
if (userInfo) {
return userInfo;
}
} catch (error) {
console.error(error);
}
};

export const postZipCode = async () => {
try {
const response = await client.post(`/api/members/zipCode`);
if(!response) throw new Error('fail to post ZipCode')
return response;
} catch (error) {
console.error(error);
}
};

export const getNewToken = async () => {
try {
const response = await client.get('/api/reissue', { withCredentials: true });
if (!response) throw new Error('getNewToken: no response data');
return response;
} catch (error) {
console.error(error);
}
};

export const getMydata = async () => {
try {
const response = await client.get('/api/members/me');
if (!response) throw new Error('getNewTOken: no response data');
return response;
} catch (error) {
console.error(error);
}
};

export const deleteUserInfo = async () => {
try {
const response = await client.delete('/api/members/me', {
withCredentials: true,
});
if (!response) throw new Error('deleteUserInfo: no response');
return response;
} catch (error) {
console.error(error);
}
};

export const postLogout = async () => {
try {
const response = await client.post('/api/logout', { withCredentials: true });
if (!response) throw new Error('postLogout: failed to logout');
return response;
} catch (error) {
console.error(error);
}
};
Comment on lines +62 to +70
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오잉 로그아웃 요청 함수가 2개 있네용?

74 changes: 61 additions & 13 deletions src/apis/client.ts
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

하나의 페이지에서 api를 여러개 호출 + 마침 토큰도 유효하지 않을 경우, 토큰 재발급 요청도 여러번 발생할 수 있을 것 같아요. 토큰 갱신이 한 번만 진행되도록 로직을 보완하면 어떨까요?!

Original file line number Diff line number Diff line change
@@ -1,21 +1,69 @@
import axios from 'axios';

import useAuthStore from '@/stores/authStore';

import { getNewToken } from './auth';

const client = axios.create({
baseURL: import.meta.env.VITE_API_URL,
});

// client.interceptors.request.use(
// (config) => {
// const token = localStorage.getItem('authToken');
// if (token) {
// config.headers['Authorization'] = `Bearer ${token}`;
// }
// return config;
// },
// (error) => {
// //TODO: 에러처리
// return Promise.reject(error);
// },
// );
client.interceptors.request.use(
(config) => {
const { accessToken } = useAuthStore.getState();
console.log(config.url);
console.log(accessToken);
if (config.url !== '/auth/reissue' && accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
console.log('intercepter', config.headers);
}
return config
;
},
(error) => {
const { logout } = useAuthStore.getState();
logout();
window.location.replace('/login');
return Promise.reject(error);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return문의 요 코드는 콘솔창에 에러 출력하는 코드인가용?

},
);

client.interceptors.response.use(
(response) => response,
async (error) => {
const { setAccessToken, logout } = useAuthStore.getState();
const originalRequest = error.config;

if (!originalRequest) {
return Promise.reject(error);
}

if (
(error.response.status === 401 ||
error.response.status === 403 ||
error.response.data.message === 'Unauthorized') &&
!originalRequest._retry
) {
originalRequest._retry = true;

try {
const response = await getNewToken();
const newToken = response?.data.accessToken;

if (!newToken) throw new Error('Failed to Refresh Token');

setAccessToken(newToken);
originalRequest.headers.Authorization = `Bearer ${newToken}`;

return client(originalRequest);
} catch (e) {
logout();
window.location.replace('/login');
return Promise.reject(e);
}
}
return Promise.reject(error);
},
);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 파트가 엑세스토큰이 만료될 시에 리프레시 토큰으로 새 토큰을 가져오는 로직인가요?!


export default client;
14 changes: 7 additions & 7 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ queryClient.setDefaultOptions({
});

createRoot(document.getElementById('root')!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<App />
</BrowserRouter>
</QueryClientProvider>
</StrictMode>,
// <StrictMode>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<App />
</BrowserRouter>
</QueryClientProvider>,
// </StrictMode>,
Comment on lines +18 to +24
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오잉 strictMode는 임시로 끄신건가용?

);
89 changes: 89 additions & 0 deletions src/pages/Auth/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/* eslint-disable @typescript-eslint/no-unused-expressions */
import { useEffect } from 'react';
import { useNavigate } from 'react-router';

import { getUserToken, getMydata, deleteUserInfo, postZipCode } from '@/apis/auth';
import useAuthStore from '@/stores/authStore';

const AuthCallbackPage = () => {
const stateToken = new URLSearchParams(window.location.search).get('state');
const redirectURL = new URLSearchParams(window.location.search).get('redirect');
Comment on lines +9 to +10
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

소셜로그인 후 로그인 페이지 리다이렉트 될때 이 코드를 사용해서 토큰값을 저장하는걸까용?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아닌가 유저 정보를 가져오는건가?!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redirectUrl은 로그인 이후에 온보딩으로 갈지 홈으로 갈지 정해주는 쿼리파라미터값 같은데 어느 페이지에서 넘어올때 이런 값들이 담겨져서 오는건가용??

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오호라 redirect까지 넘겨주시는걸로 백엔드 분과 얘기가 된 건가요?? (영섭님이었나)

저는 개인적으로 이 방식의 경우, 프론트 측에서 언제든지 path 변경을 할 수도 있고, 그런 상황에서는 백엔드 측에서도 같이 수정을 하는 번거로움이 있을 것 같다는 단점이 있다고 생각이 되어요.

그래서 새로운 유저인지 아닌지만 구분해서 넘겨주시면 저희가 홈으로 보낼지 온보딩으로 보낼지 로직을 처리하는 게 더 나을 것 같다는 의견이긴 합니다


const { setZipCode, setAccessToken, login } = useAuthStore();

const navigate = useNavigate();

const setUserInfo = async (stateToken: string) => {
try {
const response = await getUserToken(stateToken);
if (!response) throw new Error('Error Fetching userInfo');

const userInfo = response.data;
if (userInfo) {
login();
userInfo.accessToken && setAccessToken(userInfo.accessToken);

if (redirectURL == 'home') {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

비교 연산자가 ===가 아닌 ==인 이유가 있을까용?

const zipCodeResponse = await getMydata();
if (!zipCodeResponse) throw new Error('Error Fetching userInfo');
const zipCode = zipCodeResponse.data.data.zipCode;
zipCode && setZipCode(zipCode);

console.log(
'isLoggedIn',
useAuthStore.getState().isLoggedIn,
'access',
useAuthStore.getState().accessToken,
'zipCode',
useAuthStore.getState().zipCode,
);
} else if (redirectURL === 'onboarding') {
const createZipCodeResponse = await postZipCode();
if (!createZipCodeResponse) throw new Error('Error creating ZipCode');
const zipCode = createZipCodeResponse.data.data.zipCode;
console.log(createZipCodeResponse);
const newAccessToken = createZipCodeResponse.headers['Authorization'];
setZipCode(zipCode);
setAccessToken(newAccessToken);
console.log(
'isLoggedIn',
useAuthStore.getState().isLoggedIn,
'access',
useAuthStore.getState().accessToken,
'zipCode',
useAuthStore.getState().zipCode,
);
}
} else {
navigate('/login');
}
} catch (error) {
console.error(error);
}
};

const redirection = () => {
if (redirectURL === 'onboarding') navigate('/onboarding');
else if (redirectURL === 'home') navigate('/');
else navigate('/notFound');
Comment on lines +68 to +70
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 이동된 페이지에서 뒤로가기 하면 여기로 다시 진입 가능한가욧?!

};

useEffect(() => {
if (stateToken) {
setUserInfo(stateToken as string);
redirection();
} else navigate('/notFound');
}, []);

const handleLeave = async () => {
try {
const response = await deleteUserInfo();
console.log(response);
} catch (error) {
console.error(error);
}
};
return <button onClick={() => handleLeave()}>탈퇴</button>;
};

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드 다시 보니까 의문이 생겼어요!! 이거 임시 코드인가요? 뭘하든 로직 처리 후 다른 페이지로 이동하느라 볼 수가 없는 ui인 것 같아서요!

export default AuthCallbackPage;
9 changes: 9 additions & 0 deletions src/pages/Login/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { socialLogin } from '@/apis/auth';
import { GoogleIcon, KakaoIcon, NaverIcon, StampIcon } from '@/assets/icons';

import Background from './components/Background';

const LoginPage = () => {
type LoginType = 'kakao' | 'naver' | 'google';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 type은 밖으로 빼두는 게 좋을 것 같아요! 타입 선언이 렌더링 영향권에 두지 않아도 될 것 같아서요


const handleLogin = (loginType: LoginType) => {
socialLogin(loginType);
};
return (
<>
<main className="mt-10 flex grow flex-col items-center justify-between">
Expand All @@ -22,20 +28,23 @@ const LoginPage = () => {
type="button"
className="rounded-full bg-[#03C75A] p-3.5"
aria-label="네이버 로그인"
onClick={() => handleLogin('naver')}
>
<NaverIcon />
</button>
<button
type="button"
className="rounded-full bg-[#FEE500] p-3.5"
aria-label="카카오 로그인"
onClick={() => handleLogin('kakao')}
>
<KakaoIcon />
</button>
<button
type="button"
className="border-gray-5 rounded-full border bg-white p-3.5"
aria-label="구글 로그인"
onClick={() => handleLogin('google')}
>
<GoogleIcon />
</button>
Expand Down
Loading