Skip to content

Commit 250c23d

Browse files
committed
Merge branch 'develop'
2 parents a535457 + 938ba73 commit 250c23d

9 files changed

Lines changed: 216 additions & 80 deletions

File tree

src/features/home/api/search.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,6 @@ export const useSearchHistory = () => {
3535
onSuccess: () => {
3636
console.log("검색 히스토리 저장");
3737
},
38-
onError: err => console.log(err),
38+
// onError: err => console.log(err),
3939
});
4040
};

src/features/mypage/api/account.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
import api from "@/shared/api/api";
22
import { API_ENDPOINTS } from "@/shared/consts/endpoints";
3+
import { USER_ERROR } from "@/shared/consts/errorCodes";
34
import { useMutation } from "@tanstack/react-query";
5+
import { toast } from "react-toastify";
6+
import type { AxiosError } from "axios";
7+
import useUserStore from "@/shared/model/useUserStore";
48

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

1014
export const useDeleteAccount = (onSuccess: () => void) => {
11-
return useMutation({
15+
return useMutation<unknown, AxiosError<{ code: string; message: string }>>({
1216
mutationFn: deleteAccount,
1317
onSuccess: () => onSuccess?.(),
14-
onError: err => console.error(err),
18+
onError: e => {
19+
if (e?.response?.data?.code === USER_ERROR.ALREADY_WITHDRAWN) {
20+
toast.error(e.response.data.message);
21+
useUserStore.getState().logout();
22+
}
23+
},
1524
});
1625
};

src/features/mypage/api/myEdit.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import { API_ENDPOINTS } from "@/shared/consts/endpoints";
99
import { SHARED_QUERY_KEY } from "@/shared/consts/queryKeys";
1010
import { HOME_QUERY_KEY } from "@/features/home/consts/queryKeys";
1111
import { useMutation, useQueryClient } from "@tanstack/react-query";
12+
import { USER_ERROR } from "@/shared/consts/errorCodes";
13+
import { toast } from "react-toastify";
14+
import type { AxiosError } from "axios";
1215

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

28-
return useMutation({
31+
return useMutation<
32+
unknown,
33+
AxiosError<{ code: string; message: string }>,
34+
InterestDataDto,
35+
{ previous: InterestTypeDto[] | undefined; previousRecommend: unknown }
36+
>({
2937
mutationFn: (body: InterestDataDto) => putMyInterst(body),
3038

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

4452
return { previous, previousRecommend };
4553
},
46-
onError: (_err, _payload, context) => {
54+
onError: (e, _, context) => {
55+
if (e?.response?.data?.code == USER_ERROR.INVALID_INTEREST) {
56+
toast.error(e.response.data.message);
57+
}
58+
4759
if (context?.previous) {
4860
queryClient.setQueryData(queryKey, context.previous);
4961
}
@@ -78,6 +90,5 @@ export const usePatchMyProfile = (onSuccess?: () => void) => {
7890
});
7991
onSuccess?.();
8092
},
81-
onError: err => console.log(err),
8293
});
8394
};
Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { useMutation } from "@tanstack/react-query";
22
import { useNavigate } from "react-router-dom";
3+
import type { AxiosError } from "axios";
34
import api from "@/shared/api/api";
45
import { API_ENDPOINTS } from "@/shared/consts/endpoints";
56
import type { OnboardingRequestType } from "./onboarding.types";
7+
import { USER_ERROR } from "@/shared/consts/errorCodes";
8+
import { toast } from "react-toastify";
69

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

15-
return useMutation({
18+
return useMutation<
19+
unknown,
20+
AxiosError<{ code: string; message: string }>,
21+
OnboardingRequestType
22+
>({
1623
mutationFn: (body: OnboardingRequestType) => postOnboarding(body),
1724
onSuccess: () => navigate("/"),
18-
onError: err => console.log(err),
25+
onError: err => {
26+
if (err.response?.data.code === USER_ERROR.INVALID_INTEREST) {
27+
toast.error(err.response.data.message);
28+
}
29+
},
1930
});
2031
};

src/main.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,38 @@
11
import { StrictMode } from "react";
22
import { createRoot } from "react-dom/client";
33
import "@/app/styles/index.css";
4-
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4+
import { QueryClient, QueryClientProvider, MutationCache } from "@tanstack/react-query";
55
import axios from "axios";
66
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
77
import { ThemeProvider } from "@/app/providers/ThemProvider.tsx";
88
import { HelmetProvider } from "react-helmet-async";
99
import App from "@/app/App";
1010
import router from "@/app/routes";
1111
import { initGlobalNavigate } from "@/shared/lib/globalNavigate";
12+
import { toast } from "react-toastify";
1213

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

16+
const isServerError = (error: unknown) =>
17+
axios.isAxiosError(error) &&
18+
(error.response?.status === 500 || error.response?.status === 503);
19+
1520
const queryClient = new QueryClient({
21+
mutationCache: new MutationCache({
22+
onError: (error) => {
23+
if (isServerError(error)) {
24+
toast.error("서버에 문제가 발생했습니다. 잠시 후 다시 시도해주세요.");
25+
}
26+
},
27+
}),
1628
defaultOptions: {
1729
queries: {
1830
retry: (failureCount, error) => {
1931
if (axios.isAxiosError(error) && !error.response) return false;
32+
if (isServerError(error)) return false;
2033
return failureCount < 3;
2134
},
35+
throwOnError: (error) => isServerError(error),
2236
},
2337
},
2438
});

src/mocks/handlers.ts

Lines changed: 89 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { http, HttpResponse } from "msw";
1+
import { http, HttpResponse, passthrough } from "msw";
22
import {
33
AUTH_ERROR,
4-
ACTIVITY_ERROR,
5-
USER_ERROR,
4+
BOOKMARK_ERROR,
65
POST_ERROR,
6+
READPOST_ERROR,
7+
USER_ERROR,
78
COMMON_ERROR,
89
} from "@/shared/consts/errorCodes";
910

@@ -44,23 +45,37 @@ export const scenarioAuth = {
4445

4546
/** 북마크 에러 시나리오 */
4647
export const scenarioBookmark = {
47-
/** 북마크 추가 => 이미 북마크한 게시글 */
48+
// POST /api/v1/activities/bookmarks
49+
/** 북마크 추가 => 이미 북마크한 게시글 (BOOKMARK409_1) */
4850
alreadyBookmarked: http.post(url("/api/v1/activities/bookmarks"), () =>
49-
err(ACTIVITY_ERROR.ALREADY_BOOKMARKED, "이미 북마크한 게시글입니다.", 409),
51+
err(BOOKMARK_ERROR.ALREADY_BOOKMARKED, "이미 북마크한 게시글입니다.", 409),
52+
),
53+
/** 북마크 추가 => 게시글을 찾을 수 없음 (POST404_1) */
54+
postPostNotFound: http.post(url("/api/v1/activities/bookmarks"), () =>
55+
err(POST_ERROR.NOT_FOUND, "게시글을 찾을 수 없습니다.", 404),
5056
),
5157

52-
/** 북마크 삭제 => 북마크를 찾을 수 없음 */
58+
// DELETE /api/v1/activities/bookmarks
59+
/** 북마크 삭제 => 북마크를 찾을 수 없음 (BOOKMARK404_1) */
5360
bookmarkNotFound: http.delete(url("/api/v1/activities/bookmarks"), () =>
54-
err(ACTIVITY_ERROR.BOOKMARK_NOT_FOUND, "북마크를 찾을 수 없습니다.", 404),
61+
err(BOOKMARK_ERROR.BOOKMARK_NOT_FOUND, "북마크를 찾을 수 없습니다.", 404),
62+
),
63+
/** 북마크 삭제 => 게시글을 찾을 수 없음 (POST404_1) */
64+
deletePostNotFound: http.delete(url("/api/v1/activities/bookmarks"), () =>
65+
err(POST_ERROR.NOT_FOUND, "게시글을 찾을 수 없습니다.", 404),
5566
),
5667
};
5768

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

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

8499
/** 공통 에러 시나리오 */
85100
export const scenarioCommon = {
86-
/** 서버 에러 */
87-
internalServer: http.get(url("/api/*"), () =>
101+
/** 서버 에러 (전체) — query: ErrorBoundary fallback / mutation: toast */
102+
internalServer: http.all(url("/api/*"), () =>
88103
err(
89104
COMMON_ERROR.INTERNAL_SERVER,
90105
"서버 에러, 관리자에게 문의 바랍니다.",
91106
500,
92107
),
93108
),
94109

95-
/** 서비스 점검 */
96-
serviceUnavailable: http.get(url("/api/*"), () =>
110+
/** 서버 에러 (query만) — GET만 막아 mutation은 정상 동작 */
111+
internalServerQuery: http.get(url("/api/*"), () =>
112+
err(
113+
COMMON_ERROR.INTERNAL_SERVER,
114+
"서버 에러, 관리자에게 문의 바랍니다.",
115+
500,
116+
),
117+
),
118+
119+
/** 서버 에러 (mutation만) — POST/PATCH/DELETE만 막아 query는 정상 동작 */
120+
internalServerMutation: [
121+
http.post(url("/api/v1/auth/refresh"), () => passthrough()),
122+
http.post(url("/api/*"), () =>
123+
err(
124+
COMMON_ERROR.INTERNAL_SERVER,
125+
"서버 에러, 관리자에게 문의 바랍니다.",
126+
500,
127+
),
128+
),
129+
http.patch(url("/api/*"), () =>
130+
err(
131+
COMMON_ERROR.INTERNAL_SERVER,
132+
"서버 에러, 관리자에게 문의 바랍니다.",
133+
500,
134+
),
135+
),
136+
http.delete(url("/api/*"), () =>
137+
err(
138+
COMMON_ERROR.INTERNAL_SERVER,
139+
"서버 에러, 관리자에게 문의 바랍니다.",
140+
500,
141+
),
142+
),
143+
http.put(url("/api/*"), () =>
144+
err(
145+
COMMON_ERROR.INTERNAL_SERVER,
146+
"서버 에러, 관리자에게 문의 바랍니다.",
147+
500,
148+
),
149+
),
150+
],
151+
152+
/** 서비스 점검 — query: ErrorBoundary fallback / mutation: toast */
153+
serviceUnavailable: http.all(url("/api/*"), () =>
97154
err(
98155
COMMON_ERROR.SERVICE_UNAVAILABLE,
99156
"서버가 일시적으로 사용중지 되었습니다.",
@@ -115,18 +172,24 @@ export const scenarioNetwork = {
115172

116173
export const handlers = [
117174
// 인증
118-
// scenarioAuth.refreshMismatch, // 401: refresh 불일치 => 즉시 로그아웃 확인
119-
// scenarioAuth.withdrawn, // 403: 탈퇴 회원 => 즉시 로그아웃 확인
120-
// 북마크
121-
// scenarioBookmark.alreadyBookmarked, // 409: 중복 북마크
122-
// scenarioBookmark.bookmarkNotFound, // 404: 북마크 없음
175+
// scenarioAuth.refreshMismatch, // 401: refresh 불일치 => 즉시 로그아웃 -O
176+
// scenarioAuth.withdrawn, // 403: 탈퇴 회원 => 즉시 로그아웃 -O
177+
// 북마크 POST
178+
// scenarioBookmark.alreadyBookmarked, // 409: 중복 북마크 -O
179+
// scenarioBookmark.postPostNotFound, // 404: 게시글 없음
180+
// 북마크 DELETE
181+
// scenarioBookmark.bookmarkNotFound, // 404: 북마크 없음 -O
182+
// scenarioBookmark.deletePostNotFound, // 404: 게시글 없음
183+
// 읽은 게시글 POST
184+
// scenarioReadPost.postNotFound, // 404: 게시글 없음
185+
// scenarioReadPost.readPostFailed, // 500: 조회수 증가 실패 => 전역 toast -O
123186
// 유저
124-
// scenarioUser.invalidInterest, // 400: 유효하지 않은 관심사
125-
// scenarioUser.alreadyWithdrawn, // 400: 이미 탈퇴한 회원
187+
// scenarioUser.invalidInterest, // 400: 유효하지 않은 관심사
188+
// scenarioUser.alreadyWithdrawn, // 400: 이미 탈퇴한 회원 -O
126189
// 공통
127-
// scenarioCommon.internalServer, // 500: 서버 에러
128-
// scenarioCommon.serviceUnavailable, // 503: 서비스 점검
190+
// scenarioCommon.internalServer, // 500: 서버 에러
191+
// ...scenarioCommon.internalServerMutation,
192+
// scenarioCommon.serviceUnavailable, // 503: 서비스 점검
129193
// 네트워크
130-
// scenarioNetwork.offline, // 연결 끊김 (ERR_NETWORK)
131-
// scenarioNetwork.bookmarkOffline,// 북마크 API만 연결 끊김
194+
// scenarioNetwork.offline, // 연결 끊김 (ERR_NETWORK) -O
132195
];

0 commit comments

Comments
 (0)