diff --git a/src/app/error.tsx b/src/app/error.tsx index 1a900dd4..3b03163e 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -1,9 +1,11 @@ "use client"; + import ServerErrorPage from "@/shared/error/ui/500"; + export default function GlobalError({ error }: { error: Error }) { return ( - - + + diff --git a/src/app/my/page.tsx b/src/app/my/page.tsx index b6f55c8f..e417fc30 100644 --- a/src/app/my/page.tsx +++ b/src/app/my/page.tsx @@ -17,15 +17,16 @@ export default async function Page() { const queryClient = new QueryClient(); try { - await queryClient.fetchQuery({ - queryKey: ["userProfile"], - queryFn: getMyProfile, - }); - - await queryClient.fetchQuery({ - queryKey: ["myPosts", DEFAULT_TAB], - queryFn: () => getMyPosts(DEFAULT_TAB), - }); + await Promise.all([ + queryClient.fetchQuery({ + queryKey: ["userProfile"], + queryFn: getMyProfile, + }), + queryClient.fetchQuery({ + queryKey: ["myPosts", DEFAULT_TAB], + queryFn: () => getMyPosts(DEFAULT_TAB), + }), + ]); } catch (e) { handleError(e); } diff --git a/src/features/editPost/lib/usePostEditModal.ts b/src/features/editPost/lib/usePostEditModal.ts index 584cb74f..445dcab3 100644 --- a/src/features/editPost/lib/usePostEditModal.ts +++ b/src/features/editPost/lib/usePostEditModal.ts @@ -43,7 +43,7 @@ export const usePostEditModal = (handlers?: { handlers?.onFailure?.(); openModal("normal", { - message: "게시물 수정 중 오류가 발생했습니다. " + message, + message: "게시물 수정을 실패했습니다.", buttonText: "확인", onClick: () => closeModal(), }); diff --git a/src/features/editProfile/lib/useProfileEditModal.ts b/src/features/editProfile/lib/useProfileEditModal.ts new file mode 100644 index 00000000..fabdfe45 --- /dev/null +++ b/src/features/editProfile/lib/useProfileEditModal.ts @@ -0,0 +1,56 @@ +"use client"; + +import { useModalStore } from "@/shared/model/modal.store"; + +export const useEditProfileModal = ( + userProfile?: { + userId: number; + nickname: string; + introduction?: string; + imageUrl?: string; + category?: string; + }, + handlers?: { + onSuccess?: () => void; + onFailure?: () => void; + }, +) => { + const { openModal, closeModal } = useModalStore(); + + const openEditProfileModal = () => { + if (!userProfile) return; + + openModal("editProfile", { + ...userProfile, + onClose: () => closeModal(), + + onSave: async () => { + closeModal(); + + handlers?.onSuccess?.(); + + setTimeout(() => { + openModal("normal", { + message: "프로필이 성공적으로 수정되었습니다.", + buttonText: "확인", + onClick: () => closeModal(), + }); + }, 100); + }, + + onError: () => { + closeModal(); + + handlers?.onFailure?.(); + + openModal("normal", { + message: "프로필 수정에 실패했습니다.", + buttonText: "확인", + onClick: () => closeModal(), + }); + }, + }); + }; + + return { openEditProfileModal }; +}; diff --git a/src/features/like/lib/useLike.ts b/src/features/like/lib/useLike.ts index 2db575be..69c2d2e8 100644 --- a/src/features/like/lib/useLike.ts +++ b/src/features/like/lib/useLike.ts @@ -1,8 +1,10 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { updateFavorite } from "../api/updateFavorite"; +import { useModalStore } from "@/shared/model/modal.store"; export function useLike(postingId?: number) { const queryClient = useQueryClient(); + const { openModal, closeModal } = useModalStore(); const mutation = useMutation({ mutationFn: async (liked: boolean) => { @@ -33,7 +35,13 @@ export function useLike(postingId?: number) { onError: (err, newLiked, context) => { if (!postingId || !context?.previousData) return; + openModal("normal", { + message: "좋아요 처리 중 오류가 발생했습니다.", + onClick: () => closeModal(), + }); queryClient.setQueryData(["postDetail", postingId], context.previousData); + + console.error(err); }, }); diff --git a/src/shared/api/fetcher.server.ts b/src/shared/api/fetcher.server.ts index f960a420..2653b7a5 100644 --- a/src/shared/api/fetcher.server.ts +++ b/src/shared/api/fetcher.server.ts @@ -1,10 +1,15 @@ import { headers } from "next/headers"; -import { NotFoundError, ServerError, AuthorizationError } from "../error/error"; +import { + NotFoundError, + ServerError, + AuthorizationError, + BaseError, +} from "../error/error"; import { getBaseUrl } from "./config"; export async function serverFetch( endpoint: string, - options: RequestInit, + options: RequestInit = {}, ): Promise { const { headers: extraHeaders = {}, ...restOptions } = options; @@ -22,28 +27,29 @@ export async function serverFetch( }, cache: "no-store", ...restOptions, - }); - - if (res.status === 401) { - throw new AuthorizationError( - "세션이 만료되었습니다.\n다시 로그인 해주세요.", + }).catch(() => { + throw new ServerError( + "네트워크 오류가 발생했습니다.\n인터넷 연결을 확인하고 다시 시도해주세요.", + res.status, ); - } + }); if (!res.ok) { - if (res.status === 404) throw new NotFoundError(); - if (res.status >= 500) throw new ServerError(); + let message = `요청 실패 (${res.status})`; - let message = `API Error ${res.status}`; try { const data = await res.json(); - message = data.message ?? data.detail ?? message; + if (typeof data === "object" && data?.message) { + message = data.message; + } } catch { - const text = await res.text(); - if (text) message = text; + // 기본 메시지 } - throw new ServerError(message); + if (res.status === 401) throw new AuthorizationError(); + if (res.status === 404) throw new NotFoundError(); + if (res.status >= 500) throw new ServerError(message, res.status); + throw new BaseError(message, res.status); } return res.json() as Promise; diff --git a/src/shared/api/fetcher.ts b/src/shared/api/fetcher.ts index b49d1907..ea75254c 100644 --- a/src/shared/api/fetcher.ts +++ b/src/shared/api/fetcher.ts @@ -1,4 +1,9 @@ -import { AuthorizationError, NotFoundError, ServerError } from "../error/error"; +import { + AuthorizationError, + NotFoundError, + ServerError, + BaseError, +} from "../error/error"; export async function apiFetch( endpoint: string, @@ -16,26 +21,32 @@ export async function apiFetch( ...restOptions, }); - if (res.status === 401 && !noAuth) { - throw new AuthorizationError( - "세션이 만료되었습니다.\n다시 로그인 해주세요.", - ); - } - if (!res.ok) { - if (res.status === 404) throw new NotFoundError(); - if (res.status >= 500) throw new ServerError(); + let message = `요청 실패 (${res.status})`; - let message = `API Error ${res.status}`; try { const data = await res.json(); - message = data.message ?? data.detail ?? message; + if (typeof data === "object" && data?.message) { + message = data.message; + } } catch { - const text = await res.text(); - if (text) message = text; + // 기본 메시지 + } + + if (res.status === 401 && !noAuth) { + throw new AuthorizationError( + "세션이 만료되었습니다.\n다시 로그인 해주세요.", + ); } - throw new ServerError(message); + if (res.status === 404) { + throw new NotFoundError(); + } + + if (res.status >= 500) { + throw new ServerError(message, res.status); + } + throw new BaseError(message, res.status); } return res.json() as Promise; diff --git a/src/shared/error/error.ts b/src/shared/error/error.ts index 66b57be4..5b6a8229 100644 --- a/src/shared/error/error.ts +++ b/src/shared/error/error.ts @@ -1,10 +1,10 @@ export class BaseError extends Error { constructor( message: string, - public readonly status?: number, + public readonly status: number, ) { super(message); - this.name = this.constructor.name; + this.name = new.target.name; } } @@ -21,7 +21,7 @@ export class NotFoundError extends BaseError { } export class ServerError extends BaseError { - constructor(message = "서버 오류가 발생했습니다.") { - super(message, 500); + constructor(message = "서버 오류가 발생했습니다.", status: number) { + super(message, status); } } diff --git a/src/shared/error/ui/500.tsx b/src/shared/error/ui/500.tsx index 7b9ceedb..de036617 100644 --- a/src/shared/error/ui/500.tsx +++ b/src/shared/error/ui/500.tsx @@ -12,10 +12,19 @@ export default function ServerErrorPage({ message }: { message?: string }) { return (
-

서버 오류 발생

-

- {message ?? "잠시 후 다시 시도해주세요."} +

문제가 발생했어요

+ +

+ {message ?? + "일시적인 문제로 페이지를 불러오지 못했어요.\n잠시 후 다시 시도해주세요."}

+ +
); } diff --git a/src/widgets/mypage/ui/Client/MyPage.client.tsx b/src/widgets/mypage/ui/Client/MyPage.client.tsx index 54ec3ca7..ddc35111 100644 --- a/src/widgets/mypage/ui/Client/MyPage.client.tsx +++ b/src/widgets/mypage/ui/Client/MyPage.client.tsx @@ -1,17 +1,16 @@ "use client"; -import { useState, useCallback } from "react"; +import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import Profile, { ProfileProps } from "@/entities/user/ui/card/Profile"; import ProfileSkeleton from "@/entities/user/ui/card/ProfileSkeleton"; import PostCard from "@/entities/post/ui/card/PostCard"; import { PostCreateButton } from "@/features/createPost/ui/PostCreateButton/PostCreateButton"; import Tab from "@/widgets/mypage/ui/Tab.tsx/Tab"; -import { useModalStore } from "@/shared/model/modal.store"; import { usePostCreateModal } from "@/features/createPost/lib/usePostCreateModal"; +import { useEditProfileModal } from "@/features/editProfile/lib/useProfileEditModal"; 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" }, @@ -29,80 +28,45 @@ const emptyMessageMap: Record = { export default function MyPageClient({ defaultTab }: { defaultTab: string }) { const [selectedTab, setSelectedTab] = useState(defaultTab); - const { openModal, closeModal } = useModalStore(); const { 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(); }, }); - const handleEditProfile = useCallback(() => { - if (!userProfile) return; - - openModal("editProfile", { - ...userProfile, - onClose: () => closeModal(), - onSave: async () => { - closeModal(); - setTimeout(() => { - openModal("normal", { - message: "프로필이 성공적으로 수정되었습니다.", - buttonText: "확인", - onClick: () => closeModal(), - }); - }, 100); - await refetchUserProfile(); - }, - onError: () => { - closeModal(); - openModal("normal", { - message: "프로필 수정에 실패했습니다.", - buttonText: "확인", - onClick: () => closeModal(), - }); - }, - }); - }, [userProfile, openModal, closeModal, refetchUserProfile]); + const { openEditProfileModal } = useEditProfileModal(userProfile, { + onSuccess: async () => { + await refetchUserProfile(); + }, + }); const posts = postsData?.data || []; return (
- {profileLoading || !userProfile ? ( + {profileLoading ? ( ) : ( - + )}
diff --git a/src/widgets/postDetail/ui/DetailPage.client.tsx b/src/widgets/postDetail/ui/DetailPage.client.tsx index 2d9b6e49..9cc2bcd3 100644 --- a/src/widgets/postDetail/ui/DetailPage.client.tsx +++ b/src/widgets/postDetail/ui/DetailPage.client.tsx @@ -4,9 +4,6 @@ import { useQuery } from "@tanstack/react-query"; 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, @@ -15,33 +12,18 @@ export default function PostDetailPageClient({ }) { const id = Number(postingId); - const { - data: post, - isLoading, - isError, - error, - } = useQuery({ + const { data: post } = useQuery({ queryKey: ["postDetail", id], queryFn: () => getPostDetail(id), - staleTime: Infinity, }); - if (isError) { - handleError(error); - } - - if (isLoading) { - return ; - } - - if (!post) { - notFound(); - } - return (
- - + +
); } diff --git a/src/widgets/postDetail/ui/PostDetailSection.tsx b/src/widgets/postDetail/ui/PostDetailSection.tsx index 10cbfcf2..947acd92 100644 --- a/src/widgets/postDetail/ui/PostDetailSection.tsx +++ b/src/widgets/postDetail/ui/PostDetailSection.tsx @@ -9,7 +9,6 @@ import PostCarousel from "@/entities/post/ui/carousel/PostCarousel"; import LikeButton from "@/features/like/ui/LikeButton"; import Button from "@/shared/ui/Button/Button"; import UserIcon from "@/shared/icons/user.svg"; -import Link from "next/link"; import { useLike } from "@/features/like/lib/useLike"; import { usePostEditModal } from "@/features/editPost/lib/usePostEditModal"; @@ -19,7 +18,6 @@ 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"; import { apiFetch } from "@/shared/api/fetcher"; export function PostDetailSection({ post }: { post: PostDetail }) { @@ -29,33 +27,16 @@ export function PostDetailSection({ post }: { post: PostDetail }) { const { openModal, closeModal } = useModalStore(); const openChat = useChatStore((s) => s.mount); - const { - data: seller, - isError: isSellerError, - error: sellerError, - } = useQuery({ + const { data: seller } = useQuery({ queryKey: ["seller", post.sellerId], queryFn: () => getUser(post.sellerId), enabled: !!post.sellerId, }); - - if (isSellerError) { - handleError(sellerError); - } - - const { - data: chattingRoomStatus, - isError: isChattingRoomStatusError, - error: chattingRoomStatusError, - } = useQuery({ + const { data: chattingRoomStatus } = 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); @@ -89,6 +70,7 @@ export function PostDetailSection({ post }: { post: PostDetail }) { }); return; } + if (chattingRoomStatus) { openChat({ postingId: post.postingId, @@ -106,15 +88,19 @@ export function PostDetailSection({ post }: { post: PostDetail }) { await apiFetch(`/api/postings/${post.postingId}`, { method: "DELETE", }); + queryClient.removeQueries({ queryKey: ["postDetail", post.postingId], }); + queryClient.invalidateQueries({ queryKey: ["sellerPosts", post.sellerId], }); + queryClient.invalidateQueries({ queryKey: ["myPosts"], }); + closeModal(); openModal("normal", { message: "삭제가 완료되었습니다.", @@ -124,7 +110,13 @@ export function PostDetailSection({ post }: { post: PostDetail }) { }, }); } catch (err) { - console.error("게시물 삭제 실패:", err); + closeModal(); + openModal("normal", { + message: "게시글 삭제에 실패했습니다.", + onClick: () => closeModal(), + }); + + console.error(err); } }, onCancel: closeModal, @@ -147,12 +139,14 @@ export function PostDetailSection({ post }: { post: PostDetail }) { )} + {seller?.nickname || "판매자"}
+

{post.title}

@@ -165,12 +159,14 @@ export function PostDetailSection({ post }: { post: PostDetail }) { className="absolute top-1 right-5" />
+
채팅 {post.chatCount}· 관심 {post.likeCount}· 조회 {post.viewCount} +
{post.isOwner ? ( <> diff --git a/src/widgets/postDetail/ui/SellerPostsSection.tsx b/src/widgets/postDetail/ui/SellerPostsSection.tsx index 52d61614..c2e86a7f 100644 --- a/src/widgets/postDetail/ui/SellerPostsSection.tsx +++ b/src/widgets/postDetail/ui/SellerPostsSection.tsx @@ -59,10 +59,6 @@ export function SellerPostsSection({ ), )}
- - {isFetchingNextPage && ( -

불러오는 중...

- )}
); }