Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 0 additions & 58 deletions src/AppRouter.tsx

This file was deleted.

50 changes: 32 additions & 18 deletions src/components/common/ErrorFallback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="flex flex-col items-center justify-center gap-4 h-full w-full text-center p-6">
<h1 className="text-3xl font-bold text-custom-point">
문제가 발생했어요
</h1>
<p className="text-muted-foreground text-sm">{getMessage(error)}</p>
<p className="text-muted-foreground text-sm whitespace-pre-line">
{getMessage(error)}
</p>

<div className="flex gap-3 mt-4">
<Button onClick={resetErrorBoundary}>다시 시도</Button>
{retryable && (
<Button onClick={resetErrorBoundary} variant="outline">
다시 시도
</Button>
)}
<Button
onClick={() => navigate(PATH.HOME)}
onClick={() => navigate(-1)}
className="bg-custom-point text-custom-black">
홈으로
이전 페이지로
</Button>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/constants/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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앱을 다시 실행해 보시겠어요?",
Expand Down
10 changes: 4 additions & 6 deletions src/hooks/queries/shorts/useShortsInfiniteQuery.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
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<GetShortsResponse>({
export const useShortsInfiniteQuery = () => {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useSuspenseInfiniteQuery<GetShortsResponse>({
queryKey: ["shorts"],
queryFn: ({ pageParam = null }) =>
getShorts({ cursor: pageParam as number | undefined }),
getNextPageParam: (lastPage) =>
lastPage.hasNext ? lastPage.nextCursor : undefined,
initialPageParam: null,
staleTime: 60 * 1000,
enabled: options?.enabled ?? true,
});

const shorts = data?.pages.flatMap((page) => page.items) ?? [];
Expand All @@ -22,6 +21,5 @@ export const useShortsInfiniteQuery = (options?: { enabled?: boolean }) => {
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
};
};
4 changes: 2 additions & 2 deletions src/hooks/queries/user/useUserInformationQuery.ts
Original file line number Diff line number Diff line change
@@ -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<UserInformation>({
const { data: userInformation } = useSuspenseQuery<UserInformation>({
queryKey: ["userInformation"],
queryFn: () => getUserInformation(),
staleTime: 60 * 60 * 1000,
Expand Down
15 changes: 6 additions & 9 deletions src/hooks/shorts/useShortsToShow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 !== "";
};
Expand All @@ -24,30 +22,30 @@ 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,
...shorts.filter((s) => !isSameShortsId(s.shortsId, currentShortsId)),
];
}

// 단일 쇼츠 로딩 실패 기존 목록 반환
// 실패 fallback: 기존 리스트
return shorts.filter(Boolean);
}, [
currentShortsId,
Expand All @@ -61,6 +59,5 @@ export function useShortsToShow(currentShortsId?: string) {
shortsToShow,
fetchNextPage,
hasNextPage,
isLoading: isLoading || isSingleShortsLoading,
};
}
2 changes: 1 addition & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
13 changes: 10 additions & 3 deletions src/pages/MyPage.tsx
Original file line number Diff line number Diff line change
@@ -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 <MyLayout userInformation={userInformation} />;
};

const MyPage = () => {
return (
<AsyncBoundary>
<MyPageContent />
</AsyncBoundary>
);
};

export default MyPage;
24 changes: 24 additions & 0 deletions src/pages/NotFoundPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button";

const NotFoundPage = () => {
const navigate = useNavigate();

return (
<div className="flex flex-col items-center justify-center h-screen bg-black text-white text-center gap-4 px-6">
<h1 className="text-3xl font-bold text-custom-point">
페이지를 찾을 수 없습니다
</h1>
<p className="text-custom-gray">
요청하신 주소가 존재하지 않거나 변경되었습니다.
</p>
<Button
onClick={() => navigate("/home")}
className="bg-custom-point text-black font-bold px-6 py-2 mt-4">
홈으로
</Button>
</div>
);
};

export default NotFoundPage;
32 changes: 23 additions & 9 deletions src/pages/ShortsPage.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 (
<AsyncBoundary>
<ShortsContent cardRefs={cardRefs} currentShortsId={currentShortsId} />
</AsyncBoundary>
);
};

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 (
<ShortsLayout
shorts={shortsToShow}
fetchNextPage={fetchNextPage}
hasNextPage={hasNextPage}
isLoading={isLoading}
isLoading={false}
cardRefs={cardRefs}
/>
);
};
}

export default ShortsPage;
Loading