diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index fd152fbd..e13ce259 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -2,13 +2,11 @@ import React, { useState } from "react"; import { LoginForm } from "@/features/auth/ui/LoginForm/LoginForm"; -import { Modal } from "@/shared/ui/Modal/Modal"; import { useRouter } from "next/navigation"; import { useAuthStore } from "@/features/auth/model/auth.store"; export default function SignUpPage() { const router = useRouter(); - const [errorMessage, setErrorMessage] = useState(null); const { setIsLogined } = useAuthStore(); return (
@@ -19,19 +17,7 @@ export default function SignUpPage() { 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 c6ce3d3d..e734718b 100644 --- a/src/app/my/page.tsx +++ b/src/app/my/page.tsx @@ -7,7 +7,7 @@ import Tab from "@/widgets/mypage/ui/Tab.tsx/Tab"; import { apiFetch } from "@/shared/api/fetcher"; import { useModalStore } from "@/shared/model/modal.store"; import { usePostCreateModal } from "@/features/createPost/lib/usePostCreateModal"; - +import { handleError } from "@/shared/error/errorHandler"; const options = [ { label: "판매중 상품", value: "selling" }, { label: "판매완료 상품", value: "sold" }, @@ -48,7 +48,7 @@ const Mypage = () => { }); setUserProfile(data); } catch (error) { - console.error("유저 정보 로딩 실패: ", error); + handleError(error, "유저 프로필을 불러오는 중 오류가 발생했습니다."); } } @@ -68,7 +68,7 @@ const Mypage = () => { ); setPosts(res.data); } catch (error) { - console.error("현재 유저 관련 게시글 불러오기 실패 : ", error); + handleError(error, "현재 유저 게시물을 불러오는 중 오류가 발생했습니다."); setPosts([]); } finally { setLoading(false); diff --git a/src/entities/chat/lib/useChatOtherUser.ts b/src/entities/chat/lib/useChatOtherUser.ts index c14015ba..e17fbd3d 100644 --- a/src/entities/chat/lib/useChatOtherUser.ts +++ b/src/entities/chat/lib/useChatOtherUser.ts @@ -1,12 +1,11 @@ import { useEffect, useState } from "react"; import { User } from "@/entities/user/model/types/user"; import { apiFetch } from "@/shared/api/fetcher"; -import { useModalStore } from "@/shared/model/modal.store"; +import { handleError } from "@/shared/error/errorHandler"; export const useChatOtherUser = (otherId: number) => { const [otherUser, setOtherUser] = useState(null); const [isLoading, setIsLoading] = useState(false); - const { openModal, closeModal } = useModalStore(); useEffect(() => { async function fetchUser() { @@ -16,11 +15,8 @@ export const useChatOtherUser = (otherId: number) => { method: "GET", }); setOtherUser(res); - } catch { - openModal("normal", { - message: "상대 유저 정보 조회에 실패했습니다.", - onClick: () => closeModal(), - }); + } catch (error) { + handleError(error, "상대 유저 정보를 가져오는 중 오류가 발생했습니다."); } finally { setIsLoading(false); } diff --git a/src/entities/chat/lib/useChatPost.ts b/src/entities/chat/lib/useChatPost.ts index 30cab466..8901f831 100644 --- a/src/entities/chat/lib/useChatPost.ts +++ b/src/entities/chat/lib/useChatPost.ts @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { PostDetail } from "@/entities/post/model/types/post"; import { apiFetch } from "@/shared/api/fetcher"; import { useModalStore } from "@/shared/model/modal.store"; +import { handleError } from "@/shared/error/errorHandler"; export const useChatPost = (postingId: number) => { const [post, setPost] = useState(null); @@ -16,11 +17,8 @@ export const useChatPost = (postingId: number) => { method: "GET", }); setPost(res); - } catch { - openModal("normal", { - message: "게시물 정보 조회에 실패했습니다.", - onClick: () => closeModal(), - }); + } catch (error) { + handleError(error, "게시물 정보 조회 중 오류가 발생했습니다."); } finally { setIsLoading(false); } diff --git a/src/entities/chat/model/socket.ts b/src/entities/chat/model/socket.ts index 702de261..2dace2c3 100644 --- a/src/entities/chat/model/socket.ts +++ b/src/entities/chat/model/socket.ts @@ -1,7 +1,8 @@ -import { error } from "console"; import { MessageProps } from "./types"; import { useAuthStore } from "@/features/auth/model/auth.store"; import { refreshAccessToken } from "@/shared/api/refresh"; +import { handleError } from "@/shared/error/errorHandler"; +import { ServerError } from "@/shared/error/error"; export interface ChatSocketEvents { onOpen?: () => void; @@ -62,7 +63,11 @@ export class ChatSocket { this.events.onMessage?.(msg); } } catch (err) { - console.error("[Socket] Message parse error:", err); + handleError( + new ServerError(undefined, () => { + location.replace("/"); + }), + ); } }; @@ -79,7 +84,11 @@ export class ChatSocket { }; this.socket.onerror = (err) => { - console.error("[Socket] Error:", err); + handleError( + new ServerError(undefined, () => { + location.replace("/"); + }), + ); this.events.onError?.(err); reject(err); }; diff --git a/src/entities/chat/ui/ChattingRoom/ChattingRoom.tsx b/src/entities/chat/ui/ChattingRoom/ChattingRoom.tsx index 20fc8ea1..1837c6a8 100644 --- a/src/entities/chat/ui/ChattingRoom/ChattingRoom.tsx +++ b/src/entities/chat/ui/ChattingRoom/ChattingRoom.tsx @@ -15,6 +15,7 @@ import { useInfiniteScroll } from "@/shared/lib/useInfiniteScroll"; import { getPostDetail } from "@/entities/post/api/getPostDetail"; import { getUser } from "@/entities/user/api/getUser"; import { useQuery } from "@tanstack/react-query"; +import { handleError } from "@/shared/error/errorHandler"; export const ChattingRoom = ({ postingId, @@ -157,11 +158,8 @@ export const ChattingRoom = ({ }, ); setChatId(res.chatId); //useChatSocket hook을 통한 소켓 자동 재연결. - } catch { - openModal("normal", { - message: "채팅방 생성에 실패했습니다.", - onClick: closeModal, - }); + } catch (error) { + handleError(error, "채팅방 생성 중 오류가 발생했습니다."); } } diff --git a/src/features/auth/ui/LoginForm/LoginForm.tsx b/src/features/auth/ui/LoginForm/LoginForm.tsx index b2810fb6..8a5b9bbb 100644 --- a/src/features/auth/ui/LoginForm/LoginForm.tsx +++ b/src/features/auth/ui/LoginForm/LoginForm.tsx @@ -5,19 +5,19 @@ import { Input } from "@/entities/user/ui/Input/Input"; import Button from "@/shared/ui/Button/Button"; import { useAuthStore } from "../../model/auth.store"; import { apiFetch } from "@/shared/api/fetcher"; +import { handleError } from "@/shared/error/errorHandler"; +import { AuthorizationError } from "@/shared/error/error"; type FormSize = "sm" | "md" | "lg"; interface LoginFormProps { size?: FormSize; onSuccess?: () => void; - onError?: (msg: string) => void; } export const LoginForm = ({ size = "lg", onSuccess, - onError, ...props }: LoginFormProps) => { const [email, setEmail] = useState(""); @@ -44,8 +44,12 @@ export const LoginForm = ({ setAccessToken(res.accessToken); onSuccess?.(); - } catch { - onError?.("로그인에 실패하였습니다.\n이메일과 비밀번호를 확인해주세요."); + } catch (error) { + handleError( + new AuthorizationError( + "로그인에 실패하였습니다.\n이메일과 비밀번호를 확인해주세요.", + ), + ); } }; diff --git a/src/features/auth/ui/SignUpForm/SignUpForm.stories.tsx b/src/features/auth/ui/SignUpForm/SignUpForm.stories.tsx index 3b8d277e..8c3acf5b 100644 --- a/src/features/auth/ui/SignUpForm/SignUpForm.stories.tsx +++ b/src/features/auth/ui/SignUpForm/SignUpForm.stories.tsx @@ -9,7 +9,6 @@ const meta: Meta = { }, argTypes: { onSuccess: { action: "success" }, - onError: { action: "error" }, }, }; diff --git a/src/features/auth/ui/SignUpForm/SignUpForm.tsx b/src/features/auth/ui/SignUpForm/SignUpForm.tsx index 3a349e3c..d1c17662 100644 --- a/src/features/auth/ui/SignUpForm/SignUpForm.tsx +++ b/src/features/auth/ui/SignUpForm/SignUpForm.tsx @@ -6,13 +6,14 @@ import { DropDown } from "@/shared/ui/DropDown/DropDown"; import Button from "@/shared/ui/Button/Button"; import { apiFetch } from "@/shared/api/fetcher"; import cn from "@/shared/lib/cn"; +import { handleError } from "@/shared/error/errorHandler"; +import { ServerError } from "@/shared/error/error"; interface SignUpFormProps { onSuccess?: () => void; - onError?: (msg: string) => void; } -export const SignUpForm = ({ onSuccess, onError }: SignUpFormProps) => { +export const SignUpForm = ({ onSuccess }: SignUpFormProps) => { //input text 상태 const [email, setEmail] = useState(""); const [nickname, setNickname] = useState(""); @@ -149,7 +150,11 @@ export const SignUpForm = ({ onSuccess, onError }: SignUpFormProps) => { } else if (error.message.includes("NICKNAME_DUPLICATE")) { setNicknameError("이미 사용 중인 닉네임입니다."); } else { - onError?.("회원가입에 실패했습니다. 잠시 후 다시 시도해주세요."); + handleError( + new ServerError( + "회원가입 중 에러가 발생했습니다.\n잠시후 다시 시도해주세요.", + ), + ); } } } diff --git a/src/features/createPost/lib/usePostCreateModal.ts b/src/features/createPost/lib/usePostCreateModal.ts index 7b71604f..63d5135f 100644 --- a/src/features/createPost/lib/usePostCreateModal.ts +++ b/src/features/createPost/lib/usePostCreateModal.ts @@ -24,17 +24,6 @@ export const usePostCreateModal = (handlers?: { }); }, 100); }, - onError: (message: string) => { - closeModal(); - - handlers?.onFailure?.(); - - openModal("normal", { - message: "게시물 등록 중 오류가 발생했습니다. " + message, - buttonText: "확인", - onClick: () => closeModal(), - }); - }, }); }; diff --git a/src/features/createPost/ui/PostCreateModal/PostCreateModal.tsx b/src/features/createPost/ui/PostCreateModal/PostCreateModal.tsx index 46529047..69960f3c 100644 --- a/src/features/createPost/ui/PostCreateModal/PostCreateModal.tsx +++ b/src/features/createPost/ui/PostCreateModal/PostCreateModal.tsx @@ -14,19 +14,18 @@ import "swiper/css/navigation"; import "swiper/css/pagination"; import { apiFetch } from "@/shared/api/fetcher"; import { uploadImage } from "@/shared/api/uploadImage"; +import { handleError } from "@/shared/error/errorHandler"; export interface PostCreateModalProps { className?: string; onClose?: () => void; onCreate?: () => void; - onError?: (message: string) => void; } export const PostCreateModal = ({ className, onClose, onCreate, - onError, }: PostCreateModalProps) => { const [images, setImages] = useState([]); const [title, setTitle] = useState(""); @@ -87,11 +86,7 @@ export const PostCreateModal = ({ console.log("게시글 생성 성공! : ", res); onCreate?.(); } catch (error) { - console.error("게시글 생성 실패 : ", error); - if (error instanceof Error) { - onError?.(error.message); - } - onError?.(String(error)); + handleError(error, "게시글 생성 중 오류가 발생했습니다."); } }; diff --git a/src/features/editPost/ui/PostEditModal.tsx b/src/features/editPost/ui/PostEditModal.tsx index fb87a357..ebfddaf2 100644 --- a/src/features/editPost/ui/PostEditModal.tsx +++ b/src/features/editPost/ui/PostEditModal.tsx @@ -14,6 +14,7 @@ import "swiper/css/navigation"; import "swiper/css/pagination"; import { apiFetch } from "@/shared/api/fetcher"; import { uploadImage } from "@/shared/api/uploadImage"; +import { handleError } from "@/shared/error/errorHandler"; const ImageSwiperSlide = ( idx: number, @@ -144,9 +145,7 @@ export const PostEditModal = ({ console.log("게시글 수정 성공! : ", res); onEdit?.(); } catch (error) { - console.error("게시글 수정 실패 : ", error); - const message = error instanceof Error ? error.message : String(error); - onError?.(message); + handleError(error, "게시글 수정 중 오류가 발생했습니다."); } }; diff --git a/src/features/editProfile/ui/ProfileEditModal/ProfileEditModal.tsx b/src/features/editProfile/ui/ProfileEditModal/ProfileEditModal.tsx index 8988fcc9..b9f2e762 100644 --- a/src/features/editProfile/ui/ProfileEditModal/ProfileEditModal.tsx +++ b/src/features/editProfile/ui/ProfileEditModal/ProfileEditModal.tsx @@ -9,6 +9,7 @@ import { DropDown } from "@/shared/ui/DropDown/DropDown"; import Button from "@/shared/ui/Button/Button"; import { apiFetch } from "@/shared/api/fetcher"; import { uploadImage } from "@/shared/api/uploadImage"; +import { handleError } from "@/shared/error/errorHandler"; export interface ProfileEditModalProps { imageUrl?: string; @@ -79,10 +80,7 @@ export const ProfileEditModal = ({ onSave?.(); } catch (error) { - if (error instanceof Error) { - console.error("프로필 수정 실패:", error); - onError?.(error); - } else onError?.(new Error(String(error))); + handleError(error, "프로필 수정 중 오류가 발생했습니다."); } finally { setLoading(false); } diff --git a/src/shared/api/fetcher.ts b/src/shared/api/fetcher.ts index 47632c48..2c0ee63c 100644 --- a/src/shared/api/fetcher.ts +++ b/src/shared/api/fetcher.ts @@ -1,6 +1,7 @@ import { useAuthStore } from "@/features/auth/model/auth.store"; import { refreshAccessToken } from "./refresh"; import { useModalStore } from "@/shared/model/modal.store"; +import { AuthorizationError, NotFoundError, ServerError } from "../error/error"; const BASE_URL = process.env.NEXT_PUBLIC_API_URL; @@ -9,8 +10,7 @@ export async function apiFetch( options: RequestInit & { noAuth?: boolean }, ): Promise { const { headers, noAuth, ...restOptions } = options; - const { accessToken, setAccessToken, logout } = useAuthStore.getState(); - const { openModal, closeModal } = useModalStore.getState(); + const { accessToken } = useAuthStore.getState(); const defaultHeaders: HeadersInit = { "Content-Type": "application/json", @@ -31,7 +31,11 @@ export async function apiFetch( if (res.status === 401 && !noAuth) { const newToken = await refreshAccessToken(); - if (!newToken) throw new Error("세션 만료"); + if (!newToken) + throw new AuthorizationError( + "세션이 만료되었습니다.\n다시 로그인해주세요", + () => location.replace("/login"), + ); defaultHeaders["Authorization"] = `Bearer ${newToken}`; @@ -45,15 +49,23 @@ export async function apiFetch( } if (!res.ok) { - let message = `API Error ${res.status}`; - try { - const data = await res.json(); - message = data.message ?? data.detail ?? message; - } catch { - const text = await res.text(); - if (text) message = text; + // 분기처리 + if (res.status === 404) { + throw new NotFoundError("요청한 리소스를 찾을 수 없습니다.", () => { + location.replace("/"); + }); } - throw new Error(message); + + if (res.status >= 500) { + throw new ServerError("서버 오류가 발생했습니다.", () => { + location.replace("/"); + }); + } + + // 기타 오류 → 500 취급 + throw new ServerError("알 수 없는 서버 오류", () => { + location.replace("/"); + }); } return res.json() as Promise; diff --git a/src/shared/api/refresh.ts b/src/shared/api/refresh.ts index 4e3187b7..90324c4a 100644 --- a/src/shared/api/refresh.ts +++ b/src/shared/api/refresh.ts @@ -1,5 +1,6 @@ import { useAuthStore } from "@/features/auth/model/auth.store"; import { useModalStore } from "@/shared/model/modal.store"; +import { AuthorizationError } from "../error/error"; const BASE_URL = process.env.NEXT_PUBLIC_API_URL; @@ -12,17 +13,9 @@ export async function refreshAccessToken(): Promise { method: "POST", credentials: "include", }); + console.log(res); if (!res.ok) { - logout(); - openModal("normal", { - message: "세션이 만료되었습니다. 다시 로그인 해주세요.", - buttonText: "확인", - onClick: () => { - closeModal(); - location.replace("/login"); - }, - }); return null; } @@ -33,16 +26,11 @@ export async function refreshAccessToken(): Promise { return newToken; } catch (err) { - logout(); - openModal("normal", { - message: "세션이 만료되었습니다. 다시 로그인 해주세요.", - buttonText: "확인", - onClick: () => { - closeModal(); + throw new AuthorizationError( + "세션이 만료되었습니다.\n다시 로그인 해주세요", + () => { location.replace("/login"); }, - }); - - return null; + ); } } diff --git a/src/shared/api/uploadImage.ts b/src/shared/api/uploadImage.ts index 2284f315..9caa7f7b 100644 --- a/src/shared/api/uploadImage.ts +++ b/src/shared/api/uploadImage.ts @@ -1,3 +1,5 @@ +import { ServerError } from "../error/error"; + const BASE_URL = process.env.NEXT_PUBLIC_API_URL; export async function uploadImage(imageFile: File): Promise { @@ -20,17 +22,16 @@ export async function uploadImage(imageFile: File): Promise { const text = await res.text(); if (text) errorMessage = text; } - throw new Error(errorMessage); + throw new ServerError(errorMessage, () => location.replace("/")); } // 정상 응답 처리 const data = await res.json(); return data.imageUrl; } catch (err) { - console.error("이미지 업로드 중 오류 발생:", err); - if (err instanceof Error) { - throw new Error(err.message); - } - throw new Error("알 수 없는 오류가 발생했습니다."); + throw new ServerError( + "이미지 업로드 중 네트워크 오류가 발생했습니다.", + () => location.replace("/"), + ); } } diff --git a/src/shared/error/error.ts b/src/shared/error/error.ts new file mode 100644 index 00000000..3a79b0b6 --- /dev/null +++ b/src/shared/error/error.ts @@ -0,0 +1,35 @@ +export class BaseError extends Error { + defaultMessage: string; + onConfirm?: () => void; + + constructor( + defaultMessage: string, + message?: string, + onConfirm?: () => void, + ) { + super(message ?? defaultMessage); + this.name = this.constructor.name; + this.defaultMessage = defaultMessage; + this.onConfirm = onConfirm; + } +} + +export class AuthorizationError extends BaseError { + constructor(message?: string, onConfirm?: () => void) { + super("인증이 필요합니다.", message, onConfirm); + } +} + +export class NotFoundError extends BaseError { + resource?: string; + + constructor(message?: string, onConfirm?: () => void) { + super("요청한 리소스를 찾을 수 없습니다.", message, onConfirm); + } +} + +export class ServerError extends BaseError { + constructor(message?: string, onConfirm?: () => void) { + super("서버 오류가 발생했습니다.", message, onConfirm); + } +} diff --git a/src/shared/error/errorHandler.ts b/src/shared/error/errorHandler.ts new file mode 100644 index 00000000..1899c483 --- /dev/null +++ b/src/shared/error/errorHandler.ts @@ -0,0 +1,21 @@ +import { useModalStore } from "../model/modal.store"; +import { BaseError } from "./error"; + +export function handleError(error: unknown, context?: string) { + let message = + error instanceof BaseError + ? error.message + : "알 수 없는 오류가 발생했습니다."; + message = context ? `${context}\n${message}` : message; + + const { openModal, closeModal } = useModalStore.getState(); + + console.error(error, context); + openModal("normal", { + message, + onClick: () => { + closeModal(); + if (error instanceof BaseError) error.onConfirm?.(); + }, + }); +} diff --git a/src/widgets/postDetail/ui/PostDetailSection.tsx b/src/widgets/postDetail/ui/PostDetailSection.tsx index b4ca6217..b6263b3d 100644 --- a/src/widgets/postDetail/ui/PostDetailSection.tsx +++ b/src/widgets/postDetail/ui/PostDetailSection.tsx @@ -16,6 +16,7 @@ import { usePostEditModal } from "@/features/editPost/lib/usePostEditModal"; import { useChatStore } from "@/features/chat/model/chat.store"; import { useModalStore } from "@/shared/model/modal.store"; import { PostDetail } from "@/entities/post/model/types/post"; +import { handleError } from "@/shared/error/errorHandler"; export function PostDetailSection({ post }: { post: PostDetail }) { const router = useRouter(); @@ -76,7 +77,7 @@ export function PostDetailSection({ post }: { post: PostDetail }) { }, }); } catch (err) { - console.error("게시물 삭제 실패:", err); + handleError(err, "게시물 삭제 도중 오류가 발생했습니다."); } }, onCancel: closeModal,