diff --git a/src/api/ErrorMessage.ts b/src/api/ErrorMessage.ts index 7b16a57..1e71200 100644 --- a/src/api/ErrorMessage.ts +++ b/src/api/ErrorMessage.ts @@ -3,6 +3,7 @@ export const ErrorMessage = { LOGIN: '로그인에 실패했어요', PROFILE_UPDATE: '프로필을 업데이트하지 못했어요', REFRESH: '세션 업데이트에 실패했어요', + UNAUTHORIZED: '로그인이 필요해요', DASHBOARDS: '대시보드 정보를 불러오지 못했어요', CLASSROOM: '강의실 정보를 불러오지 못했어요', SEARCH: '검색 결과를 불러오지 못했어요', @@ -21,5 +22,6 @@ export const ErrorMessage = { } as const; export const API_FETCH_ERROR = 'API_FETCH_ERROR' +export const SESSION_ERROR = 'SESSION_ERROR' type ErrorMessage = (typeof ErrorMessage)[keyof typeof ErrorMessage]; diff --git a/src/api/courses/courses.ts b/src/api/courses/courses.ts index ecd3df3..ba478d2 100644 --- a/src/api/courses/courses.ts +++ b/src/api/courses/courses.ts @@ -1,8 +1,9 @@ import getQueryURL from '@/src/util/http/getQueryURL'; import { Endpoints } from '../Endpoints'; -import { API_FETCH_ERROR, ErrorMessage } from '../ErrorMessage'; +import { ErrorMessage } from '../ErrorMessage'; import { getAuthorizedHeaders } from '@/src/util/http/getAuthorizedHeaders'; import { QueryKeys } from '../queryKeys'; +import { fetchErrorHandling } from '@/src/util/http/fetchErrorHandling'; export async function enrollLectureInNewCourse( params: EnrollLectureInNewCourseParams @@ -17,9 +18,7 @@ export async function enrollLectureInNewCourse( if (res.ok) { return (await res.json()) as Promise; } else { - return Promise.reject( - new Error(ErrorMessage.ENROLLMENT, { cause: API_FETCH_ERROR }) - ); + return fetchErrorHandling(res, ErrorMessage.ENROLLMENT); } }); } @@ -38,9 +37,7 @@ export async function enrollLectureInExistingCourse( if (res.ok) { return (await res.json()) as Promise; } else { - return Promise.reject( - new Error(ErrorMessage.ENROLLMENT, { cause: API_FETCH_ERROR }) - ); + return fetchErrorHandling(res, ErrorMessage.ENROLLMENT); } }); } @@ -55,9 +52,7 @@ export async function fetchCourseDetail(course_id: number) { if (res.ok) { return (await res.json()) as Promise; } else { - return Promise.reject( - new Error(ErrorMessage.DETAIL_COURSE, { cause: API_FETCH_ERROR }) - ); + return fetchErrorHandling(res, ErrorMessage.DETAIL_COURSE); } }); } @@ -71,9 +66,7 @@ export async function fetchClassroom() { if (res.ok) { return (await res.json()) as Promise; } else { - return Promise.reject( - new Error(ErrorMessage.CLASSROOM, { cause: API_FETCH_ERROR }) - ); + return fetchErrorHandling(res, ErrorMessage.CLASSROOM); } }); } @@ -87,9 +80,7 @@ export async function deleteCourse(course_id: number) { if (res.ok) { return (await res.json()) as Promise; } else { - return Promise.reject( - new Error(ErrorMessage.CLASSROOM, { cause: API_FETCH_ERROR }) - ); + return fetchErrorHandling(res, ErrorMessage.CLASSROOM); } }); } diff --git a/src/api/dashboards/dashboards.ts b/src/api/dashboards/dashboards.ts index d03a40b..5475c35 100644 --- a/src/api/dashboards/dashboards.ts +++ b/src/api/dashboards/dashboards.ts @@ -1,6 +1,7 @@ import { getAuthorizedHeaders } from '@/src/util/http/getAuthorizedHeaders'; import { Endpoints } from '../Endpoints'; -import { API_FETCH_ERROR, ErrorMessage } from '../ErrorMessage'; +import { ErrorMessage } from '../ErrorMessage'; +import { fetchErrorHandling } from '@/src/util/http/fetchErrorHandling'; export async function fetchDashboardInfo() { const headers = await getAuthorizedHeaders(); @@ -11,9 +12,7 @@ export async function fetchDashboardInfo() { if (res.ok) { return (await res.json()) as Promise; } else { - return Promise.reject( - new Error(ErrorMessage.DASHBOARDS, { cause: API_FETCH_ERROR }) - ); + return fetchErrorHandling(res, ErrorMessage.DASHBOARDS); } }); } diff --git a/src/api/lectures/search.ts b/src/api/lectures/search.ts index 861d21e..c24ab58 100644 --- a/src/api/lectures/search.ts +++ b/src/api/lectures/search.ts @@ -1,7 +1,8 @@ import getQueryURL from '@/src/util/http/getQueryURL'; import { Endpoints } from '../Endpoints'; -import { API_FETCH_ERROR, ErrorMessage } from '../ErrorMessage'; +import { ErrorMessage } from '../ErrorMessage'; import { getAuthorizedHeaders } from '@/src/util/http/getAuthorizedHeaders'; +import { fetchErrorHandling } from '@/src/util/http/fetchErrorHandling'; export async function fetchLecturesByKeyword(params: SearchLectureParams) { const headers = await getAuthorizedHeaders(); @@ -12,9 +13,7 @@ export async function fetchLecturesByKeyword(params: SearchLectureParams) { if (res.ok) { return (await res.json()) as Promise; } else { - return Promise.reject( - new Error(ErrorMessage.SEARCH, { cause: API_FETCH_ERROR }) - ); + return fetchErrorHandling(res, ErrorMessage.SEARCH); } }); } @@ -34,9 +33,7 @@ export async function fetchLectureDetail( if (res.ok) { return (await res.json()) as Promise; } else { - return Promise.reject( - new Error(ErrorMessage.DETAIL, { cause: API_FETCH_ERROR }) - ); + return fetchErrorHandling(res, ErrorMessage.DETAIL); } }); } @@ -56,9 +53,7 @@ export async function fetchLectureDetailIndex( if (res.ok) { return (await res.json()) as Promise; } else { - return Promise.reject( - new Error(ErrorMessage.DETAIL_INDEX, { cause: API_FETCH_ERROR }) - ); + return fetchErrorHandling(res, ErrorMessage.DETAIL_INDEX); } }); } @@ -78,9 +73,7 @@ export async function fetchLectureDetailReview( if (res.ok) { return (await res.json()) as Promise; } else { - return Promise.reject( - new Error(ErrorMessage.DETAIL_REVIEW, { cause: API_FETCH_ERROR }) - ); + return fetchErrorHandling(res, ErrorMessage.DETAIL_REVIEW); } }); } @@ -94,9 +87,7 @@ export async function fetchLectureRecommendations() { if (res.ok) { return (await res.json()) as Promise; } else { - return Promise.reject( - new Error(ErrorMessage.RECOMMENDATIONS, { cause: API_FETCH_ERROR }) - ); + return fetchErrorHandling(res, ErrorMessage.RECOMMENDATIONS); } }); } diff --git a/src/api/lectures/time.ts b/src/api/lectures/time.ts index 866a61a..35ea493 100644 --- a/src/api/lectures/time.ts +++ b/src/api/lectures/time.ts @@ -1,7 +1,8 @@ import { getAuthorizedHeaders } from '@/src/util/http/getAuthorizedHeaders'; import { Endpoints } from '../Endpoints'; -import { API_FETCH_ERROR, ErrorMessage } from '../ErrorMessage'; +import { ErrorMessage } from '../ErrorMessage'; import getQueryURL from '@/src/util/http/getQueryURL'; +import { fetchErrorHandling } from '@/src/util/http/fetchErrorHandling'; export async function updateViewDuration( params: CourseTakingParams, @@ -25,9 +26,7 @@ export async function updateViewDuration( if (res.ok) { return (await res.json()) as Promise; } else { - return Promise.reject( - new Error(ErrorMessage.COURSE_TAKING, { cause: API_FETCH_ERROR }) - ); + return fetchErrorHandling(res, ErrorMessage.COURSE_TAKING); } }); } diff --git a/src/api/materials/materials.ts b/src/api/materials/materials.ts index 1c7a190..ef9528d 100644 --- a/src/api/materials/materials.ts +++ b/src/api/materials/materials.ts @@ -1,5 +1,6 @@ +import { fetchErrorHandling } from '@/src/util/http/fetchErrorHandling'; import { Endpoints } from '../Endpoints'; -import { API_FETCH_ERROR, ErrorMessage } from '../ErrorMessage'; +import { ErrorMessage } from '../ErrorMessage'; import { getAuthorizedHeaders } from '@/src/util/http/getAuthorizedHeaders'; export async function fetchCourseMaterials(course_video_id: number) { @@ -11,9 +12,7 @@ export async function fetchCourseMaterials(course_video_id: number) { if (res.ok) { return (await res.json()) as Promise; } else { - return Promise.reject( - new Error(ErrorMessage.COURSE_MATERIALS, { cause: API_FETCH_ERROR }) - ); + return fetchErrorHandling(res, ErrorMessage.COURSE_MATERIALS); } }); } @@ -32,9 +31,7 @@ export async function updateCourseLectureNotes( if (res.ok) { return (await res.json()) as Promise; } else { - return Promise.reject( - new Error(ErrorMessage.LECTURENOTES, { cause: API_FETCH_ERROR }) - ); + return fetchErrorHandling(res, ErrorMessage.LECTURENOTES); } }); } @@ -53,9 +50,7 @@ export async function updateCourseQuizGrade( if (res.ok) { return (await res.json()) as Promise; } else { - return Promise.reject( - new Error(ErrorMessage.QUIZZES, { cause: API_FETCH_ERROR }) - ); + return fetchErrorHandling(res, ErrorMessage.QUIZZES); } }); } diff --git a/src/api/members/members.ts b/src/api/members/members.ts index 708dfc1..1fa7bc0 100644 --- a/src/api/members/members.ts +++ b/src/api/members/members.ts @@ -2,6 +2,7 @@ import getHeaders from '@/src/util/http/getHeaders'; import { Endpoints } from '../Endpoints'; import { API_FETCH_ERROR, ErrorMessage } from '../ErrorMessage'; import { getAuthorizedHeaders } from '@/src/util/http/getAuthorizedHeaders'; +import { fetchErrorHandling } from '@/src/util/http/fetchErrorHandling'; export async function fetchUserAuthWithCredential( credential: GoogleLoginCredential @@ -36,9 +37,7 @@ export async function fetchUserAuthWithRefreshToken( if (res.ok) { return (await res.json()) as Promise; } else { - return Promise.reject( - new Error(ErrorMessage.REFRESH, { cause: API_FETCH_ERROR }) - ); + return fetchErrorHandling(res, ErrorMessage.REFRESH); } }); } @@ -54,9 +53,7 @@ export async function updateUserProfile(name: string) { if (res.ok) { return (await res.json()) as Promise; } else { - return Promise.reject( - new Error(ErrorMessage.PROFILE_UPDATE, { cause: API_FETCH_ERROR }) - ); + fetchErrorHandling(res, ErrorMessage.PROFILE_UPDATE); } }); } diff --git a/src/api/reviews/reviews.ts b/src/api/reviews/reviews.ts index 28a87c4..4a25a3a 100644 --- a/src/api/reviews/reviews.ts +++ b/src/api/reviews/reviews.ts @@ -1,6 +1,7 @@ import { getAuthorizedHeaders } from '@/src/util/http/getAuthorizedHeaders'; import { Endpoints } from '../Endpoints'; -import { API_FETCH_ERROR, ErrorMessage } from '../ErrorMessage'; +import { ErrorMessage } from '../ErrorMessage'; +import { fetchErrorHandling } from '@/src/util/http/fetchErrorHandling'; export async function fetchReviewListByCourse(courseId: number) { const headers = await getAuthorizedHeaders(); @@ -11,9 +12,7 @@ export async function fetchReviewListByCourse(courseId: number) { if (res.ok) { return (await res.json()) as Promise; } else { - return Promise.reject( - new Error(ErrorMessage.REVIEW_LIST, { cause: API_FETCH_ERROR }) - ); + return fetchErrorHandling(res, ErrorMessage.REVIEW_LIST); } }); } @@ -32,9 +31,7 @@ export async function updateLectureReview( if (res.ok) { return (await res.json()) as Promise; } else { - return Promise.reject( - new Error(ErrorMessage.REVIEW_UPDATE, { cause: API_FETCH_ERROR }) - ); + return fetchErrorHandling(res, ErrorMessage.REVIEW_UPDATE); } }); } diff --git a/src/components/gnb/NavBar.tsx b/src/components/gnb/NavBar.tsx index a02ba5d..5169a48 100644 --- a/src/components/gnb/NavBar.tsx +++ b/src/components/gnb/NavBar.tsx @@ -38,6 +38,7 @@ export default function NavBar({ logo, profileDropdown }: Props) { const { mutate } = useMutation(() => updateUserProfile(name), { onSuccess: (data) => { setIsEditMode(false); + if (!data) return; setName(() => data.name); } }); diff --git a/src/constants/query/query.ts b/src/constants/query/query.ts index ef2876c..42ca2fe 100644 --- a/src/constants/query/query.ts +++ b/src/constants/query/query.ts @@ -2,3 +2,4 @@ import { ONE_MINUTE_IN_MS } from "../time/time"; export const CACHE_TIME = ONE_MINUTE_IN_MS * 30; export const STALE_TIME = ONE_MINUTE_IN_MS * 5; +export const UNAUTHORIZED = 401; diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index 005f9b3..de0c293 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -7,13 +7,26 @@ import { ErrorMessage } from '../api/ErrorMessage'; import { ONE_MINUTE_IN_MS } from '../constants/time/time'; import setErrorToast from '../util/toast/setErrorToast'; -const refreshInterval = ONE_MINUTE_IN_MS * 29; - export default function useAuth() { const router = useRouter(); const { data: session, status, update } = useSession(); + const refreshToken = { refresh_token: session?.refresh_token ?? '' }; + const calculateRefreshInterval = () => { + if (!session) return 0; + + const NOW = Date.now(); + const EXPIRES_AT = session.expires_at * 1000; + + const REFRESH_INTERVAL = EXPIRES_AT - NOW - ONE_MINUTE_IN_MS; + + if (REFRESH_INTERVAL <= 0) { + return 0; + } + return REFRESH_INTERVAL; + }; + const silentRefresh = async () => { const response = await fetchUserAuthWithRefreshToken(refreshToken).catch( async () => { @@ -27,7 +40,7 @@ export default function useAuth() { useQuery([QueryKeys.REFRESH], silentRefresh, { enabled: !!session, - refetchInterval: refreshInterval, + refetchInterval: calculateRefreshInterval, refetchOnMount: false, refetchOnWindowFocus: false, refetchIntervalInBackground: true, diff --git a/src/providers/QueryProvider.tsx b/src/providers/QueryProvider.tsx index f051d24..1829088 100644 --- a/src/providers/QueryProvider.tsx +++ b/src/providers/QueryProvider.tsx @@ -3,7 +3,19 @@ import { useState } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; export default function QueryProvider({ children }: React.PropsWithChildren) { - const [queryClient] = useState(() => new QueryClient({})); + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + retry: 2 + }, + mutations: { + retry: 2 + } + } + }) + ); return ( {children} diff --git a/src/util/http/fetchErrorHandling.ts b/src/util/http/fetchErrorHandling.ts new file mode 100644 index 0000000..02a4807 --- /dev/null +++ b/src/util/http/fetchErrorHandling.ts @@ -0,0 +1,16 @@ +import { + API_FETCH_ERROR, + ErrorMessage, + SESSION_ERROR +} from '@/src/api/ErrorMessage'; +import { UNAUTHORIZED } from '@/src/constants/query/query'; + +export function fetchErrorHandling(res: Response, errorMessage: string) { + if (res.status === UNAUTHORIZED) { + return Promise.reject( + new Error(ErrorMessage.UNAUTHORIZED, { cause: SESSION_ERROR }) + ); + } else { + return Promise.reject(new Error(errorMessage, { cause: API_FETCH_ERROR })); + } +}