diff --git a/src/app/router.tsx b/src/app/router.tsx index c44821ea..f82c0d75 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -12,14 +12,12 @@ import LogIn from '@/pages/logIn'; import Game from '@/pages/play/game'; import LobbyPage, { getLobbyInfo } from '@/pages/play/lobby'; import Mode from '@/pages/play/mode'; -import Ranking from '@/pages/ranking'; +import Ranking, { getRank } from '@/pages/ranking'; import Result, { getGameResult } from '@/pages/result'; import Settings from '@/pages/settings'; import Tutorial from '@/pages/tutorial'; -import { checkToken } from '@/services/auth'; -import { getRank } from '@/services/rank'; - +import { checkToken } from '@/features/auth'; import { ROUTE } from '@/shared/constants'; const router = createBrowserRouter([ @@ -111,6 +109,8 @@ const router = createBrowserRouter([ { path: ROUTE.game, element: , + loader: checkToken, + errorElement: , }, ]); diff --git a/src/components/frame/with-buttons/index.tsx b/src/components/frame/with-buttons/index.tsx index b0d7935d..322e0afb 100644 --- a/src/components/frame/with-buttons/index.tsx +++ b/src/components/frame/with-buttons/index.tsx @@ -38,7 +38,7 @@ const BackButton = ({ pathname }: { pathname: string }) => { const DISABLED = { rightButtons: new Set([ROUTE.error, ROUTE.result]), - menuButton: new Set([ROUTE.tutorial]), + menuButton: [ROUTE.tutorial, ROUTE.error], }; const BasicContentFrame = ({ children }: { children: ReactNode }) => { @@ -46,7 +46,9 @@ const BasicContentFrame = ({ children }: { children: ReactNode }) => { const rightButtonsDisabled = DISABLED.rightButtons.has(pathname); - const menuButtonDisabled = DISABLED.menuButton.has(pathname); + const menuButtonDisabled = DISABLED.menuButton.some((route) => + pathname.startsWith(route), + ); return (
diff --git a/src/features/auth/api.ts b/src/features/auth/api.ts index d7e8e596..89d24b3b 100644 --- a/src/features/auth/api.ts +++ b/src/features/auth/api.ts @@ -8,22 +8,37 @@ const setToken = (token: string, status: number) => { return false; }; +const axiosConfig = { + headers: { + 'X-Bypass-Authorization': true, + }, +}; + export const loginAsMember = async (account: Account) => { - const { data, status } = await client.post(`/user/login`, account, { - headers: { - 'X-Bypass-Authorization': true, - }, - }); + const { data, status } = await client.post( + `/user/login`, + account, + axiosConfig, + ); return setToken(data, status); }; export const loginAsGuest = async () => { return client - .get('/user/guest', { - headers: { - 'X-Bypass-Authorization': true, - }, - }) + .get('/user/guest', axiosConfig) .then(({ data, status }) => setToken(data, status)) .catch(() => false); }; + +export const checkToken = async () => { + const isTokenValid = await client + .get('/user/token') + .then(({ data }) => data); + + if (!isTokenValid) { + localStorage.removeItem('token'); + throw new Error(); + } + + return isTokenValid; +}; diff --git a/src/features/auth/hooks.ts b/src/features/auth/hooks.ts index d7c102f4..e4be9e82 100644 --- a/src/features/auth/hooks.ts +++ b/src/features/auth/hooks.ts @@ -1,18 +1,20 @@ +import { useQueryClient } from '@tanstack/react-query'; import { type AxiosError } from 'axios'; import { useNavigate } from 'react-router-dom'; import { loginAsGuest, loginAsMember } from './api'; -import { removeUserQuery } from '@/models/user'; +import { userQueryKey } from '@/models/user'; import { ROUTE } from '@/shared/constants'; import { setFullScreen } from '@/shared/utils'; export const useLogout = () => { const navigate = useNavigate(); + const queryClient = useQueryClient(); const logout = () => { window.localStorage.removeItem('token'); - removeUserQuery(); + queryClient.removeQueries({ queryKey: userQueryKey }); navigate('/'); }; @@ -21,9 +23,10 @@ export const useLogout = () => { export const useLogin = () => { const navigate = useNavigate(); + const queryClient = useQueryClient(); - const login = async (account: Account) => { - removeUserQuery(); + const memberLogin = async (account: Account) => { + queryClient.removeQueries({ queryKey: userQueryKey }); return loginAsMember(account) .then((isSuccess) => { @@ -42,14 +45,8 @@ export const useLogin = () => { }); }; - return { login }; -}; - -export const useGuestLogin = () => { - const navigate = useNavigate(); - - const login = async () => { - removeUserQuery(); + const guestLogin = async () => { + queryClient.removeQueries({ queryKey: userQueryKey }); const isSuccess = await loginAsGuest(); if (!isSuccess) { @@ -59,5 +56,5 @@ export const useGuestLogin = () => { return navigate(ROUTE.tutorial, { replace: true }); }; - return { guestLogin: login }; + return { memberLogin, guestLogin }; }; diff --git a/src/features/auth/index.ts b/src/features/auth/index.ts index 4cc90d02..8c350af1 100644 --- a/src/features/auth/index.ts +++ b/src/features/auth/index.ts @@ -1 +1,2 @@ export * from './hooks'; +export { checkToken } from './api'; diff --git a/src/features/sign-up/api.ts b/src/features/sign-up/api.ts index 9ba0ddb1..e74f1df7 100644 --- a/src/features/sign-up/api.ts +++ b/src/features/sign-up/api.ts @@ -1,6 +1,5 @@ import type { AxiosError } from 'axios'; -import { invalidateUserQuery } from '@/models/user'; import { client } from '@/shared/api'; const axiosConfig = { @@ -22,7 +21,6 @@ export const upgradeToMember = async (formData: SignUpForm) => { .post('/user/guest', formData) .then(({ data }) => { console.debug(data); - invalidateUserQuery(); return true; }) .catch(({ response }: AxiosError) => { diff --git a/src/features/sign-up/hooks.ts b/src/features/sign-up/hooks.ts new file mode 100644 index 00000000..03bd50c4 --- /dev/null +++ b/src/features/sign-up/hooks.ts @@ -0,0 +1,21 @@ +import { useQueryClient } from '@tanstack/react-query'; + +import { registerMember, upgradeToMember } from './api'; + +import { userQueryKey } from '@/models/user'; + +export const useSignUp = (isGuest: boolean) => { + const queryClient = useQueryClient(); + + if (!isGuest) { + return registerMember; + } + + return async (formData: SignUpForm) => { + const isSuccess = await upgradeToMember(formData); + if (isSuccess) { + await queryClient.invalidateQueries({ queryKey: userQueryKey }); + } + return isSuccess; + }; +}; diff --git a/src/features/sign-up/ui.tsx b/src/features/sign-up/ui.tsx index 0ef2b38a..2a6b500c 100644 --- a/src/features/sign-up/ui.tsx +++ b/src/features/sign-up/ui.tsx @@ -1,7 +1,8 @@ import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { checkIdDuplicate, registerMember, upgradeToMember } from './api'; +import { checkIdDuplicate } from './api'; +import { useSignUp } from './hooks'; import { validateSignUpForm } from './utils'; import ColoredButton from '@/components/button/ColoredButton'; @@ -17,8 +18,9 @@ interface Props { isGuest?: boolean; } -const SignUp = ({ closeModal, isGuest }: Props) => { +const SignUp = ({ closeModal, isGuest = false }: Props) => { const navigate = useNavigate(); + const signUp = useSignUp(isGuest); const [errorMessage, setErrorMessage] = useState(ERROR_MESSAGE.welcome); const [form, setForm] = useState({ id: '', @@ -48,7 +50,6 @@ const SignUp = ({ closeModal, isGuest }: Props) => { return; } - const signUp = isGuest ? upgradeToMember : registerMember; const isSuccess = await signUp(form); if (!isSuccess) { return; diff --git a/src/models/user/hooks.ts b/src/models/user/hooks.ts index cf518270..aae6d93b 100644 --- a/src/models/user/hooks.ts +++ b/src/models/user/hooks.ts @@ -1,12 +1,12 @@ -import { useQuery } from '@tanstack/react-query'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; import { getDefaultStore, useAtomValue, useSetAtom } from 'jotai'; import { getUser } from './api'; import { queryKey } from './constants'; import { updateUserCodeAtom, userCodeAtom, userStaleAtom } from './store'; -import { invalidateUserQuery } from './utils'; export const useUserInfo = () => { + const queryClient = useQueryClient(); const setUserCode = useSetAtom(updateUserCodeAtom); const store = getDefaultStore(); const isStale = store.get(userStaleAtom); @@ -18,14 +18,15 @@ export const useUserInfo = () => { setUserCode(user.userCode); return user; }, - throwOnError: true, refetchOnWindowFocus: false, refetchOnMount: isStale, }); + const refetchUser = () => queryClient.invalidateQueries({ queryKey }); + return { user: data, - refetchUser: invalidateUserQuery, + refetchUser, }; }; diff --git a/src/models/user/index.ts b/src/models/user/index.ts index fd70c425..28cd58ea 100644 --- a/src/models/user/index.ts +++ b/src/models/user/index.ts @@ -1,2 +1,2 @@ export * from './hooks'; -export * from './utils'; +export { queryKey as userQueryKey } from './constants'; diff --git a/src/models/user/utils.ts b/src/models/user/utils.ts deleted file mode 100644 index ed47d356..00000000 --- a/src/models/user/utils.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { queryKey } from './constants'; - -import { queryClient } from '@/shared/api'; - -export const removeUserQuery = () => queryClient.removeQueries({ queryKey }); - -export const invalidateUserQuery = () => - queryClient.invalidateQueries({ queryKey }); diff --git a/src/pages/error/constants.ts b/src/pages/error/constants.ts index 834e1560..1a55e297 100644 --- a/src/pages/error/constants.ts +++ b/src/pages/error/constants.ts @@ -1,6 +1,6 @@ export const ERROR_MESSAGE = { '401': '로그인 정보가 유효하지 않습니다!\n다시 로그인 하시겠습니까?', - '404': '페이지를 찾을 수 없습니다!\n이전 화면으로 돌아가시겠습니까?', + '404': '페이지를 찾을 수 없습니다!\n홈으로 돌아가시겠습니까?', '500': '서버에 에러가 발생했습니다!\n 개발자에게 문의하세요.', '400': '네트워크 연결 상태를\n확인해주세요', }; diff --git a/src/pages/error/index.tsx b/src/pages/error/index.tsx index ea52c5d9..cefe08ab 100644 --- a/src/pages/error/index.tsx +++ b/src/pages/error/index.tsx @@ -1,29 +1,30 @@ import { useNavigate, useParams } from 'react-router-dom'; -import * as constants from './constants'; +import { ERROR_MESSAGE } from './constants'; import * as styles from './index.css'; import ColoredButton from '@/components/button/ColoredButton'; -import type { errorCode } from '@/pages/error/type'; - import { ROUTE } from '@/shared/constants'; const Error = () => { const navigate = useNavigate(); - const code = useParams().code as errorCode; + const code = useParams().code as ErrorCode; const goBack = () => { - if (code === '401') { - navigate(ROUTE.main, { replace: true }); - return; + switch (code) { + case '401': + return navigate(ROUTE.main, { replace: true }); + case '404': + return navigate(ROUTE.home, { replace: true }); + default: + return navigate(-1); } - navigate(-1); }; return (
-
{constants.ERROR_MESSAGE[code]}
+
{ERROR_MESSAGE[code]}
); diff --git a/src/pages/error/type.d.ts b/src/pages/error/type.d.ts deleted file mode 100644 index ac93db31..00000000 --- a/src/pages/error/type.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { ERROR_MESSAGE } from '@/pages/error/constants'; - -type errorCode = keyof typeof ERROR_MESSAGE; diff --git a/src/pages/error/types.d.ts b/src/pages/error/types.d.ts new file mode 100644 index 00000000..13d42f29 --- /dev/null +++ b/src/pages/error/types.d.ts @@ -0,0 +1 @@ +type ErrorCode = keyof typeof import('./constants').ERROR_MESSAGE; diff --git a/src/pages/landing/index.tsx b/src/pages/landing/index.tsx index 96a3e6a3..f9aaab75 100644 --- a/src/pages/landing/index.tsx +++ b/src/pages/landing/index.tsx @@ -4,12 +4,12 @@ import * as styles from './index.css'; import ColoredButton from '@/components/button/ColoredButton/index'; -import { useGuestLogin } from '@/features/auth'; +import { useLogin } from '@/features/auth'; import { ROUTE } from '@/shared/constants'; const Landing = () => { const navigate = useNavigate(); - const { guestLogin } = useGuestLogin(); + const { guestLogin } = useLogin(); const clickLogIn = () => { navigate(ROUTE.login); diff --git a/src/pages/logIn/index.tsx b/src/pages/logIn/index.tsx index 858ce984..3c4a3dcb 100644 --- a/src/pages/logIn/index.tsx +++ b/src/pages/logIn/index.tsx @@ -17,7 +17,7 @@ const LogIn = () => { id: '', password: '', }); - const { login } = useLogin(); + const { memberLogin } = useLogin(); const { modalRef, openModal, closeModal } = useModal(); @@ -30,7 +30,7 @@ const LogIn = () => { }; const handleLogInClick = async () => { - const isLoginSuccess = await login(logInInfo); + const isLoginSuccess = await memberLogin(logInInfo); if (!isLoginSuccess) { setIsClicked(false); openModal(); diff --git a/src/pages/ranking/api.ts b/src/pages/ranking/api.ts new file mode 100644 index 00000000..079e0d33 --- /dev/null +++ b/src/pages/ranking/api.ts @@ -0,0 +1,13 @@ +import { client } from '@/shared/api'; + +export const getRank = async () => { + return client + .get<{ + refreshTime: string; + players: Profile[]; + }>('rank/list') + .then(({ data }) => { + console.debug('rank:', data); + return data.players; + }); +}; diff --git a/src/pages/ranking/index.tsx b/src/pages/ranking/index.tsx index 5c994cda..76af409e 100644 --- a/src/pages/ranking/index.tsx +++ b/src/pages/ranking/index.tsx @@ -1,42 +1,2 @@ -import { useLoaderData } from 'react-router-dom'; - -import * as constants from './constants'; -import * as styles from './index.css'; - -import RankingItemBox from '@/components/ranking'; - -import { useUserInfo } from '@/models/user'; - -const Ranking = () => { - const { rankList } = useLoaderData() as { - rankList: Profile[]; - }; - const { user: myRank } = useUserInfo(); - - if (!myRank) { - return null; - } - - return ( -
-
-
{constants.RANKING_TITLE}
- {myRank.guest && ( -
{constants.GUEST_TEXT}
- )} -
-
- {rankList.map((item) => ( - - ))} -
- {myRank && ( -
- -
- )} -
- ); -}; - -export default Ranking; +export { getRank } from './api'; +export { default } from './page'; diff --git a/src/pages/ranking/page.tsx b/src/pages/ranking/page.tsx new file mode 100644 index 00000000..d2ffb649 --- /dev/null +++ b/src/pages/ranking/page.tsx @@ -0,0 +1,40 @@ +import { useLoaderData } from 'react-router-dom'; + +import * as constants from './constants'; +import * as styles from './index.css'; + +import RankingItemBox from '@/components/ranking'; + +import { useUserInfo } from '@/models/user'; + +const Ranking = () => { + const rankList = useLoaderData() as Profile[]; + const { user: myRank } = useUserInfo(); + + if (!myRank) { + return null; + } + + return ( +
+
+
{constants.RANKING_TITLE}
+ {myRank.guest && ( +
{constants.GUEST_TEXT}
+ )} +
+
+ {rankList.map((item) => ( + + ))} +
+ {myRank && ( +
+ +
+ )} +
+ ); +}; + +export default Ranking; diff --git a/src/pages/settings/NicknameChange.tsx b/src/pages/settings/NicknameChange.tsx index 8e69a68c..dc38b25e 100644 --- a/src/pages/settings/NicknameChange.tsx +++ b/src/pages/settings/NicknameChange.tsx @@ -1,12 +1,12 @@ import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import { patchNicknameChange } from './api'; import * as styles from './index.css'; import ColoredButton from '@/components/button/ColoredButton'; import Input from '@/components/input'; -import { patchNicknameChange } from '@/services/auth'; import { ERROR_MESSAGE, ROUTE } from '@/shared/constants'; diff --git a/src/pages/settings/PasswordChange.tsx b/src/pages/settings/PasswordChange.tsx index a98ac9ab..bc56cec1 100644 --- a/src/pages/settings/PasswordChange.tsx +++ b/src/pages/settings/PasswordChange.tsx @@ -1,11 +1,11 @@ import React, { useState } from 'react'; +import { patchPasswordChange } from './api'; import * as styles from './index.css'; import ColoredButton from '@/components/button/ColoredButton'; import Input from '@/components/input'; -import { patchPasswordChange } from '@/services/auth'; import { ERROR_MESSAGE } from '@/shared/constants'; diff --git a/src/pages/settings/api.ts b/src/pages/settings/api.ts new file mode 100644 index 00000000..f3c8d423 --- /dev/null +++ b/src/pages/settings/api.ts @@ -0,0 +1,47 @@ +import { client } from '@/shared/api'; + +export const patchNicknameChange = async (nickname: string) => { + try { + await client.patch('/user/nickname', { nickname }); + return true; + } catch (e) { + return false; + } +}; + +export const patchPasswordChange = async ( + prePassword: string, + newPassword: string, +) => { + try { + await client.patch('/user/password', { + prePassword, + newPassword, + }); + return true; + } catch (e) { + return false; + } +}; + +export const postConfirmPassword = async (password: string) => { + try { + const response = await client.post('/user/confirm', { password }); + // console.log(response.data); + return response.data; + } catch (e) { + return false; + } +}; + +export const deleteUserInfo = async () => { + await client + .delete('/user') + .then(() => { + window.location.href = '/'; + }) + // eslint-disable-next-line + .catch((e) => { + console.log(e); + }); +}; diff --git a/src/pages/settings/delete-account.tsx b/src/pages/settings/delete-account.tsx index 6111f65f..7dea2238 100644 --- a/src/pages/settings/delete-account.tsx +++ b/src/pages/settings/delete-account.tsx @@ -1,11 +1,11 @@ import React, { useState } from 'react'; +import { deleteUserInfo, postConfirmPassword } from './api'; import * as styles from './index.css'; import ColoredButton from '@/components/button/ColoredButton'; import Input from '@/components/input'; -import { deleteUserInfo, postConfirmPassword } from '@/services/auth'; import { ERROR_MESSAGE } from '@/shared/constants'; diff --git a/src/services/auth.ts b/src/services/auth.ts deleted file mode 100644 index 8ea474f5..00000000 --- a/src/services/auth.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { NicknameInfo, PasswordInfo } from '@/types/auth'; - -import { api } from '@/shared/api'; - -export const patchNicknameChange = async (nickname: string) => { - try { - const changeNickname: NicknameInfo = { - nickname, - }; - await api.patch( - true, - '/user/nickname', - changeNickname, - ); - return true; - } catch (e) { - return false; - } -}; - -export const patchPasswordChange = async ( - prePassword: string, - newPassword: string, -) => { - try { - const changePassword: PasswordInfo = { - prePassword, - newPassword, - }; - - await api.patch( - true, - '/user/password', - changePassword, - ); - return true; - } catch (e) { - return false; - } -}; - -export const postConfirmPassword = async (password: string) => { - try { - const response = await api.post( - true, - '/user/confirm', - { password }, - ); - // console.log(response.data); - return response.data; - } catch (e) { - return false; - } -}; - -export const deleteUserInfo = async () => { - await api - .delete(true, '/user') - .then(() => { - window.location.href = '/'; - }) - // eslint-disable-next-line - .catch((e) => { - console.log(e); - }); -}; - -export const checkToken = async () => { - return api - .get(true, '/user/token') - .then((response) => { - const isTokenValid = response.data; - if (!isTokenValid) { - throw new Error(); - } - return isTokenValid; - }) - .catch((err) => { - localStorage.removeItem('token'); - throw err; - }); -}; diff --git a/src/services/rank.ts b/src/services/rank.ts deleted file mode 100644 index c47fcc8a..00000000 --- a/src/services/rank.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { api } from '@/shared/api'; - -const requestRankList = async () => { - return api.get(true, 'rank/list').then((res) => { - return res.data.players; - }); -}; - -export const getRank = async () => { - const rankList = await requestRankList(); - - return { rankList }; -}; diff --git a/src/types/auth.d.ts b/src/types/auth.d.ts deleted file mode 100644 index 914ebfa0..00000000 --- a/src/types/auth.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface NicknameInfo { - nickname: string; -} - -export interface PasswordInfo { - prePassword: string; - newPassword: string; -} diff --git a/src/types/rank.d.ts b/src/types/rank.d.ts deleted file mode 100644 index 7de94732..00000000 --- a/src/types/rank.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -interface RankInfo { - refreshTime: string; - players: Profile[]; -}