diff --git a/package-lock.json b/package-lock.json index b63983a9..868cd78b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "react": "^18", "react-day-picker": "^8.10.1", "react-dom": "^18", + "react-error-boundary": "^4.1.2", "react-hook-form": "^7.53.0", "react-intersection-observer": "^9.13.1", "tailwind-merge": "^2.5.4", @@ -6778,6 +6779,17 @@ "react": "^18.3.1" } }, + "node_modules/react-error-boundary": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.1.2.tgz", + "integrity": "sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-hook-form": { "version": "7.53.0", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.0.tgz", diff --git a/package.json b/package.json index b78062a6..6202fe9f 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "react": "^18", "react-day-picker": "^8.10.1", "react-dom": "^18", + "react-error-boundary": "^4.1.2", "react-hook-form": "^7.53.0", "react-intersection-observer": "^9.13.1", "tailwind-merge": "^2.5.4", diff --git a/src/apis/groups.api.ts b/src/apis/groups.api.ts index b00b19a0..54772f37 100644 --- a/src/apis/groups.api.ts +++ b/src/apis/groups.api.ts @@ -9,34 +9,26 @@ import { Task } from '@/types/tasks.types'; import { axiosInstance } from './_axiosInstance'; export const getGroup = async (id: number) => { - try { - const response = await axiosInstance({ - method: 'GET', - url: `groups/${id}`, - }); - return response.data; - } catch (error) { - throw new Error('팀 정보를 가져오는 데 실패했습니다.'); - } + const response = await axiosInstance({ + method: 'GET', + url: `groups/${id}`, + }); + return response.data; }; export const postGroup = async ({ name, image }: PostGroupRequest) => { - try { - const response = await axiosInstance.post( - 'groups', - { name, image }, - { - headers: { - 'Content-Type': 'application/json', - }, - } - ); - const body = response.data; + const response = await axiosInstance.post( + 'groups', + { name, image }, + { + headers: { + 'Content-Type': 'application/json', + }, + } + ); + const body = response.data; - return body; - } catch (error) { - throw new Error('팀 생성하는 데 실패했습니다.'); - } + return body; }; export const patchGroup = async ({ id, name, image }: UpdateGroupRequest) => { @@ -63,22 +55,18 @@ export async function postInviteGroup({ userEmail, token, }: InviteGroupRequest) { - try { - const response = await axiosInstance.post( - 'groups/accept-invitation', - { userEmail, token }, - { - headers: { - 'Content-Type': 'application/json', - }, - } - ); - const body = response.data; + const response = await axiosInstance.post( + 'groups/accept-invitation', + { userEmail, token }, + { + headers: { + 'Content-Type': 'application/json', + }, + } + ); + const body = response.data; - return body; - } catch (error) { - throw new Error('그룹 참여에 실패했습니다.'); - } + return body; } export const getInviteGroup = async (id: number) => { @@ -90,44 +78,32 @@ export const getInviteGroup = async (id: number) => { }; export const postTaskList = async (groupId: number, name: string) => { - try { - const response = await axiosInstance({ - method: 'POST', - url: `/groups/${groupId}/task-lists`, - data: { name }, - headers: { - 'Content-Type': 'application/json', - }, - }); - return response.data; - } catch (error) { - throw new Error('할 일 목록를 생성하는 데 실패했습니다.'); - } + const response = await axiosInstance({ + method: 'POST', + url: `/groups/${groupId}/task-lists`, + data: { name }, + headers: { + 'Content-Type': 'application/json', + }, + }); + return response.data; }; export async function getTasks(id: number, date: string) { - try { - const response = await axiosInstance({ - method: 'GET', - url: `groups/${id}/tasks`, - data: { date }, - headers: { - 'Content-Type': 'application/json', - }, - }); - return response.data; - } catch (error) { - throw new Error('할 일을 불러오는 데 실패했습니다.'); - } + const response = await axiosInstance({ + method: 'GET', + url: `groups/${id}/tasks`, + data: { date }, + headers: { + 'Content-Type': 'application/json', + }, + }); + return response.data; } export async function deleteMember(groupId: number, memberUserId: number) { - try { - await axiosInstance({ - method: 'DELETE', - url: `groups/${groupId}/member/${memberUserId}`, - }); - } catch (error) { - throw new Error('멤버 삭제에 실패했습니다.'); - } + await axiosInstance({ + method: 'DELETE', + url: `groups/${groupId}/member/${memberUserId}`, + }); } diff --git a/src/apis/taskList.api.ts b/src/apis/taskList.api.ts index b8b7ef5a..cdaced62 100644 --- a/src/apis/taskList.api.ts +++ b/src/apis/taskList.api.ts @@ -2,32 +2,24 @@ import { TaskList } from '@/types/tasklist.types'; import { axiosInstance } from './_axiosInstance'; export const postTaskList = async (groupId: number, name: string) => { - try { - const response = await axiosInstance({ - method: 'POST', - url: `/groups/${groupId}/task-lists`, - data: { name }, - headers: { - 'Content-Type': 'application/json', - }, - }); - return response.data; - } catch (error) { - throw new Error('할 일 목록를 생성하는 데 실패했습니다.'); - } + const response = await axiosInstance({ + method: 'POST', + url: `/groups/${groupId}/task-lists`, + data: { name }, + headers: { + 'Content-Type': 'application/json', + }, + }); + return response.data; }; export const deleteTaskList = async (groupId: number, taskListId: number) => { - try { - const response = await axiosInstance({ - method: 'DELETE', - url: `/groups/${groupId}/task-lists/${taskListId}`, - headers: { - 'Content-Type': 'application/json', - }, - }); - return response; - } catch (error) { - throw new Error(''); - } + const response = await axiosInstance({ + method: 'DELETE', + url: `/groups/${groupId}/task-lists/${taskListId}`, + headers: { + 'Content-Type': 'application/json', + }, + }); + return response; }; diff --git a/src/components/TaskList/TaskLists.tsx b/src/components/TaskList/TaskLists.tsx index 9d039150..a6928c34 100644 --- a/src/components/TaskList/TaskLists.tsx +++ b/src/components/TaskList/TaskLists.tsx @@ -41,7 +41,7 @@ function TaskItem({ taskList, taskListColor, isMember }: TaskItemProps) { const handleTaskClick = (e: React.MouseEvent) => { setSelectedTaskList(taskList); - router.push(`/${taskList.groupId}/tasks`); + router.push(`/teams/${taskList.groupId}/tasks`); e.stopPropagation(); }; diff --git a/src/components/common/Badge.tsx b/src/components/common/Badge.tsx index 585bb0e1..d51b7141 100644 --- a/src/components/common/Badge.tsx +++ b/src/components/common/Badge.tsx @@ -12,11 +12,11 @@ function Badge(props: BadgeProps): JSX.Element { const no = count === 0 && left === 0; const badgeClass = `flex items-center gap-1 rounded-2xl bg-primary pb-1 pl-2 pr-2 pt-1 shadow-md`; - const ongoingSrc = 'icons/Progress_ongoing.svg'; - const checkSrc = 'icons/Progress_done.svg'; - const bestSrc = 'icons/Best.svg'; - const doneSrc = 'icons/Check_lightGreen.svg'; - const xSrc = 'icons/X.svg'; + const ongoingSrc = '../icons/Progress_ongoing.svg'; + const checkSrc = '../icons/Progress_done.svg'; + const bestSrc = '../icons/Best.svg'; + const doneSrc = '../icons/Check_lightGreen.svg'; + const xSrc = '../icons/X.svg'; let imageSrc; let altText; diff --git a/src/components/common/error/ErrorBoundary.tsx b/src/components/common/error/ErrorBoundary.tsx new file mode 100644 index 00000000..dd0941fa --- /dev/null +++ b/src/components/common/error/ErrorBoundary.tsx @@ -0,0 +1,19 @@ +import { QueryErrorResetBoundary } from '@tanstack/react-query'; +import { ErrorBoundary } from 'react-error-boundary'; +import ErrorFallback from './ErrorFallback'; + +export default function GlobalBoundary({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {({ reset }) => ( + + {children} + + )} + + ); +} diff --git a/src/components/common/error/ErrorFallback.tsx b/src/components/common/error/ErrorFallback.tsx new file mode 100644 index 00000000..41256cd5 --- /dev/null +++ b/src/components/common/error/ErrorFallback.tsx @@ -0,0 +1,62 @@ +import Button, { + ButtonBackgroundColor, + ButtonBorderColor, + ButtonPadding, + ButtonStyle, + TextColor, + TextSize, +} from '@/components/common/Button/Button'; + +import axios from 'axios'; +import { useRouter } from 'next/router'; +import { useState } from 'react'; +import { FallbackProps } from 'react-error-boundary'; + +export default function ErrorFallback({ + error, + resetErrorBoundary, +}: FallbackProps) { + const [redirected, setRedirected] = useState(false); + const router = useRouter(); + if ( + axios.isAxiosError(error) && + error.response?.status === 401 && + !redirected + ) { + router.push('/signin'); // 로그인 페이지로 리다이렉트 + setRedirected(true); + return; + } + return ( +
+
+

+ E + R + R + O + R +

+
+

+ {redirected ? '로그인 후 시도해주세요!' : error.response?.data.message} +

+ + +
+ ); +} diff --git a/src/components/layouts/ui/header/ui/header.menu.tsx b/src/components/layouts/ui/header/ui/header.menu.tsx index f437c299..eadeaa00 100644 --- a/src/components/layouts/ui/header/ui/header.menu.tsx +++ b/src/components/layouts/ui/header/ui/header.menu.tsx @@ -35,7 +35,7 @@ export function HeaderMenu({ className="flex items-center justify-center" > 수정하기 diff --git a/src/components/layouts/ui/header/ui/header.nav.tsx b/src/components/layouts/ui/header/ui/header.nav.tsx index ed72d54e..a487a921 100644 --- a/src/components/layouts/ui/header/ui/header.nav.tsx +++ b/src/components/layouts/ui/header/ui/header.nav.tsx @@ -108,7 +108,7 @@ export default function HeaderNav() { {memberships && memberships.map((m) => (
  • - + )} 수정하기 diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index f8d578ed..cca9cb39 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,3 +1,4 @@ +import GlobalBoundary from '@/components/common/error/ErrorBoundary'; import RootLayout from '@/components/layouts/RootLayout'; import { OAuthProvider } from '@/providers/OAuthProvider'; import { QueryProvider } from '@/providers/QueryProvider'; @@ -11,14 +12,16 @@ export default function MyApp({ pageProps: { session, ...pageProps }, }: AppProps) { return ( - - - - - - - - - + + + + + + + + + + + ); } diff --git a/src/pages/boards/index.tsx b/src/pages/boards/index.tsx index 49ec9a95..d4d10d41 100644 --- a/src/pages/boards/index.tsx +++ b/src/pages/boards/index.tsx @@ -31,6 +31,7 @@ function Boards() { const handleSearchKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { setSearchQuery(searchValue); + router.replace( { query: { diff --git a/src/pages/[id]/editteam/index.tsx b/src/pages/teams/[id]/editteam/index.tsx similarity index 100% rename from src/pages/[id]/editteam/index.tsx rename to src/pages/teams/[id]/editteam/index.tsx diff --git a/src/pages/[id]/index.tsx b/src/pages/teams/[id]/index.tsx similarity index 94% rename from src/pages/[id]/index.tsx rename to src/pages/teams/[id]/index.tsx index 0f3ee844..4979e27e 100644 --- a/src/pages/[id]/index.tsx +++ b/src/pages/teams/[id]/index.tsx @@ -20,7 +20,6 @@ import { Modal } from '@/components/modal'; import TaskLists from '@/components/TaskList/TaskLists'; import Members from '@/components/Team/Members'; import Report from '@/components/Team/Report'; -import { useRedirect } from '@/hooks/useRedirect'; import { useToast } from '@/hooks/useToast'; import { useDeleteTeamMutation, @@ -33,12 +32,11 @@ import Head from 'next/head'; import Image from 'next/image'; import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; -import WithOutTeam from '../withoutteam'; export default function TeamPage() { const router = useRouter(); const { id } = router.query; - const { data: group, isError, isFetched } = useTeamQuery(Number(id)); + const { data: group, isFetched } = useTeamQuery(Number(id)); const { data: inviteLink } = useInviteGroupQuery(Number(id)); const { data: user } = useGetUser(); const [isAdmin, setIsAdmin] = useState(false); @@ -46,7 +44,6 @@ export default function TeamPage() { const [isDeleteTeamModal, setIsDeleteTeamModal] = useState(false); const deleteTeam = useDeleteTeamMutation(); const { toast } = useToast(); - useRedirect(); useEffect(() => { if (user) { @@ -71,9 +68,6 @@ export default function TeamPage() { ); } - if (isError || !group) { - return ; - } const handleInviteGroup = () => { if (id && inviteLink) { @@ -97,7 +91,7 @@ export default function TeamPage() { }; const handleEditTeam = () => { - router.push(`${group.id}/editteam/`); + router.push(`/teams/${group?.id}/editteam/`); }; const handleDeleteModal = () => { @@ -111,7 +105,7 @@ export default function TeamPage() { return ( <> - {group.name} 팀 페이지 - Coworkers + {group?.name} 팀 페이지 - Coworkers

    멤버

    - ({group.members.length}개) + ({group?.members.length}개)

    {isMember && ( @@ -236,7 +230,7 @@ export default function TeamPage() { )} - + @@ -246,7 +240,7 @@ export default function TeamPage() { width={25} height={25} /> - {group.name} + {group?.name} 팀을 삭제하시겠어요? 삭제된 할 팀은 복구할 수 없습니다. diff --git a/src/pages/[id]/tasks/index.tsx b/src/pages/teams/[id]/tasks/index.tsx similarity index 100% rename from src/pages/[id]/tasks/index.tsx rename to src/pages/teams/[id]/tasks/index.tsx diff --git a/src/providers/QueryProvider.tsx b/src/providers/QueryProvider.tsx index ec791093..cb1fa8b7 100644 --- a/src/providers/QueryProvider.tsx +++ b/src/providers/QueryProvider.tsx @@ -11,6 +11,8 @@ function makeQueryClient() { queries: { // 클라이언트의 즉시 다시 요청에 대응하도록, 기본 캐싱 시간(min)을 설정. staleTime: 60 * 1000, + retry: 0, + throwOnError: true, }, }, });