diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx deleted file mode 100644 index ad5359c..0000000 --- a/src/AppRouter.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { RouterProvider, createBrowserRouter } from "react-router-dom"; -import App from "@/App"; -import { PATH } from "@/constants/path"; -import ContentDetailPage from "@/pages/ContentDetailPage"; -import HomePage from "@/pages/HomePage"; -import LandingPage from "@/pages/LandingPage"; -import MyPage from "@/pages/MyPage"; -import OnBoardingPage from "@/pages/OnBoardingPage"; -import RedirectPage from "@/pages/RedirectPage"; -import AdminPage from "./pages/AdminPage"; -import SearchPage from "./pages/SearchPage"; -import ShortsPage from "./pages/ShortsPage"; - -const AppRouter = () => { - const router = createBrowserRouter([ - { - path: PATH.ROOT, - element: , - children: [ - { - path: "", - element: , - }, - { - path: PATH.HOME, - element: , - }, - { - path: PATH.CONTENT_DETAIL, - element: , - }, - { - path: PATH.SHORTS, - element: , - }, - { - path: PATH.REDIRECT, - element: , - }, - { - path: PATH.ONBOARDING, - element: , - }, - { - path: PATH.MYPAGE, - element: , - }, - { path: PATH.SEARCH, element: }, - ], - }, - { - path: PATH.ADMIN, - element: , - }, - ]); - return ; -}; -export default AppRouter; diff --git a/src/components/common/ErrorFallback.tsx b/src/components/common/ErrorFallback.tsx index e56b233..2b9b4d4 100644 --- a/src/components/common/ErrorFallback.tsx +++ b/src/components/common/ErrorFallback.tsx @@ -2,43 +2,57 @@ import { useNavigate } from "react-router-dom"; import { HTTPError } from "@/apis/HTTPError"; import { Button } from "@/components/ui/button"; import { HTTP_ERROR_MESSAGES } from "@/constants/api"; -import { PATH } from "@/constants/path"; interface ErrorFallbackProps { error: Error; resetErrorBoundary: () => void; } +const shouldRetry = (err: Error): boolean => { + if (err instanceof HTTPError) { + const retryableStatuses = [500, 502, 503]; + return retryableStatuses.includes(err.status); + } + return false; +}; -const ErrorFallback = ({ error, resetErrorBoundary }: ErrorFallbackProps) => { - const navigate = useNavigate(); +const getMessage = (err: Error): string => { + if (err instanceof HTTPError) { + const message = + HTTP_ERROR_MESSAGES[err.status as keyof typeof HTTP_ERROR_MESSAGES]; - const getMessage = (err: Error): string => { - if (err instanceof HTTPError) { - const message = - HTTP_ERROR_MESSAGES[err.status as keyof typeof HTTP_ERROR_MESSAGES]; + if (typeof message === "function") { + return message(err.status); + } - if (typeof message === "function") { - return message(err.status); - } + return message ?? HTTP_ERROR_MESSAGES.DEFAULT(err.status); + } - return message ?? HTTP_ERROR_MESSAGES.DEFAULT(err.status); - } + return err.message || HTTP_ERROR_MESSAGES.UNKNOWN; +}; - return err.message || HTTP_ERROR_MESSAGES.UNKNOWN; - }; +const ErrorFallback = ({ error, resetErrorBoundary }: ErrorFallbackProps) => { + const navigate = useNavigate(); + const retryable = shouldRetry(error); return (

문제가 발생했어요

-

{getMessage(error)}

+

+ {getMessage(error)} +

+
- + {retryable && ( + + )}
diff --git a/src/constants/api.ts b/src/constants/api.ts index 3bfa40b..c239a96 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -46,7 +46,7 @@ export const AUTH_ERROR_CODE = { } as const; export const HTTP_ERROR_MESSAGES = { - 404: "찾으시는 콘텐츠가 존재하지 않아요.\n주소가 정확한지 확인해 주세요.", + 404: "찾으시는 콘텐츠가 존재하지 않아요.", 500: "서버에 문제가 발생했어요.\n조금 뒤에 다시 시도해 주세요.", DEFAULT: (status: number) => `문제가 발생했어요. (에러 코드: ${status})`, UNKNOWN: "알 수 없는 오류가 발생했어요.\n앱을 다시 실행해 보시겠어요?", diff --git a/src/hooks/queries/shorts/useShortsInfiniteQuery.ts b/src/hooks/queries/shorts/useShortsInfiniteQuery.ts index 1cb8eb8..ddf4154 100644 --- a/src/hooks/queries/shorts/useShortsInfiniteQuery.ts +++ b/src/hooks/queries/shorts/useShortsInfiniteQuery.ts @@ -1,10 +1,10 @@ -import { useInfiniteQuery } from "@tanstack/react-query"; +import { useSuspenseInfiniteQuery } from "@tanstack/react-query"; import { getShorts } from "@/apis/shorts/getShorts"; import type { GetShortsResponse } from "@/types/shorts"; -export const useShortsInfiniteQuery = (options?: { enabled?: boolean }) => { - const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = - useInfiniteQuery({ +export const useShortsInfiniteQuery = () => { + const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = + useSuspenseInfiniteQuery({ queryKey: ["shorts"], queryFn: ({ pageParam = null }) => getShorts({ cursor: pageParam as number | undefined }), @@ -12,7 +12,6 @@ export const useShortsInfiniteQuery = (options?: { enabled?: boolean }) => { lastPage.hasNext ? lastPage.nextCursor : undefined, initialPageParam: null, staleTime: 60 * 1000, - enabled: options?.enabled ?? true, }); const shorts = data?.pages.flatMap((page) => page.items) ?? []; @@ -22,6 +21,5 @@ export const useShortsInfiniteQuery = (options?: { enabled?: boolean }) => { fetchNextPage, hasNextPage, isFetchingNextPage, - isLoading, }; }; diff --git a/src/hooks/queries/user/useUserInformationQuery.ts b/src/hooks/queries/user/useUserInformationQuery.ts index e28586a..dcaa2f8 100644 --- a/src/hooks/queries/user/useUserInformationQuery.ts +++ b/src/hooks/queries/user/useUserInformationQuery.ts @@ -1,9 +1,9 @@ -import { useQuery } from "@tanstack/react-query"; +import { useSuspenseQuery } from "@tanstack/react-query"; import getUserInformation from "@/apis/user/getUserInformation"; import type { UserInformation } from "@/types/user"; const useUserInformationQuery = () => { - const { data: userInformation } = useQuery({ + const { data: userInformation } = useSuspenseQuery({ queryKey: ["userInformation"], queryFn: () => getUserInformation(), staleTime: 60 * 60 * 1000, diff --git a/src/hooks/shorts/useShortsToShow.ts b/src/hooks/shorts/useShortsToShow.ts index 0f84f19..443257f 100644 --- a/src/hooks/shorts/useShortsToShow.ts +++ b/src/hooks/shorts/useShortsToShow.ts @@ -3,10 +3,8 @@ import { useShortsByIdQuery } from "@/hooks/queries/shorts/useShortsByIdQuery"; import { useShortsInfiniteQuery } from "@/hooks/queries/shorts/useShortsInfiniteQuery"; export function useShortsToShow(currentShortsId?: string) { - const { shorts, fetchNextPage, hasNextPage, isLoading } = - useShortsInfiniteQuery(); + const { shorts, fetchNextPage, hasNextPage } = useShortsInfiniteQuery(); - // 타입 안전한 ID 비교 함수 const isSameShortsId = (shortsId: number, targetId: string) => { return String(shortsId) === targetId && targetId !== ""; }; @@ -24,22 +22,22 @@ export function useShortsToShow(currentShortsId?: string) { }); const shortsToShow = useMemo(() => { - // currentShortsId가 없으면 기존 목록 반환 + // ID 없으면 기존 리스트 반환 if (!currentShortsId) { return shorts.filter(Boolean); } - // 이미 목록에 있으면 기존 목록 반환 + // 이미 포함되어 있으면 기존 리스트 반환 if (alreadyHasShort) { return shorts.filter(Boolean); } - // 단일 쇼츠가 아직 로딩 중이면 빈 배열 반환 (로딩 완료까지 기다림) + // 단일 쇼츠 로딩 중이면 Suspense fallback이 표시될 것이므로, 일단 비워둠 if (isSingleShortsLoading) { return []; } - // 단일 쇼츠를 성공적으로 가져왔으면 맨 앞에 추가 + // 단일 쇼츠 성공 시, 가장 앞에 추가 if (singleShorts) { return [ singleShorts, @@ -47,7 +45,7 @@ export function useShortsToShow(currentShortsId?: string) { ]; } - // 단일 쇼츠 로딩 실패 시 기존 목록 반환 + // 실패 fallback: 기존 리스트 return shorts.filter(Boolean); }, [ currentShortsId, @@ -61,6 +59,5 @@ export function useShortsToShow(currentShortsId?: string) { shortsToShow, fetchNextPage, hasNextPage, - isLoading: isLoading || isSingleShortsLoading, }; } diff --git a/src/main.tsx b/src/main.tsx index b0a2d7a..98cc018 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,7 +2,7 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { QueryClientProvider } from "@tanstack/react-query"; import { queryClient } from "@/hooks/queries/common/queryClient"; -import AppRouter from "./AppRouter"; +import AppRouter from "./router/AppRouter"; import "./index.css"; createRoot(document.getElementById("root")!).render( diff --git a/src/pages/MyPage.tsx b/src/pages/MyPage.tsx index 9d9439d..faad0e0 100644 --- a/src/pages/MyPage.tsx +++ b/src/pages/MyPage.tsx @@ -1,12 +1,19 @@ +import AsyncBoundary from "@/components/common/AsyncBoundary"; import MyLayout from "@/components/My/MyLayout"; import useUserInformationQuery from "@/hooks/queries/user/useUserInformationQuery"; -const MyPage = () => { +const MyPageContent = () => { const { userInformation } = useUserInformationQuery(); - if (!userInformation) return null; - return ; }; +const MyPage = () => { + return ( + + + + ); +}; + export default MyPage; diff --git a/src/pages/NotFoundPage.tsx b/src/pages/NotFoundPage.tsx new file mode 100644 index 0000000..fc1c6c9 --- /dev/null +++ b/src/pages/NotFoundPage.tsx @@ -0,0 +1,24 @@ +import { useNavigate } from "react-router-dom"; +import { Button } from "@/components/ui/button"; + +const NotFoundPage = () => { + const navigate = useNavigate(); + + return ( +
+

+ 페이지를 찾을 수 없습니다 +

+

+ 요청하신 주소가 존재하지 않거나 변경되었습니다. +

+ +
+ ); +}; + +export default NotFoundPage; diff --git a/src/pages/ShortsPage.tsx b/src/pages/ShortsPage.tsx index 24becb1..c11652c 100644 --- a/src/pages/ShortsPage.tsx +++ b/src/pages/ShortsPage.tsx @@ -1,6 +1,8 @@ +/* eslint-disable no-use-before-define */ import { useRef, useEffect } from "react"; import { useParams } from "react-router-dom"; import { useQueryClient } from "@tanstack/react-query"; +import AsyncBoundary from "@/components/common/AsyncBoundary"; import ShortsLayout from "@/components/shorts/ShortsLayout"; import { useActiveShortsId } from "@/hooks/shorts/useActiveShortsId"; import { useShortsToShow } from "@/hooks/shorts/useShortsToShow"; @@ -9,36 +11,48 @@ import { useShortsWatchTimeTracker } from "@/hooks/shorts/useShortsWatchTimeTrac const ShortsPage = () => { const { id: currentShortsId } = useParams<{ id: string }>(); const queryClient = useQueryClient(); - const { shortsToShow, fetchNextPage, hasNextPage, isLoading } = - useShortsToShow(currentShortsId); - const cardRefs = useRef<(HTMLDivElement | null)[]>([]); - // 쇼츠 페이지를 벗어날 때 무한스크롤 캐시 리셋 useEffect(() => { return () => { - // cleanup 함수: 컴포넌트가 언마운트될 때 실행 queryClient.resetQueries({ queryKey: ["shorts"] }); }; }, [queryClient]); + useShortsWatchTimeTracker({ activeShortsId: currentShortsId }); + + return ( + + + + ); +}; + +function ShortsContent({ + cardRefs, + currentShortsId, +}: { + cardRefs: React.MutableRefObject<(HTMLDivElement | null)[]>; + currentShortsId?: string; +}) { + const { shortsToShow, fetchNextPage, hasNextPage } = + useShortsToShow(currentShortsId); + useActiveShortsId({ shortsToShow, cardRefs, currentShortsId, }); - useShortsWatchTimeTracker({ activeShortsId: currentShortsId }); - return ( ); -}; +} export default ShortsPage; diff --git a/src/router/AppRouter.tsx b/src/router/AppRouter.tsx new file mode 100644 index 0000000..7d1d4b3 --- /dev/null +++ b/src/router/AppRouter.tsx @@ -0,0 +1,100 @@ +import { Suspense } from "react"; +import { RouterProvider, createBrowserRouter } from "react-router-dom"; +import App from "@/App"; +import { PATH } from "@/constants/path"; +import * as Lazy from "./lazy"; + +const AppRouter = () => { + const router = createBrowserRouter([ + { + path: PATH.ROOT, + element: , + children: [ + { + path: "", + element: ( + + + + ), + }, + { + path: PATH.HOME, + element: ( + + + + ), + }, + { + path: PATH.CONTENT_DETAIL, + element: ( + + + + ), + }, + { + path: PATH.SHORTS, + element: ( + + + + ), + }, + { + path: PATH.REDIRECT, + element: ( + + + + ), + }, + { + path: PATH.ONBOARDING, + element: ( + + + + ), + }, + { + path: PATH.MYPAGE, + element: ( + + + + ), + }, + { + path: PATH.SEARCH, + element: ( + + + + ), + }, + { + path: "*", + element: ( + + + + ), + }, + ], + }, + { + path: PATH.ADMIN, + element: ( + + + + ), + }, + ]); + + return ; +}; + +export default AppRouter; diff --git a/src/router/lazy.ts b/src/router/lazy.ts new file mode 100644 index 0000000..c1f5862 --- /dev/null +++ b/src/router/lazy.ts @@ -0,0 +1,14 @@ +import { lazy } from "react"; + +export const LandingPage = lazy(() => import("@/pages/LandingPage")); +export const HomePage = lazy(() => import("@/pages/HomePage")); +export const ContentDetailPage = lazy( + () => import("@/pages/ContentDetailPage") +); +export const ShortsPage = lazy(() => import("@/pages/ShortsPage")); +export const RedirectPage = lazy(() => import("@/pages/RedirectPage")); +export const OnBoardingPage = lazy(() => import("@/pages/OnBoardingPage")); +export const MyPage = lazy(() => import("@/pages/MyPage")); +export const SearchPage = lazy(() => import("@/pages/SearchPage")); +export const AdminPage = lazy(() => import("@/pages/AdminPage")); +export const NotFoundPage = lazy(() => import("@/pages/NotFoundPage")); diff --git a/tailwind.config.js b/tailwind.config.js index 6748332..25ac597 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -60,7 +60,7 @@ export default { }, custom: { black: "#141516", - point: "#40FEA4", + point: "#2FC88F", gray: "#A1A1A1", darkgray: "#4A4949", },