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
2 changes: 1 addition & 1 deletion src/features/home/api/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@ export const useSearchHistory = () => {
onSuccess: () => {
console.log("검색 히스토리 저장");
},
onError: err => console.log(err),
// onError: err => console.log(err),
});
};
13 changes: 11 additions & 2 deletions src/features/mypage/api/account.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import api from "@/shared/api/api";
import { API_ENDPOINTS } from "@/shared/consts/endpoints";
import { USER_ERROR } from "@/shared/consts/errorCodes";
import { useMutation } from "@tanstack/react-query";
import { toast } from "react-toastify";
import type { AxiosError } from "axios";
import useUserStore from "@/shared/model/useUserStore";

export const deleteAccount = async () => {
const { data } = await api.patch(API_ENDPOINTS.users.me.withdrawal);
return data;
};

export const useDeleteAccount = (onSuccess: () => void) => {
return useMutation({
return useMutation<unknown, AxiosError<{ code: string; message: string }>>({
mutationFn: deleteAccount,
onSuccess: () => onSuccess?.(),
onError: err => console.error(err),
onError: e => {
if (e?.response?.data?.code === USER_ERROR.ALREADY_WITHDRAWN) {
toast.error(e.response.data.message);
useUserStore.getState().logout();
}
},
});
};
17 changes: 14 additions & 3 deletions src/features/mypage/api/myEdit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { API_ENDPOINTS } from "@/shared/consts/endpoints";
import { SHARED_QUERY_KEY } from "@/shared/consts/queryKeys";
import { HOME_QUERY_KEY } from "@/features/home/consts/queryKeys";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { USER_ERROR } from "@/shared/consts/errorCodes";
import { toast } from "react-toastify";
import type { AxiosError } from "axios";

// 내 관심사 수정 =>mypage
export const putMyInterst = async (body: InterestDataDto) => {
Expand All @@ -25,7 +28,12 @@ export const usePutMyInterst = () => {
HOME_QUERY_KEY.POSTS_RECOMMEND,
] as const;

return useMutation({
return useMutation<
unknown,
AxiosError<{ code: string; message: string }>,
InterestDataDto,
{ previous: InterestTypeDto[] | undefined; previousRecommend: unknown }
>({
mutationFn: (body: InterestDataDto) => putMyInterst(body),

onMutate: async (payload: InterestDataDto) => {
Expand All @@ -43,7 +51,11 @@ export const usePutMyInterst = () => {

return { previous, previousRecommend };
},
onError: (_err, _payload, context) => {
onError: (e, _, context) => {
if (e?.response?.data?.code == USER_ERROR.INVALID_INTEREST) {
toast.error(e.response.data.message);
}

if (context?.previous) {
queryClient.setQueryData(queryKey, context.previous);
}
Expand Down Expand Up @@ -78,6 +90,5 @@ export const usePatchMyProfile = (onSuccess?: () => void) => {
});
onSuccess?.();
},
onError: err => console.log(err),
});
};
15 changes: 13 additions & 2 deletions src/features/onboarding/api/onboarding.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { useMutation } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import type { AxiosError } from "axios";
import api from "@/shared/api/api";
import { API_ENDPOINTS } from "@/shared/consts/endpoints";
import type { OnboardingRequestType } from "./onboarding.types";
import { USER_ERROR } from "@/shared/consts/errorCodes";
import { toast } from "react-toastify";

export const postOnboarding = async (body: OnboardingRequestType) => {
const res = await api.post(API_ENDPOINTS.onboarding.complete, body);
Expand All @@ -12,9 +15,17 @@ export const postOnboarding = async (body: OnboardingRequestType) => {
export const useSubmitOnboarding = () => {
const navigate = useNavigate();

return useMutation({
return useMutation<
unknown,
AxiosError<{ code: string; message: string }>,
OnboardingRequestType
>({
mutationFn: (body: OnboardingRequestType) => postOnboarding(body),
onSuccess: () => navigate("/"),
onError: err => console.log(err),
onError: err => {
if (err.response?.data.code === USER_ERROR.INVALID_INTEREST) {
toast.error(err.response.data.message);
}
},
});
};
16 changes: 15 additions & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,38 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "@/app/styles/index.css";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { QueryClient, QueryClientProvider, MutationCache } from "@tanstack/react-query";
import axios from "axios";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ThemeProvider } from "@/app/providers/ThemProvider.tsx";
import { HelmetProvider } from "react-helmet-async";
import App from "@/app/App";
import router from "@/app/routes";
import { initGlobalNavigate } from "@/shared/lib/globalNavigate";
import { toast } from "react-toastify";

initGlobalNavigate((path) => router.navigate(path));

const isServerError = (error: unknown) =>
axios.isAxiosError(error) &&
(error.response?.status === 500 || error.response?.status === 503);

const queryClient = new QueryClient({
mutationCache: new MutationCache({
onError: (error) => {
if (isServerError(error)) {
toast.error("서버에 문제가 발생했습니다. 잠시 후 다시 시도해주세요.");
}
},
}),
defaultOptions: {
queries: {
retry: (failureCount, error) => {
if (axios.isAxiosError(error) && !error.response) return false;
if (isServerError(error)) return false;
return failureCount < 3;
},
throwOnError: (error) => isServerError(error),
},
},
});
Expand Down
115 changes: 89 additions & 26 deletions src/mocks/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { http, HttpResponse } from "msw";
import { http, HttpResponse, passthrough } from "msw";
import {
AUTH_ERROR,
ACTIVITY_ERROR,
USER_ERROR,
BOOKMARK_ERROR,
POST_ERROR,
READPOST_ERROR,
USER_ERROR,
COMMON_ERROR,
} from "@/shared/consts/errorCodes";

Expand Down Expand Up @@ -44,23 +45,37 @@ export const scenarioAuth = {

/** 북마크 에러 시나리오 */
export const scenarioBookmark = {
/** 북마크 추가 => 이미 북마크한 게시글 */
// POST /api/v1/activities/bookmarks
/** 북마크 추가 => 이미 북마크한 게시글 (BOOKMARK409_1) */
alreadyBookmarked: http.post(url("/api/v1/activities/bookmarks"), () =>
err(ACTIVITY_ERROR.ALREADY_BOOKMARKED, "이미 북마크한 게시글입니다.", 409),
err(BOOKMARK_ERROR.ALREADY_BOOKMARKED, "이미 북마크한 게시글입니다.", 409),
),
/** 북마크 추가 => 게시글을 찾을 수 없음 (POST404_1) */
postPostNotFound: http.post(url("/api/v1/activities/bookmarks"), () =>
err(POST_ERROR.NOT_FOUND, "게시글을 찾을 수 없습니다.", 404),
),

/** 북마크 삭제 => 북마크를 찾을 수 없음 */
// DELETE /api/v1/activities/bookmarks
/** 북마크 삭제 => 북마크를 찾을 수 없음 (BOOKMARK404_1) */
bookmarkNotFound: http.delete(url("/api/v1/activities/bookmarks"), () =>
err(ACTIVITY_ERROR.BOOKMARK_NOT_FOUND, "북마크를 찾을 수 없습니다.", 404),
err(BOOKMARK_ERROR.BOOKMARK_NOT_FOUND, "북마크를 찾을 수 없습니다.", 404),
),
/** 북마크 삭제 => 게시글을 찾을 수 없음 (POST404_1) */
deletePostNotFound: http.delete(url("/api/v1/activities/bookmarks"), () =>
err(POST_ERROR.NOT_FOUND, "게시글을 찾을 수 없습니다.", 404),
),
};

/** 게시글 에러 시나리오 */
export const scenarioPost = {
/** 게시글 조회 => 찾을 수 없음 */
notFound: http.get(url("/api/v2/posts/*"), () =>
/** 읽은 게시글 에러 시나리오 (POST /api/v1/activities/read-posts) */
export const scenarioReadPost = {
/** 게시글을 찾을 수 없음 (POST404_1) */
postNotFound: http.post(url("/api/v1/activities/read-posts"), () =>
err(POST_ERROR.NOT_FOUND, "게시글을 찾을 수 없습니다.", 404),
),
/** 조회수 증가 실패 (READ_POST500_1) — 전역 toast 동작 확인 */
readPostFailed: http.post(url("/api/v1/activities/read-posts"), () =>
err(READPOST_ERROR.READ_POST, "조회수 증가에 실패했습니다.", 500),
),
};

/** 유저 에러 시나리오 */
Expand All @@ -83,17 +98,59 @@ export const scenarioUser = {

/** 공통 에러 시나리오 */
export const scenarioCommon = {
/** 서버 에러 */
internalServer: http.get(url("/api/*"), () =>
/** 서버 에러 (전체) — query: ErrorBoundary fallback / mutation: toast */
internalServer: http.all(url("/api/*"), () =>
err(
COMMON_ERROR.INTERNAL_SERVER,
"서버 에러, 관리자에게 문의 바랍니다.",
500,
),
),

/** 서비스 점검 */
serviceUnavailable: http.get(url("/api/*"), () =>
/** 서버 에러 (query만) — GET만 막아 mutation은 정상 동작 */
internalServerQuery: http.get(url("/api/*"), () =>
err(
COMMON_ERROR.INTERNAL_SERVER,
"서버 에러, 관리자에게 문의 바랍니다.",
500,
),
),

/** 서버 에러 (mutation만) — POST/PATCH/DELETE만 막아 query는 정상 동작 */
internalServerMutation: [
http.post(url("/api/v1/auth/refresh"), () => passthrough()),
http.post(url("/api/*"), () =>
err(
COMMON_ERROR.INTERNAL_SERVER,
"서버 에러, 관리자에게 문의 바랍니다.",
500,
),
),
http.patch(url("/api/*"), () =>
err(
COMMON_ERROR.INTERNAL_SERVER,
"서버 에러, 관리자에게 문의 바랍니다.",
500,
),
),
http.delete(url("/api/*"), () =>
err(
COMMON_ERROR.INTERNAL_SERVER,
"서버 에러, 관리자에게 문의 바랍니다.",
500,
),
),
http.put(url("/api/*"), () =>
err(
COMMON_ERROR.INTERNAL_SERVER,
"서버 에러, 관리자에게 문의 바랍니다.",
500,
),
),
],

/** 서비스 점검 — query: ErrorBoundary fallback / mutation: toast */
serviceUnavailable: http.all(url("/api/*"), () =>
err(
COMMON_ERROR.SERVICE_UNAVAILABLE,
"서버가 일시적으로 사용중지 되었습니다.",
Expand All @@ -115,18 +172,24 @@ export const scenarioNetwork = {

export const handlers = [
// 인증
// scenarioAuth.refreshMismatch, // 401: refresh 불일치 => 즉시 로그아웃 확인
// scenarioAuth.withdrawn, // 403: 탈퇴 회원 => 즉시 로그아웃 확인
// 북마크
// scenarioBookmark.alreadyBookmarked, // 409: 중복 북마크
// scenarioBookmark.bookmarkNotFound, // 404: 북마크 없음
// scenarioAuth.refreshMismatch, // 401: refresh 불일치 => 즉시 로그아웃 -O
// scenarioAuth.withdrawn, // 403: 탈퇴 회원 => 즉시 로그아웃 -O
// 북마크 POST
// scenarioBookmark.alreadyBookmarked, // 409: 중복 북마크 -O
// scenarioBookmark.postPostNotFound, // 404: 게시글 없음
// 북마크 DELETE
// scenarioBookmark.bookmarkNotFound, // 404: 북마크 없음 -O
// scenarioBookmark.deletePostNotFound, // 404: 게시글 없음
// 읽은 게시글 POST
// scenarioReadPost.postNotFound, // 404: 게시글 없음
// scenarioReadPost.readPostFailed, // 500: 조회수 증가 실패 => 전역 toast -O
// 유저
// scenarioUser.invalidInterest, // 400: 유효하지 않은 관심사
// scenarioUser.alreadyWithdrawn, // 400: 이미 탈퇴한 회원
// scenarioUser.invalidInterest, // 400: 유효하지 않은 관심사
// scenarioUser.alreadyWithdrawn, // 400: 이미 탈퇴한 회원 -O
// 공통
// scenarioCommon.internalServer, // 500: 서버 에러
// scenarioCommon.serviceUnavailable, // 503: 서비스 점검
// scenarioCommon.internalServer, // 500: 서버 에러
// ...scenarioCommon.internalServerMutation,
// scenarioCommon.serviceUnavailable, // 503: 서비스 점검
// 네트워크
// scenarioNetwork.offline, // 연결 끊김 (ERR_NETWORK)
// scenarioNetwork.bookmarkOffline,// 북마크 API만 연결 끊김
// scenarioNetwork.offline, // 연결 끊김 (ERR_NETWORK) -O
];
Loading
Loading