(null);
- const { setIsLogined } = useAuthStore();
- const { openModal, closeModal } = useModalStore();
-
- useEffect(() => {
- queryClient.clear();
- }, []);
+import { Suspense } from "react";
+import LoginPageClient from "@/widgets/main/ui/Client/LoginPage.client";
+export default function Page() {
return (
-
-
로그인
- {
- openModal("normal", {
- message: "로그인에 성공하였습니다.",
- onClick: () => {
- closeModal();
- setIsLogined(true);
- router.push("/"); //메인 페이지 이동
- },
- });
- }}
- onError={(msg) => {
- setErrorMessage(msg);
- }}
- />
-
- {!!errorMessage && (
- setErrorMessage(null)}
- className=""
- />
- )}
-
+
+
+
);
}
diff --git a/src/app/my/page.tsx b/src/app/my/page.tsx
index a8f55534..4b11121d 100644
--- a/src/app/my/page.tsx
+++ b/src/app/my/page.tsx
@@ -7,21 +7,26 @@ import {
import { getMyPosts } from "@/entities/user/api/getMyPosts.server";
import { getMyProfile } from "@/entities/user/api/getMyProfile.server";
import MyPageClient from "@/widgets/mypage/ui/Client/MyPage.client";
+import { handleError } from "@/shared/error/handleError";
const DEFAULT_TAB = "selling";
export default async function Page() {
const queryClient = new QueryClient();
- await queryClient.prefetchQuery({
- queryKey: ["userProfile"],
- queryFn: getMyProfile,
- });
+ try {
+ await queryClient.fetchQuery({
+ queryKey: ["userProfile"],
+ queryFn: getMyProfile,
+ });
- await queryClient.prefetchQuery({
- queryKey: ["myPosts", DEFAULT_TAB],
- queryFn: () => getMyPosts(DEFAULT_TAB),
- });
+ await queryClient.fetchQuery({
+ queryKey: ["myPosts", DEFAULT_TAB],
+ queryFn: () => getMyPosts(DEFAULT_TAB),
+ });
+ } catch (e) {
+ handleError(e);
+ }
return (
diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx
new file mode 100644
index 00000000..c83bc0fc
--- /dev/null
+++ b/src/app/not-found.tsx
@@ -0,0 +1,5 @@
+import NotFoundErrorPage from "@/shared/error/ui/404";
+
+export default function GlobalNotFound() {
+ return ;
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index d91cdc64..e358a99c 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -5,6 +5,7 @@ import {
} from "@tanstack/react-query";
import { getPosts } from "@/entities/post/api/getPosts.server";
import HomePageClient from "@/widgets/main/ui/Client/HomePage.client";
+import { handleError } from "@/shared/error/handleError";
export default async function Page({
searchParams,
@@ -18,17 +19,21 @@ export default async function Page({
const queryClient = new QueryClient();
- await queryClient.prefetchInfiniteQuery({
- queryKey: ["posts", initialCategory, initialSort, initialKeyword],
- queryFn: ({ pageParam = 1 }) =>
- getPosts({
- category: initialCategory,
- sort: initialSort,
- page: pageParam,
- keyword: initialKeyword,
- }),
- initialPageParam: 1,
- });
+ try {
+ await queryClient.fetchInfiniteQuery({
+ queryKey: ["posts", initialCategory, initialSort, initialKeyword],
+ queryFn: ({ pageParam = 1 }) =>
+ getPosts({
+ category: initialCategory,
+ sort: initialSort,
+ page: pageParam,
+ keyword: initialKeyword,
+ }),
+ initialPageParam: 1,
+ });
+ } catch (e) {
+ handleError(e);
+ }
return (
diff --git a/src/entities/chat/lib/useChatMessages.ts b/src/entities/chat/lib/useChatMessages.ts
index 2f0ae757..374c8cd8 100644
--- a/src/entities/chat/lib/useChatMessages.ts
+++ b/src/entities/chat/lib/useChatMessages.ts
@@ -3,19 +3,18 @@ import {
useInfiniteQuery,
useQueryClient,
} from "@tanstack/react-query";
-import { useEffect, useRef, useMemo } from "react";
+import { useRef, useMemo } from "react";
import { apiFetch } from "@/shared/api/fetcher";
import { MessagesResponse, MessageProps, Chat } from "../model/types";
-import { useModalStore } from "@/shared/model/modal.store";
export const useChatMessages = (chatId: number | null) => {
const queryClient = useQueryClient();
const scrollContainerRef = useRef(null);
const messagesEndRef = useRef(null);
- const { openModal, closeModal } = useModalStore();
const {
data,
+ isError,
error,
fetchNextPage,
hasNextPage,
@@ -38,15 +37,6 @@ export const useChatMessages = (chatId: number | null) => {
lastPage.hasNext ? lastPage.nextCursor : undefined,
});
- useEffect(() => {
- if (error) {
- openModal("normal", {
- message: "메시지 로딩 중 에러가 발생했습니다.",
- onClick: closeModal,
- });
- }
- }, [error]);
-
const messages: MessageProps[] = useMemo(() => {
if (!data) return [];
@@ -121,6 +111,7 @@ export const useChatMessages = (chatId: number | null) => {
hasNextPage,
isMessagesFirstLoading,
isMessagesLoading,
+ isError,
error,
scrollContainerRef,
messagesEndRef,
diff --git a/src/entities/chat/ui/ChattingRoom/ChattingRoom.tsx b/src/entities/chat/ui/ChattingRoom/ChattingRoom.tsx
index 5bc67d22..b48f0106 100644
--- a/src/entities/chat/ui/ChattingRoom/ChattingRoom.tsx
+++ b/src/entities/chat/ui/ChattingRoom/ChattingRoom.tsx
@@ -18,6 +18,7 @@ import { getPostDetail } from "@/entities/post/api/getPostDetail";
import { getUser } from "@/entities/user/api/getUser";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { createChattingRoom } from "@/features/chat/api/createChattingRoom";
+import { handleError } from "@/shared/error/handleError";
export const ChattingRoom = ({
postingId,
@@ -36,31 +37,46 @@ export const ChattingRoom = ({
data: post,
isLoading: isPostLoading,
isError: isPostingError,
+ error: postingError,
} = useQuery({
queryKey: ["postDetail", postingId],
queryFn: () => getPostDetail(postingId),
});
+ if (isPostingError) {
+ handleError(postingError);
+ }
+
const {
data: otherUser,
isLoading: isOtherUserLoading,
isError: isOtherUserError,
+ error: otherUserError,
} = useQuery({
queryKey: ["otherUser", otherId],
queryFn: () => getUser(otherId),
});
+ if (isOtherUserError) {
+ handleError(otherUserError);
+ }
+
const {
messages,
pushMessageToCache,
fetchMoreMessages,
hasNextPage,
- isMessagesFirstLoading,
+ isError: isChatMessagesError,
+ error: chatMessagesError,
isMessagesLoading,
scrollContainerRef,
messagesEndRef,
} = useChatMessages(chatId);
+ if (isChatMessagesError) {
+ handleError(chatMessagesError);
+ }
+
const {
postStatus: currentPostStatus,
dealStatus: currentDealStatus,
diff --git a/src/features/chat/ui/ChatList.tsx b/src/features/chat/ui/ChatList.tsx
index 8959d2f3..437c6b1b 100644
--- a/src/features/chat/ui/ChatList.tsx
+++ b/src/features/chat/ui/ChatList.tsx
@@ -5,6 +5,7 @@ import { fetchChatList } from "../model/chat.api";
import type { Chat } from "@/entities/chat/model/types";
import { useQuery } from "@tanstack/react-query";
import { DealStatus } from "@/entities/chat/model/types";
+import { handleError } from "@/shared/error/handleError";
interface ChatListProps {
onSelect: (info: {
@@ -29,12 +30,12 @@ const ChatList = ({ onSelect, tab = "all" }: ChatListProps) => {
queryFn: () => fetchChatList(role),
});
- if (isLoading) {
- return 불러오는 중...
;
+ if (isError) {
+ handleError(error);
}
- if (isError) {
- console.error("채팅 목록 불러오기 실패:", error);
+ if (isLoading) {
+ return 불러오는 중...
;
}
if (!chats?.length) {
diff --git a/src/shared/api/fetcher.server.ts b/src/shared/api/fetcher.server.ts
index 176cf5dd..a29359c4 100644
--- a/src/shared/api/fetcher.server.ts
+++ b/src/shared/api/fetcher.server.ts
@@ -1,5 +1,5 @@
import { cookies } from "next/headers";
-import { redirect } from "next/navigation";
+import { AuthorizationError, NotFoundError, ServerError } from "../error/error";
const BASE_URL = process.env.NEXT_PUBLIC_API_URL;
@@ -34,7 +34,9 @@ export async function serverFetch(
const refreshToken = cookieStore.get("refreshToken")?.value;
if (!refreshToken) {
- redirect("/login");
+ throw new AuthorizationError(
+ "세션이 만료되었습니다.\n다시 로그인 해주세요.",
+ );
}
const refreshRes = await fetch("/api/auth/refresh", {
@@ -59,13 +61,23 @@ export async function serverFetch(
...restOptions,
});
} else {
- redirect("/login");
+ throw new AuthorizationError(
+ "세션이 만료되었습니다.\n다시 로그인 해주세요.",
+ );
}
}
+ if (res.status === 404) {
+ throw new NotFoundError();
+ }
+
+ if (res.status >= 500) {
+ throw new ServerError();
+ }
+
if (!res.ok) {
const text = await res.text();
- throw new Error(text || `API Error ${res.status}`);
+ throw new ServerError(text);
}
return res.json() as Promise;
diff --git a/src/shared/api/fetcher.ts b/src/shared/api/fetcher.ts
index 34afef4e..1c54e836 100644
--- a/src/shared/api/fetcher.ts
+++ b/src/shared/api/fetcher.ts
@@ -1,5 +1,5 @@
import { useAuthStore } from "@/features/auth/model/auth.store";
-import { useModalStore } from "@/shared/model/modal.store";
+import { AuthorizationError, NotFoundError, ServerError } from "../error/error";
const BASE_URL = process.env.NEXT_PUBLIC_API_URL;
@@ -15,7 +15,6 @@ export async function apiFetch(
const { headers, noAuth, useBaseUrl = true, ...restOptions } = options;
const { accessToken, setAccessToken, logout } = useAuthStore.getState();
- const { openModal, closeModal } = useModalStore.getState();
const defaultHeaders: HeadersInit = {
"Content-Type": "application/json",
@@ -42,17 +41,9 @@ export async function apiFetch(
if (!refreshed.ok) {
logout();
-
- openModal("normal", {
- message: "세션이 만료되었습니다. 다시 로그인 해주세요.",
- buttonText: "확인",
- onClick: () => {
- closeModal();
- location.replace("/login");
- },
- });
-
- throw new Error("세션 만료");
+ throw new AuthorizationError(
+ "세션이 만료되었습니다.\n다시 로그인 해주세요.",
+ );
}
const { accessToken: newToken } = await refreshed.json();
@@ -69,6 +60,13 @@ export async function apiFetch(
}
if (!res.ok) {
+ if (res.status === 404) {
+ throw new NotFoundError();
+ }
+
+ if (res.status >= 500) {
+ throw new ServerError();
+ }
let message = `API Error ${res.status}`;
try {
@@ -79,7 +77,7 @@ export async function apiFetch(
if (text) message = text;
}
- throw new Error(message);
+ throw new ServerError(message);
}
return res.json() as Promise;
diff --git a/src/shared/error/error.ts b/src/shared/error/error.ts
new file mode 100644
index 00000000..66b57be4
--- /dev/null
+++ b/src/shared/error/error.ts
@@ -0,0 +1,27 @@
+export class BaseError extends Error {
+ constructor(
+ message: string,
+ public readonly status?: number,
+ ) {
+ super(message);
+ this.name = this.constructor.name;
+ }
+}
+
+export class AuthorizationError extends BaseError {
+ constructor(message = "인증이 필요합니다.") {
+ super(message, 401);
+ }
+}
+
+export class NotFoundError extends BaseError {
+ constructor(message = "요청한 리소스를 찾을 수 없습니다.") {
+ super(message, 404);
+ }
+}
+
+export class ServerError extends BaseError {
+ constructor(message = "서버 오류가 발생했습니다.") {
+ super(message, 500);
+ }
+}
diff --git a/src/shared/error/handleError.ts b/src/shared/error/handleError.ts
new file mode 100644
index 00000000..ce700811
--- /dev/null
+++ b/src/shared/error/handleError.ts
@@ -0,0 +1,19 @@
+import { notFound, redirect } from "next/navigation";
+import { AuthorizationError, NotFoundError, ServerError } from "./error";
+
+export function handleError(error: unknown) {
+ console.log(error);
+ if (error instanceof AuthorizationError) {
+ redirect("/login?expired=true");
+ }
+
+ if (error instanceof NotFoundError) {
+ notFound(); //가장 가까운 not-found.tsx로 이동
+ }
+
+ if (error instanceof ServerError) {
+ throw error;
+ }
+
+ throw error;
+}
diff --git a/src/shared/error/ui/404.tsx b/src/shared/error/ui/404.tsx
new file mode 100644
index 00000000..f9e43b0b
--- /dev/null
+++ b/src/shared/error/ui/404.tsx
@@ -0,0 +1,28 @@
+"use client";
+
+import { useEffect } from "react";
+import { useChatStore } from "@/features/chat/model/chat.store";
+import Link from "next/link";
+
+export default function NotFoundErrorPage({ message }: { message?: string }) {
+ const { unmount } = useChatStore();
+
+ useEffect(() => {
+ unmount();
+ }, [unmount]);
+
+ return (
+
+
페이지를 찾을 수 없습니다
+
+ {message ?? "요청한 페이지가 존재하지 않습니다."}
+
+
+ 홈으로 돌아가기
+
+
+ );
+}
diff --git a/src/shared/error/ui/500.tsx b/src/shared/error/ui/500.tsx
new file mode 100644
index 00000000..7b9ceedb
--- /dev/null
+++ b/src/shared/error/ui/500.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+import { useEffect } from "react";
+import { useChatStore } from "@/features/chat/model/chat.store";
+
+export default function ServerErrorPage({ message }: { message?: string }) {
+ const { unmount } = useChatStore();
+
+ useEffect(() => {
+ unmount();
+ }, [unmount]);
+
+ return (
+
+
서버 오류 발생
+
+ {message ?? "잠시 후 다시 시도해주세요."}
+
+
+ );
+}
diff --git a/src/widgets/main/ui/Client/LoginPage.client.tsx b/src/widgets/main/ui/Client/LoginPage.client.tsx
new file mode 100644
index 00000000..ec7ee67a
--- /dev/null
+++ b/src/widgets/main/ui/Client/LoginPage.client.tsx
@@ -0,0 +1,50 @@
+"use client";
+
+import React, { useEffect, useState } from "react";
+import { LoginForm } from "@/features/auth/ui/LoginForm/LoginForm";
+import { Modal } from "@/shared/ui/Modal/Modal";
+import { useRouter, useSearchParams } from "next/navigation";
+import { useAuthStore } from "@/features/auth/model/auth.store";
+import { useChatStore } from "@/features/chat/model/chat.store";
+export default function LoginPageClient() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const expired = searchParams.get("expired");
+
+ const [errorMessage, setErrorMessage] = useState(null);
+ const { setIsLogined, logout } = useAuthStore();
+ const { unmount } = useChatStore();
+
+ useEffect(() => {
+ if (expired === "true") {
+ unmount();
+ logout();
+ setErrorMessage("세션이 만료되었습니다.\n다시 로그인해주세요.");
+ }
+ }, [expired]);
+
+ return (
+
+
로그인
+ {
+ setIsLogined(true);
+ router.push("/"); //메인 페이지 이동
+ }}
+ onError={(msg) => {
+ setErrorMessage(msg);
+ }}
+ />
+
+ {!!errorMessage && (
+ setErrorMessage(null)}
+ className=""
+ />
+ )}
+
+ );
+}
diff --git a/src/widgets/mypage/ui/Client/MyPage.client.tsx b/src/widgets/mypage/ui/Client/MyPage.client.tsx
index 17dfa2ba..54ec3ca7 100644
--- a/src/widgets/mypage/ui/Client/MyPage.client.tsx
+++ b/src/widgets/mypage/ui/Client/MyPage.client.tsx
@@ -11,6 +11,7 @@ import { useModalStore } from "@/shared/model/modal.store";
import { usePostCreateModal } from "@/features/createPost/lib/usePostCreateModal";
import { getMyPosts, getMyProfile } from "@/entities/user/api/mypage";
import type { Post } from "@/entities/post/model/types/post";
+import { handleError } from "@/shared/error/handleError";
const options = [
{ label: "판매중 상품", value: "selling" },
@@ -34,20 +35,32 @@ export default function MyPageClient({ defaultTab }: { defaultTab: string }) {
data: userProfile,
isLoading: profileLoading,
refetch: refetchUserProfile,
+ isError: isUserProfileError,
+ error: userProfileError,
} = useQuery({
queryKey: ["userProfile"],
queryFn: getMyProfile,
});
+ if (isUserProfileError) {
+ handleError(userProfileError);
+ }
+
const {
data: postsData,
isLoading: postsLoading,
refetch: refetchPosts,
+ isError: isUserPostsError,
+ error: userPostsError,
} = useQuery<{ data: Post[] }>({
queryKey: ["myPosts", selectedTab],
queryFn: () => getMyPosts(selectedTab),
});
+ if (isUserPostsError) {
+ handleError(userPostsError);
+ }
+
const { openPostCreateModal } = usePostCreateModal({
onSuccess: async () => {
await refetchPosts();
diff --git a/src/widgets/postDetail/ui/DetailPage.client.tsx b/src/widgets/postDetail/ui/DetailPage.client.tsx
index 31526411..2d9b6e49 100644
--- a/src/widgets/postDetail/ui/DetailPage.client.tsx
+++ b/src/widgets/postDetail/ui/DetailPage.client.tsx
@@ -5,6 +5,8 @@ import { getPostDetail } from "@/entities/post/api/getPostDetail";
import { PostDetailSection } from "@/widgets/postDetail/ui/PostDetailSection";
import { SellerPostsSection } from "@/widgets/postDetail/ui/SellerPostsSection";
import { PostDetailSkeleton } from "@/widgets/postDetail/ui/DetailSkeleton";
+import { handleError } from "@/shared/error/handleError";
+import { notFound } from "next/navigation";
export default function PostDetailPageClient({
postingId,
@@ -13,20 +15,27 @@ export default function PostDetailPageClient({
}) {
const id = Number(postingId);
- const { data: post, isLoading } = useQuery({
+ const {
+ data: post,
+ isLoading,
+ isError,
+ error,
+ } = useQuery({
queryKey: ["postDetail", id],
queryFn: () => getPostDetail(id),
staleTime: Infinity,
});
+ if (isError) {
+ handleError(error);
+ }
+
if (isLoading) {
return ;
}
if (!post) {
- return (
- 게시글 정보를 찾을 수 없습니다.
- );
+ notFound();
}
return (
diff --git a/src/widgets/postDetail/ui/PostDetailSection.tsx b/src/widgets/postDetail/ui/PostDetailSection.tsx
index 30b14ee3..ea4ef82b 100644
--- a/src/widgets/postDetail/ui/PostDetailSection.tsx
+++ b/src/widgets/postDetail/ui/PostDetailSection.tsx
@@ -19,6 +19,7 @@ import { useAuthStore } from "@/features/auth/model/auth.store";
import { PostDetail } from "@/entities/post/model/types/post";
import PostStatusBadge from "@/entities/post/ui/badge/PostStatusBadge";
import { getChattingRoomStatus } from "@/features/chat/api/getChattingRoomStatus";
+import { handleError } from "@/shared/error/handleError";
export function PostDetailSection({ post }: { post: PostDetail }) {
const router = useRouter();
@@ -27,17 +28,33 @@ export function PostDetailSection({ post }: { post: PostDetail }) {
const { openModal, closeModal } = useModalStore();
const openChat = useChatStore((s) => s.mount);
- const { data: seller } = useQuery({
+ const {
+ data: seller,
+ isError: isSellerError,
+ error: sellerError,
+ } = useQuery({
queryKey: ["seller", post.sellerId],
queryFn: () => getUser(post.sellerId),
enabled: !!post.sellerId,
});
- const { data: chattingRoomStatus } = useQuery({
+ if (isSellerError) {
+ handleError(sellerError);
+ }
+
+ const {
+ data: chattingRoomStatus,
+ isError: isChattingRoomStatusError,
+ error: chattingRoomStatusError,
+ } = useQuery({
queryKey: ["findRoom", post.postingId],
queryFn: () => getChattingRoomStatus(post.postingId),
});
+ if (isChattingRoomStatusError) {
+ handleError(chattingRoomStatusError);
+ }
+
const { toggleLike, isLoading: isLikeLoading } = useLike(post.postingId);
const handleToggleLike = () => toggleLike(!post.isFavorite);