diff --git a/src/components/Header/DashboardHeader/MemberProfiles.tsx b/src/components/Header/DashboardHeader/MemberProfiles.tsx index 10ebcaa3..6a161ae7 100644 --- a/src/components/Header/DashboardHeader/MemberProfiles.tsx +++ b/src/components/Header/DashboardHeader/MemberProfiles.tsx @@ -26,8 +26,10 @@ const genMemberProfiles = (members: Member[], distance: number) => // NOTE: 대시보드 헤더 구성원 프로필 목록 컴포넌트 export default function MemberProfiles({ id }: MemberProfilesProps) { - const { data, isLoading, error } = useFetchData(['members', id, 1], () => - getMembersList(Number(id)), + const { data, isLoading, error } = useFetchData( + ['members', id, 1], + () => getMembersList(Number(id)), + !!id, ); if (isLoading) { diff --git a/src/components/Header/DashboardHeader/index.tsx b/src/components/Header/DashboardHeader/index.tsx index 8e59a74a..8c8d1d70 100644 --- a/src/components/Header/DashboardHeader/index.tsx +++ b/src/components/Header/DashboardHeader/index.tsx @@ -22,7 +22,7 @@ export default function DashboardHeader() { data: dashboard, isLoading, error, - } = useFetchData(['dashboard', id], () => getDashboard(id as string)); + } = useFetchData(['dashboard', id], () => getDashboard(id as string), !!id); if (isLoading || !id) { return ; diff --git a/src/components/Redirect/index.tsx b/src/components/Redirect/index.tsx index fbe22ae9..3cbc4378 100644 --- a/src/components/Redirect/index.tsx +++ b/src/components/Redirect/index.tsx @@ -1,45 +1,63 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; import { useRouter } from 'next/router'; +import { useEffect } from 'react'; import { useSelector } from 'react-redux'; -import useRedirectIfAuth from '@/hooks/useRedirectIfAuth'; -import useRedirectIfNotAuth from '@/hooks/useRedirectIfNotAuth'; +import useRedirect from '@/hooks/useRedirect'; import { RootState } from '@/store/store'; // NOTE: 권한에 따라 페이지 이동 export default function Redirect({ children }: { children: React.ReactNode }) { - const router = useRouter(); + const redirect = useRedirect(); const { user } = useSelector((state: RootState) => state.user); + const router = useRouter(); const currentPath = router.pathname; - const redirectIfAuth = useRedirectIfAuth(); - const redirectIfNotAuth = useRedirectIfNotAuth(); - - // NOTE: 로그인 한 경우 mydashboard를 default로 - if (currentPath === '/' && user) { - router.replace('/mydashboard'); - } - - // NOTE: 잘못된 주소 접근 시 3초 뒤 적절한 페이지로 이동 - if (currentPath === '/404') { - const nextPath = user ? '/mydashboard' : '/'; - setTimeout(() => router.push(nextPath), 3000); - } - - // NOTE: 로그인한 사용자가 로그인/회원가입 접근 시 mydashboard로 이동 - if (['/signup', '/signin'].includes(currentPath)) { - const isRedirecting = redirectIfAuth(); - if (isRedirecting) { - return <>; + // NOTE: QueryCache로 글로벌 콜백 설정 + const queryClient = useQueryClient(); + const queryCache = queryClient.getQueryCache(); + queryCache.config = { + onError: (error) => { + if (error instanceof AxiosError) { + switch (error.response?.status) { + case 401: + redirect('/signin', '로그인이 필요합니다'); + break; + case 403: + case 404: + if (user) redirect('/mydashboard', '접근 권한이 없습니다'); + else redirect('/signin', '접근 권한이 없습니다'); + break; + default: + throw error; + } + } + }, + }; + + useEffect(() => { + // NOTE: 로그인 한 경우 mydashboard를 default로 + if (currentPath === '/' && user) { + router.replace('/mydashboard'); + } + + // NOTE: 잘못된 주소 접근 시 3초 뒤 적절한 페이지로 이동 + if (currentPath === '/404') { + const nextPath = user ? '/mydashboard' : '/'; + setTimeout(() => router.push(nextPath), 3000); + } + + // NOTE: 로그인/회원가입 - 로그인 O -> 내 대시보드 + if (['/signup', '/signin'].includes(currentPath) && user) { + redirect('/mydashboard', '이미 로그인했습니다'); } - } - // TODO: 추후 리팩토링 예정 - if (['/mypage', '/mydashboard'].includes(currentPath)) { - const isRedirecting = redirectIfNotAuth(); - if (isRedirecting) { - return <>; + // NOTE: 내 대시보드/계정관리 - 로그인 X -> 로그인 + if (['/mypage', '/mydashboard'].includes(currentPath) && !user) { + redirect('/signin', '로그인이 필요합니다'); } - } + }, [currentPath, router.query.id]); return children; } diff --git a/src/containers/dashboard/ColumnsSection.tsx b/src/containers/dashboard/ColumnsSection.tsx index 53d0606d..73089938 100644 --- a/src/containers/dashboard/ColumnsSection.tsx +++ b/src/containers/dashboard/ColumnsSection.tsx @@ -8,13 +8,11 @@ import Column from './Column'; import useFetchData from '@/hooks/useFetchData'; import useModal from '@/hooks/useModal'; -import useRedirectIfNotMember from '@/hooks/useRedirectIfNotMember'; import instance from '@/services/axios'; -import { getColumnsList, getDashboard } from '@/services/getService'; +import { getColumnsList } from '@/services/getService'; import { moveToOtherColumn } from '@/services/putService'; import { RootState } from '@/store/store'; import { ColumnsResponse } from '@/types/Column.interface'; -import { checkPublic } from '@/utils/shareAccount'; interface ColumnsSectionProps { dashboardId: string; @@ -23,10 +21,8 @@ interface ColumnsSectionProps { export default function ColumnsSection({ dashboardId }: ColumnsSectionProps) { const queryClient = useQueryClient(); const { openNewColumnModal, openNotificationModal } = useModal(); - const redirectIfNotMember = useRedirectIfNotMember(); const { user } = useSelector((state: RootState) => state.user); const [isMember, setIsMember] = useState(true); - const [isPublic, setIsPublic] = useState(false); const { data: columns, @@ -37,31 +33,17 @@ export default function ColumnsSection({ dashboardId }: ColumnsSectionProps) { const columnList = columns?.data || []; useEffect(() => { - const handleRedirect = async () => { + const handleCheckMember = async () => { + if (!dashboardId) return; try { - const newIsPublic = await checkPublic(Number(dashboardId)); - setIsPublic(newIsPublic); - if (!newIsPublic && dashboardId) { - await getDashboard(String(dashboardId)); - } + await instance.get(`/dashboards/${dashboardId}`, { + headers: { memberTest: true }, + }); } catch { - redirectIfNotMember(); - } - }; - - const handleCheckMember = async () => { - if (dashboardId) { - try { - await instance.get(`/dashboards/${dashboardId}`, { - headers: { memberTest: true }, - }); - } catch { - setIsMember(false); - } + setIsMember(false); } }; - handleRedirect(); handleCheckMember(); }, [dashboardId, user]); @@ -106,7 +88,7 @@ export default function ColumnsSection({ dashboardId }: ColumnsSectionProps) { ) : (
    {columnList.map((column, index) => ( diff --git a/src/containers/dashboard/edit/DashboardModifySection.tsx b/src/containers/dashboard/edit/DashboardModifySection.tsx index b4b70109..f3b72115 100644 --- a/src/containers/dashboard/edit/DashboardModifySection.tsx +++ b/src/containers/dashboard/edit/DashboardModifySection.tsx @@ -70,7 +70,7 @@ export default function DashboardModifySection({ initIsPublic, onPublicChange }: data: dashboard, isLoading, error, - } = useFetchData(['dashboard', id], () => getDashboard(id as string)); + } = useFetchData(['dashboard', id], () => getDashboard(id as string), !!id); const { data: favoriteList } = useFetchData( ['favorites', favoriteUserId], diff --git a/src/containers/dashboard/edit/InvitedMembersSection.tsx b/src/containers/dashboard/edit/InvitedMembersSection.tsx index 02c25f7e..34781c93 100644 --- a/src/containers/dashboard/edit/InvitedMembersSection.tsx +++ b/src/containers/dashboard/edit/InvitedMembersSection.tsx @@ -21,8 +21,10 @@ export default function InvitedMembersSection() { const { id } = router.query; const [currentChunk, setCurrentChunk] = useState(1); - const { data, isLoading, error } = useFetchData(['invitations', id, currentChunk], () => - getDashboardInvitations(Number(id), currentChunk, 5), + const { data, isLoading, error } = useFetchData( + ['invitations', id, currentChunk], + () => getDashboardInvitations(Number(id), currentChunk, 5), + !!id, ); const totalPage = data ? Math.max(1, Math.ceil(data.totalCount / 5)) : 1; diff --git a/src/containers/dashboard/edit/MembersSection.tsx b/src/containers/dashboard/edit/MembersSection.tsx index d3574fec..e51caf51 100644 --- a/src/containers/dashboard/edit/MembersSection.tsx +++ b/src/containers/dashboard/edit/MembersSection.tsx @@ -24,8 +24,10 @@ export default function MembersSection({ onDeleteMember }: MemberSectionProps) { const { id } = router.query; const [currentChunk, setCurrentChunk] = useState(1); - const { data, isLoading, error } = useFetchData(['members', id, currentChunk], () => - getMembersList(Number(id), currentChunk, 4), + const { data, isLoading, error } = useFetchData( + ['members', id, currentChunk], + () => getMembersList(Number(id), currentChunk, 4), + !!id, ); const totalPage = data ? Math.max(1, Math.ceil(data.totalCount / 4)) : 1; diff --git a/src/containers/dashboard/edit/index.tsx b/src/containers/dashboard/edit/index.tsx index 3cad30ef..275bedb9 100644 --- a/src/containers/dashboard/edit/index.tsx +++ b/src/containers/dashboard/edit/index.tsx @@ -9,33 +9,17 @@ import InvitedMembersSection from './InvitedMembersSection'; import MembersSection from './MembersSection'; import useDeleteData from '@/hooks/useDeleteData'; -import useFetchData from '@/hooks/useFetchData'; import useModal from '@/hooks/useModal'; -import useRedirectIfNoPermission from '@/hooks/useRedirectIfNoPermission'; import { deleteDashboard } from '@/services/deleteService'; -import { getDashboard } from '@/services/getService'; -import { Dashboard } from '@/types/Dashboard.interface'; import { DeleteDashboardInput } from '@/types/delete/DeleteDashboardInput.interface'; import { checkPublic } from '@/utils/shareAccount'; export default function DashboardEdit() { - const redirectIfNoPermission = useRedirectIfNoPermission(); const { openConfirmModal } = useModal(); const queryClient = useQueryClient(); const router = useRouter(); const { id } = router.query; - // NOTE: Redirection - const { data: dashboard, error } = useFetchData(['dashboard', id], () => getDashboard(id as string)); - - useEffect(() => { - if (dashboard) { - redirectIfNoPermission(dashboard.userId); - } else if (error) { - redirectIfNoPermission(-1); - } - }, [dashboard, error]); - // NOTE: 공유 대시보드 여부 확인 const [isPublic, setIsPublic] = useState(false); @@ -51,12 +35,9 @@ export default function DashboardEdit() { useEffect(() => { const handleInitIsPublic = async () => { - try { - setIsPublic(await checkPublic(Number(id))); - } catch { - redirectIfNoPermission(-1); - } + setIsPublic(await checkPublic(Number(id))); }; + if (id) handleInitIsPublic(); }, [id]); diff --git a/src/hooks/useDeleteData.ts b/src/hooks/useDeleteData.ts index af2febeb..b814a966 100644 --- a/src/hooks/useDeleteData.ts +++ b/src/hooks/useDeleteData.ts @@ -1,6 +1,8 @@ import { MutationFunction, useMutation } from '@tanstack/react-query'; import { AxiosError } from 'axios'; +import useModal from './useModal'; + interface UseDeleteProps { mutationFn: MutationFunction; handleSuccess?: () => void; @@ -9,6 +11,7 @@ interface UseDeleteProps { // NOTE: 삭제 요청을 돕는 훅 const useDeleteData = ({ mutationFn, handleSuccess, handleError }: UseDeleteProps) => { + const { openNotificationModal } = useModal(); return useMutation({ mutationFn, onSuccess: () => { @@ -24,9 +27,9 @@ const useDeleteData = ({ mutationFn, handleSuccess, handleError }: UseDeleteP } else { // NOTE: 에러 처리는 일관되게 서버 메세지 있는 경우 보여주고, 아니면 로그 출력하도록 했습니다. if (error instanceof AxiosError) { - alert(error.response?.data.message); + openNotificationModal({ text: error.response?.data.message }); } else { - alert('실패했습니다.'); + openNotificationModal({ text: '실패했습니다.' }); console.log(error); } } diff --git a/src/hooks/useFetchData.ts b/src/hooks/useFetchData.ts index a7aae690..9c6796c4 100644 --- a/src/hooks/useFetchData.ts +++ b/src/hooks/useFetchData.ts @@ -14,13 +14,11 @@ const useFetchData = ( const response = await getService(); return response.data; } catch (error) { - throw new Error('데이터를 불러오는 중 에러 발생: ' + error); + throw error; } }, - retry: 1, // NOTE: 요청 실패시 1회까지만 재시도하도록 제한 - refetchInterval: refetchInterval, // NOTE: 주기적인 초대내역 수신을 위해 설정 enabled: enabled, - refetchInterval: refetchInterval, + refetchInterval: refetchInterval, // NOTE: 주기적인 초대내역 수신을 위해 설정 }); }; diff --git a/src/hooks/useRedirect.ts b/src/hooks/useRedirect.ts new file mode 100644 index 00000000..6dfb2228 --- /dev/null +++ b/src/hooks/useRedirect.ts @@ -0,0 +1,25 @@ +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; + +import useModal from '@/hooks/useModal'; + +const useRedirect = () => { + const { openNotificationModal } = useModal(); + const router = useRouter(); + const { id } = router.query; + const [initialCheck, setInitialCheck] = useState(true); + + useEffect(() => { + setInitialCheck(true); + }, [router.pathname, id]); + + return (path: string, text: string) => { + if (initialCheck) { + openNotificationModal({ text }); + router.replace(path); + setInitialCheck(false); + } + }; +}; + +export default useRedirect; diff --git a/src/hooks/useRedirectIfAuth.ts b/src/hooks/useRedirectIfAuth.ts deleted file mode 100644 index b8ae5ffe..00000000 --- a/src/hooks/useRedirectIfAuth.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useRouter } from 'next/router'; -import { useState } from 'react'; -import { useSelector } from 'react-redux'; - -import useModal from '@/hooks/useModal'; -import { RootState } from '@/store/store'; - -const useRedirectIfAuth = () => { - const { user } = useSelector((state: RootState) => state.user); - const { openNotificationModal } = useModal(); - const router = useRouter(); - const [isRedirecting, setIsRedirecting] = useState(false); - const [initialCheck, setInitialCheck] = useState(true); - - return () => { - if (initialCheck) { - if (user) { - openNotificationModal({ text: '이미 로그인하셨습니다.' }); - setIsRedirecting(true); - router.replace('/mydashboard'); - } - setIsRedirecting(false); - setInitialCheck(false); - } - - return isRedirecting; - }; -}; - -export default useRedirectIfAuth; diff --git a/src/hooks/useRedirectIfNoPermission.ts b/src/hooks/useRedirectIfNoPermission.ts deleted file mode 100644 index d7df9879..00000000 --- a/src/hooks/useRedirectIfNoPermission.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { useRouter } from 'next/router'; -import { useState } from 'react'; -import { useSelector } from 'react-redux'; - -import useModal from '@/hooks/useModal'; -import { RootState } from '@/store/store'; - -const useRedirectIfNoPermission = () => { - const { user } = useSelector((state: RootState) => state.user); - const { openNotificationModal } = useModal(); - const router = useRouter(); - const [isRedirecting, setIsRedirecting] = useState(false); - const [initialCheck, setInitialCheck] = useState(true); - - return (creatorId: number) => { - if (initialCheck) { - if (creatorId !== user?.id) { - openNotificationModal({ text: '접근 권한이 없습니다.' }); - setIsRedirecting(true); - if (user) { - router.replace('/mydashboard'); - } else { - router.replace('/signin'); - } - } - setIsRedirecting(false); - setInitialCheck(false); - } - return isRedirecting; - }; -}; - -export default useRedirectIfNoPermission; diff --git a/src/hooks/useRedirectIfNotAuth.ts b/src/hooks/useRedirectIfNotAuth.ts deleted file mode 100644 index bef73e1e..00000000 --- a/src/hooks/useRedirectIfNotAuth.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useRouter } from 'next/router'; -import { useState } from 'react'; -import { useSelector } from 'react-redux'; - -import useModal from '@/hooks/useModal'; -import { RootState } from '@/store/store'; - -const useRedirectIfNotAuth = () => { - const { user } = useSelector((state: RootState) => state.user); - const { openNotificationModal } = useModal(); - const router = useRouter(); - const [isRedirecting, setIsRedirecting] = useState(false); - const [initialCheck, setInitialCheck] = useState(true); - - return () => { - if (initialCheck) { - if (!user) { - openNotificationModal({ text: '로그인이 필요합니다.' }); - setIsRedirecting(true); - router.replace('/signin'); - } - setIsRedirecting(false); - setInitialCheck(false); - } - - return isRedirecting; - }; -}; - -export default useRedirectIfNotAuth; diff --git a/src/hooks/useRedirectIfNotMember.ts b/src/hooks/useRedirectIfNotMember.ts deleted file mode 100644 index 5d7da60c..00000000 --- a/src/hooks/useRedirectIfNotMember.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useRouter } from 'next/router'; -import { useState } from 'react'; -import { useSelector } from 'react-redux'; - -import useModal from '@/hooks/useModal'; -import { RootState } from '@/store/store'; - -const useRedirectIfNotMember = () => { - const { user } = useSelector((state: RootState) => state.user); - const { openNotificationModal } = useModal(); - const router = useRouter(); - const [isRedirecting, setIsRedirecting] = useState(false); - const [initialCheck, setInitialCheck] = useState(true); - - return () => { - if (initialCheck) { - openNotificationModal({ text: '접근 권한이 없습니다.' }); - setIsRedirecting(true); - if (user) { - router.replace('/mydashboard'); - } else { - router.replace('/signin'); - } - setInitialCheck(false); - } - return isRedirecting; - }; -}; - -export default useRedirectIfNotMember; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index ea463dec..dc94bd8c 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -12,7 +12,14 @@ import Redirect from '@/components/Redirect'; import MainLayout from '@/layouts/MainLayout'; import { store, persistor } from '@/store/store'; -const queryClient = new QueryClient(); +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 0, + refetchOnWindowFocus: false, + }, + }, +}); export default function App({ Component, pageProps }: AppProps) { return (