diff --git a/.pnp.cjs b/.pnp.cjs index f33d1544a..0bf6ee377 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -36,7 +36,7 @@ const RAW_RUNTIME_STATE = ["@sentry/cli", "npm:2.45.0"],\ ["@sentry/nextjs", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:10.43.0"],\ ["@svgr/webpack", "npm:8.1.0"],\ - ["@tanstack/react-query", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:5.28.6"],\ + ["@tanstack/react-query", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:5.90.21"],\ ["@testing-library/jest-dom", "npm:5.17.0"],\ ["@testing-library/react", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:13.4.0"],\ ["@testing-library/user-event", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:13.5.0"],\ @@ -7010,27 +7010,27 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["@tanstack/query-core", [\ - ["npm:5.28.6", {\ - "packageLocation": "./.yarn/cache/@tanstack-query-core-npm-5.28.6-6bd1f84a2d-e9ae8d80a8.zip/node_modules/@tanstack/query-core/",\ + ["npm:5.90.20", {\ + "packageLocation": "./.yarn/cache/@tanstack-query-core-npm-5.90.20-fe193b58bc-25e38f4382.zip/node_modules/@tanstack/query-core/",\ "packageDependencies": [\ - ["@tanstack/query-core", "npm:5.28.6"]\ + ["@tanstack/query-core", "npm:5.90.20"]\ ],\ "linkType": "HARD"\ }]\ ]],\ ["@tanstack/react-query", [\ - ["npm:5.28.6", {\ - "packageLocation": "./.yarn/cache/@tanstack-react-query-npm-5.28.6-ea0a1ece1c-f7706485f3.zip/node_modules/@tanstack/react-query/",\ + ["npm:5.90.21", {\ + "packageLocation": "./.yarn/cache/@tanstack-react-query-npm-5.90.21-4400cf02c2-5bb4b6be7a.zip/node_modules/@tanstack/react-query/",\ "packageDependencies": [\ - ["@tanstack/react-query", "npm:5.28.6"]\ + ["@tanstack/react-query", "npm:5.90.21"]\ ],\ "linkType": "SOFT"\ }],\ - ["virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:5.28.6", {\ - "packageLocation": "./.yarn/__virtual__/@tanstack-react-query-virtual-147ceec9c5/0/cache/@tanstack-react-query-npm-5.28.6-ea0a1ece1c-f7706485f3.zip/node_modules/@tanstack/react-query/",\ + ["virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:5.90.21", {\ + "packageLocation": "./.yarn/__virtual__/@tanstack-react-query-virtual-2c374d90ab/0/cache/@tanstack-react-query-npm-5.90.21-4400cf02c2-5bb4b6be7a.zip/node_modules/@tanstack/react-query/",\ "packageDependencies": [\ - ["@tanstack/query-core", "npm:5.28.6"],\ - ["@tanstack/react-query", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:5.28.6"],\ + ["@tanstack/query-core", "npm:5.90.20"],\ + ["@tanstack/react-query", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:5.90.21"],\ ["@types/react", "npm:19.2.10"],\ ["react", "npm:19.2.4"]\ ],\ @@ -13447,7 +13447,7 @@ const RAW_RUNTIME_STATE = ["@sentry/cli", "npm:2.45.0"],\ ["@sentry/nextjs", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:10.43.0"],\ ["@svgr/webpack", "npm:8.1.0"],\ - ["@tanstack/react-query", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:5.28.6"],\ + ["@tanstack/react-query", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:5.90.21"],\ ["@testing-library/jest-dom", "npm:5.17.0"],\ ["@testing-library/react", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:13.4.0"],\ ["@testing-library/user-event", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:13.5.0"],\ diff --git a/.yarn/cache/@tanstack-query-core-npm-5.28.6-6bd1f84a2d-e9ae8d80a8.zip b/.yarn/cache/@tanstack-query-core-npm-5.28.6-6bd1f84a2d-e9ae8d80a8.zip deleted file mode 100644 index ab2efd2ba..000000000 Binary files a/.yarn/cache/@tanstack-query-core-npm-5.28.6-6bd1f84a2d-e9ae8d80a8.zip and /dev/null differ diff --git a/.yarn/cache/@tanstack-query-core-npm-5.90.20-fe193b58bc-25e38f4382.zip b/.yarn/cache/@tanstack-query-core-npm-5.90.20-fe193b58bc-25e38f4382.zip new file mode 100644 index 000000000..71d5708a6 Binary files /dev/null and b/.yarn/cache/@tanstack-query-core-npm-5.90.20-fe193b58bc-25e38f4382.zip differ diff --git a/.yarn/cache/@tanstack-react-query-npm-5.28.6-ea0a1ece1c-f7706485f3.zip b/.yarn/cache/@tanstack-react-query-npm-5.28.6-ea0a1ece1c-f7706485f3.zip deleted file mode 100644 index 84118e0e8..000000000 Binary files a/.yarn/cache/@tanstack-react-query-npm-5.28.6-ea0a1ece1c-f7706485f3.zip and /dev/null differ diff --git a/.yarn/cache/@tanstack-react-query-npm-5.90.21-4400cf02c2-5bb4b6be7a.zip b/.yarn/cache/@tanstack-react-query-npm-5.90.21-4400cf02c2-5bb4b6be7a.zip new file mode 100644 index 000000000..b1b63cefe Binary files /dev/null and b/.yarn/cache/@tanstack-react-query-npm-5.90.21-4400cf02c2-5bb4b6be7a.zip differ diff --git a/package.json b/package.json index 5cedfb3cc..2de139b93 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "@bcsdlab/koin": "^0.0.15", "@bcsdlab/utils": "^0.0.15", "@next/third-parties": "latest", + "@tanstack/react-query": "^5.90.21", "@sentry/nextjs": "^10", - "@tanstack/react-query": "^5.28.6", "axios": "^0.27.2", "dayjs": "^1.11.12", "embla-carousel-autoplay": "^8.0.4", diff --git a/src/api/abTest/queries.ts b/src/api/abTest/queries.ts new file mode 100644 index 000000000..00a3befd5 --- /dev/null +++ b/src/api/abTest/queries.ts @@ -0,0 +1,30 @@ +import { queryOptions } from '@tanstack/react-query'; +import { ABTestAssignResponse } from './entity'; +import { abTestAssign } from './index'; + +type DefaultABTestAssignResponse = ABTestAssignResponse | { access_history_id: null; variable_name: string }; + +const getDefaultABTestResponse = (): DefaultABTestAssignResponse => ({ + access_history_id: null, + variable_name: 'default', +}); + +export const abTestQueryKeys = { + all: ['ab-test'] as const, + assign: (title: string, authorization?: string, accessHistoryId?: string | number | null) => + [...abTestQueryKeys.all, 'assign', title, authorization ?? '', accessHistoryId ?? ''] as const, +}; + +export const abTestQueries = { + assign: (title: string, authorization?: string, accessHistoryId?: string | number | null) => + queryOptions({ + queryKey: abTestQueryKeys.assign(title, authorization, accessHistoryId), + queryFn: async () => { + try { + return await abTestAssign(title, authorization || undefined, accessHistoryId); + } catch { + return getDefaultABTestResponse(); + } + }, + }), +}; diff --git a/src/api/articles/mutations.ts b/src/api/articles/mutations.ts new file mode 100644 index 000000000..5d61d07b3 --- /dev/null +++ b/src/api/articles/mutations.ts @@ -0,0 +1,77 @@ +import { mutationOptions, QueryClient } from '@tanstack/react-query'; +import { + LostItemArticlesRequestDTO, + ReportItemArticleRequestDTO, + UpdateLostItemArticleRequestDTO, +} from './entity'; +import { articleQueryKeys } from './queries'; +import { + deleteLostItemArticle, + postBlockLostItemChatroom, + postFoundLostItem, + postLostItemArticle, + postLostItemChatroom, + postReportLostItemArticle, + putLostItemArticle, +} from './index'; + +const invalidateLostItemAll = (queryClient: QueryClient) => + queryClient.invalidateQueries({ queryKey: articleQueryKeys.lostItemAll }); + +const invalidateLostItemChatroomAll = (queryClient: QueryClient) => + queryClient.invalidateQueries({ queryKey: articleQueryKeys.lostItemChatroomAll }); + +export const articleMutations = { + createLostItem: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: async (data: LostItemArticlesRequestDTO) => { + const response = await postLostItemArticle(token, data); + return response.id; + }, + onSuccess: () => invalidateLostItemAll(queryClient), + }), + + updateLostItem: (queryClient: QueryClient, token: string, articleId: number) => + mutationOptions({ + mutationFn: async (data: UpdateLostItemArticleRequestDTO) => { + const response = await putLostItemArticle(token, articleId, data); + return response.id; + }, + onSuccess: () => invalidateLostItemAll(queryClient), + }), + + deleteLostItem: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: (articleId: number) => deleteLostItemArticle(token, articleId), + onSuccess: () => invalidateLostItemAll(queryClient), + }), + + reportLostItem: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: ({ articleId, reports }: { articleId: number; reports: ReportItemArticleRequestDTO['reports'] }) => + postReportLostItemArticle(token, articleId, { reports }), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: articleQueryKeys.all }); + await invalidateLostItemAll(queryClient); + }, + }), + + toggleLostItemFound: (queryClient: QueryClient, token: string, articleId: number) => + mutationOptions({ + mutationFn: () => postFoundLostItem(token, articleId), + onSuccess: () => queryClient.invalidateQueries({ queryKey: articleQueryKeys.lostItemDetail(articleId) }), + }), + + createLostItemChatroom: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: (articleId: number) => postLostItemChatroom(token, articleId), + onSuccess: () => invalidateLostItemChatroomAll(queryClient), + }), + + blockLostItemChatroom: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: ({ articleId, chatroomId }: { articleId: number; chatroomId: number }) => + postBlockLostItemChatroom(token, articleId, chatroomId), + onSuccess: () => invalidateLostItemChatroomAll(queryClient), + }), +}; diff --git a/src/api/articles/queries.ts b/src/api/articles/queries.ts new file mode 100644 index 000000000..ce0423f05 --- /dev/null +++ b/src/api/articles/queries.ts @@ -0,0 +1,120 @@ +import { infiniteQueryOptions, queryOptions } from '@tanstack/react-query'; +import { LostItemArticlesRequest, SearchLostItemArticleRequest } from './entity'; +import { + getArticle, + getArticles, + getLostItemChatroomDetail, + getLostItemChatroomList, + getLostItemChatroomMessagesV2, + getHotArticles, + getLostItemArticles, + getLostItemSearch, + getLostItemStat, + getSingleLostItemArticle, +} from './index'; + +type LostItemInfiniteListParams = Omit; + +type LostItemSearchParams = Required> & { + page: number; + limit: number; +}; + +export const articleQueryKeys = { + all: ['articles'] as const, + listRoot: ['articles', 'list'] as const, + list: (page: string) => [...articleQueryKeys.listRoot, page] as const, + hot: ['articles', 'hot'] as const, + detail: (id: string) => ['articles', 'detail', id] as const, + lostItemAll: ['lostItem'] as const, + lostItemListRoot: ['lostItem', 'list'] as const, + lostItemList: (params: LostItemArticlesRequest) => [...articleQueryKeys.lostItemListRoot, params] as const, + lostItemInfiniteListRoot: ['lostItem', 'infinite-list'] as const, + lostItemInfiniteList: (params: LostItemInfiniteListParams) => + [...articleQueryKeys.lostItemInfiniteListRoot, params] as const, + lostItemDetail: (articleId: number) => ['lostItem', 'detail', articleId] as const, + lostItemSearch: (params: LostItemSearchParams) => ['lostItem', 'search', params] as const, + lostItemStat: ['lostItem', 'stat'] as const, + lostItemChatroomAll: ['chatroom', 'lost-item'] as const, + lostItemChatroomList: ['chatroom', 'lost-item', 'list'] as const, + lostItemChatroomDetail: (articleId: number | string | null, chatroomId: number | string | null) => + ['chatroom', 'lost-item', 'detail', articleId, chatroomId] as const, + lostItemChatroomMessages: (articleId: number | string | null, chatroomId: number | string | null) => + ['chatroom', 'lost-item', 'messages', articleId, chatroomId] as const, +}; + +export const articleQueries = { + list: (token: string, page: string) => + queryOptions({ + queryKey: articleQueryKeys.list(page), + queryFn: () => getArticles(token, page), + }), + + hot: () => + queryOptions({ + queryKey: articleQueryKeys.hot, + queryFn: getHotArticles, + }), + + detail: (id: string) => + queryOptions({ + queryKey: articleQueryKeys.detail(id), + queryFn: () => getArticle(id), + }), + + lostItemList: (token: string, params: LostItemArticlesRequest) => + queryOptions({ + queryKey: articleQueryKeys.lostItemList(params), + queryFn: () => getLostItemArticles(token, params), + }), + + lostItemInfiniteList: (token: string, params: LostItemInfiniteListParams) => + infiniteQueryOptions({ + queryKey: articleQueryKeys.lostItemInfiniteList(params), + initialPageParam: 1, + queryFn: ({ pageParam }) => getLostItemArticles(token, { ...params, page: pageParam }), + getNextPageParam: (lastPage) => { + if (lastPage.total_page > lastPage.current_page) { + return lastPage.current_page + 1; + } + + return undefined; + }, + }), + + lostItemDetail: (token: string, articleId: number) => + queryOptions({ + queryKey: articleQueryKeys.lostItemDetail(articleId), + queryFn: () => getSingleLostItemArticle(token, articleId), + }), + + lostItemSearch: (params: LostItemSearchParams) => + queryOptions({ + queryKey: articleQueryKeys.lostItemSearch(params), + queryFn: () => getLostItemSearch(params), + }), + + lostItemStat: () => + queryOptions({ + queryKey: articleQueryKeys.lostItemStat, + queryFn: getLostItemStat, + }), + + lostItemChatroomList: (token: string) => + queryOptions({ + queryKey: articleQueryKeys.lostItemChatroomList, + queryFn: () => getLostItemChatroomList(token), + }), + + lostItemChatroomDetail: (token: string, articleId: number, chatroomId: number) => + queryOptions({ + queryKey: articleQueryKeys.lostItemChatroomDetail(articleId, chatroomId), + queryFn: () => getLostItemChatroomDetail(token, articleId, chatroomId), + }), + + lostItemChatroomMessages: (token: string, articleId: number, chatroomId: number) => + queryOptions({ + queryKey: articleQueryKeys.lostItemChatroomMessages(articleId, chatroomId), + queryFn: () => getLostItemChatroomMessagesV2(token, articleId, chatroomId), + }), +}; diff --git a/src/api/auth/queries.ts b/src/api/auth/queries.ts new file mode 100644 index 000000000..b90e28d2c --- /dev/null +++ b/src/api/auth/queries.ts @@ -0,0 +1,34 @@ +import { queryOptions } from '@tanstack/react-query'; +import { GeneralUserResponse, UserAcademicInfoResponse, UserResponse } from './entity'; +import { getGeneralUser, getUser, getUserAcademicInfo } from './index'; + +type AuthUserType = 'STUDENT' | 'GENERAL'; +type AuthUserInfoResponse = UserResponse | GeneralUserResponse; + +const getUserInfo = (token: string, userType: AuthUserType): Promise => { + if (userType === 'STUDENT') { + return getUser(token); + } + + return getGeneralUser(token); +}; + +export const authQueryKeys = { + all: ['auth'] as const, + userInfo: (token: string, userType: AuthUserType) => [...authQueryKeys.all, 'user-info', token, userType] as const, + userAcademicInfo: (token: string) => [...authQueryKeys.all, 'user-academic-info', token] as const, +}; + +export const authQueries = { + userInfo: (token: string, userType: AuthUserType) => + queryOptions({ + queryKey: authQueryKeys.userInfo(token, userType), + queryFn: () => (token ? getUserInfo(token, userType) : null), + }), + + userAcademicInfo: (token: string) => + queryOptions({ + queryKey: authQueryKeys.userAcademicInfo(token), + queryFn: () => (token ? getUserAcademicInfo(token) : null), + }), +}; diff --git a/src/api/banner/queries.ts b/src/api/banner/queries.ts new file mode 100644 index 000000000..594b28bad --- /dev/null +++ b/src/api/banner/queries.ts @@ -0,0 +1,22 @@ +import { queryOptions } from '@tanstack/react-query'; +import { getBannerCategoryList, getBanners } from './index'; + +export const bannerQueryKeys = { + all: ['banner'] as const, + categories: () => [...bannerQueryKeys.all, 'categories'] as const, + list: (categoryId: number) => [...bannerQueryKeys.all, 'list', categoryId] as const, +}; + +export const bannerQueries = { + categories: () => + queryOptions({ + queryKey: bannerQueryKeys.categories(), + queryFn: getBannerCategoryList, + }), + + list: (categoryId: number) => + queryOptions({ + queryKey: bannerQueryKeys.list(categoryId), + queryFn: () => getBanners(categoryId), + }), +}; diff --git a/src/api/bus/queries.ts b/src/api/bus/queries.ts new file mode 100644 index 000000000..37857df3f --- /dev/null +++ b/src/api/bus/queries.ts @@ -0,0 +1,92 @@ +import { queryOptions, skipToken } from '@tanstack/react-query'; +import { + BusRouteParams, + CityBusParams, + Depart, + Arrival, + ExpressCourse, + ShuttleCourse, +} from './entity'; +import { + getBusNoticeInfo, + getBusRouteInfo, + getBusTimetableInfo, + getCityBusTimetableInfo, + getShuttleCourseInfo, + getShuttleTimetableDetailInfo, +} from './index'; + +export interface BusRouteQueryParams extends Omit { + depart: Depart | ''; + arrival: Arrival | ''; +} + +export const busQueryKeys = { + all: ['bus'] as const, + notice: () => [...busQueryKeys.all, 'notice'] as const, + shuttleCourse: () => [...busQueryKeys.all, 'courses', 'shuttle'] as const, + timetable: ['bus', 'timetable'] as const, + shuttleTimetable: (course: ShuttleCourse) => + [...busQueryKeys.timetable, 'shuttle', course.bus_type, course.direction, course.region] as const, + expressTimetable: (course: ExpressCourse) => + [...busQueryKeys.timetable, 'express', course.bus_type, course.direction, course.region] as const, + cityTimetable: (course: CityBusParams) => + [...busQueryKeys.timetable, 'city', course.bus_number, course.direction] as const, + shuttleTimetableDetail: (id: string | null) => [...busQueryKeys.all, 'shuttle', 'timetable', id] as const, + route: (params: BusRouteQueryParams) => [...busQueryKeys.all, 'route', JSON.stringify(params)] as const, +}; + +export const busQueries = { + notice: () => + queryOptions({ + queryKey: busQueryKeys.notice(), + queryFn: getBusNoticeInfo, + }), + + shuttleCourse: () => + queryOptions({ + queryKey: busQueryKeys.shuttleCourse(), + queryFn: getShuttleCourseInfo, + }), + + shuttleTimetable: (course: ShuttleCourse) => + queryOptions({ + queryKey: busQueryKeys.shuttleTimetable(course), + queryFn: () => getBusTimetableInfo(course), + }), + + expressTimetable: (course: ExpressCourse) => + queryOptions({ + queryKey: busQueryKeys.expressTimetable(course), + queryFn: () => getBusTimetableInfo(course), + }), + + cityTimetable: (course: CityBusParams) => + queryOptions({ + queryKey: busQueryKeys.cityTimetable(course), + queryFn: () => getCityBusTimetableInfo(course), + }), + + shuttleTimetableDetail: (id: string | null) => + queryOptions({ + queryKey: busQueryKeys.shuttleTimetableDetail(id), + queryFn: id ? () => getShuttleTimetableDetailInfo({ id }) : skipToken, + }), + + route: (params: BusRouteQueryParams) => { + const { depart, arrival, ...rest } = params; + + return queryOptions({ + queryKey: busQueryKeys.route(params), + queryFn: + depart && arrival + ? () => + getBusRouteInfo({ + ...rest, + depart: depart as Depart, + arrival: arrival as Arrival, + }) + : skipToken, + }); + }, +}; diff --git a/src/api/cafeteria/mutations.ts b/src/api/cafeteria/mutations.ts new file mode 100644 index 000000000..fe0c46e69 --- /dev/null +++ b/src/api/cafeteria/mutations.ts @@ -0,0 +1,20 @@ +import { mutationOptions, QueryClient } from '@tanstack/react-query'; +import { cafeteriaQueryKeys } from './queries'; +import { cancelCafeteriaDiningLike, likeCafeteriaDining } from './index'; + +const invalidateDinings = (queryClient: QueryClient, date: string) => + queryClient.invalidateQueries({ queryKey: cafeteriaQueryKeys.dinings(date) }); + +export const cafeteriaMutations = { + likeDining: (queryClient: QueryClient, token: string, date: string) => + mutationOptions({ + mutationFn: (diningId: number) => likeCafeteriaDining(diningId, token), + onSuccess: () => invalidateDinings(queryClient, date), + }), + + cancelLikeDining: (queryClient: QueryClient, token: string, date: string) => + mutationOptions({ + mutationFn: (diningId: number) => cancelCafeteriaDiningLike(diningId, token), + onSuccess: () => invalidateDinings(queryClient, date), + }), +}; diff --git a/src/api/cafeteria/queries.ts b/src/api/cafeteria/queries.ts new file mode 100644 index 000000000..1394b83d8 --- /dev/null +++ b/src/api/cafeteria/queries.ts @@ -0,0 +1,15 @@ +import { queryOptions } from '@tanstack/react-query'; +import { getCafeteriaDinings } from './index'; + +export const cafeteriaQueryKeys = { + all: ['cafeteria'] as const, + dinings: (date: string) => [...cafeteriaQueryKeys.all, 'dinings', date] as const, +}; + +export const cafeteriaQueries = { + dinings: (date: string) => + queryOptions({ + queryKey: cafeteriaQueryKeys.dinings(date), + queryFn: () => getCafeteriaDinings(date), + }), +}; diff --git a/src/api/callvan/mutations.ts b/src/api/callvan/mutations.ts new file mode 100644 index 000000000..34fdf3ac4 --- /dev/null +++ b/src/api/callvan/mutations.ts @@ -0,0 +1,90 @@ +import { mutationOptions, QueryClient } from '@tanstack/react-query'; +import { CallvanReportRequest, CreateCallvanRequest, SendChatRequest } from './entity'; +import { callvanQueryKeys } from './queries'; +import { + cancelCallvan, + closeCallvanPost, + completeCallvanPost, + createCallvan, + deleteAllNotifications, + joinCallvan, + markAllNotificationsRead, + markNotificationRead, + reopenCallvanPost, + reportCallvanParticipant, + sendCallvanChat, +} from './index'; + +const invalidateCallvanInfiniteList = (queryClient: QueryClient) => + queryClient.invalidateQueries({ queryKey: callvanQueryKeys.infiniteListRoot }); + +const invalidateCallvanNotifications = (queryClient: QueryClient) => + queryClient.invalidateQueries({ queryKey: callvanQueryKeys.notifications }); + +export const callvanMutations = { + create: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: (data: CreateCallvanRequest) => createCallvan(token, data), + onSuccess: () => invalidateCallvanInfiniteList(queryClient), + }), + + join: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: (postId: number) => joinCallvan(token, postId), + onSuccess: () => invalidateCallvanInfiniteList(queryClient), + }), + + cancel: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: (postId: number) => cancelCallvan(token, postId), + onSuccess: () => invalidateCallvanInfiniteList(queryClient), + }), + + close: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: (postId: number) => closeCallvanPost(token, postId), + onSuccess: () => invalidateCallvanInfiniteList(queryClient), + }), + + reopen: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: (postId: number) => reopenCallvanPost(token, postId), + onSuccess: () => invalidateCallvanInfiniteList(queryClient), + }), + + complete: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: (postId: number) => completeCallvanPost(token, postId), + onSuccess: () => invalidateCallvanInfiniteList(queryClient), + }), + + report: (queryClient: QueryClient, token: string, postId: number) => + mutationOptions({ + mutationFn: (data: CallvanReportRequest) => reportCallvanParticipant(token, postId, data), + onSuccess: () => queryClient.invalidateQueries({ queryKey: callvanQueryKeys.postDetail(postId) }), + }), + + markAllNotificationsRead: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: () => markAllNotificationsRead(token), + onSuccess: () => invalidateCallvanNotifications(queryClient), + }), + + markNotificationRead: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: (notificationId: number) => markNotificationRead(token, notificationId), + onSuccess: () => invalidateCallvanNotifications(queryClient), + }), + + deleteAllNotifications: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: () => deleteAllNotifications(token), + onSuccess: () => invalidateCallvanNotifications(queryClient), + }), + + sendChat: (queryClient: QueryClient, token: string, postId: number) => + mutationOptions({ + mutationFn: (data: SendChatRequest) => sendCallvanChat(token, postId, data), + onSuccess: () => queryClient.invalidateQueries({ queryKey: callvanQueryKeys.chat(postId) }), + }), +}; diff --git a/src/api/callvan/queries.ts b/src/api/callvan/queries.ts new file mode 100644 index 000000000..1ca9fe369 --- /dev/null +++ b/src/api/callvan/queries.ts @@ -0,0 +1,66 @@ +import { infiniteQueryOptions, queryOptions } from '@tanstack/react-query'; +import { CallvanListRequest } from './entity'; +import { getCallvanChat, getCallvanList, getCallvanNotifications, getCallvanPostDetail } from './index'; + +const CALLVAN_LIST_LIMIT = 10; + +type CallvanInfiniteListParams = Omit; + +export const callvanQueryKeys = { + all: ['callvan'] as const, + listRoot: ['callvan', 'list'] as const, + list: (params: CallvanListRequest) => [...callvanQueryKeys.listRoot, params] as const, + infiniteListRoot: ['callvan', 'infinite-list'] as const, + infiniteList: (params: CallvanInfiniteListParams) => [...callvanQueryKeys.infiniteListRoot, params] as const, + notifications: ['callvan', 'notifications'] as const, + postDetail: (postId: number) => ['callvan', 'post-detail', postId] as const, + chat: (postId: number) => ['callvan', 'chat', postId] as const, +}; + +export const callvanQueries = { + list: (token: string, params: CallvanListRequest) => + queryOptions({ + queryKey: callvanQueryKeys.list(params), + queryFn: () => getCallvanList(token, params), + }), + + infiniteList: (token: string, params: CallvanInfiniteListParams) => + infiniteQueryOptions({ + queryKey: callvanQueryKeys.infiniteList(params), + initialPageParam: 1, + queryFn: ({ pageParam }) => + getCallvanList(token, { + ...params, + page: pageParam, + limit: CALLVAN_LIST_LIMIT, + }), + getNextPageParam: (lastPage) => { + if (lastPage.current_page < lastPage.total_page) { + return lastPage.current_page + 1; + } + + return undefined; + }, + }), + + notifications: (token: string) => + queryOptions({ + queryKey: callvanQueryKeys.notifications, + queryFn: () => getCallvanNotifications(token), + }), + + postDetail: (token: string, postId: number) => + queryOptions({ + queryKey: callvanQueryKeys.postDetail(postId), + queryFn: () => getCallvanPostDetail(token, postId), + staleTime: 60000, + }), + + chat: (token: string, postId: number) => + queryOptions({ + queryKey: callvanQueryKeys.chat(postId), + queryFn: () => getCallvanChat(token, postId), + staleTime: 0, + refetchInterval: 1000, + }), +}; diff --git a/src/api/club/mutations.ts b/src/api/club/mutations.ts new file mode 100644 index 000000000..b9c69d343 --- /dev/null +++ b/src/api/club/mutations.ts @@ -0,0 +1,226 @@ +import { mutationOptions, QueryClient } from '@tanstack/react-query'; +import { clubQueryKeys } from './queries'; +import type { ClubEventRequest, ClubRecruitmentRequest, NewClubData, NewClubManager } from './entity'; +import { + deleteClubEvent, + deleteClubEventNotification, + deleteClubLike, + deleteClubRecruitment, + deleteClubRecruitmentNotification, + postClub, + postClubEvent, + postClubEventNotification, + postClubRecruitment, + postClubRecruitmentNotification, + putClubDetail, + putClubEvent, + putClubLike, + putClubRecruitment, + putNewClubManager, +} from './index'; + +interface ClubMutationCallbacks { + onSuccess?: () => void | Promise; +} + +const invalidateClubListQueries = async (queryClient: QueryClient, includeHot = false) => { + const tasks = [queryClient.invalidateQueries({ queryKey: clubQueryKeys.listRoot() })]; + + if (includeHot) { + tasks.push(queryClient.invalidateQueries({ queryKey: clubQueryKeys.hot() })); + } + + await Promise.all(tasks); +}; + +const invalidateClubDetailAndListQueries = async (queryClient: QueryClient, clubId: number, includeHot = false) => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: clubQueryKeys.detailRoot(clubId) }), + invalidateClubListQueries(queryClient, includeHot), + ]); +}; + +const invalidateRecruitmentQueries = async (queryClient: QueryClient, clubId: number) => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: clubQueryKeys.recruitment(clubId) }), + queryClient.invalidateQueries({ queryKey: clubQueryKeys.listRoot() }), + ]); +}; + +const invalidateEventListQueries = async (queryClient: QueryClient, clubId: number | string) => { + await queryClient.invalidateQueries({ queryKey: clubQueryKeys.eventListRoot(clubId) }); +}; + +export const clubMutations = { + toggleLikeForList: (queryClient: QueryClient, callbacks: ClubMutationCallbacks = {}) => + mutationOptions({ + mutationFn: ({ token, clubId, isLiked }: { token: string; clubId: number; isLiked: boolean }) => + isLiked ? deleteClubLike(token, clubId) : putClubLike(token, clubId), + onSuccess: async () => { + await invalidateClubListQueries(queryClient); + await callbacks.onSuccess?.(); + }, + }), + + likeForDetail: (queryClient: QueryClient, token: string, clubId: number, callbacks: ClubMutationCallbacks = {}) => + mutationOptions({ + mutationFn: () => putClubLike(token, clubId), + onSuccess: async () => { + await invalidateClubDetailAndListQueries(queryClient, clubId); + await callbacks.onSuccess?.(); + }, + }), + + unlikeForDetail: (queryClient: QueryClient, token: string, clubId: number, callbacks: ClubMutationCallbacks = {}) => + mutationOptions({ + mutationFn: () => deleteClubLike(token, clubId), + onSuccess: async () => { + await invalidateClubDetailAndListQueries(queryClient, clubId); + await callbacks.onSuccess?.(); + }, + }), + + create: (queryClient: QueryClient, token: string, callbacks: ClubMutationCallbacks = {}) => + mutationOptions({ + mutationFn: (data: NewClubData) => postClub(token, data), + onSuccess: async () => { + await invalidateClubListQueries(queryClient); + await callbacks.onSuccess?.(); + }, + }), + + update: (queryClient: QueryClient, token: string, clubId: number | string, callbacks: ClubMutationCallbacks = {}) => + mutationOptions({ + mutationFn: (data: NewClubData) => putClubDetail(token, data, clubId), + onSuccess: async () => { + await invalidateClubDetailAndListQueries(queryClient, Number(clubId), true); + await callbacks.onSuccess?.(); + }, + }), + + createRecruitment: (queryClient: QueryClient, token: string, clubId: number, callbacks: ClubMutationCallbacks = {}) => + mutationOptions({ + mutationFn: (data: ClubRecruitmentRequest) => postClubRecruitment(token, clubId, data), + onSuccess: async () => { + await invalidateRecruitmentQueries(queryClient, clubId); + await callbacks.onSuccess?.(); + }, + }), + + updateRecruitment: (queryClient: QueryClient, token: string, clubId: number, callbacks: ClubMutationCallbacks = {}) => + mutationOptions({ + mutationFn: (data: ClubRecruitmentRequest) => putClubRecruitment(token, clubId, data), + onSuccess: async () => { + await invalidateRecruitmentQueries(queryClient, clubId); + await callbacks.onSuccess?.(); + }, + }), + + deleteRecruitment: (queryClient: QueryClient, token: string, clubId: number, callbacks: ClubMutationCallbacks = {}) => + mutationOptions({ + mutationFn: () => deleteClubRecruitment(token, clubId), + onSuccess: async () => { + await invalidateRecruitmentQueries(queryClient, clubId); + await callbacks.onSuccess?.(); + }, + }), + + createEvent: (queryClient: QueryClient, token: string, clubId: number, callbacks: ClubMutationCallbacks = {}) => + mutationOptions({ + mutationFn: (data: ClubEventRequest) => postClubEvent(token, clubId, data), + onSuccess: async () => { + await invalidateEventListQueries(queryClient, clubId); + await callbacks.onSuccess?.(); + }, + }), + + updateEvent: (queryClient: QueryClient, token: string, clubId: number, callbacks: ClubMutationCallbacks = {}) => + mutationOptions({ + mutationFn: ({ eventId, data }: { eventId: number; data: ClubEventRequest }) => + putClubEvent(token, clubId, eventId, data), + onSuccess: async (_, variables) => { + await Promise.all([ + invalidateEventListQueries(queryClient, clubId), + queryClient.invalidateQueries({ queryKey: clubQueryKeys.eventDetail(clubId, variables.eventId) }), + ]); + await callbacks.onSuccess?.(); + }, + }), + + deleteEvent: (queryClient: QueryClient, token: string, clubId: number, callbacks: ClubMutationCallbacks = {}) => + mutationOptions({ + mutationFn: (eventId: number) => deleteClubEvent(token, clubId, eventId), + onSuccess: async (_, eventId) => { + await Promise.all([ + invalidateEventListQueries(queryClient, clubId), + queryClient.invalidateQueries({ queryKey: clubQueryKeys.eventDetail(clubId, eventId) }), + ]); + await callbacks.onSuccess?.(); + }, + }), + + mandateManager: (queryClient: QueryClient, token: string, clubId: number, callbacks: ClubMutationCallbacks = {}) => + mutationOptions({ + mutationFn: (data: NewClubManager) => putNewClubManager(token, data), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: clubQueryKeys.detailRoot(clubId) }); + await callbacks.onSuccess?.(); + }, + }), + + subscribeRecruitmentNotification: ( + queryClient: QueryClient, + token: string, + clubId: number, + callbacks: ClubMutationCallbacks = {}, + ) => + mutationOptions({ + mutationFn: () => postClubRecruitmentNotification(token, clubId), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: clubQueryKeys.detailRoot(clubId) }); + await callbacks.onSuccess?.(); + }, + }), + + unsubscribeRecruitmentNotification: ( + queryClient: QueryClient, + token: string, + clubId: number, + callbacks: ClubMutationCallbacks = {}, + ) => + mutationOptions({ + mutationFn: () => deleteClubRecruitmentNotification(token, clubId), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: clubQueryKeys.detailRoot(clubId) }); + await callbacks.onSuccess?.(); + }, + }), + + subscribeEventNotification: ( + queryClient: QueryClient, + token: string, + clubId: number, + callbacks: ClubMutationCallbacks = {}, + ) => + mutationOptions({ + mutationFn: (eventId: number) => postClubEventNotification(token, clubId, eventId), + onSuccess: async () => { + await invalidateEventListQueries(queryClient, clubId); + await callbacks.onSuccess?.(); + }, + }), + + unsubscribeEventNotification: ( + queryClient: QueryClient, + token: string, + clubId: number, + callbacks: ClubMutationCallbacks = {}, + ) => + mutationOptions({ + mutationFn: (eventId: number) => deleteClubEventNotification(token, clubId, eventId), + onSuccess: async () => { + await invalidateEventListQueries(queryClient, clubId); + await callbacks.onSuccess?.(); + }, + }), +}; diff --git a/src/api/club/queries.ts b/src/api/club/queries.ts new file mode 100644 index 000000000..842de1f09 --- /dev/null +++ b/src/api/club/queries.ts @@ -0,0 +1,141 @@ +import { isKoinError } from '@bcsdlab/koin'; +import { queryOptions } from '@tanstack/react-query'; +import type { ClubRecruitmentResponse, HotClubResponse } from './entity'; +import { + getClubCategories, + getClubDetail, + getClubEventDetail, + getClubEventList, + getClubList, + getClubQnA, + getHotClub, + getRecruitmentClub, +} from './index'; + +const EMPTY_HOT_CLUB: HotClubResponse = { + club_id: -1, + name: '인기 동아리가 없어요', + image_url: '', +}; + +const EMPTY_RECRUITMENT: ClubRecruitmentResponse = { + id: 0, + status: 'NONE', + dday: 0, + start_date: '', + end_date: '', + image_url: '', + content: '', + is_manager: false, +}; + +interface ClubListQueryParams { + token?: string | null; + categoryId?: number; + sortType?: string; + isRecruiting?: boolean; + clubName?: string; +} + +type ClubViewerScope = 'auth' | 'guest'; + +const getViewerScope = (token?: string | null): ClubViewerScope => (token ? 'auth' : 'guest'); + +export const clubQueryKeys = { + all: ['club'] as const, + categories: (token?: string | null) => [...clubQueryKeys.all, 'categories', getViewerScope(token)] as const, + listRoot: () => [...clubQueryKeys.all, 'list'] as const, + list: ({ token, categoryId, sortType, isRecruiting, clubName }: ClubListQueryParams) => + [ + ...clubQueryKeys.listRoot(), + getViewerScope(token), + categoryId ?? null, + sortType ?? '', + Boolean(isRecruiting), + clubName ?? '', + ] as const, + hot: () => [...clubQueryKeys.all, 'hot'] as const, + detailRoot: (clubId?: number | string) => + clubId === undefined ? [...clubQueryKeys.all, 'detail'] as const : [...clubQueryKeys.all, 'detail', Number(clubId)] as const, + detail: (clubId: number, token?: string | null) => + [...clubQueryKeys.detailRoot(clubId), getViewerScope(token)] as const, + recruitment: (clubId: number) => [...clubQueryKeys.all, 'recruitment', clubId] as const, + eventListRoot: (clubId?: string | number) => + clubId === undefined + ? [...clubQueryKeys.all, 'event-list'] as const + : [...clubQueryKeys.all, 'event-list', clubId] as const, + eventList: (clubId: string | number, eventType: string, token?: string | null) => + [...clubQueryKeys.eventListRoot(clubId), eventType, getViewerScope(token)] as const, + eventDetail: (clubId: string | number, eventId: string | number) => + [...clubQueryKeys.all, 'event-detail', clubId, eventId] as const, + qna: (clubId: number | string, token?: string | null) => + [...clubQueryKeys.all, 'qna', clubId, getViewerScope(token)] as const, +}; + +export const clubQueries = { + categories: (token?: string | null) => + queryOptions({ + queryKey: clubQueryKeys.categories(token), + queryFn: () => getClubCategories(token ?? undefined), + }), + + list: ({ token, categoryId, sortType, isRecruiting, clubName }: ClubListQueryParams) => + queryOptions({ + queryKey: clubQueryKeys.list({ token, categoryId, sortType, isRecruiting, clubName }), + queryFn: () => getClubList(token ?? undefined, categoryId, sortType, isRecruiting, clubName), + }), + + hot: () => + queryOptions({ + queryKey: clubQueryKeys.hot(), + queryFn: async () => { + try { + return await getHotClub(); + } catch (error) { + if (isKoinError(error) && error.status === 404) { + return EMPTY_HOT_CLUB; + } + throw error; + } + }, + }), + + detail: (clubId: number, token?: string | null) => + queryOptions({ + queryKey: clubQueryKeys.detail(clubId, token), + queryFn: () => getClubDetail(token ?? '', clubId), + }), + + recruitment: (clubId: number) => + queryOptions({ + queryKey: clubQueryKeys.recruitment(clubId), + queryFn: async () => { + try { + return await getRecruitmentClub(clubId); + } catch (error) { + if (isKoinError(error) && error.status === 404) { + return EMPTY_RECRUITMENT; + } + throw error; + } + }, + }), + + eventList: (clubId: string | number, eventType: 'RECENT' | 'ONGOING' | 'UPCOMING' | 'ENDED', token?: string | null) => + queryOptions({ + queryKey: clubQueryKeys.eventList(clubId, eventType, token), + queryFn: () => getClubEventList(clubId, eventType, token ?? undefined), + }), + + eventDetail: (clubId: string | number, eventId: string | number) => + queryOptions({ + queryKey: clubQueryKeys.eventDetail(clubId, eventId), + queryFn: () => getClubEventDetail(clubId, eventId), + }), + + qna: (clubId: number | string, token?: string | null) => + queryOptions({ + queryKey: clubQueryKeys.qna(clubId, token), + queryFn: () => getClubQnA(token ?? '', Number(clubId)), + }), +}; diff --git a/src/api/coopshop/queries.ts b/src/api/coopshop/queries.ts new file mode 100644 index 000000000..5aab7eaa8 --- /dev/null +++ b/src/api/coopshop/queries.ts @@ -0,0 +1,22 @@ +import { queryOptions } from '@tanstack/react-query'; +import { getAllShopInfo, getCafeteriaInfo } from './index'; + +export const coopshopQueryKeys = { + all: ['coopshop'] as const, + allShopInfo: () => [...coopshopQueryKeys.all, 'all-shop-info'] as const, + cafeteriaInfo: () => [...coopshopQueryKeys.all, 'cafeteria-info'] as const, +}; + +export const coopshopQueries = { + allShopInfo: () => + queryOptions({ + queryKey: coopshopQueryKeys.allShopInfo(), + queryFn: getAllShopInfo, + }), + + cafeteriaInfo: () => + queryOptions({ + queryKey: coopshopQueryKeys.cafeteriaInfo(), + queryFn: getCafeteriaInfo, + }), +}; diff --git a/src/api/course/queries.ts b/src/api/course/queries.ts new file mode 100644 index 000000000..e2eb8e7c9 --- /dev/null +++ b/src/api/course/queries.ts @@ -0,0 +1,24 @@ +import { queryOptions } from '@tanstack/react-query'; +import { CourseRequestParams } from './entity'; +import { getCourseSearch, getPreCourseList } from './index'; + +export const courseQueryKeys = { + all: ['course'] as const, + search: (params: CourseRequestParams) => [...courseQueryKeys.all, 'search', params] as const, + preCourseList: (timetableFrameId: number) => [...courseQueryKeys.all, 'pre-course-list', timetableFrameId] as const, +}; + +export const courseQueries = { + search: (params: CourseRequestParams) => + queryOptions({ + queryKey: courseQueryKeys.search(params), + queryFn: () => getCourseSearch(params.name || undefined, params.department || undefined, params.year, params.semester), + }), + + preCourseList: (token: string, timetableFrameId: number) => + queryOptions({ + queryKey: courseQueryKeys.preCourseList(timetableFrameId), + queryFn: () => getPreCourseList(token, timetableFrameId), + gcTime: 0, + }), +}; diff --git a/src/api/dept/queries.ts b/src/api/dept/queries.ts new file mode 100644 index 000000000..3e4bf2a45 --- /dev/null +++ b/src/api/dept/queries.ts @@ -0,0 +1,22 @@ +import { queryOptions } from '@tanstack/react-query'; +import { getDeptList, getDeptMajorList } from './index'; + +export const deptQueryKeys = { + all: ['dept'] as const, + list: () => [...deptQueryKeys.all, 'list'] as const, + majorList: () => [...deptQueryKeys.all, 'major-list'] as const, +}; + +export const deptQueries = { + list: () => + queryOptions({ + queryKey: deptQueryKeys.list(), + queryFn: getDeptList, + }), + + majorList: () => + queryOptions({ + queryKey: deptQueryKeys.majorList(), + queryFn: getDeptMajorList, + }), +}; diff --git a/src/api/graduationCalculator/queries.ts b/src/api/graduationCalculator/queries.ts new file mode 100644 index 000000000..c66d33492 --- /dev/null +++ b/src/api/graduationCalculator/queries.ts @@ -0,0 +1,40 @@ +import { queryOptions } from '@tanstack/react-query'; +import { Semester } from './entity'; +import { calculateGraduationCredits, getCourseType, getGeneralEducation } from './index'; + +export const graduationCalculatorQueryKeys = { + all: ['graduation-calculator'] as const, + creditsByCourseType: ['graduation-calculator', 'credits-by-course-type'] as const, + generalEducation: ['graduation-calculator', 'general-education'] as const, + courseType: (semester: Semester, name: string, generalEducationArea?: string) => + [ + 'graduation-calculator', + 'course-type', + { + year: semester.year, + term: semester.term, + name, + generalEducationArea: generalEducationArea ?? '', + }, + ] as const, +}; + +export const graduationCalculatorQueries = { + creditsByCourseType: (token: string) => + queryOptions({ + queryKey: graduationCalculatorQueryKeys.creditsByCourseType, + queryFn: () => calculateGraduationCredits(token), + }), + + generalEducation: (token: string) => + queryOptions({ + queryKey: graduationCalculatorQueryKeys.generalEducation, + queryFn: () => getGeneralEducation(token), + }), + + courseType: (token: string, semester: Semester, name: string, generalEducationArea?: string) => + queryOptions({ + queryKey: graduationCalculatorQueryKeys.courseType(semester, name, generalEducationArea), + queryFn: () => getCourseType(token, semester, name, generalEducationArea), + }), +}; diff --git a/src/api/review/mutations.ts b/src/api/review/mutations.ts new file mode 100644 index 000000000..31eb43df8 --- /dev/null +++ b/src/api/review/mutations.ts @@ -0,0 +1,43 @@ +import { mutationOptions, QueryClient } from '@tanstack/react-query'; +import { storeQueryKeys } from 'api/store/queries'; +import { ReviewRequest } from './entity'; +import { postStoreReview, putStoreReview } from './index'; + +interface ReviewMutationCallbacks { + onSuccess?: () => void | Promise; +} + +const invalidateStoreReviewQueries = async (queryClient: QueryClient, shopId: string) => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: storeQueryKeys.reviews(Number(shopId)) }), + queryClient.invalidateQueries({ queryKey: storeQueryKeys.myReviews(shopId) }), + queryClient.invalidateQueries({ queryKey: storeQueryKeys.detail(shopId) }), + queryClient.invalidateQueries({ queryKey: storeQueryKeys.detailPage(shopId) }), + ]); +}; + +export const reviewMutations = { + add: (queryClient: QueryClient, token: string, shopId: string, callbacks: ReviewMutationCallbacks = {}) => + mutationOptions({ + mutationFn: (reviewData: ReviewRequest) => postStoreReview(token, shopId, reviewData), + onSuccess: async () => { + await invalidateStoreReviewQueries(queryClient, shopId); + await callbacks.onSuccess?.(); + }, + }), + + edit: ( + queryClient: QueryClient, + token: string, + shopId: string, + reviewId: string, + callbacks: ReviewMutationCallbacks = {}, + ) => + mutationOptions({ + mutationFn: (reviewData: ReviewRequest) => putStoreReview(token, shopId, reviewId, reviewData), + onSuccess: async () => { + await invalidateStoreReviewQueries(queryClient, shopId); + await callbacks.onSuccess?.(); + }, + }), +}; diff --git a/src/api/review/queries.ts b/src/api/review/queries.ts new file mode 100644 index 000000000..79cb58b75 --- /dev/null +++ b/src/api/review/queries.ts @@ -0,0 +1,15 @@ +import { queryOptions } from '@tanstack/react-query'; +import { getStoreReview } from './index'; + +export const reviewQueryKeys = { + all: ['review'] as const, + detail: (shopId: string, reviewId: string) => [...reviewQueryKeys.all, Number(shopId), reviewId] as const, +}; + +export const reviewQueries = { + detail: (token: string, shopId: string, reviewId: string) => + queryOptions({ + queryKey: reviewQueryKeys.detail(shopId, reviewId), + queryFn: () => getStoreReview(token, shopId, reviewId), + }), +}; diff --git a/src/api/room/queries.ts b/src/api/room/queries.ts new file mode 100644 index 000000000..de1ee9892 --- /dev/null +++ b/src/api/room/queries.ts @@ -0,0 +1,22 @@ +import { queryOptions } from '@tanstack/react-query'; +import { getRoomDetailInfo, getRoomList } from './index'; + +export const roomQueryKeys = { + all: ['room'] as const, + list: () => [...roomQueryKeys.all, 'list'] as const, + detail: (id: string) => [...roomQueryKeys.all, 'detail', id] as const, +}; + +export const roomQueries = { + list: () => + queryOptions({ + queryKey: roomQueryKeys.list(), + queryFn: getRoomList, + }), + + detail: (id: string) => + queryOptions({ + queryKey: roomQueryKeys.detail(id), + queryFn: () => getRoomDetailInfo(id), + }), +}; diff --git a/src/api/store/mutations.ts b/src/api/store/mutations.ts new file mode 100644 index 000000000..921e75494 --- /dev/null +++ b/src/api/store/mutations.ts @@ -0,0 +1,49 @@ +import { mutationOptions, QueryClient } from '@tanstack/react-query'; +import { ReviewReportRequest } from './entity'; +import { storeQueryKeys } from './queries'; +import { deleteReview, postReviewReport } from './index'; + +interface StoreMutationCallbacks { + onSuccess?: () => void | Promise; +} + +const invalidateStoreReviewQueries = async (queryClient: QueryClient, shopId: string) => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: storeQueryKeys.reviews(Number(shopId)) }), + queryClient.invalidateQueries({ queryKey: storeQueryKeys.myReviews(shopId) }), + queryClient.invalidateQueries({ queryKey: storeQueryKeys.detail(shopId) }), + queryClient.invalidateQueries({ queryKey: storeQueryKeys.detailPage(shopId) }), + ]); +}; + +export const storeMutations = { + deleteReview: ( + queryClient: QueryClient, + reviewId: number, + shopId: string, + token: string, + callbacks: StoreMutationCallbacks = {}, + ) => + mutationOptions({ + mutationFn: () => deleteReview(reviewId, shopId, token), + onSuccess: async () => { + await invalidateStoreReviewQueries(queryClient, shopId); + await callbacks.onSuccess?.(); + }, + }), + + reportReview: ( + queryClient: QueryClient, + shopId: string, + reviewId: string, + token: string, + callbacks: StoreMutationCallbacks = {}, + ) => + mutationOptions({ + mutationFn: (data: ReviewReportRequest) => postReviewReport(Number(shopId), Number(reviewId), data, token), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: storeQueryKeys.reviews(Number(shopId)) }); + await callbacks.onSuccess?.(); + }, + }), +}; diff --git a/src/api/store/queries.ts b/src/api/store/queries.ts new file mode 100644 index 000000000..b8db76244 --- /dev/null +++ b/src/api/store/queries.ts @@ -0,0 +1,130 @@ +import { infiniteQueryOptions, queryOptions } from '@tanstack/react-query'; +import { StoreFilterType, StoreSorterType } from './entity'; +import { + getAllEvent, + getMyReview, + getRelateSearch, + getReviewList, + getStoreBenefitCategory, + getStoreBenefitList, + getStoreCategories, + getStoreDetailInfo, + getStoreDetailMenu, + getStoreEventList, + getStoreListV2, +} from './index'; + +interface StoreListQueryParams { + sorter: StoreSorterType; + filter: StoreFilterType[]; + query?: string; +} + +interface StoreReviewListQueryParams { + shopId: number; + page: number; + sorter: string; + token?: string; +} + +export const storeQueryKeys = { + all: ['store'] as const, + categories: () => [...storeQueryKeys.all, 'categories'] as const, + listV2: ({ sorter, filter, query }: StoreListQueryParams) => + [...storeQueryKeys.all, 'list-v2', { sorter, filter, query: query ?? '' }] as const, + allEvents: () => [...storeQueryKeys.all, 'all-events'] as const, + detail: (id: string) => [...storeQueryKeys.all, 'detail', id] as const, + detailMenu: (id: string) => [...storeQueryKeys.all, 'detail-menu', id] as const, + detailPage: (id: string) => [...storeQueryKeys.all, 'detail-page', id] as const, + eventList: (id: string) => [...storeQueryKeys.all, 'event-list', id] as const, + benefitCategory: () => [...storeQueryKeys.all, 'benefit-category'] as const, + benefitList: (id: string) => [...storeQueryKeys.all, 'benefit-list', id] as const, + relatedSearch: (query: string) => [...storeQueryKeys.all, 'related-search', query] as const, + reviews: (shopId: number) => ['review', shopId] as const, + reviewFeed: (shopId: number, sorter: string) => [...storeQueryKeys.reviews(shopId), sorter] as const, + reviewList: ({ shopId, page, sorter }: Omit) => + [...storeQueryKeys.reviewFeed(shopId, sorter), page] as const, + myReviews: (shopId: string) => ['review', 'my-review', shopId] as const, + myReview: (shopId: string, sorter: string) => [...storeQueryKeys.myReviews(shopId), sorter] as const, +}; + +export const storeQueries = { + categories: () => + queryOptions({ + queryKey: storeQueryKeys.categories(), + queryFn: getStoreCategories, + }), + + listV2: ({ sorter, filter, query }: StoreListQueryParams) => + queryOptions({ + queryKey: storeQueryKeys.listV2({ sorter, filter, query }), + queryFn: () => getStoreListV2(sorter, filter, query), + }), + + allEvents: () => + queryOptions({ + queryKey: storeQueryKeys.allEvents(), + queryFn: getAllEvent, + }), + + detail: (id: string) => + queryOptions({ + queryKey: storeQueryKeys.detail(id), + queryFn: () => getStoreDetailInfo(id), + }), + + detailMenu: (id: string) => + queryOptions({ + queryKey: storeQueryKeys.detailMenu(id), + queryFn: () => getStoreDetailMenu(id), + }), + + eventList: (id: string) => + queryOptions({ + queryKey: storeQueryKeys.eventList(id), + queryFn: () => getStoreEventList(id), + }), + + benefitCategory: () => + queryOptions({ + queryKey: storeQueryKeys.benefitCategory(), + queryFn: getStoreBenefitCategory, + }), + + benefitList: (id: string) => + queryOptions({ + queryKey: storeQueryKeys.benefitList(id), + queryFn: () => getStoreBenefitList(id), + }), + + relatedSearch: (query: string) => + queryOptions({ + queryKey: storeQueryKeys.relatedSearch(query), + queryFn: () => getRelateSearch(query), + }), + + reviewList: ({ shopId, page, sorter, token }: StoreReviewListQueryParams) => + queryOptions({ + queryKey: storeQueryKeys.reviewList({ shopId, page, sorter }), + queryFn: () => getReviewList(shopId, page, sorter, token), + }), + + reviewFeed: ({ shopId, sorter, token }: Omit) => + infiniteQueryOptions({ + queryKey: storeQueryKeys.reviewFeed(shopId, sorter), + initialPageParam: 1, + queryFn: ({ pageParam }) => getReviewList(shopId, pageParam, sorter, token), + getNextPageParam: (lastPage) => { + if (lastPage.total_page > lastPage.current_page) { + return lastPage.current_page + 1; + } + return undefined; + }, + }), + + myReview: (shopId: string, sorter: string, token: string) => + queryOptions({ + queryKey: storeQueryKeys.myReview(shopId, sorter), + queryFn: () => getMyReview(shopId, sorter, token), + }), +}; diff --git a/src/api/timetable/mutations.ts b/src/api/timetable/mutations.ts new file mode 100644 index 000000000..9bec0ecf6 --- /dev/null +++ b/src/api/timetable/mutations.ts @@ -0,0 +1,145 @@ +import { mutationOptions, QueryClient } from '@tanstack/react-query'; +import { graduationCalculatorQueryKeys } from 'api/graduationCalculator/queries'; +import { + AddTimetableFrameRequest, + AddTimetableLectureCustomRequest, + AddTimetableLectureRegularRequest, + RollbackTimetableLectureRequest, + Semester, + TimetableCustomLecture, + TimetableFrameInfo, + TimetableRegularLecture, +} from './entity'; +import { timetableQueryKeys } from './queries'; +import { + addTimetableFrame, + addTimetableLectureCustom, + addTimetableLectureRegular, + deleteSemester, + deleteTimetableFrame, + deleteTimetableLecture, + editTimetableFrame, + editTimetableLectureCustom, + editTimetableLectureRegular, + rollbackTimetableFrame, + rollbackTimetableLecture, +} from './index'; + +type DeleteTimetableFrameVariables = { + id: number; +}; + +type EditTimetableLectureRegularVariables = { + timetableFrameId: number; + editedLecture: TimetableRegularLecture; + token: string; +}; + +type EditTimetableLectureCustomVariables = { + timetableFrameId: number; + editedLecture: TimetableCustomLecture; + token: string; +}; + +const invalidateFrameList = (queryClient: QueryClient, semester: Semester) => + queryClient.invalidateQueries({ queryKey: timetableQueryKeys.frameList(semester) }); + +export const timetableMutations = { + addSemester: (queryClient: QueryClient, token: string, semester: Semester) => + mutationOptions({ + mutationFn: (data: AddTimetableFrameRequest) => addTimetableFrame(data, token), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: timetableQueryKeys.mySemester() }); + await invalidateFrameList(queryClient, semester); + }, + }), + + addFrame: (queryClient: QueryClient, token: string, semester: Semester) => + mutationOptions({ + mutationFn: (data: AddTimetableFrameRequest) => addTimetableFrame(data, token), + onSuccess: () => invalidateFrameList(queryClient, semester), + }), + + updateFrame: (queryClient: QueryClient, token: string, semester: Semester) => + mutationOptions({ + mutationFn: (frameInfo: TimetableFrameInfo) => + editTimetableFrame(token, frameInfo.id!, { name: frameInfo.name, is_main: frameInfo.is_main }), + onSuccess: () => invalidateFrameList(queryClient, semester), + }), + + deleteFrame: (queryClient: QueryClient, token: string, semester: Semester) => + mutationOptions({ + mutationFn: ({ id }: DeleteTimetableFrameVariables) => deleteTimetableFrame(token, id), + onSuccess: () => invalidateFrameList(queryClient, semester), + }), + + rollbackFrame: (queryClient: QueryClient, token: string, semester: Semester) => + mutationOptions({ + mutationFn: (timetableFrameId: number) => rollbackTimetableFrame(token, timetableFrameId), + onSuccess: () => invalidateFrameList(queryClient, semester), + }), + + deleteSemester: (queryClient: QueryClient, token: string, semester: Semester) => + mutationOptions({ + mutationFn: () => deleteSemester(token, semester), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: timetableQueryKeys.mySemester() }); + await invalidateFrameList(queryClient, semester); + await queryClient.invalidateQueries({ queryKey: graduationCalculatorQueryKeys.all }); + }, + }), + + addLectureRegular: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: (data: AddTimetableLectureRegularRequest) => addTimetableLectureRegular(data, token), + onSuccess: (data, variables) => { + queryClient.setQueryData(timetableQueryKeys.lectureInfo(variables.timetable_frame_id), data); + }, + }), + + addLectureCustom: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: (data: AddTimetableLectureCustomRequest) => addTimetableLectureCustom(data, token), + onSuccess: (data, variables) => { + queryClient.setQueryData(timetableQueryKeys.lectureInfo(variables.timetable_frame_id), data); + }, + }), + + editLectureRegular: (queryClient: QueryClient) => + mutationOptions({ + mutationFn: ({ timetableFrameId, editedLecture, token }: EditTimetableLectureRegularVariables) => + editTimetableLectureRegular( + { timetable_frame_id: timetableFrameId, timetable_lecture: editedLecture }, + token, + ), + onSuccess: async (data, variables) => { + queryClient.setQueryData(timetableQueryKeys.lectureInfo(variables.timetableFrameId), data); + await queryClient.invalidateQueries({ queryKey: graduationCalculatorQueryKeys.all }); + await queryClient.invalidateQueries({ queryKey: timetableQueryKeys.allLectures }); + }, + }), + + editLectureCustom: (queryClient: QueryClient) => + mutationOptions({ + mutationFn: ({ timetableFrameId, editedLecture, token }: EditTimetableLectureCustomVariables) => + editTimetableLectureCustom({ timetable_frame_id: timetableFrameId, timetable_lecture: editedLecture }, token), + onSuccess: (data, variables) => { + queryClient.setQueryData(timetableQueryKeys.lectureInfo(variables.timetableFrameId), data); + }, + }), + + deleteLecture: (queryClient: QueryClient, authorization: string) => + mutationOptions({ + mutationFn: (id: number) => deleteTimetableLecture(authorization, id), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: timetableQueryKeys.lectureInfoAll }); + await queryClient.invalidateQueries({ queryKey: graduationCalculatorQueryKeys.all }); + }, + }), + + rollbackLecture: (queryClient: QueryClient, token: string, timetableFrameId: number) => + mutationOptions({ + mutationFn: (data: RollbackTimetableLectureRequest) => rollbackTimetableLecture(data, token), + onSuccess: () => queryClient.invalidateQueries({ queryKey: timetableQueryKeys.lectureInfo(timetableFrameId) }), + }), +}; diff --git a/src/api/timetable/queries.ts b/src/api/timetable/queries.ts new file mode 100644 index 000000000..9269f0101 --- /dev/null +++ b/src/api/timetable/queries.ts @@ -0,0 +1,109 @@ +import { queryOptions } from '@tanstack/react-query'; +import { Semester, TimetableFrameListResponse, VersionType } from './entity'; +import { + getLectureList, + getMySemester, + getSemesterInfoList, + getTimetableAllLectureInfo, + getTimetableFrame, + getTimetableLectureInfo, + getVersion, +} from './index'; + +const MY_SEMESTER_INFO_KEY = 'my_semester'; +const SEMESTER_INFO_KEY = 'semester'; +const LECTURE_LIST_KEY = 'lecture'; +const TIMETABLE_FRAME_KEY = 'timetable_frame'; +const TIMETABLE_INFO_LIST = 'TIMETABLE_INFO_LIST'; +const ALL_LECTURES_KEY = 'allLectures'; + +type TimetableUserType = 'STUDENT' | 'GENERAL' | '' | null; + +type MySemesterQueryParams = { + userType?: TimetableUserType; +}; + +type FrameListQueryParams = { + fallbackOnError?: boolean; + userType?: TimetableUserType; +}; + +const canUseStudentTimetableQuery = (token: string, userType?: TimetableUserType) => + Boolean(token) && (!userType || userType === 'STUDENT'); + +export const createDefaultTimetableFrameList = (): TimetableFrameListResponse => [ + { + id: null, + name: '기본 시간표', + is_main: true, + }, +]; + +export const timetableQueryKeys = { + mySemester: () => [MY_SEMESTER_INFO_KEY] as const, + semesterInfo: () => [SEMESTER_INFO_KEY] as const, + lectureList: (semester: Semester) => [LECTURE_LIST_KEY, semester] as const, + frameList: (semester: Semester) => [`${TIMETABLE_FRAME_KEY}${semester.year}${semester.term}`] as const, + lectureInfoAll: [TIMETABLE_INFO_LIST] as const, + lectureInfo: (timetableFrameId: number) => [TIMETABLE_INFO_LIST, timetableFrameId] as const, + allLectures: [ALL_LECTURES_KEY] as const, + version: (type: VersionType) => [type] as const, +}; + +export const timetableQueries = { + mySemester: (token: string, { userType }: MySemesterQueryParams = {}) => + queryOptions({ + queryKey: timetableQueryKeys.mySemester(), + queryFn: () => (canUseStudentTimetableQuery(token, userType) ? getMySemester(token) : null), + }), + + semesterInfo: () => + queryOptions({ + queryKey: timetableQueryKeys.semesterInfo(), + queryFn: getSemesterInfoList, + }), + + lectureList: (semester: Semester) => + queryOptions({ + queryKey: timetableQueryKeys.lectureList(semester), + queryFn: () => getLectureList(semester), + }), + + frameList: (token: string, semester: Semester, { fallbackOnError = false, userType }: FrameListQueryParams = {}) => + queryOptions({ + queryKey: timetableQueryKeys.frameList(semester), + queryFn: async () => { + if (!canUseStudentTimetableQuery(token, userType)) { + return createDefaultTimetableFrameList(); + } + + if (!fallbackOnError) { + return getTimetableFrame(token, semester); + } + + try { + return await getTimetableFrame(token, semester); + } catch { + return createDefaultTimetableFrameList(); + } + }, + }), + + lectureInfo: (authorization: string, timetableFrameId: number) => + queryOptions({ + queryKey: timetableQueryKeys.lectureInfo(timetableFrameId), + queryFn: () => (authorization ? getTimetableLectureInfo(authorization, timetableFrameId) : null), + }), + + allLectures: (token: string) => + queryOptions({ + queryKey: timetableQueryKeys.allLectures, + queryFn: () => (token ? getTimetableAllLectureInfo(token) : null), + }), + + version: (type: VersionType) => + queryOptions({ + queryKey: timetableQueryKeys.version(type), + queryFn: () => getVersion(type), + }), +}; diff --git a/src/components/Articles/LostItemChatPage/components/DeleteModal/hooks/useBlockLostItemChatroom.ts b/src/components/Articles/LostItemChatPage/components/DeleteModal/hooks/useBlockLostItemChatroom.ts index 0a252b183..5135715c9 100644 --- a/src/components/Articles/LostItemChatPage/components/DeleteModal/hooks/useBlockLostItemChatroom.ts +++ b/src/components/Articles/LostItemChatPage/components/DeleteModal/hooks/useBlockLostItemChatroom.ts @@ -1,7 +1,7 @@ import { useRouter } from 'next/router'; import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { postBlockLostItemChatroom } from 'api/articles'; +import { articleMutations } from 'api/articles/mutations'; import ROUTES from 'static/routes'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; @@ -10,12 +10,12 @@ const useDeleteLostItemChatroom = () => { const token = useTokenState(); const queryClient = useQueryClient(); const router = useRouter(); + const mutation = articleMutations.blockLostItemChatroom(queryClient, token); const { mutate } = useMutation({ - mutationFn: ({ articleId, chatroomId }: { articleId: number; chatroomId: number }) => - postBlockLostItemChatroom(token, articleId, chatroomId), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['chatroom'] }); + ...mutation, + onSuccess: async (...args) => { + await mutation.onSuccess?.(...args); showToast('success', '채팅방이 차단되었습니다.'); router.push(ROUTES.LostItemChat()); }, diff --git a/src/components/Articles/LostItemChatPage/hooks/useChatPolling.ts b/src/components/Articles/LostItemChatPage/hooks/useChatPolling.ts index b82c7e1a5..9dab73860 100644 --- a/src/components/Articles/LostItemChatPage/hooks/useChatPolling.ts +++ b/src/components/Articles/LostItemChatPage/hooks/useChatPolling.ts @@ -8,13 +8,8 @@ import { useQueryClient, useSuspenseQuery, } from '@tanstack/react-query'; -import { - getLostItemChatroomDetail, - getLostItemChatroomList, - getLostItemChatroomMessagesV2, - postLeaveLostItemChatroomV2, - postLostItemChatroomMessageV2, -} from 'api/articles'; +import { postLeaveLostItemChatroomV2, postLostItemChatroomMessageV2 } from 'api/articles'; +import { articleQueries, articleQueryKeys } from 'api/articles/queries'; import { getCachedMessages, cacheMessages, clearChatroomCache } from 'utils/db/chatDB'; import showToast from 'utils/ts/showToast'; @@ -31,8 +26,7 @@ const useChatPolling = ({ token, articleId, chatroomId, isOnline = true }: UseCh const queryClient = useQueryClient(); const { data: chatroomList } = useSuspenseQuery({ - queryKey: ['chatroom', 'lost-item', 'list'], - queryFn: () => getLostItemChatroomList(token), + ...articleQueries.lostItemChatroomList(token), staleTime: isOnline ? 0 : Infinity, refetchInterval: isOnline ? POLLING_INTERVAL_MS : false, refetchIntervalInBackground: false, @@ -48,7 +42,7 @@ const useChatPolling = ({ token, articleId, chatroomId, isOnline = true }: UseCh useEffect(() => { if (numericArticleId == null || numericChatroomId == null) return; - const queryKey = ['chatroom', 'lost-item', 'messages', defaultArticleId, defaultChatroomId]; + const queryKey = articleQueryKeys.lostItemChatroomMessages(defaultArticleId, defaultChatroomId); const existing = queryClient.getQueryData(queryKey); if (existing) return; @@ -60,20 +54,22 @@ const useChatPolling = ({ token, articleId, chatroomId, isOnline = true }: UseCh }, [queryClient, numericArticleId, numericChatroomId, defaultArticleId, defaultChatroomId]); const { data: chatroomDetail } = useQuery({ - queryKey: ['chatroom', 'lost-item', 'detail', defaultArticleId, defaultChatroomId], - queryFn: - defaultArticleId && defaultChatroomId && isOnline - ? () => getLostItemChatroomDetail(token, Number(defaultArticleId), Number(defaultChatroomId)) - : skipToken, + ...(defaultArticleId && defaultChatroomId && isOnline + ? articleQueries.lostItemChatroomDetail(token, Number(defaultArticleId), Number(defaultChatroomId)) + : { + queryKey: articleQueryKeys.lostItemChatroomDetail(defaultArticleId, defaultChatroomId), + queryFn: skipToken, + }), placeholderData: keepPreviousData, }); const { data: messages } = useQuery({ - queryKey: ['chatroom', 'lost-item', 'messages', defaultArticleId, defaultChatroomId], - queryFn: - defaultArticleId && defaultChatroomId && isOnline - ? () => getLostItemChatroomMessagesV2(token, Number(defaultArticleId), Number(defaultChatroomId)) - : skipToken, + ...(defaultArticleId && defaultChatroomId && isOnline + ? articleQueries.lostItemChatroomMessages(token, Number(defaultArticleId), Number(defaultChatroomId)) + : { + queryKey: articleQueryKeys.lostItemChatroomMessages(defaultArticleId, defaultChatroomId), + queryFn: skipToken, + }), placeholderData: keepPreviousData, refetchInterval: isOnline && defaultArticleId && defaultChatroomId ? POLLING_INTERVAL_MS : false, refetchIntervalInBackground: false, @@ -86,41 +82,6 @@ const useChatPolling = ({ token, articleId, chatroomId, isOnline = true }: UseCh } }, [messages, numericArticleId, numericChatroomId]); - const leaveRoom = useCallback( - (aId: number, cId: number) => { - postLeaveLostItemChatroomV2(token, aId, cId).catch((error) => { - if (isKoinError(error)) { - showToast('error', error.message || '채팅방 퇴장을 실패하였습니다'); - } else { - showToast('error', '채팅방 퇴장을 실패하였습니다'); - sendClientError(error); - } - }); - }, - [token], - ); - - const prevRoomRef = useRef<{ articleId: number; chatroomId: number } | null>(null); - - useEffect(() => { - if (numericArticleId != null && numericChatroomId != null) { - if ( - prevRoomRef.current && - (prevRoomRef.current.articleId !== numericArticleId || prevRoomRef.current.chatroomId !== numericChatroomId) - ) { - leaveRoom(prevRoomRef.current.articleId, prevRoomRef.current.chatroomId); - } - prevRoomRef.current = { articleId: numericArticleId, chatroomId: numericChatroomId }; - } - - return () => { - if (prevRoomRef.current) { - leaveRoom(prevRoomRef.current.articleId, prevRoomRef.current.chatroomId); - prevRoomRef.current = null; - } - }; - }, [numericArticleId, numericChatroomId, leaveRoom]); - const { mutate: sendMessage } = useMutation({ mutationFn: ({ content, isImage = false }: { content: string; isImage?: boolean }) => { if (defaultArticleId == null || defaultChatroomId == null) { @@ -134,7 +95,7 @@ const useChatPolling = ({ token, articleId, chatroomId, isOnline = true }: UseCh }, onSuccess: () => { queryClient.invalidateQueries({ - queryKey: ['chatroom', 'lost-item', 'messages', defaultArticleId, defaultChatroomId], + queryKey: articleQueryKeys.lostItemChatroomMessages(defaultArticleId, defaultChatroomId), }); }, onError: (error) => { @@ -160,7 +121,7 @@ const useChatPolling = ({ token, articleId, chatroomId, isOnline = true }: UseCh clearChatroomCache(numericArticleId, numericChatroomId); } queryClient.invalidateQueries({ - queryKey: ['chatroom', 'lost-item', 'list'], + queryKey: articleQueryKeys.lostItemChatroomList, }); }, onError: (error) => { @@ -173,15 +134,50 @@ const useChatPolling = ({ token, articleId, chatroomId, isOnline = true }: UseCh }, }); + const leaveRoom = useCallback( + (aId: number, cId: number) => { + postLeaveLostItemChatroomV2(token, aId, cId).catch((error) => { + if (isKoinError(error)) { + showToast('error', error.message || '채팅방 퇴장을 실패하였습니다'); + } else { + showToast('error', '채팅방 퇴장을 실패하였습니다'); + sendClientError(error); + } + }); + }, + [token], + ); + + const prevRoomRef = useRef<{ articleId: number; chatroomId: number } | null>(null); + + useEffect(() => { + if (numericArticleId != null && numericChatroomId != null) { + if ( + prevRoomRef.current && + (prevRoomRef.current.articleId !== numericArticleId || prevRoomRef.current.chatroomId !== numericChatroomId) + ) { + leaveRoom(prevRoomRef.current.articleId, prevRoomRef.current.chatroomId); + } + prevRoomRef.current = { articleId: numericArticleId, chatroomId: numericChatroomId }; + } + + return () => { + if (prevRoomRef.current) { + leaveRoom(prevRoomRef.current.articleId, prevRoomRef.current.chatroomId); + prevRoomRef.current = null; + } + }; + }, [numericArticleId, numericChatroomId, leaveRoom]); + const invalidateChatroomList = useCallback(() => { queryClient.invalidateQueries({ - queryKey: ['chatroom', 'lost-item', 'list'], + queryKey: articleQueryKeys.lostItemChatroomList, }); }, [queryClient]); const invalidateMessages = useCallback(() => { queryClient.invalidateQueries({ - queryKey: ['chatroom', 'lost-item', 'messages', defaultArticleId, defaultChatroomId], + queryKey: articleQueryKeys.lostItemChatroomMessages(defaultArticleId, defaultChatroomId), }); }, [queryClient, defaultArticleId, defaultChatroomId]); diff --git a/src/components/Articles/LostItemDetailPage/components/LatestLostItemList/index.tsx b/src/components/Articles/LostItemDetailPage/components/LatestLostItemList/index.tsx index 5f9795931..c92932104 100644 --- a/src/components/Articles/LostItemDetailPage/components/LatestLostItemList/index.tsx +++ b/src/components/Articles/LostItemDetailPage/components/LatestLostItemList/index.tsx @@ -1,15 +1,20 @@ import Link from 'next/link'; -import useLostItemArticles from 'components/Articles/hooks/useLostItemArticles'; +import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; +import { articleQueries } from 'api/articles/queries'; import FoundChip from 'components/Articles/LostItemDetailPage/components/FoundChip'; import ROUTES from 'static/routes'; +import useTokenState from 'utils/hooks/state/useTokenState'; import useInfiniteScroll from 'utils/hooks/ui/useInfiniteScroll'; import styles from './LatestLostItemList.module.scss'; function LatestLostItemList() { - const { data, hasNextPage, fetchNextPage, isFetchingNextPage } = useLostItemArticles({ - limit: 10, - sort: 'LATEST', - }); + const token = useTokenState(); + const { data, hasNextPage, fetchNextPage, isFetchingNextPage } = useSuspenseInfiniteQuery( + articleQueries.lostItemInfiniteList(token, { + limit: 10, + sort: 'LATEST', + }), + ); const observerRef = useInfiniteScroll(fetchNextPage, hasNextPage, isFetchingNextPage); const articles = data.pages.flatMap((page) => page.articles); diff --git a/src/components/Articles/LostItemDetailPage/hooks/usePostFoundLostItem.ts b/src/components/Articles/LostItemDetailPage/hooks/usePostFoundLostItem.ts index fd813d221..0982196a5 100644 --- a/src/components/Articles/LostItemDetailPage/hooks/usePostFoundLostItem.ts +++ b/src/components/Articles/LostItemDetailPage/hooks/usePostFoundLostItem.ts @@ -1,20 +1,18 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { postFoundLostItem } from 'api/articles'; +import { articleMutations } from 'api/articles/mutations'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; const usePostFoundLostItem = (articleId: number) => { const token = useTokenState(); const queryClient = useQueryClient(); + const mutation = articleMutations.toggleLostItemFound(queryClient, token, articleId); const { mutate, isPending } = useMutation({ - mutationFn: async () => { - const response = await postFoundLostItem(token, articleId); - return response; - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['lostItem', 'detail', articleId] }); + ...mutation, + onSuccess: async (...args) => { + await mutation.onSuccess?.(...args); showToast('success', '상태가 변경되었습니다.'); }, onError: (e) => { diff --git a/src/components/Articles/LostItemDetailPage/hooks/usePostLostItemChatroom.ts b/src/components/Articles/LostItemDetailPage/hooks/usePostLostItemChatroom.ts index 7d25623e7..20b8cfbee 100644 --- a/src/components/Articles/LostItemDetailPage/hooks/usePostLostItemChatroom.ts +++ b/src/components/Articles/LostItemDetailPage/hooks/usePostLostItemChatroom.ts @@ -1,18 +1,15 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { postLostItemChatroom } from 'api/articles'; +import { articleMutations } from 'api/articles/mutations'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; const usePostLostItemChatroom = () => { const token = useTokenState(); const queryClient = useQueryClient(); + const mutation = articleMutations.createLostItemChatroom(queryClient, token); const { mutateAsync } = useMutation({ - mutationFn: async (articleId: number) => { - const response = await postLostItemChatroom(token, articleId); - return response; - }, - onSuccess: () => queryClient.invalidateQueries({ queryKey: ['chatroom', 'lost-item'] }), + ...mutation, onError: (e) => { if (isKoinError(e)) { showToast('error', e.message); diff --git a/src/components/Articles/LostItemDetailPage/hooks/useSingleLostItemArticle.ts b/src/components/Articles/LostItemDetailPage/hooks/useSingleLostItemArticle.ts deleted file mode 100644 index d6a6fe3c3..000000000 --- a/src/components/Articles/LostItemDetailPage/hooks/useSingleLostItemArticle.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getSingleLostItemArticle } from 'api/articles'; -import useTokenState from 'utils/hooks/state/useTokenState'; - -const useSingleLostItemArticle = (articleId: number) => { - const token = useTokenState(); - - const { data: article } = useSuspenseQuery({ - queryKey: ['lostItem', 'detail', articleId], - queryFn: async () => { - const response = await getSingleLostItemArticle(token, articleId); - return response; - }, - }); - - return { article }; -}; - -export default useSingleLostItemArticle; diff --git a/src/components/Articles/LostItemEditPage/hooks/usePutLostItemArticle.ts b/src/components/Articles/LostItemEditPage/hooks/usePutLostItemArticle.ts index 0fd80e9f7..ea759893d 100644 --- a/src/components/Articles/LostItemEditPage/hooks/usePutLostItemArticle.ts +++ b/src/components/Articles/LostItemEditPage/hooks/usePutLostItemArticle.ts @@ -1,21 +1,18 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { putLostItemArticle } from 'api/articles'; -import { UpdateLostItemArticleRequestDTO } from 'api/articles/entity'; +import { articleMutations } from 'api/articles/mutations'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; const usePutLostItemArticle = (articleId: number) => { const token = useTokenState(); const queryClient = useQueryClient(); + const mutation = articleMutations.updateLostItem(queryClient, token, articleId); const { status, mutateAsync } = useMutation({ - mutationFn: async (data: UpdateLostItemArticleRequestDTO) => { - const response = await putLostItemArticle(token, articleId, data); - return response.id; - }, - onSuccess: () => { + ...mutation, + onSuccess: async (...args) => { + await mutation.onSuccess?.(...args); showToast('success', '게시글 수정이 완료되었습니다.'); - queryClient.invalidateQueries({ queryKey: ['lostItem'] }); }, onError: (e) => { if (isKoinError(e)) { diff --git a/src/components/Articles/LostItemEditPage/index.tsx b/src/components/Articles/LostItemEditPage/index.tsx index 45d1380c7..c83dfd99f 100644 --- a/src/components/Articles/LostItemEditPage/index.tsx +++ b/src/components/Articles/LostItemEditPage/index.tsx @@ -1,13 +1,15 @@ import { useState } from 'react'; import { useRouter } from 'next/router'; +import { useSuspenseQuery } from '@tanstack/react-query'; import { LostItemImageDTO } from 'api/articles/entity'; +import { articleQueries } from 'api/articles/queries'; import LostItemPageTemplate from 'components/Articles/components/LostItemPageTemplate'; import { FindUserCategory, useArticlesLogger } from 'components/Articles/hooks/useArticlesLogger'; import { useLostItemForm } from 'components/Articles/hooks/useLostItemForm'; -import useSingleLostItemArticle from 'components/Articles/LostItemDetailPage/hooks/useSingleLostItemArticle'; import usePutLostItemArticle from 'components/Articles/LostItemEditPage/hooks/usePutLostItemArticle'; import LostItemForm from 'components/Articles/LostItemWritePage/components/LostItemForm'; import ROUTES from 'static/routes'; +import useTokenState from 'utils/hooks/state/useTokenState'; import { getYyyyMmDd } from 'utils/ts/calendar'; interface LostItemEditPageProps { @@ -29,8 +31,9 @@ const EDIT_TITLES = { export default function LostItemEditPage({ articleId }: LostItemEditPageProps) { const router = useRouter(); + const token = useTokenState(); const { logLostItemModifyComplete } = useArticlesLogger(); - const { article } = useSingleLostItemArticle(articleId); + const { data: article } = useSuspenseQuery(articleQueries.lostItemDetail(token, articleId)); const { status, mutateAsync: putLostItem } = usePutLostItemArticle(articleId); const type = article.type as 'FOUND' | 'LOST'; diff --git a/src/components/Articles/components/HotArticle/index.tsx b/src/components/Articles/components/HotArticle/index.tsx index faa2748dd..e302e0729 100644 --- a/src/components/Articles/components/HotArticle/index.tsx +++ b/src/components/Articles/components/HotArticle/index.tsx @@ -1,7 +1,7 @@ import Image from 'next/image'; import Link from 'next/link'; import { useQuery } from '@tanstack/react-query'; -import { getHotArticles } from 'api/articles'; +import { articleQueries } from 'api/articles/queries'; import LoadingSpinner from 'components/feedback/LoadingSpinner'; import ROUTES from 'static/routes'; import useLogger from 'utils/hooks/analytics/useLogger'; @@ -32,10 +32,7 @@ const LINK_LIST = [ export default function HotArticles() { const logger = useLogger(); - const { data: hotArticles, isLoading } = useQuery({ - queryKey: ['hotArticles'], - queryFn: getHotArticles, - }); + const { data: hotArticles, isLoading } = useQuery(articleQueries.hot()); if (isLoading || !hotArticles) { return ; diff --git a/src/components/Articles/hooks/useArticle.ts b/src/components/Articles/hooks/useArticle.ts deleted file mode 100644 index 789af81f8..000000000 --- a/src/components/Articles/hooks/useArticle.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getArticle } from 'api/articles'; - -const useArticle = (id: string | undefined) => { - const { data: article } = useSuspenseQuery({ - queryKey: ['article', id], - queryFn: async ({ queryKey }) => { - const queryFnParams = queryKey[1]; - - return getArticle(queryFnParams); - }, - }); - - return { article }; -}; - -export default useArticle; diff --git a/src/components/Articles/hooks/useArticles.ts b/src/components/Articles/hooks/useArticles.ts deleted file mode 100644 index 5ac48867a..000000000 --- a/src/components/Articles/hooks/useArticles.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { keepPreviousData, useQuery } from '@tanstack/react-query'; -import { articles as articlesApi } from 'api/index'; -import { isNewArticle } from 'components/Articles/utils/setArticleRegisteredDate'; -import useTokenState from 'utils/hooks/state/useTokenState'; -import type { ArticleWithNew, PaginationInfo } from 'api/articles/entity'; - -const useArticles = (page = '1') => { - const token = useTokenState(); - - const { data: articleData } = useQuery({ - queryKey: ['articles', page], - queryFn: async () => { - // if (!token) throw new Error('🚨 로그인 토큰이 필요합니다.'); - - const queryFnParams = page; - - return articlesApi.getArticles(token, queryFnParams); - }, - placeholderData: keepPreviousData, - select: (data) => { - const { - // 일관성을 유지하기 위해 변수명을 변경하지 않았습니다. - articles, - total_count, - current_count, - total_page, - current_page, - } = data; - - const currentDate = new Date(); - const articlesWithNew: ArticleWithNew[] = articles.map((article) => ({ - ...article, - isNew: isNewArticle(article.registered_at, currentDate), - })); - - const paginationInfo: PaginationInfo = { - total_count, - current_count, - total_page, - current_page, - }; - - return { articles: articlesWithNew, paginationInfo }; - }, - }); - - return articleData; -}; - -export default useArticles; diff --git a/src/components/Articles/hooks/useDeleteLostItemArticles.ts b/src/components/Articles/hooks/useDeleteLostItemArticles.ts index b1a4cffe4..c6f74235f 100644 --- a/src/components/Articles/hooks/useDeleteLostItemArticles.ts +++ b/src/components/Articles/hooks/useDeleteLostItemArticles.ts @@ -1,6 +1,6 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { deleteLostItemArticle } from 'api/articles'; +import { articleMutations } from 'api/articles/mutations'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; @@ -11,11 +11,12 @@ interface UseDeleteLostItemArticleProps { const useDeleteLostItemArticle = ({ onSuccess }: UseDeleteLostItemArticleProps = {}) => { const token = useTokenState(); const queryClient = useQueryClient(); + const mutation = articleMutations.deleteLostItem(queryClient, token); const { mutate } = useMutation({ - mutationFn: (id: number) => deleteLostItemArticle(token, id), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['articles', 'lostitem'] }); + ...mutation, + onSuccess: async (...args) => { + await mutation.onSuccess?.(...args); showToast('success', '게시글이 삭제되었습니다.'); onSuccess?.(); }, diff --git a/src/components/Articles/hooks/useLostItemArticles.ts b/src/components/Articles/hooks/useLostItemArticles.ts deleted file mode 100644 index 16acad5d3..000000000 --- a/src/components/Articles/hooks/useLostItemArticles.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; -import { getLostItemArticles } from 'api/articles'; -import { LostItemArticlesRequest } from 'api/articles/entity'; -import useTokenState from 'utils/hooks/state/useTokenState'; - -const useLostItemArticles = (params: LostItemArticlesRequest) => { - const token = useTokenState(); - - const { data, hasNextPage, fetchNextPage, isFetchingNextPage } = useSuspenseInfiniteQuery({ - queryKey: ['lostItem', params], - initialPageParam: 1, - queryFn: ({ pageParam }) => getLostItemArticles(token, { ...params, page: pageParam }), - getNextPageParam: (last) => { - if (last.total_page > last.current_page) return last.current_page + 1; - return undefined; - }, - }); - - return { - data, - hasNextPage, - fetchNextPage, - isFetchingNextPage, - }; -}; - -export default useLostItemArticles; diff --git a/src/components/Articles/hooks/useLostItemPagination.ts b/src/components/Articles/hooks/useLostItemPagination.ts deleted file mode 100644 index c29a63b5d..000000000 --- a/src/components/Articles/hooks/useLostItemPagination.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { keepPreviousData, useQuery } from '@tanstack/react-query'; -import { getLostItemArticles } from 'api/articles'; -import { LostItemArticlesRequest } from 'api/articles/entity'; -import useTokenState from 'utils/hooks/state/useTokenState'; - -export const useLostItemPagination = (params: LostItemArticlesRequest) => { - const token = useTokenState(); - - return useQuery({ - queryKey: ['lostItemPagination', params], - queryFn: () => getLostItemArticles(token, params), - placeholderData: keepPreviousData, - select: (data) => ({ - articles: data.articles, - paginationInfo: { - total_count: data.total_count, - current_count: data.current_count, - total_page: data.total_page, - current_page: data.current_page, - }, - }), - }); -}; - -export default useLostItemPagination; diff --git a/src/components/Articles/hooks/useLostItemSearch.ts b/src/components/Articles/hooks/useLostItemSearch.ts deleted file mode 100644 index a4f63f668..000000000 --- a/src/components/Articles/hooks/useLostItemSearch.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { getLostItemSearch } from 'api/articles'; - -export const useLostItemSearch = (params: { query: string; page?: number; limit?: number }) => { - const trimmed = params.query.trim(); - - return useQuery({ - queryKey: ['lostItemSearch', trimmed, params.page ?? 1, params.limit ?? 10], - queryFn: () => getLostItemSearch({ query: trimmed, page: params.page ?? 1, limit: params.limit ?? 10 }), - enabled: trimmed.length > 0, - }); -}; diff --git a/src/components/Articles/hooks/usePageParams.ts b/src/components/Articles/hooks/usePageParams.ts deleted file mode 100644 index 9c18cb586..000000000 --- a/src/components/Articles/hooks/usePageParams.ts +++ /dev/null @@ -1,9 +0,0 @@ -import useParamsHandler from 'utils/hooks/routing/useParamsHandler'; - -const usePageParams = () => { - const { params } = useParamsHandler(); - - return params.page ?? '1'; -}; - -export default usePageParams; diff --git a/src/components/Articles/hooks/usePostLostItemArticles.ts b/src/components/Articles/hooks/usePostLostItemArticles.ts index f5440bcbc..1ac09c775 100644 --- a/src/components/Articles/hooks/usePostLostItemArticles.ts +++ b/src/components/Articles/hooks/usePostLostItemArticles.ts @@ -1,21 +1,18 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { postLostItemArticle } from 'api/articles'; -import { LostItemArticlesRequestDTO } from 'api/articles/entity'; +import { articleMutations } from 'api/articles/mutations'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; const usePostLostItemArticles = () => { const token = useTokenState(); const queryClient = useQueryClient(); + const mutation = articleMutations.createLostItem(queryClient, token); const { status, mutateAsync } = useMutation({ - mutationFn: async (data: LostItemArticlesRequestDTO) => { - const response = await postLostItemArticle(token, data); - return response.id; - }, - onSuccess: () => { + ...mutation, + onSuccess: async (...args) => { + await mutation.onSuccess?.(...args); showToast('success', '게시글 작성이 완료되었습니다.'); - queryClient.invalidateQueries({ queryKey: ['lostItem'] }); }, onError: (e) => { if (isKoinError(e)) { diff --git a/src/components/Articles/hooks/useReportLostItemArticle.ts b/src/components/Articles/hooks/useReportLostItemArticle.ts index aa82603d4..186e81ba2 100644 --- a/src/components/Articles/hooks/useReportLostItemArticle.ts +++ b/src/components/Articles/hooks/useReportLostItemArticle.ts @@ -1,7 +1,7 @@ import { useRouter } from 'next/router'; import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { postReportLostItemArticle } from 'api/articles'; +import { articleMutations } from 'api/articles/mutations'; import ROUTES from 'static/routes'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; @@ -10,15 +10,14 @@ export default function useReportLostItemArticle() { const router = useRouter(); const token = useTokenState(); const queryClient = useQueryClient(); + const mutation = articleMutations.reportLostItem(queryClient, token); return useMutation({ - mutationFn: ({ articleId, reports }: { articleId: number; reports: { title: string; content: string }[] }) => - postReportLostItemArticle(token, articleId, { reports }), - onSuccess: () => { + ...mutation, + onSuccess: async (...args) => { + await mutation.onSuccess?.(...args); router.push(ROUTES.LostItems()); showToast('success', '게시글이 신고되었습니다.'); - queryClient.invalidateQueries({ queryKey: ['articles'] }); - queryClient.invalidateQueries({ queryKey: ['lostitem'] }); // 다시 패치할 필요가 있는지? // queryClient.refetchQueries({ queryKey: ['articles', 'lostitem'] }); diff --git a/src/components/Articles/utils/selectArticlesData.ts b/src/components/Articles/utils/selectArticlesData.ts new file mode 100644 index 000000000..e4b21b6b8 --- /dev/null +++ b/src/components/Articles/utils/selectArticlesData.ts @@ -0,0 +1,56 @@ +import { isNewArticle } from './setArticleRegisteredDate'; +import type { + ArticleWithNew, + ArticlesResponse, + LostItemArticleForGetDTO, + LostItemArticlesResponseDTO, + PaginationInfo, +} from 'api/articles/entity'; + +export interface ArticlesListViewData { + articles: ArticleWithNew[]; + paginationInfo: PaginationInfo; +} + +export interface LostItemPaginationViewData { + articles: LostItemArticleForGetDTO[]; + paginationInfo: PaginationInfo; +} + +export const selectArticlesWithNew = (data: ArticlesResponse): ArticlesListViewData => { + const { + articles, + total_count, + current_count, + total_page, + current_page, + } = data; + + const currentDate = new Date(); + const articlesWithNew: ArticleWithNew[] = articles.map((article) => ({ + ...article, + isNew: isNewArticle(article.registered_at, currentDate), + })); + + return { + articles: articlesWithNew, + paginationInfo: { + total_count, + current_count, + total_page, + current_page, + }, + }; +}; + +export const selectLostItemPaginationData = ( + data: LostItemArticlesResponseDTO, +): LostItemPaginationViewData => ({ + articles: data.articles, + paginationInfo: { + total_count: data.total_count, + current_count: data.current_count, + total_page: data.total_page, + current_page: data.current_page, + }, +}); diff --git a/src/components/Auth/SignupPage/Steps/MobileStudentDetailStep/index.tsx b/src/components/Auth/SignupPage/Steps/MobileStudentDetailStep/index.tsx index 0ac3c5a2e..17fe2940d 100644 --- a/src/components/Auth/SignupPage/Steps/MobileStudentDetailStep/index.tsx +++ b/src/components/Auth/SignupPage/Steps/MobileStudentDetailStep/index.tsx @@ -2,8 +2,9 @@ import { useState } from 'react'; import { isKoinError } from '@bcsdlab/koin'; import { sha256 } from '@bcsdlab/utils'; -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useSuspenseQuery } from '@tanstack/react-query'; import { checkId, nicknameDuplicateCheck, signupStudent } from 'api/auth'; +import { deptQueries } from 'api/dept/queries'; import { Controller, ControllerRenderProps, FieldError, useFormContext, useFormState, useWatch } from 'react-hook-form'; import { REGEX, MESSAGES } from 'static/auth'; import { useSessionLogger } from 'utils/hooks/analytics/useSessionLogger'; @@ -11,7 +12,6 @@ import useBooleanState from 'utils/hooks/state/useBooleanState'; import showToast from 'utils/ts/showToast'; import CustomInput, { type InputMessage } from '../../components/CustomInput'; import CustomSelector from '../../components/CustomSelector'; -import useDeptList from '../../hooks/useDeptList'; import styles from './MobileStudentDetailStep.module.scss'; interface MobileVerificationProps { @@ -51,7 +51,7 @@ function MobileStudentDetailStep({ onNext }: MobileVerificationProps) { const isFormFilled = isIdPasswordValid && major && (!nicknameControl || isCorrectNickname); - const { data: deptList } = useDeptList(); + const { data: deptList } = useSuspenseQuery(deptQueries.list()); const deptOptionList = deptList.map((dept) => ({ label: dept.name, value: dept.name, diff --git a/src/components/Auth/SignupPage/Steps/StudentDetailStep/index.tsx b/src/components/Auth/SignupPage/Steps/StudentDetailStep/index.tsx index 10902fafb..591ecccd0 100644 --- a/src/components/Auth/SignupPage/Steps/StudentDetailStep/index.tsx +++ b/src/components/Auth/SignupPage/Steps/StudentDetailStep/index.tsx @@ -1,12 +1,12 @@ import { useState } from 'react'; import { isKoinError } from '@bcsdlab/koin'; import { cn, sha256 } from '@bcsdlab/utils'; -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useSuspenseQuery } from '@tanstack/react-query'; import { checkId, emailDuplicateCheck, nicknameDuplicateCheck, signupStudent } from 'api/auth'; +import { deptQueries } from 'api/dept/queries'; import BackIcon from 'assets/svg/arrow-back.svg'; import CustomSelector from 'components/Auth/SignupPage/components/CustomSelector'; import PCCustomInput, { type InputMessage } from 'components/Auth/SignupPage/components/PCCustomInput'; -import useDeptList from 'components/Auth/SignupPage/hooks/useDeptList'; import { Controller, FieldError, useFormContext, useFormState, useWatch } from 'react-hook-form'; import { REGEX, MESSAGES } from 'static/auth'; import { useSessionLogger } from 'utils/hooks/analytics/useSessionLogger'; @@ -65,7 +65,7 @@ function StudentDetail({ onNext, onBack }: VerificationProps) { studentNumber && !errors.student_number; - const { data: deptList } = useDeptList(); + const { data: deptList } = useSuspenseQuery(deptQueries.list()); const deptOptionList = deptList.map((dept) => ({ label: dept.name, value: dept.name, diff --git a/src/components/Auth/SignupPage/hooks/useDeptList.ts b/src/components/Auth/SignupPage/hooks/useDeptList.ts deleted file mode 100644 index c94498434..000000000 --- a/src/components/Auth/SignupPage/hooks/useDeptList.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getDeptList } from 'api/dept'; - -const useDeptList = () => { - const { data } = useSuspenseQuery({ - queryKey: ['dept'], - queryFn: getDeptList, - }); - - return { data }; -}; - -export default useDeptList; diff --git a/src/components/Bus/BusCoursePage/hooks/useBusPrefetch.ts b/src/components/Bus/BusCoursePage/hooks/useBusPrefetch.ts index a6498a787..16bfbcb6a 100644 --- a/src/components/Bus/BusCoursePage/hooks/useBusPrefetch.ts +++ b/src/components/Bus/BusCoursePage/hooks/useBusPrefetch.ts @@ -1,10 +1,6 @@ import { useQueryClient } from '@tanstack/react-query'; -import { - getBusTimetableInfo, - getCityBusTimetableInfo, - getShuttleTimetableDetailInfo, -} from 'api/bus'; -import { CourseBusType, DirectionType } from 'api/bus/entity'; +import { DirectionType, ExpressCourse, ShuttleCourse } from 'api/bus/entity'; +import { busQueries } from 'api/bus/queries'; export type PrefetchParams = | { @@ -13,13 +9,13 @@ export type PrefetchParams = } | { type: 'express'; - bus_type: CourseBusType; + bus_type: ExpressCourse['bus_type']; direction: DirectionType; region: string; } | { type: 'shuttle'; - bus_type: CourseBusType; + bus_type: ShuttleCourse['bus_type']; direction: DirectionType; region: string; } @@ -35,45 +31,36 @@ export default function useBusPrefetch() { const prefetchBusTimetable = async (params: PrefetchParams) => { switch (params.type) { case 'shuttle': { - return queryClient.prefetchQuery({ - queryKey: ['timetable', params.type, params.direction, params.region], - queryFn: () => - getBusTimetableInfo({ - bus_type: params.bus_type, - direction: params.direction, - region: params.region, - }), - }); + return queryClient.prefetchQuery( + busQueries.shuttleTimetable({ + bus_type: params.bus_type, + direction: params.direction, + region: params.region, + }), + ); } case 'shuttle_detail': { - return queryClient.prefetchQuery({ - queryKey: ['bus', 'shuttle', 'timetable', params.id], - queryFn: () => getShuttleTimetableDetailInfo({ id: params.id }), - }); + return queryClient.prefetchQuery(busQueries.shuttleTimetableDetail(params.id)); } case 'express': { - return queryClient.prefetchQuery({ - queryKey: ['timetable', params.type, params.direction, params.region], - queryFn: () => - getBusTimetableInfo({ - bus_type: params.bus_type, - direction: params.direction, - region: params.region, - }), - }); + return queryClient.prefetchQuery( + busQueries.expressTimetable({ + bus_type: params.bus_type, + direction: params.direction, + region: params.region, + }), + ); } case 'city': { - return queryClient.prefetchQuery({ - queryKey: ['timetable', params.bus_number, params.direction], - queryFn: () => - getCityBusTimetableInfo({ - bus_number: params.bus_number, - direction: params.direction, - }), - }); + return queryClient.prefetchQuery( + busQueries.cityTimetable({ + bus_number: params.bus_number, + direction: params.direction, + }), + ); } } } diff --git a/src/components/Bus/BusCoursePage/hooks/useBusTimetable.ts b/src/components/Bus/BusCoursePage/hooks/useBusTimetable.ts deleted file mode 100644 index 3e2fc3187..000000000 --- a/src/components/Bus/BusCoursePage/hooks/useBusTimetable.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; -import { getBusTimetableInfo, getCityBusTimetableInfo } from 'api/bus'; -import { - CityBusParams, - CityInfo, - ExpressCourse, - ShuttleCourse, -} from 'api/bus/entity'; -import useMount from 'utils/hooks/state/useMount'; - -const TIMETABLE_KEY = 'timetable'; - -interface CityTimetable { - info: CityInfo; - type: 'city'; -} - -export function useClientShuttleTimetable(course: ShuttleCourse) { - const isMount = useMount(); - - return useQuery({ - queryKey: ['timetable', 'shuttle', course.direction, course.region], - queryFn: () => getBusTimetableInfo(course), - enabled: isMount, - }); -} - -export function useExpressTimetable(course: ExpressCourse) { - return useQuery({ - queryKey: ['timetable', 'express', course.direction, course.region], - queryFn: () => - getBusTimetableInfo({ - bus_type: course.bus_type, - direction: course.direction, - region: course.region, - }), - }); -} - -export function useCityBusTimetable(course: CityBusParams): CityTimetable { - const { bus_number: busNumber, direction: busDirection } = course; - - const { data } = useSuspenseQuery({ - queryKey: [TIMETABLE_KEY, busNumber, busDirection] as const, - queryFn: ({ queryKey: [, bus_number, direction] }) => getCityBusTimetableInfo({ bus_number, direction }), - select: (response) => ({ - info: response as CityInfo, - type: 'city' as const, - }), - }); - - return data; -} diff --git a/src/components/Bus/BusCoursePage/hooks/useShuttleCourse.ts b/src/components/Bus/BusCoursePage/hooks/useShuttleCourse.ts deleted file mode 100644 index af5b3abd6..000000000 --- a/src/components/Bus/BusCoursePage/hooks/useShuttleCourse.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getShuttleCourseInfo } from 'api/bus'; - -function useShuttleCourse() { - const { data: shuttleCourse } = useSuspenseQuery({ - queryKey: ['bus', 'courses', 'shuttle'], - queryFn: async () => getShuttleCourseInfo(), - }); - - return { shuttleCourse }; -} - -export default useShuttleCourse; diff --git a/src/components/Bus/BusCoursePage/hooks/useShuttleTimetableDetail.ts b/src/components/Bus/BusCoursePage/hooks/useShuttleTimetableDetail.ts deleted file mode 100644 index 54dc92a6d..000000000 --- a/src/components/Bus/BusCoursePage/hooks/useShuttleTimetableDetail.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { skipToken, useQuery } from '@tanstack/react-query'; -import { getShuttleTimetableDetailInfo } from 'api/bus'; - -export default function useShuttleTimetableDetail(id: string | null) { - const { data: shuttleTimetableDetail } = useQuery({ - queryKey: ['bus', 'shuttle', 'timetable', id], - queryFn: id ? async () => getShuttleTimetableDetailInfo({ id }) : skipToken, - staleTime: 1000 * 60 * 10, - }); - - return { shuttleTimetableDetail }; -} diff --git a/src/components/Bus/BusNotice/index.tsx b/src/components/Bus/BusNotice/index.tsx index c7e7c26dd..6ab5a3155 100644 --- a/src/components/Bus/BusNotice/index.tsx +++ b/src/components/Bus/BusNotice/index.tsx @@ -1,8 +1,9 @@ import { useEffect, useState } from 'react'; import { useRouter } from 'next/router'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { busQueries } from 'api/bus/queries'; import InformationIcon from 'assets/svg/Bus/info.svg'; import CloseIcon from 'assets/svg/common/close/close-icon-32x32.svg'; -import useBusNotice from 'components/Bus/hooks/useBusNotice'; import ROUTES from 'static/routes'; import useLogger from 'utils/hooks/analytics/useLogger'; import useMediaQuery from 'utils/hooks/layout/useMediaQuery'; @@ -36,8 +37,8 @@ export default function BusNotice({ loggingLocation }: BusNoticeProps) { const isMobile = useMediaQuery(); const router = useRouter(); const navigate = (path: string) => router.push(path); - const res = useBusNotice(); - const { id, title } = res.data; + const { data } = useSuspenseQuery(busQueries.notice()); + const { id, title } = data; const [lastBusNotice, setLastBusNotice] = useLocalStorage('lastBusNotice', ''); const [busNoticeDismissed, setBusNoticeDismissed] = useLocalStorage('busNoticeDismissed', 'false'); const isDismissed = busNoticeDismissed === 'true'; diff --git a/src/components/Bus/BusRoutePage/components/RouteList/index.tsx b/src/components/Bus/BusRoutePage/components/RouteList/index.tsx index 5c14d56de..46a3c9e2d 100644 --- a/src/components/Bus/BusRoutePage/components/RouteList/index.tsx +++ b/src/components/Bus/BusRoutePage/components/RouteList/index.tsx @@ -1,7 +1,9 @@ +import { useQuery } from '@tanstack/react-query'; import { Arrival, BusTypeRequest, Depart } from 'api/bus/entity'; +import { busQueries } from 'api/bus/queries'; import BusRoute from 'components/Bus/BusRoutePage/components/BusRoute'; -import useBusRoute from 'components/Bus/BusRoutePage/hooks/useBusRoute'; import { UseTimeSelectReturn } from 'components/Bus/BusRoutePage/hooks/useTimeSelect'; +import { transformBusRoute } from 'components/Bus/BusRoutePage/utils/transform'; import styles from './RouteList.module.scss'; interface RouteListProps { @@ -13,12 +15,16 @@ interface RouteListProps { export default function RouteList({ timeSelect, busType, depart, arrival }: RouteListProps) { const { formattedValues } = timeSelect; - const { data } = useBusRoute({ - dayOfMonth: formattedValues.date, - time: formattedValues.time, - busType, - depart, - arrival, + const { data } = useQuery({ + ...busQueries.route({ + dayOfMonth: formattedValues.date, + time: formattedValues.time, + busType, + depart, + arrival, + }), + select: transformBusRoute, + enabled: Boolean(depart) && Boolean(arrival), }); const isReady = Boolean(depart) && Boolean(arrival); diff --git a/src/components/Bus/BusRoutePage/components/TimeDetail/TimeDetailMobile/index.tsx b/src/components/Bus/BusRoutePage/components/TimeDetail/TimeDetailMobile/index.tsx index 999ff0ba7..963560471 100644 --- a/src/components/Bus/BusRoutePage/components/TimeDetail/TimeDetailMobile/index.tsx +++ b/src/components/Bus/BusRoutePage/components/TimeDetail/TimeDetailMobile/index.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { coopshopQueries } from 'api/coopshop/queries'; import PickerColumn from 'components/Bus/BusRoutePage/components/PickerColumn'; -import useCoopSemester from 'components/Bus/BusRoutePage/hooks/useCoopSemester'; import { useTimeSelect } from 'components/Bus/BusRoutePage/hooks/useTimeSelect'; import { useBodyScrollLock } from 'utils/hooks/ui/useBodyScrollLock'; import { useEscapeKeyDown } from 'utils/hooks/ui/useEscapeKeyDown'; @@ -27,7 +28,7 @@ export default function TimeDetailMobile({ timeSelect, close }: TimeDetailMobile const [selectedHour, setSelectedHour] = useState(hour % 12); const [selectedMinute, setSelectedMinute] = useState(minute); - const { data: semesterData } = useCoopSemester(); + const { data: semesterData } = useSuspenseQuery(coopshopQueries.allShopInfo()); const { backgroundRef } = useOutsideClick({ onOutsideClick: close }); useBodyScrollLock(); diff --git a/src/components/Bus/BusRoutePage/components/TimeDetail/TimeDetailPC/index.tsx b/src/components/Bus/BusRoutePage/components/TimeDetail/TimeDetailPC/index.tsx index 5fb3ff5c2..7a893dea3 100644 --- a/src/components/Bus/BusRoutePage/components/TimeDetail/TimeDetailPC/index.tsx +++ b/src/components/Bus/BusRoutePage/components/TimeDetail/TimeDetailPC/index.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { coopshopQueries } from 'api/coopshop/queries'; import SelectDropdown from 'components/Bus/BusRoutePage/components/SelectDropdown'; -import useCoopSemester from 'components/Bus/BusRoutePage/hooks/useCoopSemester'; import { useTimeSelect } from 'components/Bus/BusRoutePage/hooks/useTimeSelect'; import { useBusLogger } from 'components/Bus/hooks/useBusLogger'; import styles from './TimeDetailPC.module.scss'; @@ -19,7 +20,7 @@ export default function TimeDetailPC({ timeSelect }: TimeDetailPCProps) { const { hour, minute } = timeSelect.timeState; const { setNow, setDayOfMonth, setHour, setMinute } = timeSelect.timeHandler; const { logDepartureNowClick } = useBusLogger(); - const { data: semesterData } = useCoopSemester(); + const { data: semesterData } = useSuspenseQuery(coopshopQueries.allShopInfo()); const displaySemester = formatSemesterLabel(semesterData.semester); diff --git a/src/components/Bus/BusRoutePage/hooks/useBusRoute.ts b/src/components/Bus/BusRoutePage/hooks/useBusRoute.ts deleted file mode 100644 index 44296d42b..000000000 --- a/src/components/Bus/BusRoutePage/hooks/useBusRoute.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { getBusRouteInfo } from 'api/bus'; -import { Arrival, BusRouteParams, Depart } from 'api/bus/entity'; -import { transformBusRoute } from 'components/Bus/BusRoutePage/utils/transform'; - -const BUS_ROUTE_KEY = 'bus-route'; - -interface BusRouteQueryParams extends Omit { - depart: Depart | ''; - arrival: Arrival | ''; -} - -const useBusRoute = (params: BusRouteQueryParams) => { - const { depart, arrival, ...rest } = params; - const isReady = Boolean(depart) && Boolean(arrival); - - return useQuery({ - queryKey: [BUS_ROUTE_KEY, JSON.stringify(params)], - queryFn: async () => { - const response = await getBusRouteInfo({ - ...rest, - depart: depart as Depart, - arrival: arrival as Arrival, - }); - return transformBusRoute(response); - }, - enabled: isReady, - }); -}; - -export default useBusRoute; diff --git a/src/components/Bus/BusRoutePage/hooks/useCoopSemester.ts b/src/components/Bus/BusRoutePage/hooks/useCoopSemester.ts deleted file mode 100644 index dfff31dee..000000000 --- a/src/components/Bus/BusRoutePage/hooks/useCoopSemester.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getAllShopInfo } from 'api/coopshop'; - -const useCoopSemester = () => - useSuspenseQuery({ - queryKey: ['coopSemester'], - queryFn: async () => getAllShopInfo(), - }); - -export default useCoopSemester; diff --git a/src/components/Bus/hooks/useBusNotice.ts b/src/components/Bus/hooks/useBusNotice.ts deleted file mode 100644 index 884883d98..000000000 --- a/src/components/Bus/hooks/useBusNotice.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getBusNoticeInfo } from 'api/bus'; - -const BUS_NOTICE_KEY = 'bus-notice'; - -const useBusNotice = () => - useSuspenseQuery({ - queryKey: [BUS_NOTICE_KEY], - queryFn: getBusNoticeInfo, - }); - -export default useBusNotice; diff --git a/src/components/Callvan/components/CallvanChatRoom/index.tsx b/src/components/Callvan/components/CallvanChatRoom/index.tsx index 736ec527c..59c35a077 100644 --- a/src/components/Callvan/components/CallvanChatRoom/index.tsx +++ b/src/components/Callvan/components/CallvanChatRoom/index.tsx @@ -1,16 +1,17 @@ import { useRef, useState, useEffect, useMemo } from 'react'; import { useRouter } from 'next/router'; +import { useSuspenseQuery } from '@tanstack/react-query'; import { CallvanChatMessage } from 'api/callvan/entity'; +import { callvanQueries } from 'api/callvan/queries'; import ArrowBackIcon from 'assets/svg/Callvan/arrow-back.svg'; import ImageUploadIcon from 'assets/svg/Callvan/image-upload.svg'; import PeopleIcon from 'assets/svg/Callvan/people.svg'; import SendIcon from 'assets/svg/Callvan/send.svg'; import { ParticipantAvatarIcon } from 'components/Callvan/components/ParticipantsList/ParticipantAvatarIcon'; -import useCallvanChat from 'components/Callvan/hooks/useCallvanChat'; -import useCallvanPostDetail from 'components/Callvan/hooks/useCallvanPostDetail'; import useSendCallvanChat from 'components/Callvan/hooks/useSendCallvanChat'; import { getParticipantColor } from 'components/Callvan/utils/participantColor'; import useLogger from 'utils/hooks/analytics/useLogger'; +import useTokenState from 'utils/hooks/state/useTokenState'; import useUploadFile from 'utils/hooks/uploadFile/useUploadFile'; import styles from './CallvanChatRoom.module.scss'; @@ -44,8 +45,10 @@ function formatKoreanDateString(dateStr: string): string { export default function CallvanChatRoom({ postId }: CallvanChatRoomProps) { const router = useRouter(); const logger = useLogger(); - const { data } = useCallvanChat(postId); - const { data: postDetail } = useCallvanPostDetail(postId); + const token = useTokenState(); + const { data } = useSuspenseQuery(callvanQueries.chat(token ?? '', postId)); + const { data: postDetail } = useSuspenseQuery(callvanQueries.postDetail(token ?? '', postId)); + const { mutate: sendMessage, isPending: isSending } = useSendCallvanChat(postId); const [inputValue, setInputValue] = useState(''); const messagesEndRef = useRef(null); diff --git a/src/components/Callvan/components/CallvanPageLayout/index.tsx b/src/components/Callvan/components/CallvanPageLayout/index.tsx index 6bbfacdfa..8ab5a7396 100644 --- a/src/components/Callvan/components/CallvanPageLayout/index.tsx +++ b/src/components/Callvan/components/CallvanPageLayout/index.tsx @@ -1,15 +1,17 @@ import { useCallback, useState } from 'react'; import { useRouter } from 'next/router'; +import { useQuery } from '@tanstack/react-query'; import { CallvanLocation, CallvanSort, CallvanStatus } from 'api/callvan/entity'; +import { callvanQueries } from 'api/callvan/queries'; import ArrowBackIcon from 'assets/svg/Callvan/arrow-back.svg'; import CarIcon from 'assets/svg/Callvan/car.svg'; import FilterIcon from 'assets/svg/Callvan/filter.svg'; import NotificationIcon from 'assets/svg/Callvan/notification.svg'; import SearchIcon from 'assets/svg/Callvan/search.svg'; import CallvanFilterPanel from 'components/Callvan/components/CallvanFilterPanel'; -import useCallvanNotifications from 'components/Callvan/hooks/useCallvanNotifications'; import ROUTES from 'static/routes'; import useLogger from 'utils/hooks/analytics/useLogger'; +import useTokenState from 'utils/hooks/state/useTokenState'; import styles from './CallvanPageLayout.module.scss'; interface CallvanPageLayoutProps { @@ -34,7 +36,11 @@ export default function CallvanPageLayout({ const router = useRouter(); const logger = useLogger(); const [isFilterOpen, setIsFilterOpen] = useState(false); - const { data: notifications } = useCallvanNotifications(); + const token = useTokenState(); + const { data: notifications } = useQuery({ + ...callvanQueries.notifications(token ?? ''), + enabled: !!token, + }); const hasUnreadNotifications = notifications?.some((n) => !n.is_read) ?? false; diff --git a/src/components/Callvan/components/ParticipantsList/index.tsx b/src/components/Callvan/components/ParticipantsList/index.tsx index 43a5959ff..ae0920b7d 100644 --- a/src/components/Callvan/components/ParticipantsList/index.tsx +++ b/src/components/Callvan/components/ParticipantsList/index.tsx @@ -1,7 +1,7 @@ import { useRouter } from 'next/router'; import { useSuspenseQuery } from '@tanstack/react-query'; -import { getCallvanPostDetail } from 'api/callvan'; import { CallvanParticipant } from 'api/callvan/entity'; +import { callvanQueries } from 'api/callvan/queries'; import ArrowBackIcon from 'assets/svg/Callvan/arrow-back.svg'; import NotificationBellIcon from 'assets/svg/Callvan/notification.svg'; import PeopleIcon from 'assets/svg/Callvan/people.svg'; @@ -110,10 +110,7 @@ export default function ParticipantsList({ postId, token }: ParticipantsListProp const router = useRouter(); const logger = useLogger(); - const { data: post } = useSuspenseQuery({ - queryKey: ['callvanPostDetail', postId], - queryFn: () => getCallvanPostDetail(token, postId), - }); + const { data: post } = useSuspenseQuery(callvanQueries.postDetail(token, postId)); const colorIndexMap = new Map(post.participants.filter((p) => !p.is_me).map((p, i) => [p.user_id, i])); diff --git a/src/components/Callvan/hooks/useCallvanChat.ts b/src/components/Callvan/hooks/useCallvanChat.ts deleted file mode 100644 index f6e5f0208..000000000 --- a/src/components/Callvan/hooks/useCallvanChat.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getCallvanChat } from 'api/callvan'; -import useTokenState from 'utils/hooks/state/useTokenState'; - -export const CALLVAN_CHAT_QUERY_KEY = (postId: number) => ['callvanChat', postId]; - -const useCallvanChat = (postId: number) => { - const token = useTokenState(); - - return useSuspenseQuery({ - queryKey: CALLVAN_CHAT_QUERY_KEY(postId), - queryFn: () => getCallvanChat(token, postId), - staleTime: 0, - refetchInterval: 1000, - }); -}; - -export default useCallvanChat; diff --git a/src/components/Callvan/hooks/useCallvanInfiniteList.ts b/src/components/Callvan/hooks/useCallvanInfiniteList.ts deleted file mode 100644 index 8247d8c46..000000000 --- a/src/components/Callvan/hooks/useCallvanInfiniteList.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useInfiniteQuery } from '@tanstack/react-query'; -import { getCallvanList } from 'api/callvan'; -import { CallvanListRequest } from 'api/callvan/entity'; -import useTokenState from 'utils/hooks/state/useTokenState'; - -const LIMIT = 10; - -export const useCallvanInfiniteList = (params: Omit) => { - const token = useTokenState(); - - return useInfiniteQuery({ - queryKey: ['callvanInfiniteList', params], - queryFn: ({ pageParam = 1 }) => - getCallvanList(token, { - ...params, - page: pageParam, - limit: LIMIT, - }), - enabled: !!token, - initialPageParam: 1, - getNextPageParam: (lastPage) => { - if (lastPage.current_page < lastPage.total_page) { - return lastPage.current_page + 1; - } - return undefined; - }, - }); -}; - -export default useCallvanInfiniteList; diff --git a/src/components/Callvan/hooks/useCallvanList.ts b/src/components/Callvan/hooks/useCallvanList.ts deleted file mode 100644 index 68f876e6e..000000000 --- a/src/components/Callvan/hooks/useCallvanList.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { keepPreviousData, useQuery } from '@tanstack/react-query'; -import { getCallvanList } from 'api/callvan'; -import { CallvanListRequest } from 'api/callvan/entity'; -import useTokenState from 'utils/hooks/state/useTokenState'; - -export const useCallvanList = (params: CallvanListRequest) => { - const token = useTokenState(); - - return useQuery({ - queryKey: ['callvanList', params], - queryFn: () => getCallvanList(token, params), - enabled: !!token, - placeholderData: keepPreviousData, - select: (data) => ({ - posts: data.posts, - paginationInfo: { - total_count: data.total_count, - current_page: data.current_page, - total_page: data.total_page, - }, - }), - }); -}; - -export default useCallvanList; diff --git a/src/components/Callvan/hooks/useCallvanNotifications.ts b/src/components/Callvan/hooks/useCallvanNotifications.ts deleted file mode 100644 index 01f7f93b8..000000000 --- a/src/components/Callvan/hooks/useCallvanNotifications.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { getCallvanNotifications } from 'api/callvan'; -import useTokenState from 'utils/hooks/state/useTokenState'; - -export const CALLVAN_NOTIFICATIONS_QUERY_KEY = ['callvanNotifications'] as const; - -export const useCallvanNotifications = () => { - const token = useTokenState(); - - return useQuery({ - queryKey: CALLVAN_NOTIFICATIONS_QUERY_KEY, - queryFn: () => getCallvanNotifications(token), - enabled: !!token, - }); -}; - -export default useCallvanNotifications; diff --git a/src/components/Callvan/hooks/useCallvanPostDetail.ts b/src/components/Callvan/hooks/useCallvanPostDetail.ts deleted file mode 100644 index e8afe15aa..000000000 --- a/src/components/Callvan/hooks/useCallvanPostDetail.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getCallvanPostDetail } from 'api/callvan'; -import useTokenState from 'utils/hooks/state/useTokenState'; - -export const CALLVAN_POST_DETAIL_QUERY_KEY = (postId: number) => ['callvanPostDetail', postId]; - -const useCallvanPostDetail = (postId: number) => { - const token = useTokenState(); - - return useSuspenseQuery({ - queryKey: CALLVAN_POST_DETAIL_QUERY_KEY(postId), - queryFn: () => getCallvanPostDetail(token, postId), - staleTime: 60000, - }); -}; - -export default useCallvanPostDetail; diff --git a/src/components/Callvan/hooks/useCancelCallvan.ts b/src/components/Callvan/hooks/useCancelCallvan.ts index 16d146097..a37ace155 100644 --- a/src/components/Callvan/hooks/useCancelCallvan.ts +++ b/src/components/Callvan/hooks/useCancelCallvan.ts @@ -1,17 +1,18 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { cancelCallvan } from 'api/callvan'; +import { callvanMutations } from 'api/callvan/mutations'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; const useCancelCallvan = () => { const token = useTokenState(); const queryClient = useQueryClient(); + const mutation = callvanMutations.cancel(queryClient, token); const { mutate, isPending } = useMutation({ - mutationFn: (postId: number) => cancelCallvan(token, postId), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['callvanInfiniteList'] }); + ...mutation, + onSuccess: async (...args) => { + await mutation.onSuccess?.(...args); showToast('success', '참여가 취소되었습니다.'); }, onError: (e) => { diff --git a/src/components/Callvan/hooks/useCloseCallvan.ts b/src/components/Callvan/hooks/useCloseCallvan.ts index d3adba623..1d6bf8160 100644 --- a/src/components/Callvan/hooks/useCloseCallvan.ts +++ b/src/components/Callvan/hooks/useCloseCallvan.ts @@ -1,18 +1,16 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { closeCallvanPost } from 'api/callvan'; +import { callvanMutations } from 'api/callvan/mutations'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; const useCloseCallvan = () => { const token = useTokenState(); const queryClient = useQueryClient(); + const mutation = callvanMutations.close(queryClient, token); const { mutate, isPending } = useMutation({ - mutationFn: (postId: number) => closeCallvanPost(token, postId), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['callvanInfiniteList'] }); - }, + ...mutation, onError: (e) => { if (isKoinError(e)) { showToast('error', e.message || '모집 마감에 실패했습니다.'); diff --git a/src/components/Callvan/hooks/useCompleteCallvan.ts b/src/components/Callvan/hooks/useCompleteCallvan.ts index 512cc3059..61317c7f9 100644 --- a/src/components/Callvan/hooks/useCompleteCallvan.ts +++ b/src/components/Callvan/hooks/useCompleteCallvan.ts @@ -1,18 +1,16 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { completeCallvanPost } from 'api/callvan'; +import { callvanMutations } from 'api/callvan/mutations'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; const useCompleteCallvan = () => { const token = useTokenState(); const queryClient = useQueryClient(); + const mutation = callvanMutations.complete(queryClient, token); const { mutate, isPending } = useMutation({ - mutationFn: (postId: number) => completeCallvanPost(token, postId), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['callvanInfiniteList'] }); - }, + ...mutation, onError: (e) => { if (isKoinError(e)) { showToast('error', e.message || '이용 완료 처리에 실패했습니다.'); diff --git a/src/components/Callvan/hooks/useCreateCallvan.ts b/src/components/Callvan/hooks/useCreateCallvan.ts index c002c7801..f4c8072b2 100644 --- a/src/components/Callvan/hooks/useCreateCallvan.ts +++ b/src/components/Callvan/hooks/useCreateCallvan.ts @@ -1,19 +1,16 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { createCallvan } from 'api/callvan'; -import { CreateCallvanRequest } from 'api/callvan/entity'; +import { callvanMutations } from 'api/callvan/mutations'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; const useCreateCallvan = () => { const token = useTokenState(); const queryClient = useQueryClient(); + const mutation = callvanMutations.create(queryClient, token); const { mutate, isPending } = useMutation({ - mutationFn: (data: CreateCallvanRequest) => createCallvan(token, data), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['callvanInfiniteList'] }); - }, + ...mutation, onError: (e) => { if (isKoinError(e)) { showToast('error', e.message || '게시글 작성에 실패했습니다.'); diff --git a/src/components/Callvan/hooks/useDeleteAllNotifications.ts b/src/components/Callvan/hooks/useDeleteAllNotifications.ts index 35c283c14..618d577d7 100644 --- a/src/components/Callvan/hooks/useDeleteAllNotifications.ts +++ b/src/components/Callvan/hooks/useDeleteAllNotifications.ts @@ -1,9 +1,8 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { deleteAllNotifications } from 'api/callvan'; +import { callvanMutations } from 'api/callvan/mutations'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; -import { CALLVAN_NOTIFICATIONS_QUERY_KEY } from './useCallvanNotifications'; interface UseDeleteAllNotificationsProps { onSuccess?: () => void; @@ -12,11 +11,12 @@ interface UseDeleteAllNotificationsProps { const useDeleteAllNotifications = ({ onSuccess }: UseDeleteAllNotificationsProps = {}) => { const token = useTokenState(); const queryClient = useQueryClient(); + const mutation = callvanMutations.deleteAllNotifications(queryClient, token); const { mutate } = useMutation({ - mutationFn: () => deleteAllNotifications(token), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: CALLVAN_NOTIFICATIONS_QUERY_KEY }); + ...mutation, + onSuccess: async (...args) => { + await mutation.onSuccess?.(...args); onSuccess?.(); }, onError: (e) => { diff --git a/src/components/Callvan/hooks/useJoinCallvan.ts b/src/components/Callvan/hooks/useJoinCallvan.ts index 5dabf591a..6e313af31 100644 --- a/src/components/Callvan/hooks/useJoinCallvan.ts +++ b/src/components/Callvan/hooks/useJoinCallvan.ts @@ -1,17 +1,18 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { joinCallvan } from 'api/callvan'; +import { callvanMutations } from 'api/callvan/mutations'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; const useJoinCallvan = () => { const token = useTokenState(); const queryClient = useQueryClient(); + const mutation = callvanMutations.join(queryClient, token); const { mutate, isPending } = useMutation({ - mutationFn: (postId: number) => joinCallvan(token, postId), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['callvanInfiniteList'] }); + ...mutation, + onSuccess: async (...args) => { + await mutation.onSuccess?.(...args); showToast('success', '참여가 완료되었습니다.'); }, onError: (e) => { diff --git a/src/components/Callvan/hooks/useMarkAllNotificationsRead.ts b/src/components/Callvan/hooks/useMarkAllNotificationsRead.ts index 2b98a50ec..93df8c69b 100644 --- a/src/components/Callvan/hooks/useMarkAllNotificationsRead.ts +++ b/src/components/Callvan/hooks/useMarkAllNotificationsRead.ts @@ -1,19 +1,16 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { markAllNotificationsRead } from 'api/callvan'; +import { callvanMutations } from 'api/callvan/mutations'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; -import { CALLVAN_NOTIFICATIONS_QUERY_KEY } from './useCallvanNotifications'; const useMarkAllNotificationsRead = () => { const token = useTokenState(); const queryClient = useQueryClient(); + const mutation = callvanMutations.markAllNotificationsRead(queryClient, token); const { mutate } = useMutation({ - mutationFn: () => markAllNotificationsRead(token), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: CALLVAN_NOTIFICATIONS_QUERY_KEY }); - }, + ...mutation, onError: (e) => { if (isKoinError(e)) { showToast('error', e.message); diff --git a/src/components/Callvan/hooks/useMarkNotificationRead.ts b/src/components/Callvan/hooks/useMarkNotificationRead.ts index 4cf9bdf76..f1149c28e 100644 --- a/src/components/Callvan/hooks/useMarkNotificationRead.ts +++ b/src/components/Callvan/hooks/useMarkNotificationRead.ts @@ -1,19 +1,16 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { markNotificationRead } from 'api/callvan'; +import { callvanMutations } from 'api/callvan/mutations'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; -import { CALLVAN_NOTIFICATIONS_QUERY_KEY } from './useCallvanNotifications'; const useMarkNotificationRead = () => { const token = useTokenState(); const queryClient = useQueryClient(); + const mutation = callvanMutations.markNotificationRead(queryClient, token); const { mutate } = useMutation({ - mutationFn: (notificationId: number) => markNotificationRead(token, notificationId), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: CALLVAN_NOTIFICATIONS_QUERY_KEY }); - }, + ...mutation, onError: (e) => { if (isKoinError(e)) { showToast('error', e.message); diff --git a/src/components/Callvan/hooks/useReopenCallvan.ts b/src/components/Callvan/hooks/useReopenCallvan.ts index 804d1635e..bd3f6db64 100644 --- a/src/components/Callvan/hooks/useReopenCallvan.ts +++ b/src/components/Callvan/hooks/useReopenCallvan.ts @@ -1,18 +1,16 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { reopenCallvanPost } from 'api/callvan'; +import { callvanMutations } from 'api/callvan/mutations'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; const useReopenCallvan = () => { const token = useTokenState(); const queryClient = useQueryClient(); + const mutation = callvanMutations.reopen(queryClient, token); const { mutate, isPending } = useMutation({ - mutationFn: (postId: number) => reopenCallvanPost(token, postId), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['callvanInfiniteList'] }); - }, + ...mutation, onError: (e) => { if (isKoinError(e)) { showToast('error', e.message || '재모집에 실패했습니다.'); diff --git a/src/components/Callvan/hooks/useReportCallvan.ts b/src/components/Callvan/hooks/useReportCallvan.ts index 7d144ff2a..d93217baa 100644 --- a/src/components/Callvan/hooks/useReportCallvan.ts +++ b/src/components/Callvan/hooks/useReportCallvan.ts @@ -1,19 +1,16 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { reportCallvanParticipant } from 'api/callvan'; -import { CallvanReportRequest } from 'api/callvan/entity'; +import { callvanMutations } from 'api/callvan/mutations'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; const useReportCallvan = (postId: number) => { const token = useTokenState(); const queryClient = useQueryClient(); + const mutation = callvanMutations.report(queryClient, token, postId); const { mutate, isPending } = useMutation({ - mutationFn: (data: CallvanReportRequest) => reportCallvanParticipant(token, postId, data), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['callvanPostDetail', postId] }); - }, + ...mutation, onError: (e) => { if (isKoinError(e)) { showToast('error', e.message || '신고 접수에 실패했습니다.'); diff --git a/src/components/Callvan/hooks/useSendCallvanChat.ts b/src/components/Callvan/hooks/useSendCallvanChat.ts index 739ef7ae6..7ee9025a9 100644 --- a/src/components/Callvan/hooks/useSendCallvanChat.ts +++ b/src/components/Callvan/hooks/useSendCallvanChat.ts @@ -1,20 +1,16 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { sendCallvanChat } from 'api/callvan'; -import { SendChatRequest } from 'api/callvan/entity'; +import { callvanMutations } from 'api/callvan/mutations'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; -import { CALLVAN_CHAT_QUERY_KEY } from './useCallvanChat'; const useSendCallvanChat = (postId: number) => { const token = useTokenState(); const queryClient = useQueryClient(); + const mutation = callvanMutations.sendChat(queryClient, token, postId); const { mutate, isPending } = useMutation({ - mutationFn: (data: SendChatRequest) => sendCallvanChat(token, postId, data), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: CALLVAN_CHAT_QUERY_KEY(postId) }); - }, + ...mutation, onError: (e) => { if (isKoinError(e)) { showToast('error', e.message || '메시지 전송에 실패했습니다.'); diff --git a/src/components/CampusInfo/hooks/useCampusInfo.ts b/src/components/CampusInfo/hooks/useCampusInfo.ts deleted file mode 100644 index 541530db0..000000000 --- a/src/components/CampusInfo/hooks/useCampusInfo.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getAllShopInfo } from 'api/coopshop'; - -function useCampusInfo() { - const { data: campusInfo } = useSuspenseQuery({ - queryKey: ['/coopshop'], - queryFn: async () => getAllShopInfo(), - }); - - return { campusInfo }; -} - -export default useCampusInfo; diff --git a/src/components/CampusInfo/index.tsx b/src/components/CampusInfo/index.tsx index 697f3263b..ca1cd5f4a 100644 --- a/src/components/CampusInfo/index.tsx +++ b/src/components/CampusInfo/index.tsx @@ -1,5 +1,6 @@ import { cn } from '@bcsdlab/utils'; -import useCampusInfo from './hooks/useCampusInfo'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { coopshopQueries } from 'api/coopshop/queries'; import Book from './svg/book.svg'; import Cafe from './svg/cafe.svg'; import Cut from './svg/cut.svg'; @@ -57,7 +58,7 @@ const formatDateRange = (fromDate: string, toDate: string) => { }; function CampusInfo() { - const { campusInfo } = useCampusInfo(); + const { data: campusInfo } = useSuspenseQuery(coopshopQueries.allShopInfo()); const cafeteriaInfo = campusInfo?.coop_shops.find((shop) => shop.name === '학생식당'); const filteredCampusInfo = campusInfo?.coop_shops.filter((shop) => shop.name !== '학생식당'); diff --git a/src/components/Club/ClubDetailPage/components/ClubNotificationModal/index.tsx b/src/components/Club/ClubDetailPage/components/ClubNotificationModal/index.tsx index f7f67e223..c7c472166 100644 --- a/src/components/Club/ClubDetailPage/components/ClubNotificationModal/index.tsx +++ b/src/components/Club/ClubDetailPage/components/ClubNotificationModal/index.tsx @@ -6,7 +6,7 @@ import styles from './ClubNotificationModal.module.scss'; interface ClubNotificationModalProps { closeModal: () => void; - onSubmit: () => void; + onSubmit: () => void | Promise; variant: 'recruit' | 'event'; type?: 'subscribed' | 'unsubscribed'; } @@ -36,9 +36,13 @@ export default function ClubNotificationModal({ ); } - const handleSubmit = () => { - onSubmit(); - closeModal(); + const handleSubmit = async () => { + try { + await onSubmit(); + closeModal(); + } catch { + // The mutation hook already reports the error. + } }; return ( diff --git a/src/components/Club/ClubDetailPage/hooks/useClubEvent.ts b/src/components/Club/ClubDetailPage/hooks/useClubEvent.ts index 1bdb0a3ca..fe13baf8a 100644 --- a/src/components/Club/ClubDetailPage/hooks/useClubEvent.ts +++ b/src/components/Club/ClubDetailPage/hooks/useClubEvent.ts @@ -1,6 +1,6 @@ import { useRouter } from 'next/router'; import { useSuspenseQuery } from '@tanstack/react-query'; -import { getClubEventDetail, getClubEventList } from 'api/club'; +import { clubQueries } from 'api/club/queries'; import useTokenState from 'utils/hooks/state/useTokenState'; interface ClubEventListProps { @@ -15,10 +15,7 @@ export function useClubEventList({ clubId, eventType }: ClubEventListProps) { if (!clubId) { router.push('/clubs'); } - const { data: clubEventList } = useSuspenseQuery({ - queryKey: ['clubEventList', clubId, eventType], - queryFn: async () => getClubEventList(clubId!, eventType, token), - }); + const { data: clubEventList } = useSuspenseQuery(clubQueries.eventList(clubId!, eventType, token)); return { clubEventList }; } @@ -30,10 +27,7 @@ export function useClubEventDetail(clubId: string | number | undefined, eventId: router.push('/clubs'); } - const { data: clubEventDetail } = useSuspenseQuery({ - queryKey: ['clubEventDetail', clubId, eventId], - queryFn: async () => getClubEventDetail(clubId!, eventId!), - }); + const { data: clubEventDetail } = useSuspenseQuery(clubQueries.eventDetail(clubId!, eventId!)); return { clubEventDetail }; } diff --git a/src/components/Club/ClubDetailPage/hooks/useClubLike.ts b/src/components/Club/ClubDetailPage/hooks/useClubLike.ts index 85348d9d5..4c5240a99 100644 --- a/src/components/Club/ClubDetailPage/hooks/useClubLike.ts +++ b/src/components/Club/ClubDetailPage/hooks/useClubLike.ts @@ -1,7 +1,7 @@ import { useRouter } from 'next/router'; import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { deleteClubLike, putClubLike } from 'api/club'; +import { clubMutations } from 'api/club/mutations'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; @@ -16,12 +16,7 @@ export default function useClubLikeMutation(clubId: number | string | undefined) const token = useTokenState(); const queryClient = useQueryClient(); const { status: clubLikeStatus, mutateAsync: clubLikeMutateAsync } = useMutation({ - mutationFn: async () => { - await putClubLike(token, Number(clubId)); - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['clubDetail'] }); - }, + ...clubMutations.likeForDetail(queryClient, token, Number(clubId)), onError: (e) => { if (isKoinError(e)) { showToast('error', e.message); @@ -29,12 +24,7 @@ export default function useClubLikeMutation(clubId: number | string | undefined) }, }); const { status: clubUnlikeStatus, mutateAsync: clubUnlikeMutateAsync } = useMutation({ - mutationFn: async () => { - await deleteClubLike(token, Number(clubId)); - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['clubDetail'] }); - }, + ...clubMutations.unlikeForDetail(queryClient, token, Number(clubId)), onError: (e) => { if (isKoinError(e)) { showToast('error', e.message); diff --git a/src/components/Club/ClubDetailPage/hooks/useClubManager.ts b/src/components/Club/ClubDetailPage/hooks/useClubManager.ts index aefccaa54..2695c679a 100644 --- a/src/components/Club/ClubDetailPage/hooks/useClubManager.ts +++ b/src/components/Club/ClubDetailPage/hooks/useClubManager.ts @@ -1,8 +1,7 @@ import { useRouter } from 'next/router'; import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { putNewClubManager } from 'api/club'; -import { NewClubManager } from 'api/club/entity'; +import { clubMutations } from 'api/club/mutations'; import useLogger from 'utils/hooks/analytics/useLogger'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; @@ -16,17 +15,15 @@ export default function useMandateClubManagerMutation(clubId: number | string | const token = useTokenState(); const queryClient = useQueryClient(); const { status: mandateClubManagerStatus, mutateAsync: mandateClubManagerMutateAsync } = useMutation({ - mutationFn: async (data: NewClubManager) => { - await putNewClubManager(token, data); - }, - onSuccess: () => { - logger.actionEventClick({ - team: 'CAMPUS', - event_label: 'club_delegation_authority_confirm', - value: '권한위임', - }); - queryClient.invalidateQueries({ queryKey: ['clubDetail'] }); - }, + ...clubMutations.mandateManager(queryClient, token, Number(clubId), { + onSuccess: () => { + logger.actionEventClick({ + team: 'CAMPUS', + event_label: 'club_delegation_authority_confirm', + value: '권한위임', + }); + }, + }), onError: (e) => { if (isKoinError(e)) { showToast('error', e.message); diff --git a/src/components/Club/ClubDetailPage/hooks/useClubNotification.ts b/src/components/Club/ClubDetailPage/hooks/useClubNotification.ts index ce1edbdfe..2320783ce 100644 --- a/src/components/Club/ClubDetailPage/hooks/useClubNotification.ts +++ b/src/components/Club/ClubDetailPage/hooks/useClubNotification.ts @@ -1,50 +1,38 @@ +import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { - deleteClubEventNotification, - deleteClubRecruitmentNotification, - postClubEventNotification, - postClubRecruitmentNotification, -} from 'api/club'; +import { clubMutations } from 'api/club/mutations'; import useTokenState from 'utils/hooks/state/useTokenState'; +import showToast from 'utils/ts/showToast'; export default function useClubNotification(clubId: number) { const token = useTokenState(); const queryClient = useQueryClient(); + const handleError = (error: unknown) => { + if (isKoinError(error)) { + showToast('error', error.message); + } else { + sendClientError(error); + } + }; const { mutateAsync: subscribeRecruitmentNotification } = useMutation({ - mutationFn: async () => { - await postClubRecruitmentNotification(token, clubId); - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['clubDetail', clubId] }); - }, + ...clubMutations.subscribeRecruitmentNotification(queryClient, token, clubId), + onError: handleError, }); const { mutateAsync: unsubscribeRecruitmentNotification } = useMutation({ - mutationFn: async () => { - await deleteClubRecruitmentNotification(token, clubId); - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['clubDetail', clubId] }); - }, + ...clubMutations.unsubscribeRecruitmentNotification(queryClient, token, clubId), + onError: handleError, }); const { mutateAsync: subscribeEventNotification } = useMutation({ - mutationFn: async (eventId: number) => { - await postClubEventNotification(token, clubId, eventId); - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['clubDetail', clubId] }); - }, + ...clubMutations.subscribeEventNotification(queryClient, token, clubId), + onError: handleError, }); const { mutateAsync: unsubscribeEventNotification } = useMutation({ - mutationFn: async (eventId: number) => { - await deleteClubEventNotification(token, clubId, eventId); - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['clubDetail', clubId] }); - }, + ...clubMutations.unsubscribeEventNotification(queryClient, token, clubId), + onError: handleError, }); return { diff --git a/src/components/Club/ClubDetailPage/hooks/useClubQnA.ts b/src/components/Club/ClubDetailPage/hooks/useClubQnA.ts index c02802096..042cefe1a 100644 --- a/src/components/Club/ClubDetailPage/hooks/useClubQnA.ts +++ b/src/components/Club/ClubDetailPage/hooks/useClubQnA.ts @@ -1,8 +1,9 @@ import { useRouter } from 'next/router'; import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'; -import { deleteClubQnA, getClubQnA, postClubQnA } from 'api/club'; +import { deleteClubQnA, postClubQnA } from 'api/club'; import { ClubNewQnA } from 'api/club/entity'; +import { clubQueries } from 'api/club/queries'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; @@ -18,7 +19,7 @@ export default function useClubQnA(clubId: number | string | undefined) { await postClubQnA(token, clubId!, data); }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['clubQnA', clubId] }); + queryClient.invalidateQueries({ queryKey: clubQueries.qna(clubId!, token).queryKey }); }, onError: (e) => { if (isKoinError(e)) { @@ -27,17 +28,14 @@ export default function useClubQnA(clubId: number | string | undefined) { }, }); - const { data: clubQnAData } = useSuspenseQuery({ - queryKey: ['clubQnA', clubId], - queryFn: () => getClubQnA(token, Number(clubId)), - }); + const { data: clubQnAData } = useSuspenseQuery(clubQueries.qna(clubId!, token)); const { status: deleteClubQnAStatus, mutateAsync: deleteClubQnAMutateAsync } = useMutation({ mutationFn: async (qnaId: number) => { await deleteClubQnA(token, clubId!, qnaId); }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['clubQnA', clubId] }); + queryClient.invalidateQueries({ queryKey: clubQueries.qna(clubId!, token).queryKey }); }, onError: (e) => { if (isKoinError(e)) { diff --git a/src/components/Club/ClubDetailPage/hooks/useClubRecruitment.ts b/src/components/Club/ClubDetailPage/hooks/useClubRecruitment.ts deleted file mode 100644 index 52a717738..000000000 --- a/src/components/Club/ClubDetailPage/hooks/useClubRecruitment.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { isKoinError } from '@bcsdlab/koin'; -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getRecruitmentClub } from 'api/club'; -import type { ClubRecruitmentResponse } from 'api/club/entity'; - -const EMPTY_RECRUITMENT: ClubRecruitmentResponse = { - id: 0, - status: 'NONE', - dday: 0, - start_date: '', - end_date: '', - image_url: '', - content: '', - is_manager: false, -}; - -export default function useClubRecruitment(clubId: number) { - const { data: clubRecruitmentData } = useSuspenseQuery({ - queryKey: ['clubRecruitment', clubId], - queryFn: async () => { - try { - return await getRecruitmentClub(clubId); - } catch (e) { - if (isKoinError(e) && e.status === 404) { - return EMPTY_RECRUITMENT; - } - throw e; - } - }, - }); - - return { - clubRecruitmentData, - }; -} diff --git a/src/components/Club/ClubDetailPage/hooks/useClubdetail.ts b/src/components/Club/ClubDetailPage/hooks/useClubdetail.ts index 4124b5a80..21ce4aef2 100644 --- a/src/components/Club/ClubDetailPage/hooks/useClubdetail.ts +++ b/src/components/Club/ClubDetailPage/hooks/useClubdetail.ts @@ -1,7 +1,8 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'; -import { getClubDetail, putClubInroduction } from 'api/club'; +import { putClubInroduction } from 'api/club'; import { ClubIntroductionData } from 'api/club/entity'; +import { clubQueries } from 'api/club/queries'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; @@ -9,17 +10,14 @@ export default function useClubDetail(clubId: number) { const token = useTokenState(); const queryClient = useQueryClient(); - const { data: clubDetail } = useSuspenseQuery({ - queryKey: ['clubDetail', clubId], - queryFn: () => getClubDetail(token, Number(clubId)), - }); + const { data: clubDetail } = useSuspenseQuery(clubQueries.detail(Number(clubId), token)); const { status: clubIntroductionEditStatus, mutateAsync: clubIntroductionEditMutateAsync } = useMutation({ mutationFn: async (data: ClubIntroductionData) => { await putClubInroduction(token, clubId!, data); }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['clubDetail', clubId] }); + queryClient.invalidateQueries({ queryKey: clubQueries.detail(Number(clubId), token).queryKey }); }, onError: (e) => { if (isKoinError(e)) { diff --git a/src/components/Club/ClubDetailPage/hooks/useDeleteEvent.ts b/src/components/Club/ClubDetailPage/hooks/useDeleteEvent.ts index f2835ab6e..194a5e9f8 100644 --- a/src/components/Club/ClubDetailPage/hooks/useDeleteEvent.ts +++ b/src/components/Club/ClubDetailPage/hooks/useDeleteEvent.ts @@ -1,7 +1,7 @@ import { useRouter } from 'next/router'; import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { deleteClubEvent } from 'api/club'; +import { clubMutations } from 'api/club/mutations'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; @@ -12,11 +12,11 @@ export default function useDeleteEvent() { const token = useTokenState(); return useMutation({ - mutationFn: (eventId: number) => deleteClubEvent(token, Number(id), eventId), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['clubEventList', id] }); - showToast('success', '행사 삭제되었습니다.'); - }, + ...clubMutations.deleteEvent(queryClient, token, Number(id), { + onSuccess: () => { + showToast('success', '행사 삭제되었습니다.'); + }, + }), onError: (error) => { if (isKoinError(error)) { showToast('error', error.message); diff --git a/src/components/Club/ClubDetailPage/hooks/useDeleteRecruitment.ts b/src/components/Club/ClubDetailPage/hooks/useDeleteRecruitment.ts index 2c5907d2f..a96bf20ea 100644 --- a/src/components/Club/ClubDetailPage/hooks/useDeleteRecruitment.ts +++ b/src/components/Club/ClubDetailPage/hooks/useDeleteRecruitment.ts @@ -1,7 +1,7 @@ import { useRouter } from 'next/router'; import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { deleteClubRecruitment } from 'api/club'; +import { clubMutations } from 'api/club/mutations'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; @@ -12,11 +12,11 @@ export default function useDeleteRecruitment() { const token = useTokenState(); return useMutation({ - mutationFn: () => deleteClubRecruitment(token, Number(id)), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['clubRecruitment', id] }); - showToast('success', '모집이 삭제되었습니다.'); - }, + ...clubMutations.deleteRecruitment(queryClient, token, Number(id), { + onSuccess: () => { + showToast('success', '모집이 삭제되었습니다.'); + }, + }), onError: (error) => { if (isKoinError(error)) { showToast('error', error.message); diff --git a/src/components/Club/ClubEditPage/hooks/usePutClub.ts b/src/components/Club/ClubEditPage/hooks/usePutClub.ts index cc5d7f199..3af9d55f2 100644 --- a/src/components/Club/ClubEditPage/hooks/usePutClub.ts +++ b/src/components/Club/ClubEditPage/hooks/usePutClub.ts @@ -1,8 +1,7 @@ import { useRouter } from 'next/router'; import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { putClubDetail } from 'api/club'; -import { NewClubData } from 'api/club/entity'; +import { clubMutations } from 'api/club/mutations'; import ROUTES from 'static/routes'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; @@ -16,14 +15,12 @@ export default function usePutClub(clubId: number | string | undefined) { navigate('/clubs'); } const { status, mutateAsync } = useMutation({ - mutationFn: async (data: NewClubData) => { - await putClubDetail(token, data, clubId!); - }, - onSuccess: () => { - showToast('success', '동아리 정보 수정 요청이 완료되었습니다.'); - queryClient.invalidateQueries({ queryKey: ['clubDetail'] }); - navigate(ROUTES.ClubDetail({ id: String(clubId) })); - }, + ...clubMutations.update(queryClient, token, clubId!, { + onSuccess: () => { + showToast('success', '동아리 정보 수정 요청이 완료되었습니다.'); + navigate(ROUTES.ClubDetail({ id: String(clubId) })); + }, + }), onError: (e) => { if (isKoinError(e)) { showToast('error', e.message); diff --git a/src/components/Club/ClubEventEditPage/hooks/usePutClubEvent.ts b/src/components/Club/ClubEventEditPage/hooks/usePutClubEvent.ts index 24cf20248..c331af7cc 100644 --- a/src/components/Club/ClubEventEditPage/hooks/usePutClubEvent.ts +++ b/src/components/Club/ClubEventEditPage/hooks/usePutClubEvent.ts @@ -1,8 +1,7 @@ import { useRouter } from 'next/router'; import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { putClubEvent } from 'api/club'; -import { ClubEventRequest } from 'api/club/entity'; +import { clubMutations } from 'api/club/mutations'; import ROUTES from 'static/routes'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; @@ -13,14 +12,12 @@ export default function usePutClubEvent(clubId: number) { const router = useRouter(); const { mutateAsync } = useMutation({ - mutationFn: async ({ eventId, data }: { eventId: number; data: ClubEventRequest }) => { - await putClubEvent(token, clubId, eventId, data); - }, - onSuccess: () => { - showToast('success', '동아리 행사 수정 요청이 완료되었습니다.'); - queryClient.invalidateQueries({ queryKey: ['clubDetail', 'clubEvent'] }); - router.push(ROUTES.ClubDetail({ id: String(clubId) })); - }, + ...clubMutations.updateEvent(queryClient, token, clubId, { + onSuccess: () => { + showToast('success', '동아리 행사 수정 요청이 완료되었습니다.'); + router.push(ROUTES.ClubDetail({ id: String(clubId) })); + }, + }), onError: (e) => { if (isKoinError(e)) { showToast('error', e.message); diff --git a/src/components/Club/ClubRecruitmentEditPage/hooks/usePutClubRecruitment.ts b/src/components/Club/ClubRecruitmentEditPage/hooks/usePutClubRecruitment.ts index 5b19de62d..670715ce7 100644 --- a/src/components/Club/ClubRecruitmentEditPage/hooks/usePutClubRecruitment.ts +++ b/src/components/Club/ClubRecruitmentEditPage/hooks/usePutClubRecruitment.ts @@ -1,8 +1,7 @@ import { useRouter } from 'next/router'; import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { putClubRecruitment } from 'api/club'; -import { ClubRecruitmentRequest } from 'api/club/entity'; +import { clubMutations } from 'api/club/mutations'; import ROUTES from 'static/routes'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; @@ -13,14 +12,12 @@ export default function usePutClubRecruitment(clubId: number) { const router = useRouter(); const { mutateAsync } = useMutation({ - mutationFn: async (data: ClubRecruitmentRequest) => { - await putClubRecruitment(token, clubId, data); - }, - onSuccess: () => { - showToast('success', '동아리 모집 수정 요청이 완료되었습니다.'); - queryClient.invalidateQueries({ queryKey: ['clubRecruitment'] }); - router.push(ROUTES.ClubDetail({ id: String(clubId) })); - }, + ...clubMutations.updateRecruitment(queryClient, token, clubId, { + onSuccess: () => { + showToast('success', '동아리 모집 수정 요청이 완료되었습니다.'); + router.push(ROUTES.ClubDetail({ id: String(clubId) })); + }, + }), onError: (e) => { if (isKoinError(e)) { showToast('error', e.message); diff --git a/src/components/Club/NewClubEvent/hooks/usePostNewEvent.ts b/src/components/Club/NewClubEvent/hooks/usePostNewEvent.ts index 8614ef551..766174b2f 100644 --- a/src/components/Club/NewClubEvent/hooks/usePostNewEvent.ts +++ b/src/components/Club/NewClubEvent/hooks/usePostNewEvent.ts @@ -1,8 +1,7 @@ import { useRouter } from 'next/router'; import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { postClubEvent } from 'api/club'; -import { ClubEventRequest } from 'api/club/entity'; +import { clubMutations } from 'api/club/mutations'; import ROUTES from 'static/routes'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; @@ -13,15 +12,12 @@ export default function usePostNewEvent(clubId: number | undefined) { const router = useRouter(); const { mutateAsync } = useMutation({ - mutationFn: async (data: ClubEventRequest) => { - const response = await postClubEvent(token, clubId!, data); - return response; - }, - onSuccess: () => { - showToast('success', '동아리 행사가 생성되었습니다.'); - queryClient.invalidateQueries({ queryKey: ['clubList'] }); - router.push(ROUTES.ClubDetail({ id: String(clubId) })); - }, + ...clubMutations.createEvent(queryClient, token, clubId!, { + onSuccess: () => { + showToast('success', '동아리 행사가 생성되었습니다.'); + router.push(ROUTES.ClubDetail({ id: String(clubId) })); + }, + }), onError: (e) => { if (isKoinError(e)) { showToast('error', e.message); diff --git a/src/components/Club/NewClubPage/hooks/usePostNewClub.ts b/src/components/Club/NewClubPage/hooks/usePostNewClub.ts index 809a665db..36799a187 100644 --- a/src/components/Club/NewClubPage/hooks/usePostNewClub.ts +++ b/src/components/Club/NewClubPage/hooks/usePostNewClub.ts @@ -1,8 +1,7 @@ import { useRouter } from 'next/router'; import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { postClub } from 'api/club'; -import { NewClubData } from 'api/club/entity'; +import { clubMutations } from 'api/club/mutations'; import ROUTES from 'static/routes'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; @@ -12,14 +11,12 @@ export default function usePostNewClub() { const queryClient = useQueryClient(); const router = useRouter(); const { status, mutateAsync } = useMutation({ - mutationFn: async (data: NewClubData) => { - await postClub(token, data); - }, - onSuccess: () => { - showToast('success', '동아리 생성 요청이 완료되었습니다.'); - queryClient.invalidateQueries({ queryKey: ['clubList'] }); - router.push(ROUTES.Club()); - }, + ...clubMutations.create(queryClient, token, { + onSuccess: () => { + showToast('success', '동아리 생성 요청이 완료되었습니다.'); + router.push(ROUTES.Club()); + }, + }), onError: (e) => { if (isKoinError(e)) { showToast('error', e.message); diff --git a/src/components/Club/NewClubRecruitment/hooks/usePostNewRecruitment.ts b/src/components/Club/NewClubRecruitment/hooks/usePostNewRecruitment.ts index 03e6f657b..27d647687 100644 --- a/src/components/Club/NewClubRecruitment/hooks/usePostNewRecruitment.ts +++ b/src/components/Club/NewClubRecruitment/hooks/usePostNewRecruitment.ts @@ -1,8 +1,7 @@ import { useRouter } from 'next/router'; import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { postClubRecruitment } from 'api/club'; -import { ClubRecruitmentRequest } from 'api/club/entity'; +import { clubMutations } from 'api/club/mutations'; import ROUTES from 'static/routes'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; @@ -13,15 +12,12 @@ export default function usePostNewRecruitment(clubId: number | undefined) { const router = useRouter(); const { mutateAsync } = useMutation({ - mutationFn: async (data: ClubRecruitmentRequest) => { - const response = await postClubRecruitment(token, clubId!, data); - return response; - }, - onSuccess: () => { - showToast('success', '동아리 모집이 생성되었습니다.'); - queryClient.invalidateQueries({ queryKey: ['clubRecruitment'] }); - router.push(ROUTES.ClubDetail({ id: String(clubId) })); - }, + ...clubMutations.createRecruitment(queryClient, token, clubId!, { + onSuccess: () => { + showToast('success', '동아리 모집이 생성되었습니다.'); + router.push(ROUTES.ClubDetail({ id: String(clubId) })); + }, + }), onError: (e) => { if (isKoinError(e)) { showToast('error', e.message); diff --git a/src/components/Club/hooks/useClubCategories.ts b/src/components/Club/hooks/useClubCategories.ts deleted file mode 100644 index 8df591037..000000000 --- a/src/components/Club/hooks/useClubCategories.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getClubCategories } from 'api/club'; - -function useClubCategories() { - const { data } = useSuspenseQuery({ - queryKey: ['club-categories'], - queryFn: () => getClubCategories(), - }); - return data.club_categories; -} - -export default useClubCategories; diff --git a/src/components/Club/hooks/useClubLike.ts b/src/components/Club/hooks/useClubLike.ts index db880afc1..dc2a096c5 100644 --- a/src/components/Club/hooks/useClubLike.ts +++ b/src/components/Club/hooks/useClubLike.ts @@ -1,6 +1,6 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { deleteClubLike, putClubLike } from 'api/club'; +import { clubMutations } from 'api/club/mutations'; import showToast from 'utils/ts/showToast'; interface ClubLikeProps { @@ -12,17 +12,16 @@ interface ClubLikeProps { function useClubLike() { const queryClient = useQueryClient(); const { mutate } = useMutation({ - mutationFn: ({ token, isLiked, clubId }: ClubLikeProps) => - isLiked ? deleteClubLike(token, clubId) : putClubLike(token, clubId), - onSuccess: () => queryClient.invalidateQueries({ queryKey: ['club-list'] }), + ...clubMutations.toggleLikeForList(queryClient), onError: (e) => { if (isKoinError(e)) { showToast('error', e.message); } else sendClientError(e); }, }); + return { - mutate, + mutate: (variables: ClubLikeProps) => mutate(variables), }; } diff --git a/src/components/Club/hooks/useClubList.ts b/src/components/Club/hooks/useClubList.ts deleted file mode 100644 index dd11afdb2..000000000 --- a/src/components/Club/hooks/useClubList.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { getClubList } from 'api/club'; - -interface ClubListProps { - token?: string; - categoryId?: number; - sortType?: string; - isRecruiting?: boolean; - clubName?: string; -} - -function useClubList({ token, categoryId, sortType, isRecruiting, clubName }: ClubListProps) { - const { data } = useQuery({ - queryKey: ['club-list', categoryId, sortType, isRecruiting, clubName], - queryFn: () => getClubList(token, categoryId, sortType, isRecruiting, clubName), - }); - - return data?.clubs ?? []; -} - -export default useClubList; diff --git a/src/components/Club/hooks/useHotClub.ts b/src/components/Club/hooks/useHotClub.ts deleted file mode 100644 index 714bf8415..000000000 --- a/src/components/Club/hooks/useHotClub.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { isKoinError } from '@bcsdlab/koin'; -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getHotClub } from 'api/club'; - -function useHotClub() { - const data = useSuspenseQuery({ - queryKey: ['hot-club'], - queryFn: async () => { - try { - return await getHotClub(); - } catch (e) { - if (isKoinError(e) && e.status === 404) { - return { - club_id: -1, - name: '인기 동아리가 없어요', - image_url: '', - }; - } - throw e; - } - }, - }); - return data; -} - -export default useHotClub; diff --git a/src/components/Course/hooks/useCourseQuery.ts b/src/components/Course/hooks/useCourseQuery.ts deleted file mode 100644 index f0093fb29..000000000 --- a/src/components/Course/hooks/useCourseQuery.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getCourseSearch, getPreCourseList } from 'api/course'; -import { CourseRequestParams } from 'api/course/entity'; - -export const useSuspenseCourseSearch = (params: CourseRequestParams) => { - const { name, department, year, semester } = params; - - return useSuspenseQuery({ - queryKey: ['course', 'search', params], - queryFn: () => getCourseSearch(name || undefined, department || undefined, year, semester), - }); -}; - -export const useSuspensePreCourseList = (token: string, timetableFrameId: number) => { - return useSuspenseQuery({ - queryKey: ['course', 'pre-course-list', timetableFrameId], - queryFn: () => getPreCourseList(token, timetableFrameId), - gcTime: 0, - }); -}; diff --git a/src/components/GraduationCalculatorPage/components/CourseTable/CourseTypeList/index.tsx b/src/components/GraduationCalculatorPage/components/CourseTable/CourseTypeList/index.tsx index 13dc96924..fa5e0fc3a 100644 --- a/src/components/GraduationCalculatorPage/components/CourseTable/CourseTypeList/index.tsx +++ b/src/components/GraduationCalculatorPage/components/CourseTable/CourseTypeList/index.tsx @@ -1,7 +1,8 @@ import { useRef, useState } from 'react'; import { cn } from '@bcsdlab/utils'; +import { useQuery } from '@tanstack/react-query'; +import { graduationCalculatorQueries } from 'api/graduationCalculator/queries'; import DownArrowIcon from 'assets/svg/chervron-up-grey.svg'; -import useGeneralEducation from 'components/GraduationCalculatorPage/hooks/useGeneralEducation'; import useLogger from 'utils/hooks/analytics/useLogger'; import useBooleanState from 'utils/hooks/state/useBooleanState'; import useTokenState from 'utils/hooks/state/useTokenState'; @@ -28,7 +29,10 @@ function CourseTypeList({ const [isOverHalf, setIsOverHalf] = useState(false); const token = useTokenState(); - const { generalEducation } = useGeneralEducation(token); + const { data: generalEducation } = useQuery({ + ...graduationCalculatorQueries.generalEducation(token), + enabled: !!token, + }); // '교양선택'은 교양 세부 영역 리스트에서 제외 const generalCourseType = generalEducation?.general_education_area.map((area) => area.course_type)?.slice(1) || []; const [hoveredItem, setHoveredItem] = useState(null); diff --git a/src/components/GraduationCalculatorPage/components/CreditChart/SemesterLectureListModal/index.tsx b/src/components/GraduationCalculatorPage/components/CreditChart/SemesterLectureListModal/index.tsx index 89743e6a5..dab211cbe 100644 --- a/src/components/GraduationCalculatorPage/components/CreditChart/SemesterLectureListModal/index.tsx +++ b/src/components/GraduationCalculatorPage/components/CreditChart/SemesterLectureListModal/index.tsx @@ -1,15 +1,16 @@ import { startTransition, useState } from 'react'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { authQueries } from 'api/auth/queries'; import { LectureInfo } from 'api/graduationCalculator/entity'; +import { graduationCalculatorQueries } from 'api/graduationCalculator/queries'; import CloseIcon from 'assets/svg/close-icon-grey.svg'; import SemesterCourseTable from 'components/GraduationCalculatorPage/components/CourseTable/SemesterCourseTable'; -import useCourseType from 'components/GraduationCalculatorPage/hooks/useCourseType'; import DeptListbox from 'components/TimetablePage/components/LectureList/DeptListbox'; import useAllMyLectures from 'components/TimetablePage/hooks/useAllMyLectures'; import useSelect from 'components/TimetablePage/hooks/useSelect'; import { useSemester } from 'components/TimetablePage/hooks/useSemesterOptionList'; import { Selector } from 'components/ui/Selector'; import useTokenState from 'utils/hooks/state/useTokenState'; -import useUserAcademicInfo from 'utils/hooks/state/useUserAcademicInfo'; import { useOutsideClick } from 'utils/hooks/ui/useOutsideClick'; import { pick } from 'utils/ts/object'; import styles from './SemesterLectureListModal.module.scss'; @@ -48,7 +49,7 @@ export default function SemesterLectureListModal({ const token = useTokenState(); const allMyLectures = useAllMyLectures(token); const { backgroundRef } = useOutsideClick({ onOutsideClick: onClose }); - const { data: academicInfo } = useUserAcademicInfo(); + const { data: academicInfo } = useSuspenseQuery(authQueries.userAcademicInfo(token)); const semesterOptionList = (semesters ?? []).map((semesterInfo) => ({ label: `${semesterInfo.year}년 ${semesterInfo.term}`, value: `${semesterInfo.year}년 ${semesterInfo.term}`, @@ -61,7 +62,7 @@ export default function SemesterLectureListModal({ const { value: lectureStatus, onChangeSelect: onChangeLectureStatus } = useSelect(lectureStatusOptions[0].value); const { value: department, onChangeSelect: onChangeDepartment } = useSelect(academicInfo?.department); const { value: course, onChangeSelect: onChangeCourse } = useSelect(initialCourse); - const { data: generalCourses } = useCourseType(token, semester, course!); + const { data: generalCourses } = useSuspenseQuery(graduationCalculatorQueries.courseType(token, semester, course!)); const allMyLecturesInfo = (allMyLectures ?? []) .filter((myLecture) => myLecture.course_type === course) diff --git a/src/components/GraduationCalculatorPage/components/CreditChart/index.tsx b/src/components/GraduationCalculatorPage/components/CreditChart/index.tsx index dd64163b9..543cebf5d 100644 --- a/src/components/GraduationCalculatorPage/components/CreditChart/index.tsx +++ b/src/components/GraduationCalculatorPage/components/CreditChart/index.tsx @@ -1,7 +1,8 @@ import { startTransition } from 'react'; import { cn } from '@bcsdlab/utils'; +import { useQuery } from '@tanstack/react-query'; import { GradesByCourseType } from 'api/graduationCalculator/entity'; -import useCalculateCredits from 'components/GraduationCalculatorPage/hooks/useCalculateCredits'; +import { graduationCalculatorQueries } from 'api/graduationCalculator/queries'; import { Portal } from 'components/modal/Modal/PortalProvider'; import useGetMultiMajorLecture from 'components/TimetablePage/hooks/useGetMultiMajorLecture'; import { motion, AnimatePresence } from 'framer-motion'; @@ -17,7 +18,10 @@ function CreditChart({ totalGrades }: { totalGrades: number }) { const token = useTokenState(); const portalManager = useModalPortal(); const { lock, unlock } = useScrollLock(false); - const { data: calculateCredits } = useCalculateCredits(token); + const { data: calculateCredits } = useQuery({ + ...graduationCalculatorQueries.creditsByCourseType(token), + enabled: !!token, + }); const { data: multiMajorLecture } = useGetMultiMajorLecture(token); const onClickBar = (courseType: string) => { diff --git a/src/components/GraduationCalculatorPage/components/GeneralCourse/GeneralCourseListModal/index.tsx b/src/components/GraduationCalculatorPage/components/GeneralCourse/GeneralCourseListModal/index.tsx index 993082281..15ca8acad 100644 --- a/src/components/GraduationCalculatorPage/components/GeneralCourse/GeneralCourseListModal/index.tsx +++ b/src/components/GraduationCalculatorPage/components/GeneralCourse/GeneralCourseListModal/index.tsx @@ -1,7 +1,8 @@ import { startTransition, useState } from 'react'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { graduationCalculatorQueries } from 'api/graduationCalculator/queries'; import CloseIcon from 'assets/svg/close-icon-grey.svg'; import SemesterCourseTable from 'components/GraduationCalculatorPage/components/CourseTable/SemesterCourseTable'; -import useCourseType from 'components/GraduationCalculatorPage/hooks/useCourseType'; import { useSemester } from 'components/TimetablePage/hooks/useSemesterOptionList'; import { Selector } from 'components/ui/Selector'; import useTokenState from 'utils/hooks/state/useTokenState'; @@ -27,7 +28,9 @@ function GeneralCourseListModal({ courseType, onClose }: GeneralCourseListModalP term: string; }>({ year: semesters[0].year, term: semesters[0].term }); - const { data: generalCourses } = useCourseType(token, semester, '교양선택', courseType ?? undefined); + const { data: generalCourses } = useSuspenseQuery( + graduationCalculatorQueries.courseType(token, semester, '교양선택', courseType ?? undefined), + ); const generalCourseLectures = generalCourses?.lectures ?? []; const tableData = generalCourseLectures.map((lecture) => [ diff --git a/src/components/GraduationCalculatorPage/components/GeneralCourse/index.tsx b/src/components/GraduationCalculatorPage/components/GeneralCourse/index.tsx index a66d43d99..443872084 100644 --- a/src/components/GraduationCalculatorPage/components/GeneralCourse/index.tsx +++ b/src/components/GraduationCalculatorPage/components/GeneralCourse/index.tsx @@ -1,12 +1,13 @@ /* eslint-disable react-hooks/exhaustive-deps */ import { startTransition, useEffect } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { graduationCalculatorQueries } from 'api/graduationCalculator/queries'; import BubbleTailBottom from 'assets/svg/bubble-tail-bottom.svg'; import CloseIcon from 'assets/svg/common/close/close-icon-grey.svg'; import CompletedIcon from 'assets/svg/ellipse-icon-green.svg'; import NotCompletedIcon from 'assets/svg/ellipse-icon-red.svg'; import QuestionMarkIcon from 'assets/svg/question-mark-icon.svg'; -import useGeneralEducation from 'components/GraduationCalculatorPage/hooks/useGeneralEducation'; import { Portal } from 'components/modal/Modal/PortalProvider'; import useLogger from 'utils/hooks/analytics/useLogger'; import useModalPortal from 'utils/hooks/layout/useModalPortal'; @@ -22,7 +23,10 @@ function GeneralCourse() { const portalManager = useModalPortal(); const [isTooltipOpen, openTooltip, closeTooltip] = useBooleanState(false); const token = useTokenState(); - const { generalEducation } = useGeneralEducation(token); + const { data: generalEducation } = useQuery({ + ...graduationCalculatorQueries.generalEducation(token), + enabled: !!token, + }); const requiredEducationArea = generalEducation?.general_education_area || []; const handleOpenModal = (courseType: string) => { diff --git a/src/components/GraduationCalculatorPage/components/StudentForm/index.tsx b/src/components/GraduationCalculatorPage/components/StudentForm/index.tsx index f6b00be89..20405f26e 100644 --- a/src/components/GraduationCalculatorPage/components/StudentForm/index.tsx +++ b/src/components/GraduationCalculatorPage/components/StudentForm/index.tsx @@ -1,17 +1,18 @@ import { useEffect, useState } from 'react'; -import useDepartmentMajorList from 'components/GraduationCalculatorPage/hooks/useDepartmentMajorList'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { authQueries } from 'api/auth/queries'; +import { deptQueries } from 'api/dept/queries'; import useUpdateAcademicInfo from 'components/GraduationCalculatorPage/hooks/useUpdateAcademicInfo'; import { Selector } from 'components/ui/Selector'; import useLogger from 'utils/hooks/analytics/useLogger'; import useTokenState from 'utils/hooks/state/useTokenState'; -import useUserAcademicInfo from 'utils/hooks/state/useUserAcademicInfo'; import styles from './StudentForm.module.scss'; function StudentForm() { const logger = useLogger(); const token = useTokenState(); - const { data: academicInfo } = useUserAcademicInfo(); - const { data: deptMajorList } = useDepartmentMajorList(); + const { data: academicInfo } = useSuspenseQuery(authQueries.userAcademicInfo(token)); + const { data: deptMajorList } = useSuspenseQuery(deptQueries.majorList()); const [studentNumber, setStudentNumber] = useState(academicInfo?.student_number ?? ''); const [department, setDepartment] = useState(academicInfo?.department ?? ''); diff --git a/src/components/GraduationCalculatorPage/hooks/useAgreeGraduationCreidts.ts b/src/components/GraduationCalculatorPage/hooks/useAgreeGraduationCreidts.ts index 0e5b7e584..b99f88f32 100644 --- a/src/components/GraduationCalculatorPage/hooks/useAgreeGraduationCreidts.ts +++ b/src/components/GraduationCalculatorPage/hooks/useAgreeGraduationCreidts.ts @@ -1,6 +1,7 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { agreeGraduationCredits } from 'api/graduationCalculator'; +import { graduationCalculatorQueryKeys } from 'api/graduationCalculator/queries'; import showToast from 'utils/ts/showToast'; @@ -10,7 +11,7 @@ export default function useAgreeGraduationCreidts(token: string) { mutationFn: () => agreeGraduationCredits(token), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['creditsByCourseType'] }); + queryClient.invalidateQueries({ queryKey: graduationCalculatorQueryKeys.all }); }, onError: (error) => { diff --git a/src/components/GraduationCalculatorPage/hooks/useCalculateCredits.ts b/src/components/GraduationCalculatorPage/hooks/useCalculateCredits.ts deleted file mode 100644 index be49379b9..000000000 --- a/src/components/GraduationCalculatorPage/hooks/useCalculateCredits.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { calculateGraduationCredits } from 'api/graduationCalculator'; - -export default function useCalculateCredits(token: string) { - return useQuery({ - queryKey: ['creditsByCourseType'], - queryFn: () => (token ? calculateGraduationCredits(token) : null), - }); -} diff --git a/src/components/GraduationCalculatorPage/hooks/useCourseType.ts b/src/components/GraduationCalculatorPage/hooks/useCourseType.ts deleted file mode 100644 index 136f4d171..000000000 --- a/src/components/GraduationCalculatorPage/hooks/useCourseType.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getCourseType } from 'api/graduationCalculator'; -import { Semester } from 'api/graduationCalculator/entity'; - -const useCourseType = (token: string, semester: Semester, name: string, generalEducationArea?: string) => { - const { data } = useSuspenseQuery({ - queryKey: ['courseType', semester, name, generalEducationArea], - queryFn: () => getCourseType(token, semester, name, generalEducationArea), - }); - - return { data }; -}; - -export default useCourseType; diff --git a/src/components/GraduationCalculatorPage/hooks/useDepartmentMajorList.ts b/src/components/GraduationCalculatorPage/hooks/useDepartmentMajorList.ts deleted file mode 100644 index 442236227..000000000 --- a/src/components/GraduationCalculatorPage/hooks/useDepartmentMajorList.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getDeptMajorList } from 'api/dept'; - -export default function useDepartmentMajorList() { - const { data } = useSuspenseQuery({ - queryKey: ['deptMajor'], - queryFn: getDeptMajorList, - }); - - return { data }; -} diff --git a/src/components/GraduationCalculatorPage/hooks/useExcelUpload.ts b/src/components/GraduationCalculatorPage/hooks/useExcelUpload.ts index f17791cd6..69869206e 100644 --- a/src/components/GraduationCalculatorPage/hooks/useExcelUpload.ts +++ b/src/components/GraduationCalculatorPage/hooks/useExcelUpload.ts @@ -1,5 +1,7 @@ import { DragEvent } from 'react'; import { useQueryClient } from '@tanstack/react-query'; +import { graduationCalculatorQueryKeys } from 'api/graduationCalculator/queries'; +import { timetableQueryKeys } from 'api/timetable/queries'; import usePostGraduationExcel from 'components/GraduationCalculatorPage/hooks/usePostGraduationExcel'; import { GraduationExcelUploadForPost } from 'components/GraduationCalculatorPage/ts/types'; import useLogger from 'utils/hooks/analytics/useLogger'; @@ -16,9 +18,8 @@ export function useExcelUpload() { mutate(formData as unknown as GraduationExcelUploadForPost, { onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['generalEducation'] }); - queryClient.invalidateQueries({ queryKey: ['my_semester'] }); - queryClient.invalidateQueries({ queryKey: ['creditsByCourseType'] }); + queryClient.invalidateQueries({ queryKey: graduationCalculatorQueryKeys.all }); + queryClient.invalidateQueries({ queryKey: timetableQueryKeys.mySemester() }); showToast('success', '엑셀 파일이 성공적으로 업로드되었습니다.'); }, onError: () => { diff --git a/src/components/GraduationCalculatorPage/hooks/useGeneralEducation.ts b/src/components/GraduationCalculatorPage/hooks/useGeneralEducation.ts deleted file mode 100644 index 159a1119a..000000000 --- a/src/components/GraduationCalculatorPage/hooks/useGeneralEducation.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { getGeneralEducation } from 'api/graduationCalculator'; - -const useGeneralEducation = (token: string) => { - const { data } = useQuery({ - queryKey: ['generalEducation'], - queryFn: () => getGeneralEducation(token), - enabled: !!token, - }); - - return { generalEducation: data }; -}; - -export default useGeneralEducation; diff --git a/src/components/GraduationCalculatorPage/hooks/usePostGraduationExcel.ts b/src/components/GraduationCalculatorPage/hooks/usePostGraduationExcel.ts index 0fdc8cf19..80ac00418 100644 --- a/src/components/GraduationCalculatorPage/hooks/usePostGraduationExcel.ts +++ b/src/components/GraduationCalculatorPage/hooks/usePostGraduationExcel.ts @@ -1,6 +1,7 @@ import { isKoinError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { uploadGraduationExcel } from 'api/graduationCalculator'; +import { graduationCalculatorQueryKeys } from 'api/graduationCalculator/queries'; import { GraduationExcelUploadForPost } from 'components/GraduationCalculatorPage/ts/types'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; @@ -12,7 +13,7 @@ const usePostGraduationExcel = () => { const { mutate, error } = useMutation({ mutationFn: async (data: GraduationExcelUploadForPost) => uploadGraduationExcel(data, token), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['graduation'] }); + queryClient.invalidateQueries({ queryKey: graduationCalculatorQueryKeys.all }); }, onError: (e) => { if (isKoinError(e)) { diff --git a/src/components/GraduationCalculatorPage/hooks/useUpdateAcademicInfo.ts b/src/components/GraduationCalculatorPage/hooks/useUpdateAcademicInfo.ts index 4c16178c0..ebfaf45ab 100644 --- a/src/components/GraduationCalculatorPage/hooks/useUpdateAcademicInfo.ts +++ b/src/components/GraduationCalculatorPage/hooks/useUpdateAcademicInfo.ts @@ -2,6 +2,8 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { updateAcademicInfo } from 'api/auth'; import { UpdateAcademicInfoRequest } from 'api/auth/entity'; +import { authQueryKeys } from 'api/auth/queries'; +import { graduationCalculatorQueryKeys } from 'api/graduationCalculator/queries'; import showToast from 'utils/ts/showToast'; import useAgreeGraduationCreidts from './useAgreeGraduationCreidts'; @@ -14,9 +16,8 @@ export default function useUpdateAcademicInfo(token: string) { onSuccess: () => { agreeGraduationCredits(); - queryClient.invalidateQueries({ queryKey: ['generalEducation'] }); - queryClient.invalidateQueries({ queryKey: ['creditsByCourseType'] }); - queryClient.invalidateQueries({ queryKey: ['userAcademicinfo'] }); + queryClient.invalidateQueries({ queryKey: graduationCalculatorQueryKeys.all }); + queryClient.invalidateQueries({ queryKey: authQueryKeys.all }); showToast('success', '수정하신 정보가 적용되었습니다.'); }, diff --git a/src/components/IndexComponents/IndexArticles/index.tsx b/src/components/IndexComponents/IndexArticles/index.tsx index ec038c92d..be7364c20 100644 --- a/src/components/IndexComponents/IndexArticles/index.tsx +++ b/src/components/IndexComponents/IndexArticles/index.tsx @@ -1,13 +1,20 @@ import Link from 'next/link'; +import { useQuery } from '@tanstack/react-query'; +import { articleQueries } from 'api/articles/queries'; import RightArrow from 'assets/svg/right-arrow.svg'; -import useArticles from 'components/Articles/hooks/useArticles'; import { convertArticlesTag } from 'components/Articles/utils/convertArticlesTag'; +import { selectArticlesWithNew } from 'components/Articles/utils/selectArticlesData'; import ROUTES from 'static/routes'; import useLogger from 'utils/hooks/analytics/useLogger'; +import useTokenState from 'utils/hooks/state/useTokenState'; import styles from './IndexArticles.module.scss'; export default function IndexArticles() { - const articlesData = useArticles(); + const token = useTokenState(); + const { data: articlesData } = useQuery({ + ...articleQueries.list(token, '1'), + select: selectArticlesWithNew, + }); const logger = useLogger(); return ( diff --git a/src/components/IndexComponents/IndexClub/ClubMobileViewB/index.tsx b/src/components/IndexComponents/IndexClub/ClubMobileViewB/index.tsx index c2ac040a7..1a279a29a 100644 --- a/src/components/IndexComponents/IndexClub/ClubMobileViewB/index.tsx +++ b/src/components/IndexComponents/IndexClub/ClubMobileViewB/index.tsx @@ -1,10 +1,11 @@ import { useRouter } from 'next/router'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { clubQueries } from 'api/club/queries'; import BookIcon from 'assets/svg/Club/book-icon.svg'; import ExerciseIcon from 'assets/svg/Club/exercise-icon.svg'; import HobbyIcon from 'assets/svg/Club/hobby-icon.svg'; import MikeIcon from 'assets/svg/Club/mike-icon.svg'; import ReligionIcon from 'assets/svg/Club/religion-icon.svg'; -import useClubCategories from 'components/Club/hooks/useClubCategories'; import ROUTES from 'static/routes'; import useLogger from 'utils/hooks/analytics/useLogger'; import useParamsHandler from 'utils/hooks/routing/useParamsHandler'; @@ -12,7 +13,8 @@ import styles from './ClubMobileViewB.module.scss'; function ClubMobileViewB() { const logger = useLogger(); - const clubCategories = useClubCategories(); + const { data } = useSuspenseQuery(clubQueries.categories()); + const clubCategories = data.club_categories; const router = useRouter(); const { searchParams } = useParamsHandler(); const selectedCategoryId = searchParams.get('categoryId') ? Number(searchParams.get('categoryId')) : undefined; diff --git a/src/components/IndexComponents/IndexLostItem/hooks/useLostItemStat.ts b/src/components/IndexComponents/IndexLostItem/hooks/useLostItemStat.ts deleted file mode 100644 index a756b18fe..000000000 --- a/src/components/IndexComponents/IndexLostItem/hooks/useLostItemStat.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getLostItemStat } from 'api/articles'; - -const useLostItemStat = () => { - const { data: lostItemStat } = useSuspenseQuery({ - queryKey: ['lostItemStat'], - queryFn: getLostItemStat, - }); - - return { lostItemStat }; -}; - -export default useLostItemStat; diff --git a/src/components/IndexComponents/IndexLostItem/index.tsx b/src/components/IndexComponents/IndexLostItem/index.tsx index 1763eec53..d711158c1 100644 --- a/src/components/IndexComponents/IndexLostItem/index.tsx +++ b/src/components/IndexComponents/IndexLostItem/index.tsx @@ -1,9 +1,10 @@ import { useEffect, useMemo, useState } from 'react'; import Link from 'next/link'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { articleQueries } from 'api/articles/queries'; import ChevronRightIcon from 'assets/svg/IndexPage/Bus/chevron-right.svg'; import ROUTES from 'static/routes'; import useLogger from 'utils/hooks/analytics/useLogger'; -import useLostItemStat from './hooks/useLostItemStat'; import styles from './IndexLostItem.module.scss'; const SLIDE_INTERVAL = 5000; @@ -11,7 +12,7 @@ const MIN_FOUND_COUNT = 50; function IndexLostItem() { const logger = useLogger(); - const { lostItemStat } = useLostItemStat(); + const { data: lostItemStat } = useSuspenseQuery(articleQueries.lostItemStat()); const [currentIndex, setCurrentIndex] = useState(0); const cardMessages = useMemo(() => { diff --git a/src/components/Room/RoomDetailPage/hooks/useRoomDetail.ts b/src/components/Room/RoomDetailPage/hooks/useRoomDetail.ts index 8f0736722..60e857901 100644 --- a/src/components/Room/RoomDetailPage/hooks/useRoomDetail.ts +++ b/src/components/Room/RoomDetailPage/hooks/useRoomDetail.ts @@ -1,15 +1,8 @@ import { useQuery } from '@tanstack/react-query'; -import { getRoomDetailInfo } from 'api/room'; +import { roomQueries } from 'api/room/queries'; const useRoomDetail = (id: string) => { - const { data: roomDetail } = useQuery({ - queryKey: ['roomDetail', id], - queryFn: async ({ queryKey }) => { - const queryFnParams = queryKey[1]; - - return getRoomDetailInfo(queryFnParams); - }, - }); + const { data: roomDetail } = useQuery(roomQueries.detail(id)); const roomOptions = Object.entries(roomDetail || {}).reduce((acc, [key, val]) => { if (key.startsWith('opt')) { diff --git a/src/components/Room/RoomPage/hooks/useRoomList.ts b/src/components/Room/RoomPage/hooks/useRoomList.ts deleted file mode 100644 index 68030242a..000000000 --- a/src/components/Room/RoomPage/hooks/useRoomList.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { getRoomList } from 'api/room'; - -const useRoomList = () => { - const { data: roomList } = useQuery({ - queryKey: ['roomList'], - queryFn: getRoomList, - }); - - return roomList; -}; - -export default useRoomList; diff --git a/src/components/Store/StoreBenefitPage/hooks/useBenefitCategory.ts b/src/components/Store/StoreBenefitPage/hooks/useBenefitCategory.ts deleted file mode 100644 index 8a4cf61d8..000000000 --- a/src/components/Store/StoreBenefitPage/hooks/useBenefitCategory.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getStoreBenefitCategory } from 'api/store'; - -const useBenefitCategory = () => { - return useSuspenseQuery({ - queryKey: ['benefitCategory'], - queryFn: getStoreBenefitCategory, - select: (data) => data.benefits, - }); -}; - -export default useBenefitCategory; diff --git a/src/components/Store/StoreBenefitPage/hooks/useStoreBenefitList.ts b/src/components/Store/StoreBenefitPage/hooks/useStoreBenefitList.ts deleted file mode 100644 index 8bc0a2bf0..000000000 --- a/src/components/Store/StoreBenefitPage/hooks/useStoreBenefitList.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getStoreBenefitList } from 'api/store'; - -const useStoreBenefitList = (id: string) => { - return useSuspenseQuery({ - queryKey: ['storeBenefit', id], - queryFn: async ({ queryKey }) => { - const queryFnParams = queryKey[1]; - - return getStoreBenefitList(queryFnParams); - }, - select: (data) => { - return { - storeBenefitList: data.shops, - count: data.count, - }; - }, - }); -}; - -export default useStoreBenefitList; diff --git a/src/components/Store/StoreDetailPage/components/EventTable/hooks/useStoreEventList.ts b/src/components/Store/StoreDetailPage/components/EventTable/hooks/useStoreEventList.ts deleted file mode 100644 index 329e02ca0..000000000 --- a/src/components/Store/StoreDetailPage/components/EventTable/hooks/useStoreEventList.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { getStoreEventList } from 'api/store'; - -const useStoreEventList = (id: string) => { - const { data: storeEventList, isError: isStoreEventListError } = useQuery({ - queryKey: ['storeEventList', id], - queryFn: ({ queryKey }) => getStoreEventList(queryKey[1] ?? ''), - }); - - return { - storeEventList: isStoreEventListError ? undefined : storeEventList, - }; -}; - -export default useStoreEventList; diff --git a/src/components/Store/StoreDetailPage/components/EventTable/index.tsx b/src/components/Store/StoreDetailPage/components/EventTable/index.tsx index 3b36fb0fb..8d6df25e6 100644 --- a/src/components/Store/StoreDetailPage/components/EventTable/index.tsx +++ b/src/components/Store/StoreDetailPage/components/EventTable/index.tsx @@ -1,15 +1,16 @@ import Image from 'next/image'; +import { useQuery } from '@tanstack/react-query'; import { StoreEvent } from 'api/store/entity'; +import { storeQueries } from 'api/store/queries'; import EventCard from 'components/Store/StoreDetailPage/components/EventCard'; -import useStoreMenus from './hooks/useStoreEventList'; import styles from './EventTable.module.scss'; export default function EventTable({ id }: { id: string }) { - const { storeEventList } = useStoreMenus(id); + const { data: storeEventList, isError: isStoreEventListError } = useQuery(storeQueries.eventList(id)); return (
- {storeEventList && storeEventList.events.length > 0 ? ( + {!isStoreEventListError && storeEventList && storeEventList.events.length > 0 ? ( storeEventList.events.map((event: StoreEvent) => ) ) : (
diff --git a/src/components/Store/StoreDetailPage/components/Review/components/AverageRating/AverageRating.tsx b/src/components/Store/StoreDetailPage/components/Review/components/AverageRating/AverageRating.tsx index 0d1dcb7a8..0938bd58a 100644 --- a/src/components/Store/StoreDetailPage/components/Review/components/AverageRating/AverageRating.tsx +++ b/src/components/Store/StoreDetailPage/components/Review/components/AverageRating/AverageRating.tsx @@ -1,10 +1,13 @@ +import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; +import { storeQueries } from 'api/store/queries'; import Rating from 'components/Store/StoreDetailPage/components/Review/components/Rating/Rating'; import StarList from 'components/Store/StoreDetailPage/components/Review/components/StarList/StarList'; -import { useGetReview } from 'components/Store/StoreDetailPage/hooks/useGetReview'; +import useTokenState from 'utils/hooks/state/useTokenState'; import styles from './AverageRating.module.scss'; export default function AverageRating({ id }: { id: string }) { - const { data } = useGetReview(Number(id), 'LATEST'); + const token = useTokenState(); + const { data } = useSuspenseInfiniteQuery(storeQueries.reviewFeed({ shopId: Number(id), sorter: 'LATEST', token })); const totalReviewCount = data.pages[0].total_count; const ratingObject = data.pages[0].statistics; diff --git a/src/components/Store/StoreDetailPage/components/Review/components/ReviewList/ReviewList.tsx b/src/components/Store/StoreDetailPage/components/Review/components/ReviewList/ReviewList.tsx index 145159fdb..eec15801e 100644 --- a/src/components/Store/StoreDetailPage/components/Review/components/ReviewList/ReviewList.tsx +++ b/src/components/Store/StoreDetailPage/components/Review/components/ReviewList/ReviewList.tsx @@ -1,4 +1,6 @@ import { useCallback, useDeferredValue, useEffect, useRef, useState } from 'react'; +import { keepPreviousData, useQuery, useSuspenseInfiniteQuery } from '@tanstack/react-query'; +import { storeQueries } from 'api/store/queries'; import ChervronUp from 'assets/svg/chervron-up.svg'; import NoReview from 'assets/svg/Review/no-review.svg'; import LoginRequiredModal from 'components/modal/LoginRequiredModal'; @@ -7,9 +9,8 @@ import { REVEIW_LOGIN } from 'components/Store/StoreDetailPage/components/Review import ReviewCard from 'components/Store/StoreDetailPage/components/Review/components/ReviewCard/ReviewCard'; import StarList from 'components/Store/StoreDetailPage/components/Review/components/StarList/StarList'; import { useDropdown } from 'components/Store/StoreDetailPage/hooks/useDropdown'; -import { useGetMyReview } from 'components/Store/StoreDetailPage/hooks/useGetMyReview'; -import { useGetReview } from 'components/Store/StoreDetailPage/hooks/useGetReview'; import useModalPortal from 'utils/hooks/layout/useModalPortal'; +import useTokenState from 'utils/hooks/state/useTokenState'; import { useUser } from 'utils/hooks/state/useUser'; import styles from './ReviewList.module.scss'; @@ -38,9 +39,20 @@ export default function ReviewList({ id }: { id: string }) { const [currentSortType, setCurrentSortType] = useState(sortType.최신순); const previousSortType = useDeferredValue(currentSortType); const currentSortLabel = typeToLabel[currentSortType]; - const { data, hasNextPage, fetchNextPage } = useGetReview(Number(id), previousSortType); + const token = useTokenState(); + const { data, hasNextPage, fetchNextPage } = useSuspenseInfiniteQuery( + storeQueries.reviewFeed({ + shopId: Number(id), + sorter: previousSortType, + token, + }), + ); const reviews = data.pages.flatMap((page) => page.reviews); - const { data: myReview } = useGetMyReview(id, previousSortType); + const { data: myReview } = useQuery({ + ...storeQueries.myReview(id, previousSortType, token), + enabled: !!token, + placeholderData: keepPreviousData, + }); const [isCheckboxClicked, setIsCheckboxClicked] = useState(false); const selectorRef = useRef(null); const [isSticky, setIsSticky] = useState(false); diff --git a/src/components/Store/StoreDetailPage/components/Review/components/ReviewReporting/query/useReviewReport.ts b/src/components/Store/StoreDetailPage/components/Review/components/ReviewReporting/query/useReviewReport.ts index e9afe3b8b..d17c2104a 100644 --- a/src/components/Store/StoreDetailPage/components/Review/components/ReviewReporting/query/useReviewReport.ts +++ b/src/components/Store/StoreDetailPage/components/Review/components/ReviewReporting/query/useReviewReport.ts @@ -1,7 +1,6 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { postReviewReport } from 'api/store'; -import { ReviewReportRequest } from 'api/store/entity'; +import { storeMutations } from 'api/store/mutations'; import { useKoinToast } from 'utils/hooks/koinToast/useKoinToast'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; @@ -12,11 +11,9 @@ export default function useReviewReport(shopId: string, reviewId: string) { const openToast = useKoinToast(); const { mutate } = useMutation({ - mutationFn: (data: ReviewReportRequest) => postReviewReport(Number(shopId), Number(reviewId), data, token), - onSuccess: () => { - openToast({ message: '해당 리뷰의 신고가 완료되었습니다.' }); - queryClient.invalidateQueries({ queryKey: ['review', Number(shopId)] }); - }, + ...storeMutations.reportReview(queryClient, shopId, reviewId, token, { + onSuccess: () => openToast({ message: '해당 리뷰의 신고가 완료되었습니다.' }), + }), onError: (error) => { if (isKoinError(error)) { if (error.status === 401) showToast('error', '로그인을 해주세요'); diff --git a/src/components/Store/StoreDetailPage/hooks/useDeleteReview.ts b/src/components/Store/StoreDetailPage/hooks/useDeleteReview.ts index e64a581ca..037090142 100644 --- a/src/components/Store/StoreDetailPage/hooks/useDeleteReview.ts +++ b/src/components/Store/StoreDetailPage/hooks/useDeleteReview.ts @@ -1,6 +1,6 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { deleteReview } from 'api/store'; +import { storeMutations } from 'api/store/mutations'; import { useKoinToast } from 'utils/hooks/koinToast/useKoinToast'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; @@ -10,12 +10,9 @@ export const useDeleteReview = (shopId: string, reviewId: number) => { const openToast = useKoinToast(); const queryClient = useQueryClient(); const mutation = useMutation({ - mutationFn: () => deleteReview(reviewId, shopId, token), - onSuccess: () => { - openToast({ message: '리뷰가 삭제되었습니다.' }); - queryClient.invalidateQueries({ queryKey: ['review'] }); - queryClient.invalidateQueries({ queryKey: ['storeDetail', 'storeDetailMenu', 'review', shopId] }); - }, + ...storeMutations.deleteReview(queryClient, reviewId, shopId, token, { + onSuccess: () => openToast({ message: '리뷰가 삭제되었습니다.' }), + }), onError: (e) => { if (isKoinError(e)) { showToast('error', e.message); diff --git a/src/components/Store/StoreDetailPage/hooks/useGetMyReview.ts b/src/components/Store/StoreDetailPage/hooks/useGetMyReview.ts deleted file mode 100644 index fabb81c1b..000000000 --- a/src/components/Store/StoreDetailPage/hooks/useGetMyReview.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { keepPreviousData, useQuery } from '@tanstack/react-query'; -import { getMyReview } from 'api/store'; -import useTokenState from 'utils/hooks/state/useTokenState'; - -export const useGetMyReview = (shopId: string, sorter: string) => { - const token = useTokenState(); - - const { data } = useQuery({ - queryKey: ['review', 'myReview', sorter, shopId], - queryFn: () => getMyReview(shopId, sorter, token), - enabled: !!token, - placeholderData: keepPreviousData, - }); - - return { data }; -}; diff --git a/src/components/Store/StoreDetailPage/hooks/useGetReview.ts b/src/components/Store/StoreDetailPage/hooks/useGetReview.ts deleted file mode 100644 index fae66d262..000000000 --- a/src/components/Store/StoreDetailPage/hooks/useGetReview.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; -import { getReviewList } from 'api/store'; -import useTokenState from 'utils/hooks/state/useTokenState'; - -export const useGetReview = (id: number, sorter: string) => { - const token = useTokenState(); - const { data, hasNextPage, fetchNextPage, isFetchingNextPage } = useSuspenseInfiniteQuery({ - queryKey: ['review', id, sorter], - initialPageParam: 1, - queryFn: ({ pageParam }) => getReviewList(id, pageParam, sorter, token), - getNextPageParam: (last) => { - if (last.total_page > last.current_page) return last.current_page + 1; - return undefined; // 마지막 페이지면 무한 스크롤 중단 - }, - }); - - return { - data, - hasNextPage, - fetchNextPage, - isFetchingNextPage, - }; -}; diff --git a/src/components/Store/StoreDetailPage/hooks/useStoreDetail.ts b/src/components/Store/StoreDetailPage/hooks/useStoreDetail.ts index 7c503c535..3c0c17855 100644 --- a/src/components/Store/StoreDetailPage/hooks/useStoreDetail.ts +++ b/src/components/Store/StoreDetailPage/hooks/useStoreDetail.ts @@ -1,15 +1,8 @@ import { useSuspenseQuery } from '@tanstack/react-query'; -import { getStoreDetailInfo } from 'api/store'; +import { storeQueries } from 'api/store/queries'; const useStoreDetail = (id: string) => { - const { data: storeDetail } = useSuspenseQuery({ - queryKey: ['storeDetail', id], - queryFn: async ({ queryKey }) => { - const queryFnParams = queryKey[1]; - - return getStoreDetailInfo(queryFnParams); - }, - }); + const { data: storeDetail } = useSuspenseQuery(storeQueries.detail(id)); const storeDescription = storeDetail?.description ? storeDetail?.description.replace(/(?:\/)/g, '\n') : '-'; diff --git a/src/components/Store/StoreDetailPage/hooks/useStoreMenus.ts b/src/components/Store/StoreDetailPage/hooks/useStoreMenus.ts deleted file mode 100644 index 6a49cf958..000000000 --- a/src/components/Store/StoreDetailPage/hooks/useStoreMenus.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getStoreDetailMenu } from 'api/store'; - -const useStoreMenus = (params: string) => { - const { data } = useSuspenseQuery({ - queryKey: ['storeDetailMenu', params], - queryFn: async ({ queryKey }) => { - const queryFnParams = queryKey[1]; - - return getStoreDetailMenu(queryFnParams); - }, - }); - - return { data }; -}; - -export default useStoreMenus; diff --git a/src/components/Store/StorePage/components/DesktopStoreList/index.tsx b/src/components/Store/StorePage/components/DesktopStoreList/index.tsx index 70d85ee7b..913939885 100644 --- a/src/components/Store/StorePage/components/DesktopStoreList/index.tsx +++ b/src/components/Store/StorePage/components/DesktopStoreList/index.tsx @@ -1,10 +1,11 @@ import Link from 'next/link'; import { getJosaPicker } from '@bcsdlab/utils'; +import { useSuspenseQuery } from '@tanstack/react-query'; import { StoreListV2 } from 'api/store/entity'; +import { storeQueries } from 'api/store/queries'; import EventIcon from 'assets/svg/event.svg'; import EmptyStar from 'assets/svg/Review/empty-star.svg'; import Star from 'assets/svg/Review/star.svg'; -import { useStoreCategories } from 'components/Store/StorePage/hooks/useCategoryList'; import { getCategoryDurationTime } from 'components/Store/utils/durationTime'; import ROUTES from 'static/routes'; import { StorePageType } from 'static/store'; @@ -30,7 +31,7 @@ export default function DesktopStoreList(storeListProps: StoreListProps) { const pickTopicJosa = getJosaPicker('은'); const { searchParams } = useParamsHandler(); - const { data: categories } = useStoreCategories(); + const { data: categories } = useSuspenseQuery(storeQueries.categories()); const selectedCategory = Number(searchParams.get('category')); const koreanCategory = categories?.shop_categories.find((category) => category.id === selectedCategory)?.name; diff --git a/src/components/Store/StorePage/components/EventCarousel/index.tsx b/src/components/Store/StorePage/components/EventCarousel/index.tsx index dae30014a..91859893c 100644 --- a/src/components/Store/StorePage/components/EventCarousel/index.tsx +++ b/src/components/Store/StorePage/components/EventCarousel/index.tsx @@ -1,9 +1,10 @@ import Image from 'next/image'; import { useRouter } from 'next/router'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { storeQueries } from 'api/store/queries'; import LeftBracket from 'assets/svg/left-angle-bracket.svg'; import RightBracket from 'assets/svg/right-angle-bracket.svg'; import Suspense from 'components/ssr/SSRSuspense'; -import { useGetAllEvents } from 'components/Store/StorePage/components/hooks/useGetAllEvents'; import ROUTES from 'static/routes'; import useLogger from 'utils/hooks/analytics/useLogger'; import useMediaQuery from 'utils/hooks/layout/useMediaQuery'; @@ -66,7 +67,9 @@ function Card({ shop_id, event_id, shop_name, thumbnail_images }: CardProps) { export default function EventCarousel() { const isMobile = useMediaQuery(); - const { events } = useGetAllEvents(); + const { + data: { events }, + } = useSuspenseQuery(storeQueries.allEvents()); const { emblaRef, currentIndex, scrollTo } = useCarouselController(isMobile); if (events.length < 1) return null; diff --git a/src/components/Store/StorePage/components/MobileStoreList/index.tsx b/src/components/Store/StorePage/components/MobileStoreList/index.tsx index faedca513..5fd3b67f4 100644 --- a/src/components/Store/StorePage/components/MobileStoreList/index.tsx +++ b/src/components/Store/StorePage/components/MobileStoreList/index.tsx @@ -1,11 +1,12 @@ import Link from 'next/link'; import { getJosaPicker } from '@bcsdlab/utils'; +import { useSuspenseQuery } from '@tanstack/react-query'; import { StoreListV2 } from 'api/store/entity'; +import { storeQueries } from 'api/store/queries'; import EventIcon from 'assets/svg/event.svg'; import EmptyStar from 'assets/svg/Review/empty-star.svg'; import Star from 'assets/svg/Review/star.svg'; import BenefitRotator from 'components/Store/StorePage/components/BenefitRotator'; -import { useStoreCategories } from 'components/Store/StorePage/hooks/useCategoryList'; import { getCategoryDurationTime } from 'components/Store/utils/durationTime'; import ROUTES from 'static/routes'; import { StorePageType } from 'static/store'; @@ -24,7 +25,7 @@ export default function MobileStoreList(mobileStoreListProps: MobileStoreListPro const pickTopicJosa = getJosaPicker('은'); const { searchParams } = useParamsHandler(); - const { data: categories } = useStoreCategories(); + const { data: categories } = useSuspenseQuery(storeQueries.categories()); const selectedCategory = Number(searchParams.get('category')); const koreanCategory = categories?.shop_categories.find((category) => category.id === selectedCategory)?.name; diff --git a/src/components/Store/StorePage/components/SearchBar/index.tsx b/src/components/Store/StorePage/components/SearchBar/index.tsx index 2d39e57f2..6b61a0ddc 100644 --- a/src/components/Store/StorePage/components/SearchBar/index.tsx +++ b/src/components/Store/StorePage/components/SearchBar/index.tsx @@ -1,8 +1,9 @@ import React, { useState } from 'react'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { storeQueries } from 'api/store/queries'; import MobileSearchIcon from 'assets/svg/mobile-store-search-icon.svg'; import DesktopSearchIcon from 'assets/svg/Store/search-icon.svg'; import SearchBarModal from 'components/Store/StorePage/components/SearchBarModal'; -import { useStoreCategories } from 'components/Store/StorePage/hooks/useCategoryList'; import useLogger from 'utils/hooks/analytics/useLogger'; import useMediaQuery from 'utils/hooks/layout/useMediaQuery'; import useParamsHandler from 'utils/hooks/routing/useParamsHandler'; @@ -10,7 +11,7 @@ import useBooleanState from 'utils/hooks/state/useBooleanState'; import styles from './SearchBar.module.scss'; export default function SearchBar() { - const { data: categories } = useStoreCategories(); + const { data: categories } = useSuspenseQuery(storeQueries.categories()); const { params, searchParams } = useParamsHandler(); const logger = useLogger(); const isMobile = useMediaQuery(); diff --git a/src/components/Store/StorePage/components/SearchBarModal/index.tsx b/src/components/Store/StorePage/components/SearchBarModal/index.tsx index 5eec4c990..2044d3b79 100644 --- a/src/components/Store/StorePage/components/SearchBarModal/index.tsx +++ b/src/components/Store/StorePage/components/SearchBarModal/index.tsx @@ -1,11 +1,12 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useRouter } from 'next/router'; +import { useSuspenseQuery } from '@tanstack/react-query'; import { getRelateSearch } from 'api/store'; import { RelatedSearchResponse } from 'api/store/entity'; +import { storeQueries } from 'api/store/queries'; import MobileSearchIcon from 'assets/svg/mobile-store-search-icon.svg'; import DesktopSearchIcon from 'assets/svg/Store/search-icon.svg'; import RelateSearchItem from 'components/Store/StorePage/components/RelateSearchItem'; -import { useStoreCategories } from 'components/Store/StorePage/hooks/useCategoryList'; import useLogger from 'utils/hooks/analytics/useLogger'; import useMediaQuery from 'utils/hooks/layout/useMediaQuery'; import useParamsHandler from 'utils/hooks/routing/useParamsHandler'; @@ -18,7 +19,7 @@ interface SearchBarModalProps { } export default function SearchBarModal({ onClose }: SearchBarModalProps) { const storeRef = React.useRef(null); - const { data: categories } = useStoreCategories(); + const { data: categories } = useSuspenseQuery(storeQueries.categories()); const [relateSearchItems, setRelateSearchItems] = useState(); const { params, searchParams, setParams } = useParamsHandler(); const logger = useLogger(); diff --git a/src/components/Store/StorePage/components/hooks/useGetAllEvents.ts b/src/components/Store/StorePage/components/hooks/useGetAllEvents.ts deleted file mode 100644 index 270df51c7..000000000 --- a/src/components/Store/StorePage/components/hooks/useGetAllEvents.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getAllEvent } from 'api/store'; - -export const useGetAllEvents = () => { - const { data } = useSuspenseQuery({ - queryKey: ['all-event'], - queryFn: () => getAllEvent(), - }); - - return { events: data.events }; -}; diff --git a/src/components/Store/StorePage/hooks/useCategoryList.ts b/src/components/Store/StorePage/hooks/useCategoryList.ts deleted file mode 100644 index 32c36d73e..000000000 --- a/src/components/Store/StorePage/hooks/useCategoryList.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getStoreCategories } from 'api/store'; - -export const useStoreCategories = () => { - const { data } = useSuspenseQuery({ - queryKey: ['storeCategories'], - queryFn: getStoreCategories, - }); - - return { data }; -}; diff --git a/src/components/Store/StorePage/hooks/useRelateSearch.ts b/src/components/Store/StorePage/hooks/useRelateSearch.ts deleted file mode 100644 index 0a2106d64..000000000 --- a/src/components/Store/StorePage/hooks/useRelateSearch.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { getRelateSearch } from 'api/store'; - -export const useRelateSearch = (query: string) => { - const { data } = useQuery({ - queryKey: ['relateSearch'], - queryFn: () => getRelateSearch(query), - }); - - return { data }; -}; diff --git a/src/components/Store/StoreReviewPage/hooks/useAddStoreReview.ts b/src/components/Store/StoreReviewPage/hooks/useAddStoreReview.ts index ee450dc24..9bcaa59b9 100644 --- a/src/components/Store/StoreReviewPage/hooks/useAddStoreReview.ts +++ b/src/components/Store/StoreReviewPage/hooks/useAddStoreReview.ts @@ -1,7 +1,6 @@ import { isKoinError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { postStoreReview } from 'api/review'; -import { ReviewRequest } from 'api/review/entity'; +import { reviewMutations } from 'api/review/mutations'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; @@ -9,11 +8,7 @@ export const useAddStoreReview = (id: string) => { const token = useTokenState(); const queryClient = useQueryClient(); const { mutate, error } = useMutation({ - mutationFn: (reviewData: ReviewRequest) => postStoreReview(token, id, reviewData), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['storeDetail', 'storeDetailMenu', 'review', id] }); - queryClient.invalidateQueries({ queryKey: ['review'] }); - }, + ...reviewMutations.add(queryClient, token, id), onError: (err) => { if (isKoinError(err)) { showToast('error', err.message || '에러가 발생했습니다.'); diff --git a/src/components/Store/StoreReviewPage/hooks/useEditStoreReview.ts b/src/components/Store/StoreReviewPage/hooks/useEditStoreReview.ts index 7955ecd48..c5ccc521d 100644 --- a/src/components/Store/StoreReviewPage/hooks/useEditStoreReview.ts +++ b/src/components/Store/StoreReviewPage/hooks/useEditStoreReview.ts @@ -1,7 +1,6 @@ import { isKoinError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { putStoreReview } from 'api/review'; -import { ReviewRequest } from 'api/review/entity'; +import { reviewMutations } from 'api/review/mutations'; import { useKoinToast } from 'utils/hooks/koinToast/useKoinToast'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; @@ -11,11 +10,9 @@ export const useEditStoreReview = (shopId: string, reviewId: string) => { const queryClient = useQueryClient(); const openToast = useKoinToast(); const { mutate, error } = useMutation({ - mutationFn: (reviewData: ReviewRequest) => putStoreReview(token, shopId, reviewId, reviewData), - onSuccess: () => { - queryClient.refetchQueries({ queryKey: ['review'] }); - openToast({ message: '리뷰 수정이 완료되었습니다.' }); - }, + ...reviewMutations.edit(queryClient, token, shopId, reviewId, { + onSuccess: () => openToast({ message: '리뷰 수정이 완료되었습니다.' }), + }), onError: (err) => { if (isKoinError(err)) { showToast('error', err.message || '에러가 발생했습니다.'); diff --git a/src/components/Store/StoreReviewPage/hooks/useGetStoreReview.ts b/src/components/Store/StoreReviewPage/hooks/useGetStoreReview.ts deleted file mode 100644 index f03c2f7f9..000000000 --- a/src/components/Store/StoreReviewPage/hooks/useGetStoreReview.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getStoreReview } from 'api/review'; -import useTokenState from 'utils/hooks/state/useTokenState'; - -export const useGetStoreReview = (shopId: string, reviewId: string) => { - const token = useTokenState(); - const { data } = useSuspenseQuery({ - queryKey: ['review', shopId, reviewId], - queryFn: () => getStoreReview(token, shopId, reviewId), - }); - - return data; -}; diff --git a/src/components/TimetablePage/components/MainTimetable/index.tsx b/src/components/TimetablePage/components/MainTimetable/index.tsx index 5034d7374..09d9a8ebd 100644 --- a/src/components/TimetablePage/components/MainTimetable/index.tsx +++ b/src/components/TimetablePage/components/MainTimetable/index.tsx @@ -1,9 +1,10 @@ import React from 'react'; import { useRouter } from 'next/router'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { deptQueries } from 'api/dept/queries'; import DownloadIcon from 'assets/svg/download-icon.svg'; import GraduationIcon from 'assets/svg/graduation-icon.svg'; import EditIcon from 'assets/svg/pen-icon.svg'; -import useDeptList from 'components/Auth/SignupPage/hooks/useDeptList'; import Curriculum from 'components/TimetablePage/components/Curriculum'; import Timetable from 'components/TimetablePage/components/Timetable'; import TotalGrades from 'components/TimetablePage/components/TotalGrades'; @@ -27,7 +28,7 @@ function MainTimetable({ timetableFrameId }: { timetableFrameId: number }) { const router = useRouter(); const { data: timeTableFrameList } = useTimetableFrameList(token, semester); const { myLectures } = useMyLectures(timetableFrameId); - const { data: deptList } = useDeptList(); + const { data: deptList } = useSuspenseQuery(deptQueries.list()); const { data: mySemester } = useSemesterCheck(token); const isSemesterAndTimetableExist = () => { diff --git a/src/components/TimetablePage/hooks/useAddSemester.ts b/src/components/TimetablePage/hooks/useAddSemester.ts index 2736ab60d..48d4399b6 100644 --- a/src/components/TimetablePage/hooks/useAddSemester.ts +++ b/src/components/TimetablePage/hooks/useAddSemester.ts @@ -1,21 +1,15 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { addTimetableFrame } from 'api/timetable'; -import { AddTimetableFrameRequest } from 'api/timetable/entity'; +import { timetableMutations } from 'api/timetable/mutations'; import showToast from 'utils/ts/showToast'; import { useSemester } from 'utils/zustand/semester'; -import { MY_SEMESTER_INFO_KEY } from './useMySemester'; -import { TIMETABLE_FRAME_KEY } from './useTimetableFrameList'; export default function useAddSemester(token: string) { const semester = useSemester(); const queryClient = useQueryClient(); + const mutation = timetableMutations.addSemester(queryClient, token, semester); return useMutation({ - mutationFn: (data: AddTimetableFrameRequest) => addTimetableFrame(data, token), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: [MY_SEMESTER_INFO_KEY] }); - queryClient.invalidateQueries({ queryKey: [TIMETABLE_FRAME_KEY + semester.year + semester.term] }); - }, + ...mutation, onError: (error) => { if (isKoinError(error)) { showToast('error', error.message || '학기 추가에 실패했습니다.'); diff --git a/src/components/TimetablePage/hooks/useAddTimetableFrame.ts b/src/components/TimetablePage/hooks/useAddTimetableFrame.ts index 6c044c410..9dd62b17a 100644 --- a/src/components/TimetablePage/hooks/useAddTimetableFrame.ts +++ b/src/components/TimetablePage/hooks/useAddTimetableFrame.ts @@ -1,20 +1,15 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { addTimetableFrame } from 'api/timetable'; -import { AddTimetableFrameRequest } from 'api/timetable/entity'; +import { timetableMutations } from 'api/timetable/mutations'; import showToast from 'utils/ts/showToast'; import { useSemester } from 'utils/zustand/semester'; -import { TIMETABLE_FRAME_KEY } from './useTimetableFrameList'; export default function useAddTimetableFrame(token: string) { const queryClient = useQueryClient(); const semester = useSemester(); + const mutation = timetableMutations.addFrame(queryClient, token, semester); return useMutation({ - mutationFn: (data: AddTimetableFrameRequest) => addTimetableFrame(data, token), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: [TIMETABLE_FRAME_KEY + semester.year + semester.term] }); - }, - + ...mutation, onError: (error) => { if (isKoinError(error)) { showToast('error', error.message || '시간표 프레임 추가에 실패했습니다.'); diff --git a/src/components/TimetablePage/hooks/useAddTimetableLectureCustom.ts b/src/components/TimetablePage/hooks/useAddTimetableLectureCustom.ts index 229635c67..a089d352c 100644 --- a/src/components/TimetablePage/hooks/useAddTimetableLectureCustom.ts +++ b/src/components/TimetablePage/hooks/useAddTimetableLectureCustom.ts @@ -1,17 +1,13 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { addTimetableLectureCustom } from 'api/timetable'; -import { AddTimetableLectureCustomRequest } from 'api/timetable/entity'; +import { timetableMutations } from 'api/timetable/mutations'; import showToast from 'utils/ts/showToast'; -import { TIMETABLE_INFO_LIST } from './useTimetableInfoList'; export default function useAddTimetableLectureCustom(token: string) { const queryClient = useQueryClient(); + const mutation = timetableMutations.addLectureCustom(queryClient, token); return useMutation({ - mutationFn: (data: AddTimetableLectureCustomRequest) => addTimetableLectureCustom(data, token), - onSuccess: (data, variables) => { - queryClient.setQueryData([TIMETABLE_INFO_LIST, variables.timetable_frame_id], data); - }, + ...mutation, onError: (error) => { if (isKoinError(error)) { if (error.status === 401) showToast('error', '로그인을 해주세요'); diff --git a/src/components/TimetablePage/hooks/useAddTimetableLectureRegular.ts b/src/components/TimetablePage/hooks/useAddTimetableLectureRegular.ts index d2644248a..82f595950 100644 --- a/src/components/TimetablePage/hooks/useAddTimetableLectureRegular.ts +++ b/src/components/TimetablePage/hooks/useAddTimetableLectureRegular.ts @@ -1,16 +1,13 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { addTimetableLectureRegular } from 'api/timetable'; +import { timetableMutations } from 'api/timetable/mutations'; import showToast from 'utils/ts/showToast'; -import { TIMETABLE_INFO_LIST } from './useTimetableInfoList'; export default function useAddTimetableLectureRegular(token: string) { const queryClient = useQueryClient(); + const mutation = timetableMutations.addLectureRegular(queryClient, token); return useMutation({ - mutationFn: (data: Parameters[0]) => addTimetableLectureRegular(data, token), - onSuccess: (data, variables) => { - queryClient.setQueryData([TIMETABLE_INFO_LIST, variables.timetable_frame_id], data); - }, + ...mutation, onError: (error) => { if (isKoinError(error)) { if (error.status === 401) showToast('error', '로그인을 해주세요'); diff --git a/src/components/TimetablePage/hooks/useAllMyLectures.ts b/src/components/TimetablePage/hooks/useAllMyLectures.ts index 3628147c2..433d4e411 100644 --- a/src/components/TimetablePage/hooks/useAllMyLectures.ts +++ b/src/components/TimetablePage/hooks/useAllMyLectures.ts @@ -1,12 +1,8 @@ import { useSuspenseQuery } from '@tanstack/react-query'; -import { getTimetableAllLectureInfo } from 'api/timetable'; +import { timetableQueries } from 'api/timetable/queries'; export default function useAllMyLectures(token: string) { - const { data } = useSuspenseQuery({ - queryKey: ['allLectures'], - - queryFn: () => (token ? getTimetableAllLectureInfo(token) : null), - }); + const { data } = useSuspenseQuery(timetableQueries.allLectures(token)); return data ? data.timetable : null; } diff --git a/src/components/TimetablePage/hooks/useDeleteSemester.ts b/src/components/TimetablePage/hooks/useDeleteSemester.ts index 8b977c40b..2a8fee279 100644 --- a/src/components/TimetablePage/hooks/useDeleteSemester.ts +++ b/src/components/TimetablePage/hooks/useDeleteSemester.ts @@ -1,22 +1,19 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { deleteSemester } from 'api/timetable'; import { Semester } from 'api/timetable/entity'; +import { timetableMutations } from 'api/timetable/mutations'; import useToast from 'components/feedback/Toast/useToast'; import showToast from 'utils/ts/showToast'; -import { MY_SEMESTER_INFO_KEY } from './useMySemester'; -import { TIMETABLE_FRAME_KEY } from './useTimetableFrameList'; export default function useDeleteSemester(token: string, semester: Semester) { const queryClient = useQueryClient(); const slicedSemester = `${semester.year} ${semester.term}`; const toast = useToast(); + const mutation = timetableMutations.deleteSemester(queryClient, token, semester); return useMutation({ - mutationFn: () => deleteSemester(token, semester), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: [MY_SEMESTER_INFO_KEY] }); - queryClient.invalidateQueries({ queryKey: [TIMETABLE_FRAME_KEY + semester.year + semester.term] }); - queryClient.invalidateQueries({ queryKey: ['creditsByCourseType'] }); + ...mutation, + onSuccess: async (...args) => { + await mutation.onSuccess?.(...args); toast.open({ message: `선택하신 [${slicedSemester}]가 삭제되었습니다.`, }); diff --git a/src/components/TimetablePage/hooks/useDeleteTimetableFrame.ts b/src/components/TimetablePage/hooks/useDeleteTimetableFrame.ts index fbf2ea016..40db7e157 100644 --- a/src/components/TimetablePage/hooks/useDeleteTimetableFrame.ts +++ b/src/components/TimetablePage/hooks/useDeleteTimetableFrame.ts @@ -1,16 +1,11 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { deleteTimetableFrame } from 'api/timetable'; import { TimetableFrameInfo } from 'api/timetable/entity'; +import { timetableMutations } from 'api/timetable/mutations'; import useToast from 'components/feedback/Toast/useToast'; import showToast from 'utils/ts/showToast'; import { useSemester } from 'utils/zustand/semester'; import useRollbackTimetableFrame from './useRollbackTimetableFrame'; -import { TIMETABLE_FRAME_KEY } from './useTimetableFrameList'; - -type DeleteTimetableFrameProps = { - id: number; -}; export default function useDeleteTimetableFrame(token: string, frameInfo: TimetableFrameInfo) { const queryClient = useQueryClient(); @@ -18,12 +13,12 @@ export default function useDeleteTimetableFrame(token: string, frameInfo: Timeta const semester = useSemester(); const { mutate: rollbackFrame } = useRollbackTimetableFrame(token); const recoverFrame = () => rollbackFrame(frameInfo.id!); + const mutation = timetableMutations.deleteFrame(queryClient, token, semester); return useMutation({ - mutationFn: ({ id }: DeleteTimetableFrameProps) => deleteTimetableFrame(token, id), - - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: [TIMETABLE_FRAME_KEY + semester.year + semester.term] }); + ...mutation, + onSuccess: async (...args) => { + await mutation.onSuccess?.(...args); toast.open({ message: `선택하신 [${frameInfo.name}]이 삭제되었습니다.`, recoverMessage: `[${frameInfo.name}]이 복구되었습니다.`, diff --git a/src/components/TimetablePage/hooks/useDeleteTimetableLecture.ts b/src/components/TimetablePage/hooks/useDeleteTimetableLecture.ts index 17fb6871f..a9cfbba6a 100644 --- a/src/components/TimetablePage/hooks/useDeleteTimetableLecture.ts +++ b/src/components/TimetablePage/hooks/useDeleteTimetableLecture.ts @@ -1,19 +1,14 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { deleteTimetableLecture } from 'api/timetable'; +import { timetableMutations } from 'api/timetable/mutations'; import { toast } from 'react-toastify'; -import { TIMETABLE_INFO_LIST } from './useTimetableInfoList'; export default function useDeleteTimetableLecture(authorization: string) { const queryClient = useQueryClient(); + const mutation = timetableMutations.deleteLecture(queryClient, authorization); return useMutation({ - mutationFn: (id: number) => deleteTimetableLecture(authorization, id), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: [TIMETABLE_INFO_LIST] }); - queryClient.invalidateQueries({ queryKey: ['generalEducation'] }); - queryClient.invalidateQueries({ queryKey: ['creditsByCourseType'] }); - }, + ...mutation, onError: (error) => { if (isKoinError(error)) { if (error.status === 401) toast('로그인을 해주세요'); diff --git a/src/components/TimetablePage/hooks/useEditTimetableLectureCustom.ts b/src/components/TimetablePage/hooks/useEditTimetableLectureCustom.ts index 9d59e2e30..323d40223 100644 --- a/src/components/TimetablePage/hooks/useEditTimetableLectureCustom.ts +++ b/src/components/TimetablePage/hooks/useEditTimetableLectureCustom.ts @@ -1,25 +1,16 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { editTimetableLectureCustom } from 'api/timetable'; -import { TimetableCustomLecture } from 'api/timetable/entity'; +import { timetableMutations } from 'api/timetable/mutations'; import showToast from 'utils/ts/showToast'; -import { TIMETABLE_INFO_LIST } from './useTimetableInfoList'; export default function useEditTimetableLectureCustom() { const queryClient = useQueryClient(); + const mutation = timetableMutations.editLectureCustom(queryClient); return useMutation({ - mutationFn: ({ - timetableFrameId, - editedLecture, - token, - }: { - timetableFrameId: number; - editedLecture: TimetableCustomLecture; - token: string; - }) => editTimetableLectureCustom({ timetable_frame_id: timetableFrameId, timetable_lecture: editedLecture }, token), - onSuccess: (data, variables) => { - queryClient.setQueryData([TIMETABLE_INFO_LIST, variables.timetableFrameId], data); + ...mutation, + onSuccess: async (...args) => { + await mutation.onSuccess?.(...args); showToast('success', '강의 수정이 되었습니다.'); }, onError: (error) => { diff --git a/src/components/TimetablePage/hooks/useEditTimetableLectureRegular.ts b/src/components/TimetablePage/hooks/useEditTimetableLectureRegular.ts index 2611a4718..ed5223532 100644 --- a/src/components/TimetablePage/hooks/useEditTimetableLectureRegular.ts +++ b/src/components/TimetablePage/hooks/useEditTimetableLectureRegular.ts @@ -1,29 +1,16 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { editTimetableLectureRegular } from 'api/timetable'; -import { TimetableRegularLecture } from 'api/timetable/entity'; +import { timetableMutations } from 'api/timetable/mutations'; import showToast from 'utils/ts/showToast'; -import { TIMETABLE_INFO_LIST } from './useTimetableInfoList'; export default function useEditTimetableLectureRegular() { const queryClient = useQueryClient(); + const mutation = timetableMutations.editLectureRegular(queryClient); return useMutation({ - mutationFn: ({ - timetableFrameId, - editedLecture, - token, - }: { - timetableFrameId: number; - editedLecture: TimetableRegularLecture; - token: string; - }) => - editTimetableLectureRegular({ timetable_frame_id: timetableFrameId, timetable_lecture: editedLecture }, token), - onSuccess: (data, variables) => { - queryClient.setQueryData([TIMETABLE_INFO_LIST, variables.timetableFrameId], data); - queryClient.invalidateQueries({ queryKey: ['creditsByCourseType'] }); - queryClient.invalidateQueries({ queryKey: ['generalEducation'] }); - queryClient.invalidateQueries({ queryKey: ['allLectures'] }); + ...mutation, + onSuccess: async (...args) => { + await mutation.onSuccess?.(...args); showToast('success', '강의 수정이 되었습니다.'); }, onError: (error) => { diff --git a/src/components/TimetablePage/hooks/useGetMultiMajorLecture.ts b/src/components/TimetablePage/hooks/useGetMultiMajorLecture.ts index e252390bd..81aedcead 100644 --- a/src/components/TimetablePage/hooks/useGetMultiMajorLecture.ts +++ b/src/components/TimetablePage/hooks/useGetMultiMajorLecture.ts @@ -1,12 +1,9 @@ import { useSuspenseQuery } from '@tanstack/react-query'; -import { getTimetableAllLectureInfo } from 'api/timetable'; +import { timetableQueries } from 'api/timetable/queries'; export default function useGetMultiMajorLecture(token: string) { return useSuspenseQuery({ - queryKey: ['allLectures'], - - queryFn: () => (token ? getTimetableAllLectureInfo(token) : null), - + ...timetableQueries.allLectures(token), select: (data) => (data ? data.timetable.filter((item) => item.course_type === '다전공') : null), }); } diff --git a/src/components/TimetablePage/hooks/useLectureList.ts b/src/components/TimetablePage/hooks/useLectureList.ts index 3eb2ee840..2dc732da4 100644 --- a/src/components/TimetablePage/hooks/useLectureList.ts +++ b/src/components/TimetablePage/hooks/useLectureList.ts @@ -1,14 +1,9 @@ import { useSuspenseQuery } from '@tanstack/react-query'; -import { getLectureList } from 'api/timetable'; import { Semester } from 'api/timetable/entity'; - -const SEMESTER_INFO_KEY = 'lecture'; +import { timetableQueries } from 'api/timetable/queries'; const useLectureList = (semesterKey: Semester) => { - const { data } = useSuspenseQuery({ - queryKey: [SEMESTER_INFO_KEY, semesterKey], - queryFn: () => (semesterKey ? getLectureList(semesterKey) : null), - }); + const { data } = useSuspenseQuery(timetableQueries.lectureList(semesterKey)); return { data }; }; diff --git a/src/components/TimetablePage/hooks/useMySemester.ts b/src/components/TimetablePage/hooks/useMySemester.ts index ba4c7d73f..190ba6113 100644 --- a/src/components/TimetablePage/hooks/useMySemester.ts +++ b/src/components/TimetablePage/hooks/useMySemester.ts @@ -1,15 +1,10 @@ import { useSuspenseQuery } from '@tanstack/react-query'; -import { getMySemester } from 'api/timetable'; +import { timetableQueries } from 'api/timetable/queries'; import { useTokenStore } from 'utils/zustand/auth'; -export const MY_SEMESTER_INFO_KEY = 'my_semester'; - function useSemesterCheck(token: string) { const { userType } = useTokenStore(); - const { data } = useSuspenseQuery({ - queryKey: [MY_SEMESTER_INFO_KEY], - queryFn: () => (token && userType === 'STUDENT' ? getMySemester(token) : null), - }); + const { data } = useSuspenseQuery(timetableQueries.mySemester(token, { userType })); return { data }; } diff --git a/src/components/TimetablePage/hooks/useRollbackLecture.ts b/src/components/TimetablePage/hooks/useRollbackLecture.ts index 8c733a65a..a415eb922 100644 --- a/src/components/TimetablePage/hooks/useRollbackLecture.ts +++ b/src/components/TimetablePage/hooks/useRollbackLecture.ts @@ -1,20 +1,14 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { rollbackTimetableLecture } from 'api/timetable'; -import { RollbackTimetableLectureRequest } from 'api/timetable/entity'; +import { timetableMutations } from 'api/timetable/mutations'; import showToast from 'utils/ts/showToast'; -import { TIMETABLE_INFO_LIST } from './useTimetableInfoList'; export default function useRollbackLecture(token: string, timetableFrameId: number) { const queryClient = useQueryClient(); + const mutation = timetableMutations.rollbackLecture(queryClient, token, timetableFrameId); return useMutation({ - mutationFn: (id: RollbackTimetableLectureRequest) => rollbackTimetableLecture(id, token), - - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: [TIMETABLE_INFO_LIST, timetableFrameId] }); - }, - + ...mutation, onError: (error) => { if (isKoinError(error)) { showToast('error', error.message || '강의 복구에 실패했습니다.'); diff --git a/src/components/TimetablePage/hooks/useRollbackTimetableFrame.ts b/src/components/TimetablePage/hooks/useRollbackTimetableFrame.ts index e99a84322..9be71227b 100644 --- a/src/components/TimetablePage/hooks/useRollbackTimetableFrame.ts +++ b/src/components/TimetablePage/hooks/useRollbackTimetableFrame.ts @@ -1,21 +1,16 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { rollbackTimetableFrame } from 'api/timetable'; +import { timetableMutations } from 'api/timetable/mutations'; import showToast from 'utils/ts/showToast'; import { useSemester } from 'utils/zustand/semester'; -import { TIMETABLE_FRAME_KEY } from './useTimetableFrameList'; export default function useRollbackTimetableFrame(token: string) { const queryClient = useQueryClient(); const semester = useSemester(); + const mutation = timetableMutations.rollbackFrame(queryClient, token, semester); return useMutation({ - mutationFn: (timetableFrameId: number) => rollbackTimetableFrame(token, timetableFrameId), - - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: [TIMETABLE_FRAME_KEY + semester.year + semester.term] }); - }, - + ...mutation, onError: (error) => { if (isKoinError(error)) { showToast('error', error.message || '시간표 프레임 복구에 실패했습니다.'); diff --git a/src/components/TimetablePage/hooks/useSemesterOptionList.ts b/src/components/TimetablePage/hooks/useSemesterOptionList.ts index f69d0a40f..a60e6384a 100644 --- a/src/components/TimetablePage/hooks/useSemesterOptionList.ts +++ b/src/components/TimetablePage/hooks/useSemesterOptionList.ts @@ -1,30 +1,19 @@ -import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query'; -import { getSemesterInfoList } from 'api/timetable'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { timetableQueries } from 'api/timetable/queries'; import useTokenState from 'utils/hooks/state/useTokenState'; -import useSemesterCheck, { MY_SEMESTER_INFO_KEY } from './useMySemester'; - -export const SEMESTER_INFO_KEY = 'semester'; +import useSemesterCheck from './useMySemester'; export const useSemester = () => { - const { data } = useSuspenseQuery({ - queryKey: [SEMESTER_INFO_KEY], - queryFn: getSemesterInfoList, - }); + const { data } = useSuspenseQuery(timetableQueries.semesterInfo()); return data ?? []; }; const useSemesterOptionList = () => { const token = useTokenState(); - const queryClient = useQueryClient(); const allSemesters = useSemester(); const { data: mySemesterList } = useSemesterCheck(token); - - if (mySemesterList === null) { - queryClient.invalidateQueries({ queryKey: [MY_SEMESTER_INFO_KEY] }); - } - - const semesterList = token ? mySemesterList?.semesters : allSemesters; + const semesterList = mySemesterList?.semesters ?? allSemesters; const semesterOptionList = (semesterList ?? []).map((semesterInfo) => ({ label: `${semesterInfo.year}년 ${semesterInfo.term}`, diff --git a/src/components/TimetablePage/hooks/useTimetableFrameList.ts b/src/components/TimetablePage/hooks/useTimetableFrameList.ts index 8c05dd38e..ca749c778 100644 --- a/src/components/TimetablePage/hooks/useTimetableFrameList.ts +++ b/src/components/TimetablePage/hooks/useTimetableFrameList.ts @@ -1,26 +1,11 @@ import { useSuspenseQuery } from '@tanstack/react-query'; -import { getTimetableFrame } from 'api/timetable'; import { Semester } from 'api/timetable/entity'; +import { timetableQueries } from 'api/timetable/queries'; import { useTokenStore } from 'utils/zustand/auth'; -export const TIMETABLE_FRAME_KEY = 'timetable_frame'; - function useTimetableFrameList(token: string, semester: Semester) { const { userType } = useTokenStore(); - const { data } = useSuspenseQuery({ - queryKey: [TIMETABLE_FRAME_KEY + semester.year + semester.term], - queryFn: async () => { - if (token && userType === 'STUDENT') { - try { - return await getTimetableFrame(token, semester); - } catch { - return [{ id: null, name: '기본 시간표', is_main: true }]; - } - } else { - return [{ id: null, name: '기본 시간표', is_main: true }]; - } - }, - }); + const { data } = useSuspenseQuery(timetableQueries.frameList(token, semester, { fallbackOnError: true, userType })); return { data }; } diff --git a/src/components/TimetablePage/hooks/useTimetableInfoList.ts b/src/components/TimetablePage/hooks/useTimetableInfoList.ts index 44628b6c1..240d95d25 100644 --- a/src/components/TimetablePage/hooks/useTimetableInfoList.ts +++ b/src/components/TimetablePage/hooks/useTimetableInfoList.ts @@ -1,24 +1,6 @@ import { useSuspenseQuery } from '@tanstack/react-query'; -import { getTimetableLectureInfo } from 'api/timetable'; import { TimetableLectureInfoResponse, MyLectureInfo } from 'api/timetable/entity'; -import { KoinError } from 'interfaces/APIError'; - -export const TIMETABLE_INFO_LIST = 'TIMETABLE_INFO_LIST'; - -type QueryFunction = { - authorization?: string; - timetableFrameId?: number; -}; - -function queryFunction({ - authorization, - timetableFrameId, -}: QueryFunction): () => Promise { - if (authorization && timetableFrameId) { - return () => getTimetableLectureInfo(authorization, timetableFrameId); - } - return () => Promise.resolve(null); -} +import { timetableQueries } from 'api/timetable/queries'; interface UseTimetableInfoListParams { authorization: string; @@ -26,10 +8,9 @@ interface UseTimetableInfoListParams { } function useTimetableInfoList({ authorization, timetableFrameId }: UseTimetableInfoListParams) { - const { data } = useSuspenseQuery({ - queryKey: [TIMETABLE_INFO_LIST, timetableFrameId], - queryFn: queryFunction({ authorization, timetableFrameId }), - select: (rawData) => rawData?.timetable || [], + const { data } = useSuspenseQuery({ + ...timetableQueries.lectureInfo(authorization, timetableFrameId), + select: (rawData: TimetableLectureInfoResponse | null): MyLectureInfo[] => rawData?.timetable || [], }); return { data }; diff --git a/src/components/TimetablePage/hooks/useTotalGrades.ts b/src/components/TimetablePage/hooks/useTotalGrades.ts index 9301656d1..fa14626fa 100644 --- a/src/components/TimetablePage/hooks/useTotalGrades.ts +++ b/src/components/TimetablePage/hooks/useTotalGrades.ts @@ -1,16 +1,12 @@ import { useSuspenseQuery } from '@tanstack/react-query'; -import { getTimetableLectureInfo } from 'api/timetable'; +import { timetableQueries } from 'api/timetable/queries'; import useTokenState from 'utils/hooks/state/useTokenState'; -import { TIMETABLE_INFO_LIST } from './useTimetableInfoList'; export default function useTotalGrades(timetableFrameId: number) { const token = useTokenState(); return useSuspenseQuery({ - queryKey: [TIMETABLE_INFO_LIST, timetableFrameId], - - queryFn: () => (token ? getTimetableLectureInfo(token, timetableFrameId) : null), - + ...timetableQueries.lectureInfo(token, timetableFrameId), select: (data) => data?.total_grades, }); } diff --git a/src/components/TimetablePage/hooks/useUpdateTimetableFrame.ts b/src/components/TimetablePage/hooks/useUpdateTimetableFrame.ts index 06066f7b7..5d4af7595 100644 --- a/src/components/TimetablePage/hooks/useUpdateTimetableFrame.ts +++ b/src/components/TimetablePage/hooks/useUpdateTimetableFrame.ts @@ -1,23 +1,18 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { editTimetableFrame } from 'api/timetable'; -import { TimetableFrameInfo } from 'api/timetable/entity'; +import { timetableMutations } from 'api/timetable/mutations'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; import { useSemester } from 'utils/zustand/semester'; -import { TIMETABLE_FRAME_KEY } from './useTimetableFrameList'; export default function useUpdateTimetableFrame() { const token = useTokenState(); const queryClient = useQueryClient(); const semester = useSemester(); + const mutation = timetableMutations.updateFrame(queryClient, token, semester); const mutate = useMutation({ - mutationFn: (frameInfo: TimetableFrameInfo) => - editTimetableFrame(token, frameInfo.id!, { name: frameInfo.name, is_main: frameInfo.is_main }), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: [TIMETABLE_FRAME_KEY + semester.year + semester.term] }); - }, + ...mutation, onError: (error) => { if (isKoinError(error)) { if (error.status === 400) showToast('error', error.message || '올바른 값을 입력해주세요.'); diff --git a/src/components/TimetablePage/hooks/useVersionInfo.ts b/src/components/TimetablePage/hooks/useVersionInfo.ts index 7a94de537..b829d68b0 100644 --- a/src/components/TimetablePage/hooks/useVersionInfo.ts +++ b/src/components/TimetablePage/hooks/useVersionInfo.ts @@ -1,11 +1,8 @@ import { useSuspenseQuery } from '@tanstack/react-query'; -import { getVersion } from 'api/timetable'; +import { timetableQueries } from 'api/timetable/queries'; const useVersionInfo = () => { - const { data } = useSuspenseQuery({ - queryKey: ['timetable'], - queryFn: () => getVersion('timetable'), - }); + const { data } = useSuspenseQuery(timetableQueries.version('timetable')); return { data, diff --git a/src/components/cafeteria/MobileCafeteriaPage/index.tsx b/src/components/cafeteria/MobileCafeteriaPage/index.tsx index b702cc9a7..1595c5475 100644 --- a/src/components/cafeteria/MobileCafeteriaPage/index.tsx +++ b/src/components/cafeteria/MobileCafeteriaPage/index.tsx @@ -1,12 +1,13 @@ import { useEffect, useRef } from 'react'; import { useRouter } from 'next/router'; import { cn } from '@bcsdlab/utils'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { coopshopQueries } from 'api/coopshop/queries'; import { DiningType } from 'api/dinings/entity'; import ArrowBackNewIcon from 'assets/svg/arrow-back-new.svg'; import InformationIcon from 'assets/svg/common/information/information-icon-white.svg'; import StoreCtaIcon from 'assets/svg/Store/store-cta-icon.svg'; import CafeteriaInfo from 'components/cafeteria/components/CafeteriaInfo'; -import useCoopshopCafeteria from 'components/cafeteria/hooks/useCoopshopCafeteria'; import { DINING_TYPES, DINING_TYPE_MAP } from 'static/cafeteria'; import useLogger from 'utils/hooks/analytics/useLogger'; import { useSessionLogger } from 'utils/hooks/analytics/useSessionLogger'; @@ -26,7 +27,7 @@ export default function MobileCafeteriaPage({ diningType, setDiningType }: Mobil const logger = useLogger(); const router = useRouter(); const sessionLogger = useSessionLogger(); - const { cafeteriaInfo } = useCoopshopCafeteria(); + const { data: cafeteriaInfo } = useSuspenseQuery(coopshopQueries.cafeteriaInfo()); const lastLoggedDiningTypeRef = useRef(null); const [isCafeteriaInfoOpen, openCafeteriaInfo, closeCafeteriaInfo] = useBooleanState(false); const setButtonContent = useHeaderButtonStore((state) => state.setButtonContent); diff --git a/src/components/cafeteria/PCCafeteriaPage/components/DateNavigator/index.tsx b/src/components/cafeteria/PCCafeteriaPage/components/DateNavigator/index.tsx index 232368f3f..eb519b5d6 100644 --- a/src/components/cafeteria/PCCafeteriaPage/components/DateNavigator/index.tsx +++ b/src/components/cafeteria/PCCafeteriaPage/components/DateNavigator/index.tsx @@ -1,9 +1,10 @@ import { cn } from '@bcsdlab/utils'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { coopshopQueries } from 'api/coopshop/queries'; import InformationIcon from 'assets/svg/common/information/information-icon-grey.svg'; import LeftArrow from 'assets/svg/left-angle-bracket.svg'; import RightArrow from 'assets/svg/right-angle-bracket.svg'; import CafeteriaInfo from 'components/cafeteria/components/CafeteriaInfo'; -import useCoopshopCafeteria from 'components/cafeteria/hooks/useCoopshopCafeteria'; import { useDatePicker } from 'components/cafeteria/hooks/useDatePicker'; import useModalPortal from 'utils/hooks/layout/useModalPortal'; import styles from './DateNavigator.module.scss'; @@ -41,7 +42,7 @@ const generateWeek = (today: Date) => { export default function DateNavigator() { const { currentDate, checkToday, checkPast, setPrevWeek, setNextWeek, setToday, setDate } = useDatePicker(); const portalManager = useModalPortal(); - const { cafeteriaInfo } = useCoopshopCafeteria(); + const { data: cafeteriaInfo } = useSuspenseQuery(coopshopQueries.cafeteriaInfo()); const thisWeek = generateWeek(currentDate()); diff --git a/src/components/cafeteria/hooks/useCoopshopCafeteria.ts b/src/components/cafeteria/hooks/useCoopshopCafeteria.ts deleted file mode 100644 index 6c0607a1c..000000000 --- a/src/components/cafeteria/hooks/useCoopshopCafeteria.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getCafeteriaInfo } from 'api/coopshop'; - -const COOPSHOP_CAFETERIA_KEY = 'COOPSHOP_CAFETERIA_KEY'; - -function useCoopshopCafeteria() { - const { data: cafeteriaInfo } = useSuspenseQuery({ - queryKey: [COOPSHOP_CAFETERIA_KEY], - queryFn: () => getCafeteriaInfo(), - }); - return { cafeteriaInfo }; -} - -export default useCoopshopCafeteria; diff --git a/src/components/cafeteria/hooks/useDinings.ts b/src/components/cafeteria/hooks/useDinings.ts index c8c9837e7..4f4c2eebf 100644 --- a/src/components/cafeteria/hooks/useDinings.ts +++ b/src/components/cafeteria/hooks/useDinings.ts @@ -1,19 +1,17 @@ import { useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'; -import { cancelCafeteriaDiningLike, getCafeteriaDinings, likeCafeteriaDining } from 'api/cafeteria'; +import { cafeteriaMutations } from 'api/cafeteria/mutations'; +import { cafeteriaQueries } from 'api/cafeteria/queries'; import { Dining, OriginalDining } from 'api/dinings/entity'; import { convertDateToSimpleString } from 'components/cafeteria/utils/time'; import useTokenState from 'utils/hooks/state/useTokenState'; -const DININGS_KEY = 'DININGS_KEY'; - function useDinings(date: Date) { const convertedDate = convertDateToSimpleString(date); const queryClient = useQueryClient(); const token = useTokenState(); const { data: dinings } = useSuspenseQuery({ - queryKey: [DININGS_KEY, convertedDate], - queryFn: async () => getCafeteriaDinings(convertedDate), + ...cafeteriaQueries.dinings(convertedDate), select: (data) => { if ('status' in data || !Array.isArray(data)) { return []; @@ -24,19 +22,15 @@ function useDinings(date: Date) { })) as Array; }, }); + const likeMutation = cafeteriaMutations.likeDining(queryClient, token, convertedDate); + const cancelLikeMutation = cafeteriaMutations.cancelLikeDining(queryClient, token, convertedDate); const likeDiningMutation = useMutation({ - mutationFn: async (diningId: number) => likeCafeteriaDining(diningId, token), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: [DININGS_KEY, convertedDate] }); - }, + ...likeMutation, }); const cancelLikeDiningMutation = useMutation({ - mutationFn: async (diningId: number) => cancelCafeteriaDiningLike(diningId, token), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: [DININGS_KEY, convertedDate] }); - }, + ...cancelLikeMutation, }); const likeDining = (diningId: number, isLike: boolean) => { diff --git a/src/components/ui/Banner/hooks/useBannerCategories.ts b/src/components/ui/Banner/hooks/useBannerCategories.ts deleted file mode 100644 index 615d27d09..000000000 --- a/src/components/ui/Banner/hooks/useBannerCategories.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getBannerCategoryList } from 'api/banner'; - -const useBannerCategories = () => { - const { data } = useSuspenseQuery({ - queryKey: ['bannerCategory'], - queryFn: () => getBannerCategoryList(), - }); - - return data.banner_categories; -}; - -export default useBannerCategories; diff --git a/src/components/ui/Banner/hooks/useBanners.ts b/src/components/ui/Banner/hooks/useBanners.ts deleted file mode 100644 index 372a8e402..000000000 --- a/src/components/ui/Banner/hooks/useBanners.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getBanners } from 'api/banner'; - -const useBanners = (categoryId: number) => { - const { data } = useSuspenseQuery({ - queryKey: ['banners', categoryId], - queryFn: () => getBanners(categoryId), - }); - return { data }; -}; - -export default useBanners; diff --git a/src/pages/articles/index.tsx b/src/pages/articles/index.tsx index 7dd93316b..5c1eef17b 100644 --- a/src/pages/articles/index.tsx +++ b/src/pages/articles/index.tsx @@ -1,15 +1,16 @@ import { useMemo } from 'react'; import type { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next'; import { useRouter } from 'next/router'; -import { dehydrate, QueryClient } from '@tanstack/react-query'; -import { articles as articlesApi } from 'api/index'; +import { dehydrate, keepPreviousData, QueryClient, useQuery } from '@tanstack/react-query'; +import { articleQueries } from 'api/articles/queries'; import ArticlesPageLayout from 'components/Articles/ArticlesPage'; import ArticleList from 'components/Articles/components/ArticleList'; import ArticlesHeader from 'components/Articles/components/ArticlesHeader'; import Pagination from 'components/Articles/components/Pagination'; -import useArticles from 'components/Articles/hooks/useArticles'; +import { selectArticlesWithNew } from 'components/Articles/utils/selectArticlesData'; import { SSRLayout } from 'components/layout'; import useMount from 'utils/hooks/state/useMount'; +import useTokenState from 'utils/hooks/state/useTokenState'; import { parseServerSideParams } from 'utils/ts/parseServerSideParams'; export const getServerSideProps = async (context: GetServerSidePropsContext) => { @@ -19,21 +20,10 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => const queryClient = new QueryClient(); const prefetchPromises = [ - queryClient.prefetchQuery({ - queryKey: ['hotArticles'], - queryFn: articlesApi.getHotArticles, - }), + queryClient.prefetchQuery(articleQueries.hot()), + queryClient.prefetchQuery(articleQueries.list(token ?? '', pageNumber)), ]; - if (token) { - prefetchPromises.push( - queryClient.prefetchQuery({ - queryKey: ['articles', pageNumber], - queryFn: () => articlesApi.getArticles(token, pageNumber), - }), - ); - } - await Promise.all(prefetchPromises); return { @@ -56,8 +46,13 @@ function usePageParams(initialPage: string) { } export default function ArticleListPage({ initialPage }: InferGetServerSidePropsType) { + const token = useTokenState(); const paramsPage = usePageParams(initialPage); - const articlesData = useArticles(paramsPage); + const { data: articlesData } = useQuery({ + ...articleQueries.list(token, paramsPage), + placeholderData: keepPreviousData, + select: selectArticlesWithNew, + }); const articles = articlesData?.articles ?? []; const paginationInfo = articlesData?.paginationInfo ?? { diff --git a/src/pages/auth/modifyinfo/index.tsx b/src/pages/auth/modifyinfo/index.tsx index 59a7ec8cb..9797c244c 100644 --- a/src/pages/auth/modifyinfo/index.tsx +++ b/src/pages/auth/modifyinfo/index.tsx @@ -4,8 +4,10 @@ import React, { Suspense, useEffect, useImperativeHandle, useReducer, useState } from 'react'; import { useRouter } from 'next/router'; import { cn, sha256 } from '@bcsdlab/utils'; -import { useQueryClient } from '@tanstack/react-query'; +import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query'; import { UserUpdateRequest, UserResponse, GeneralUserUpdateRequest } from 'api/auth/entity'; +import { authQueryKeys } from 'api/auth/queries'; +import { deptQueries } from 'api/dept/queries'; import BlindIcon from 'assets/svg/blind-icon.svg'; import ChevronLeft from 'assets/svg/Login/chevron-left.svg'; import CorrectIcon from 'assets/svg/Login/correct.svg'; @@ -22,7 +24,6 @@ import { } from 'components/Auth/ModifyInfoPage/hooks/useValidationContext'; import { passwordValidationReducer } from 'components/Auth/ModifyInfoPage/reducers/passwordReducer'; import CustomSelector from 'components/Auth/SignupPage/components/CustomSelector'; -import useDeptList from 'components/Auth/SignupPage/hooks/useDeptList'; import useNicknameDuplicateCheck from 'components/Auth/SignupPage/hooks/useNicknameDuplicateCheck'; import LoadingSpinner from 'components/feedback/LoadingSpinner'; import Layout from 'components/layout'; @@ -513,7 +514,7 @@ const NicknameForm = React.forwardRef((props, ref) => { const { data: userInfo } = useUser(); - const { data: deptList } = useDeptList(); + const { data: deptList } = useSuspenseQuery(deptQueries.list()); // ✅ 안전 기본값 const [studentNumber, setStudentNumber] = useState(''); @@ -1129,13 +1130,12 @@ const NameForm = React.forwardRef { const queryClient = useQueryClient(); const router = useRouter(); - const token = useTokenState(); const isMobile = useMediaQuery(); const onSuccess = () => { localStorage.setItem(STORAGE_KEY.USER_INFO_COMPLETION, COMPLETION_STATUS.COMPLETED); router.push(ROUTES.Main()); showToast('success', '성공적으로 정보를 수정하였습니다.'); - queryClient.invalidateQueries({ queryKey: ['userInfo', token] }); + queryClient.invalidateQueries({ queryKey: authQueryKeys.all }); }; const { userType } = useTokenStore(); const isStudent = userType === 'STUDENT'; diff --git a/src/pages/benefitstore/index.tsx b/src/pages/benefitstore/index.tsx index 59bbf87aa..fda5e7572 100644 --- a/src/pages/benefitstore/index.tsx +++ b/src/pages/benefitstore/index.tsx @@ -1,10 +1,8 @@ import { Suspense, useEffect } from 'react'; import { GetServerSidePropsContext } from 'next'; import { cn } from '@bcsdlab/utils'; -import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'; -import { getStoreBenefitCategory, getStoreBenefitList, getStoreCategories } from 'api/store'; -import useBenefitCategory from 'components/Store/StoreBenefitPage/hooks/useBenefitCategory'; -import useStoreBenefitList from 'components/Store/StoreBenefitPage/hooks/useStoreBenefitList'; +import { dehydrate, HydrationBoundary, QueryClient, useSuspenseQuery, type DehydratedState } from '@tanstack/react-query'; +import { storeQueries } from 'api/store/queries'; import DesktopStoreList from 'components/Store/StorePage/components/DesktopStoreList'; import EventCarousel from 'components/Store/StorePage/components/EventCarousel'; import MobileStoreList from 'components/Store/StorePage/components/MobileStoreList'; @@ -26,25 +24,12 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }; } - await queryClient.prefetchQuery({ - queryKey: ['benefitCategory'], - queryFn: getStoreBenefitCategory, - }); - - await queryClient.prefetchQuery({ - queryKey: ['storeBenefit', categoryId], - queryFn: async ({ queryKey }) => { - const queryFnParams = queryKey[1]; + await queryClient.prefetchQuery(storeQueries.benefitCategory()); - return getStoreBenefitList(queryFnParams ?? '1'); - }, - }); + await queryClient.prefetchQuery(storeQueries.benefitList(categoryId)); // StoreList 페이지에서 사용하는 API - await queryClient.prefetchQuery({ - queryKey: ['storeCategories'], - queryFn: getStoreCategories, - }); + await queryClient.prefetchQuery(storeQueries.categories()); return { props: { @@ -57,10 +42,19 @@ function StoreBenefit() { const { params, searchParams, setParams } = useParamsHandler(); const isMobile = useMediaQuery(); const logger = useLogger(); - const { data } = useStoreBenefitList(params?.category ?? '1'); + const { data } = useSuspenseQuery({ + ...storeQueries.benefitList(params?.category ?? '1'), + select: (benefitListData) => ({ + storeBenefitList: benefitListData.shops, + count: benefitListData.count, + }), + }); const { count, storeBenefitList } = data; const selectedCategory = Number(searchParams.get('category')) ?? 1; - const { data: benefitCategory } = useBenefitCategory(); + const { data: benefitCategory } = useSuspenseQuery({ + ...storeQueries.benefitCategory(), + select: (benefitCategoryData) => benefitCategoryData.benefits, + }); useEffect(() => { initializeCategoryEntryTime(); @@ -139,9 +133,9 @@ function StoreBenefit() { ); } -export default function StoreBenefitPage({ dehydrateState }: { dehydrateState: unknown }) { +export default function StoreBenefitPage({ dehydratedState }: { dehydratedState: DehydratedState }) { return ( - + {/* TODO: Loading 디자인 추가 요청 */} diff --git a/src/pages/bus/city/index.tsx b/src/pages/bus/city/index.tsx index e67cf634f..02c74b4bc 100644 --- a/src/pages/bus/city/index.tsx +++ b/src/pages/bus/city/index.tsx @@ -1,11 +1,12 @@ import { useMemo, useState } from 'react'; import { cn } from '@bcsdlab/utils'; -import { DirectionType } from 'api/bus/entity'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { CityInfo, DirectionType } from 'api/bus/entity'; +import { busQueries } from 'api/bus/queries'; import BusCoursePage from 'components/Bus/BusCoursePage'; import Template from 'components/Bus/BusCoursePage/components/ExternalTemplate'; import InfoFooter from 'components/Bus/BusCoursePage/components/InfoFooter'; import useBusPrefetch from 'components/Bus/BusCoursePage/hooks/useBusPrefetch'; -import { useCityBusTimetable } from 'components/Bus/BusCoursePage/hooks/useBusTimetable'; import dayjs from 'dayjs'; import { CITY_COURSES, CITY_COURSES_MAP } from 'static/bus'; import useLogger from 'utils/hooks/analytics/useLogger'; @@ -32,9 +33,15 @@ export default function CityBusTimetable() { const selectedDirection = CITY_COURSES_MAP.get(`${selectedBusNumber}-${selectedDirectionType}`)?.direction ?? ''; - const timetable = useCityBusTimetable({ - bus_number: selectedBusNumber, - direction: selectedDirection, + const { data: timetable } = useSuspenseQuery({ + ...busQueries.cityTimetable({ + bus_number: selectedBusNumber, + direction: selectedDirection, + }), + select: (response) => ({ + info: response as CityInfo, + type: 'city' as const, + }), }); const handleBusNumberButton = (busNum: number) => { diff --git a/src/pages/bus/express/index.tsx b/src/pages/bus/express/index.tsx index 7cf729352..88a1b23f8 100644 --- a/src/pages/bus/express/index.tsx +++ b/src/pages/bus/express/index.tsx @@ -1,10 +1,11 @@ import { useState } from 'react'; import { cn } from '@bcsdlab/utils'; +import { useQuery } from '@tanstack/react-query'; +import { busQueries } from 'api/bus/queries'; import BusCoursePage from 'components/Bus/BusCoursePage'; import Template from 'components/Bus/BusCoursePage/components/ExternalTemplate'; import InfoFooter from 'components/Bus/BusCoursePage/components/InfoFooter'; import useBusPrefetch from 'components/Bus/BusCoursePage/hooks/useBusPrefetch'; -import { useExpressTimetable } from 'components/Bus/BusCoursePage/hooks/useBusTimetable'; import dayjs from 'dayjs'; import { EXPRESS_COURSES } from 'static/bus'; import useLogger from 'utils/hooks/analytics/useLogger'; @@ -22,7 +23,7 @@ export default function ExpressBusTimetable() { const [selectedCourseId, setSelectedCourseId] = useState(0); const [destinationCategory, setDestinationCategory] = useState('병천방면'); - const { data: timetable, isLoading } = useExpressTimetable(EXPRESS_COURSES[selectedCourseId]); + const { data: timetable, isLoading } = useQuery(busQueries.expressTimetable(EXPRESS_COURSES[selectedCourseId])); const prefetchBusTimetable = useBusPrefetch(); const logger = useLogger(); diff --git a/src/pages/bus/shuttle/[routeId].tsx b/src/pages/bus/shuttle/[routeId].tsx index 6c6fb2a51..6b7723d72 100644 --- a/src/pages/bus/shuttle/[routeId].tsx +++ b/src/pages/bus/shuttle/[routeId].tsx @@ -1,11 +1,12 @@ import React, { useState } from 'react'; import { useRouter } from 'next/router'; import { cn } from '@bcsdlab/utils'; +import { useQuery } from '@tanstack/react-query'; +import { busQueries } from 'api/bus/queries'; import BusIcon from 'assets/svg/Bus/bus-icon-32x32.svg'; import InformationIcon from 'assets/svg/Bus/info-gray.svg'; import BusCoursePage from 'components/Bus/BusCoursePage'; import { ShuttleCategoryTabs } from 'components/Bus/BusCoursePage/components/ShuttleCategoryTabs'; -import useShuttleTimetableDetail from 'components/Bus/BusCoursePage/hooks/useShuttleTimetableDetail'; import useLogger from 'utils/hooks/analytics/useLogger'; import useMediaQuery from 'utils/hooks/layout/useMediaQuery'; import styles from './ShuttleDetailPage.module.scss'; @@ -18,9 +19,11 @@ export default function ShuttleDetailPage() { const router = useRouter(); const { routeId } = router.query; - const { shuttleTimetableDetail } = useShuttleTimetableDetail( - routeId ? (Array.isArray(routeId) ? routeId[0] : routeId) : null, - ); + const shuttleTimetableId = routeId ? (Array.isArray(routeId) ? routeId[0] : routeId) : null; + const { data: shuttleTimetableDetail } = useQuery({ + ...busQueries.shuttleTimetableDetail(shuttleTimetableId), + staleTime: 1000 * 60 * 10, + }); const [selectedDetail, setSelectedDetail] = useState(null); diff --git a/src/pages/bus/shuttle/index.tsx b/src/pages/bus/shuttle/index.tsx index 6fa51a4b6..1e9430b68 100644 --- a/src/pages/bus/shuttle/index.tsx +++ b/src/pages/bus/shuttle/index.tsx @@ -1,21 +1,20 @@ import React from 'react'; import { GetServerSideProps } from 'next'; import { useRouter } from 'next/router'; -import { dehydrate, QueryClient } from '@tanstack/react-query'; -import { getShuttleCourseInfo } from 'api/bus'; +import { dehydrate, QueryClient, useQuery, useSuspenseQuery } from '@tanstack/react-query'; +import { busQueries } from 'api/bus/queries'; import InformationIcon from 'assets/svg/Bus/info-gray.svg'; import RightArrow from 'assets/svg/right-arrow.svg'; import BusCoursePage, { useBusCourse } from 'components/Bus/BusCoursePage'; import InfoFooter from 'components/Bus/BusCoursePage/components/InfoFooter'; import { ShuttleCategoryTabs } from 'components/Bus/BusCoursePage/components/ShuttleCategoryTabs'; import useBusPrefetch from 'components/Bus/BusCoursePage/hooks/useBusPrefetch'; -import { useClientShuttleTimetable } from 'components/Bus/BusCoursePage/hooks/useBusTimetable'; -import useShuttleCourse from 'components/Bus/BusCoursePage/hooks/useShuttleCourse'; import { SSRLayout } from 'components/layout'; import dayjs from 'dayjs'; import { BUS_FEEDBACK_FORM, SHUTTLE_COURSES } from 'static/bus'; import useLogger from 'utils/hooks/analytics/useLogger'; import useMediaQuery from 'utils/hooks/layout/useMediaQuery'; +import useMount from 'utils/hooks/state/useMount'; import styles from './ShuttleBusTimetable.module.scss'; interface TemplateShuttleVersionProps { @@ -37,8 +36,7 @@ export const getServerSideProps: GetServerSideProps = async () => { const queryClient = new QueryClient(); await queryClient.prefetchQuery({ - queryKey: ['bus', 'courses', 'shuttle'], - queryFn: getShuttleCourseInfo, + ...busQueries.shuttleCourse(), staleTime: 1000 * 60 * 10, }); @@ -56,10 +54,14 @@ export default function ShuttleBusTimetable() { const logger = useLogger(); const { isMobile } = useBusCourse(); + const isMount = useMount(); - const { shuttleCourse } = useShuttleCourse(); + const { data: shuttleCourse } = useSuspenseQuery(busQueries.shuttleCourse()); - const { data: timetable } = useClientShuttleTimetable(SHUTTLE_COURSES[0]); + const { data: timetable } = useQuery({ + ...busQueries.shuttleTimetable(SHUTTLE_COURSES[0]), + enabled: isMount, + }); const displaySemester = shuttleCourse.semester_info.name; const updatedAt = timetable?.updated_at; diff --git a/src/pages/cafeteria/index.tsx b/src/pages/cafeteria/index.tsx index 64ca8b14a..c09b584f7 100644 --- a/src/pages/cafeteria/index.tsx +++ b/src/pages/cafeteria/index.tsx @@ -1,8 +1,8 @@ import { useEffect, useState } from 'react'; import { GetServerSidePropsContext } from 'next'; import { dehydrate, DehydratedState, HydrationBoundary, QueryClient } from '@tanstack/react-query'; -import { getCafeteriaDinings } from 'api/cafeteria'; -import { getCafeteriaInfo } from 'api/coopshop'; +import { cafeteriaQueries } from 'api/cafeteria/queries'; +import { coopshopQueries } from 'api/coopshop/queries'; import { DiningType } from 'api/dinings/entity'; import { useDatePicker } from 'components/cafeteria/hooks/useDatePicker'; import MobileCafeteriaPage from 'components/cafeteria/MobileCafeteriaPage'; @@ -23,18 +23,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { const convertedDate = convertDateToSimpleString(currentDate); - await queryClient.prefetchQuery({ - queryKey: ['DININGS_KEY', convertedDate], - queryFn: async () => { - const data = await getCafeteriaDinings(convertedDate); - return data; - }, - }); + await queryClient.prefetchQuery(cafeteriaQueries.dinings(convertedDate)); - await queryClient.prefetchQuery({ - queryKey: ['COOPSHOP_CAFETERIA_KEY'], - queryFn: () => getCafeteriaInfo(), - }); + await queryClient.prefetchQuery(coopshopQueries.cafeteriaInfo()); return { props: { diff --git a/src/pages/callvan/[postId]/participants/index.tsx b/src/pages/callvan/[postId]/participants/index.tsx index f5d279f38..a297cbccd 100644 --- a/src/pages/callvan/[postId]/participants/index.tsx +++ b/src/pages/callvan/[postId]/participants/index.tsx @@ -2,7 +2,7 @@ import { Suspense, useEffect } from 'react'; import type { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next'; import { useRouter } from 'next/router'; import { dehydrate, QueryClient } from '@tanstack/react-query'; -import { getCallvanPostDetail } from 'api/callvan'; +import { callvanQueries } from 'api/callvan/queries'; import ParticipantsList from 'components/Callvan/components/ParticipantsList'; import ROUTES from 'static/routes'; import useMediaQuery from 'utils/hooks/layout/useMediaQuery'; @@ -20,10 +20,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => try { if (token) { - await queryClient.prefetchQuery({ - queryKey: ['callvanPostDetail', postId], - queryFn: () => getCallvanPostDetail(token, postId), - }); + await queryClient.prefetchQuery(callvanQueries.postDetail(token, postId)); } } catch (error) { console.error('[SSR] callvan post detail prefetch failed:', error); diff --git a/src/pages/callvan/index.tsx b/src/pages/callvan/index.tsx index d99219bc8..ebf367acf 100644 --- a/src/pages/callvan/index.tsx +++ b/src/pages/callvan/index.tsx @@ -1,17 +1,16 @@ import { useEffect, useMemo, useState } from 'react'; import type { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next'; import { useRouter } from 'next/router'; -import { dehydrate, QueryClient } from '@tanstack/react-query'; -import { getCallvanList, getCallvanNotifications } from 'api/callvan'; +import { dehydrate, QueryClient, useInfiniteQuery } from '@tanstack/react-query'; import { CallvanListRequest } from 'api/callvan/entity'; +import { callvanQueries, callvanQueryKeys } from 'api/callvan/queries'; import CallvanList from 'components/Callvan/components/CallvanList'; import CallvanPageLayout from 'components/Callvan/components/CallvanPageLayout'; -import useCallvanInfiniteList from 'components/Callvan/hooks/useCallvanInfiniteList'; -import { CALLVAN_NOTIFICATIONS_QUERY_KEY } from 'components/Callvan/hooks/useCallvanNotifications'; import { CallvanParams, parseCallvanQuery } from 'components/Callvan/utils/callvanQuery'; import ROUTES from 'static/routes'; import useMediaQuery from 'utils/hooks/layout/useMediaQuery'; import useMount from 'utils/hooks/state/useMount'; +import useTokenState from 'utils/hooks/state/useTokenState'; import useInfiniteScroll from 'utils/hooks/ui/useInfiniteScroll'; import { parseServerSideParams } from 'utils/ts/parseServerSideParams'; import listStyles from 'components/Callvan/components/CallvanList/CallvanList.module.scss'; @@ -46,22 +45,10 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => try { await Promise.all([ - queryClient.prefetchInfiniteQuery({ - queryKey: ['callvanInfiniteList', apiParams], - queryFn: ({ pageParam = 1 }) => - getCallvanList(token ?? '', { - ...apiParams, - page: pageParam, - limit: 10, - }), - initialPageParam: 1, - }), + queryClient.prefetchInfiniteQuery(callvanQueries.infiniteList(token ?? '', apiParams)), token - ? queryClient.prefetchQuery({ - queryKey: [...CALLVAN_NOTIFICATIONS_QUERY_KEY], - queryFn: () => getCallvanNotifications(token), - }) - : queryClient.setQueryData([...CALLVAN_NOTIFICATIONS_QUERY_KEY], []), + ? queryClient.prefetchQuery(callvanQueries.notifications(token)) + : queryClient.setQueryData(callvanQueryKeys.notifications, []), ]); } catch (error) { console.error('[SSR] callvan prefetch failed:', error); @@ -111,9 +98,13 @@ interface CallvanContentProps { function CallvanContent({ params }: CallvanContentProps) { const [searchTitle, setSearchTitle] = useState(params.title); + const token = useTokenState(); const apiParams = toCallvanApiParams(params); - const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useCallvanInfiniteList(apiParams); + const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({ + ...callvanQueries.infiniteList(token ?? '', apiParams), + enabled: !!token, + }); const posts = useMemo(() => data?.pages.flatMap((page) => page.posts) ?? [], [data]); diff --git a/src/pages/callvan/notifications/index.tsx b/src/pages/callvan/notifications/index.tsx index 791c323c0..404c25107 100644 --- a/src/pages/callvan/notifications/index.tsx +++ b/src/pages/callvan/notifications/index.tsx @@ -1,23 +1,21 @@ import { useCallback, useEffect, useState } from 'react'; import type { GetServerSidePropsContext } from 'next'; import { useRouter } from 'next/router'; -import { dehydrate, QueryClient } from '@tanstack/react-query'; -import { getCallvanNotifications } from 'api/callvan'; +import { dehydrate, QueryClient, useQuery } from '@tanstack/react-query'; +import { callvanQueries, callvanQueryKeys } from 'api/callvan/queries'; import ArrowBackIcon from 'assets/svg/Callvan/arrow-back.svg'; import ThreeDotsIcon from 'assets/svg/Callvan/three-dots.svg'; import DeleteConfirmModal from 'components/Callvan/components/DeleteConfirmModal'; import NotificationCard from 'components/Callvan/components/NotificationCard'; import NotificationDropdown from 'components/Callvan/components/NotificationDropdown'; import NotificationEmptyState from 'components/Callvan/components/NotificationEmptyState'; -import useCallvanNotifications, { - CALLVAN_NOTIFICATIONS_QUERY_KEY, -} from 'components/Callvan/hooks/useCallvanNotifications'; import useDeleteAllNotifications from 'components/Callvan/hooks/useDeleteAllNotifications'; import useMarkAllNotificationsRead from 'components/Callvan/hooks/useMarkAllNotificationsRead'; import useMarkNotificationRead from 'components/Callvan/hooks/useMarkNotificationRead'; import ROUTES from 'static/routes'; import useMediaQuery from 'utils/hooks/layout/useMediaQuery'; import useMount from 'utils/hooks/state/useMount'; +import useTokenState from 'utils/hooks/state/useTokenState'; import { parseServerSideParams } from 'utils/ts/parseServerSideParams'; import styles from './CallvanNotifications.module.scss'; @@ -27,12 +25,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => try { if (token) { - await queryClient.prefetchQuery({ - queryKey: [...CALLVAN_NOTIFICATIONS_QUERY_KEY], - queryFn: () => getCallvanNotifications(token), - }); + await queryClient.prefetchQuery(callvanQueries.notifications(token)); } else { - queryClient.setQueryData([...CALLVAN_NOTIFICATIONS_QUERY_KEY], []); + queryClient.setQueryData(callvanQueryKeys.notifications, []); } } catch (error) { console.error('[SSR] callvan notifications prefetch failed:', error); @@ -49,6 +44,7 @@ export default function CallvanNotificationsPage() { const router = useRouter(); const isMobile = useMediaQuery(); const mounted = useMount(); + const token = useTokenState(); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); @@ -59,7 +55,10 @@ export default function CallvanNotificationsPage() { } }, [mounted, isMobile, router]); - const { data: notifications } = useCallvanNotifications(); + const { data: notifications } = useQuery({ + ...callvanQueries.notifications(token ?? ''), + enabled: !!token, + }); const { mutate: markAllRead } = useMarkAllNotificationsRead(); const { mutate: markRead } = useMarkNotificationRead(); const { mutate: deleteAll } = useDeleteAllNotifications({ diff --git a/src/pages/campusinfo/index.tsx b/src/pages/campusinfo/index.tsx index 1c4fa685f..b2d8212c7 100644 --- a/src/pages/campusinfo/index.tsx +++ b/src/pages/campusinfo/index.tsx @@ -1,5 +1,6 @@ import { cn } from '@bcsdlab/utils'; -import useCampusInfo from 'components/CampusInfo/hooks/useCampusInfo'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { coopshopQueries } from 'api/coopshop/queries'; import Book from 'components/CampusInfo/svg/book.svg'; import Cafe from 'components/CampusInfo/svg/cafe.svg'; import Cut from 'components/CampusInfo/svg/cut.svg'; @@ -56,7 +57,7 @@ const formatDateRange = (fromDate: string, toDate: string) => { }; function CampusInfo() { - const { campusInfo } = useCampusInfo(); + const { data: campusInfo } = useSuspenseQuery(coopshopQueries.allShopInfo()); const cafeteriaInfo = campusInfo?.coop_shops.find((shop) => shop.name === '학생식당'); const filteredCampusInfo = campusInfo?.coop_shops.filter((shop) => shop.name !== '학생식당'); diff --git a/src/pages/clubs/[id]/index.tsx b/src/pages/clubs/[id]/index.tsx index 21e15098e..f0f8b674c 100644 --- a/src/pages/clubs/[id]/index.tsx +++ b/src/pages/clubs/[id]/index.tsx @@ -2,10 +2,9 @@ import { useEffect, useState } from 'react'; import type { GetServerSidePropsContext } from 'next'; import Image from 'next/image'; import { useRouter } from 'next/router'; -import { isKoinError } from '@bcsdlab/koin'; import { cn } from '@bcsdlab/utils'; -import { dehydrate, QueryClient } from '@tanstack/react-query'; -import { getClubDetail, getClubEventDetail, getRecruitmentClub } from 'api/club'; +import { dehydrate, QueryClient, useSuspenseQuery } from '@tanstack/react-query'; +import { clubQueries } from 'api/club/queries'; import BellIcon from 'assets/svg/Club/bell-icon.svg'; import OffBellIcon from 'assets/svg/Club/bell-off-icon.svg'; import CopyIcon from 'assets/svg/Club/copy-icon.svg'; @@ -22,7 +21,6 @@ import MandateClubManagerModal from 'components/Club/ClubDetailPage/components/M import useClubDetail from 'components/Club/ClubDetailPage/hooks/useClubdetail'; import useClubLikeMutation from 'components/Club/ClubDetailPage/hooks/useClubLike'; import useClubRecruitmentNotification from 'components/Club/ClubDetailPage/hooks/useClubNotification'; -import useClubRecruitment from 'components/Club/ClubDetailPage/hooks/useClubRecruitment'; import useDeleteEvent from 'components/Club/ClubDetailPage/hooks/useDeleteEvent'; import useDeleteRecruitment from 'components/Club/ClubDetailPage/hooks/useDeleteRecruitment'; import EditConfirmModal from 'components/Club/ClubEditPage/conponents/EditConfirmModal'; @@ -39,7 +37,6 @@ import { formatPhoneNumber } from 'utils/ts/formatPhoneNumber'; import { parseServerSideParams } from 'utils/ts/parseServerSideParams'; import showToast from 'utils/ts/showToast'; import { useHeaderTitle } from 'utils/zustand/customTitle'; -import type { ClubRecruitmentResponse } from 'api/club/entity'; import styles from './ClubDetailPage.module.scss'; export const NO_SELECTED_EVENT_ID = -1; @@ -61,17 +58,6 @@ const TAB: Record = { 'Q&A': 'qna', }; -const EMPTY_RECRUITMENT: ClubRecruitmentResponse = { - id: 0, - status: 'NONE', - dday: 0, - start_date: '', - end_date: '', - image_url: '', - content: '', - is_manager: false, -}; - export const getServerSideProps = async (context: GetServerSidePropsContext) => { const { params, query } = context; const { token } = parseServerSideParams(context); @@ -96,32 +82,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => const queryClient = new QueryClient(); await Promise.all([ - queryClient.prefetchQuery({ - queryKey: ['clubDetail', clubId], - queryFn: () => getClubDetail(token ?? '', clubId), - }), - - queryClient.prefetchQuery({ - queryKey: ['clubRecruitment', clubId], - queryFn: async () => { - try { - const data = await getRecruitmentClub(clubId); - return data; - } catch (e) { - if (isKoinError(e) && e.status === 404) { - return EMPTY_RECRUITMENT; - } - throw e; - } - }, - }), + queryClient.prefetchQuery(clubQueries.detail(clubId, token)), + queryClient.prefetchQuery(clubQueries.recruitment(clubId)), ]); if (initialTab === 'event' && numericEventId !== NO_SELECTED_EVENT_ID) { - await queryClient.prefetchQuery({ - queryKey: ['clubEventDetail', clubId, numericEventId], - queryFn: () => getClubEventDetail(clubId, numericEventId), - }); + await queryClient.prefetchQuery(clubQueries.eventDetail(clubId, numericEventId)); } return { @@ -147,7 +113,7 @@ export default function ClubDetailPage({ initialClubId, initialTab, initialEvent const navigate = (path: string) => router.push(path); const { clubDetail, clubIntroductionEditStatus } = useClubDetail(initialClubId); - const { clubRecruitmentData } = useClubRecruitment(initialClubId); + const { data: clubRecruitmentData } = useSuspenseQuery(clubQueries.recruitment(initialClubId)); const { mutateAsync: deleteRecruitment } = useDeleteRecruitment(); const { mutateAsync: deleteEvent } = useDeleteEvent(); diff --git a/src/pages/clubs/index.tsx b/src/pages/clubs/index.tsx index bcba80460..d18f56cb5 100644 --- a/src/pages/clubs/index.tsx +++ b/src/pages/clubs/index.tsx @@ -2,8 +2,8 @@ import type { GetServerSidePropsContext, InferGetServerSidePropsType } from 'nex import Image from 'next/image'; import { useRouter } from 'next/router'; import { cn } from '@bcsdlab/utils'; -import { dehydrate, QueryClient } from '@tanstack/react-query'; -import { getClubList, getClubCategories } from 'api/club'; +import { dehydrate, QueryClient, useQuery, useSuspenseQuery } from '@tanstack/react-query'; +import { clubQueries } from 'api/club/queries'; import BookIcon from 'assets/svg/Club/book-icon.svg'; import ExerciseIcon from 'assets/svg/Club/exercise-icon.svg'; import HeartFilled from 'assets/svg/Club/heart-filled-icon.svg'; @@ -12,9 +12,7 @@ import HobbyIcon from 'assets/svg/Club/hobby-icon.svg'; import MikeIcon from 'assets/svg/Club/mike-icon.svg'; import ReligionIcon from 'assets/svg/Club/religion-icon.svg'; import ClubSearchContainer from 'components/Club/ClubListPage/components/ClubSearchContainer'; -import useClubCategories from 'components/Club/hooks/useClubCategories'; import useClubLike from 'components/Club/hooks/useClubLike'; -import useClubList from 'components/Club/hooks/useClubList'; import { SSRLayout } from 'components/layout'; import LoginRequiredModal from 'components/modal/LoginRequiredModal'; import { Portal } from 'components/modal/Modal/PortalProvider'; @@ -80,15 +78,16 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => const queryClient = new QueryClient(); await Promise.all([ - queryClient.prefetchQuery({ - queryKey: ['club-categories'], - queryFn: () => getClubCategories(), - }), - queryClient.prefetchQuery({ - queryKey: ['club-list', params.categoryId, params.sortType, params.isRecruiting, params.clubName], - queryFn: () => - getClubList(token, params.categoryId ?? undefined, params.sortType, params.isRecruiting, params.clubName), - }), + queryClient.prefetchQuery(clubQueries.categories()), + queryClient.prefetchQuery( + clubQueries.list({ + token, + categoryId: params.categoryId ?? undefined, + sortType: params.sortType, + isRecruiting: params.isRecruiting, + clubName: params.clubName, + }), + ), ]); return { @@ -122,15 +121,19 @@ function ClubListPage({ initialQuery, serverToken }: InferGetServerSidePropsType ? Number(searchParams.get('categoryId')) : (initialQuery.categoryId ?? undefined); - const clubCategories = useClubCategories(); + const { data: clubCategoryData } = useSuspenseQuery(clubQueries.categories()); + const clubCategories = clubCategoryData.club_categories; const { mutate: clubLikeMutate } = useClubLike(); - const clubList = useClubList({ - token, - categoryId: selectedCategoryId, - sortType: sortValue, - isRecruiting: isRecruitingParam, - clubName: clubName, - }); + const { data: clubListData } = useQuery( + clubQueries.list({ + token, + categoryId: selectedCategoryId, + sortType: sortValue, + isRecruiting: isRecruitingParam, + clubName, + }), + ); + const clubList = clubListData?.clubs ?? []; const totalCount = clubList.length; const [isAuthModalOpen, openAuthModal, closeAuthModal] = useBooleanState(false); diff --git a/src/pages/clubs/recruitment/edit/[id]/index.tsx b/src/pages/clubs/recruitment/edit/[id]/index.tsx index a737d846e..eeba3e538 100644 --- a/src/pages/clubs/recruitment/edit/[id]/index.tsx +++ b/src/pages/clubs/recruitment/edit/[id]/index.tsx @@ -1,8 +1,9 @@ import { useState } from 'react'; import { useRouter } from 'next/router'; +import { useSuspenseQuery } from '@tanstack/react-query'; import { ClubRecruitment } from 'api/club/entity'; +import { clubQueries } from 'api/club/queries'; import useClubDetail from 'components/Club/ClubDetailPage/hooks/useClubdetail'; -import useClubRecruitment from 'components/Club/ClubDetailPage/hooks/useClubRecruitment'; import usePutClubRecruitment from 'components/Club/ClubRecruitmentEditPage/hooks/usePutClubRecruitment'; import ConfirmModal from 'components/Club/NewClubRecruitment/components/ConfirmModal'; import DatePickerModal from 'components/Club/NewClubRecruitment/components/DatePickerModal'; @@ -26,7 +27,7 @@ function ClubRecruitmentEditPage({ id }: { id: string }) { const logger = useLogger(); const isMobile = useMediaQuery(); const { clubDetail } = useClubDetail(Number(id)); - const { clubRecruitmentData } = useClubRecruitment(Number(id)); + const { data: clubRecruitmentData } = useSuspenseQuery(clubQueries.recruitment(Number(id))); const { mutateAsync } = usePutClubRecruitment(Number(id)); const [modalType, setModalType] = useState<'edit' | 'editCancel'>('edit'); diff --git a/src/pages/course/index.tsx b/src/pages/course/index.tsx index 7500a75f4..6f03d2f6e 100644 --- a/src/pages/course/index.tsx +++ b/src/pages/course/index.tsx @@ -1,7 +1,9 @@ import React, { Suspense } from 'react'; import Link from 'next/link'; import { useRouter } from 'next/router'; +import { useSuspenseQuery } from '@tanstack/react-query'; import { Course, PreCourse } from 'api/course/entity'; +import { courseQueries } from 'api/course/queries'; import CourseSearchForm from 'components/Course/components/CourseSearchForm'; import CourseTable, { CreditDisplay, @@ -9,7 +11,6 @@ import CourseTable, { createPreCoursesColumns, createSelectedCoursesColumns, } from 'components/Course/components/CourseTable'; -import { useSuspenseCourseSearch, useSuspensePreCourseList } from 'components/Course/hooks/useCourseQuery'; import useCourseSearchForm from 'components/Course/hooks/useCourseSearchForm'; import useSelectedCourses, { getCourseKey } from 'components/Course/hooks/useSelectedCourses'; import useTimetableFrameList from 'components/TimetablePage/hooks/useTimetableFrameList'; @@ -28,7 +29,7 @@ interface OpenCoursesTableContentProps { function OpenCoursesTableContent({ searchParams, onAddCourse }: OpenCoursesTableContentProps) { const logger = useLogger(); - const { data: courses } = useSuspenseCourseSearch(searchParams); + const { data: courses } = useSuspenseQuery(courseQueries.search(searchParams)); const handleAddOpenCourse = (course: PreCourse) => { logger.actionEventClick({ team: 'User', event_label: 'application_training_apply', value: '' }); @@ -56,7 +57,7 @@ interface PreCoursesTableContentProps { function PreCoursesTableContent({ token, timetableFrameId, onAddCourse }: PreCoursesTableContentProps) { const logger = useLogger(); - const { data: preCourses } = useSuspensePreCourseList(token, timetableFrameId); + const { data: preCourses } = useSuspenseQuery(courseQueries.preCourseList(token, timetableFrameId)); const handleAddPreCourse = (course: PreCourse) => { logger.actionEventClick({ team: 'User', event_label: 'application_training_pre_apply', value: '' }); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index c784f7b1b..e4c81d785 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,12 +1,11 @@ import type { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next'; import { isKoinError } from '@bcsdlab/koin'; import { dehydrate, QueryClient } from '@tanstack/react-query'; -import { getArticles, getLostItemStat } from 'api/articles'; -import { getBannerCategoryList, getBanners } from 'api/banner'; -import { getHotClub } from 'api/club'; -import { HotClubResponse } from 'api/club/entity'; -import { getStoreCategories } from 'api/store'; -import { getMySemester, getSemesterInfoList, getTimetableFrame, getTimetableLectureInfo } from 'api/timetable'; +import { articleQueries } from 'api/articles/queries'; +import { bannerQueries } from 'api/banner/queries'; +import { clubQueries } from 'api/club/queries'; +import { storeQueries } from 'api/store/queries'; +import { timetableQueries } from 'api/timetable/queries'; import IndexArticles from 'components/IndexComponents/IndexArticles'; import IndexBus from 'components/IndexComponents/IndexBus'; import IndexCafeteria from 'components/IndexComponents/IndexCafeteria'; @@ -15,10 +14,6 @@ import IndexLostItem from 'components/IndexComponents/IndexLostItem'; import IndexStore from 'components/IndexComponents/IndexStore'; import IndexTimetable from 'components/IndexComponents/IndexTimetable'; import { SSRLayout } from 'components/layout'; -import { MY_SEMESTER_INFO_KEY } from 'components/TimetablePage/hooks/useMySemester'; -import { SEMESTER_INFO_KEY } from 'components/TimetablePage/hooks/useSemesterOptionList'; -import { TIMETABLE_FRAME_KEY } from 'components/TimetablePage/hooks/useTimetableFrameList'; -import { TIMETABLE_INFO_LIST } from 'components/TimetablePage/hooks/useTimetableInfoList'; import Banner from 'components/ui/Banner'; import UserInfoModal from 'components/ui/UserInfoModal'; import { COOKIE_KEY } from 'static/url'; @@ -27,21 +22,6 @@ import { parseServerSideParams } from 'utils/ts/parseServerSideParams'; import { clearServerAuthCookies, isServerAuthError } from 'utils/ts/ssrAuth'; import styles from './IndexPage.module.scss'; -const getHotClubData = async () => { - try { - return await getHotClub(); - } catch (e) { - if (isKoinError(e) && e.status === 404) { - return { - club_id: -1, - name: '인기 동아리가 없어요', - image_url: '', - } satisfies HotClubResponse; - } - throw e; - } -}; - export const getServerSideProps = async (context: GetServerSidePropsContext) => { const queryClient = new QueryClient(); let token = parseServerSideParams(context).token ?? ''; @@ -57,10 +37,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => if (!token || userType !== 'STUDENT') return null; try { - return await queryClient.fetchQuery({ - queryKey: [MY_SEMESTER_INFO_KEY], - queryFn: () => getMySemester(token), - }); + return await queryClient.fetchQuery(timetableQueries.mySemester(token, { userType })); } catch (error) { if (isServerAuthError(error)) { resetAuthContext(); @@ -74,41 +51,33 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => }; const [[banners, categories, hotClubInfo, mySemester]] = await Promise.all([ - Promise.all([getBannerCategoryList(), getStoreCategories(), getHotClubData(), fetchMySemester()]), - queryClient.prefetchQuery({ - queryKey: ['articles', '1'], - queryFn: () => getArticles(token, '1'), - }), - queryClient.prefetchQuery({ - queryKey: [SEMESTER_INFO_KEY], - queryFn: getSemesterInfoList, - }), - queryClient.prefetchQuery({ - queryKey: ['lostItemStat'], - queryFn: getLostItemStat, - }), + Promise.all([ + queryClient.fetchQuery(bannerQueries.categories()), + queryClient.fetchQuery(storeQueries.categories()), + queryClient.fetchQuery(clubQueries.hot()), + fetchMySemester(), + ]), + queryClient.prefetchQuery(articleQueries.list(token, '1')), + queryClient.prefetchQuery(timetableQueries.semesterInfo()), + queryClient.prefetchQuery(articleQueries.lostItemStat()), ]); const userSemester = mySemester?.semesters?.[0] || getRecentSemester(); const bannerCategoryId = Number(banners.banner_categories[0].id); - const bannersList = await getBanners(bannerCategoryId); + const bannersList = await queryClient.fetchQuery(bannerQueries.list(bannerCategoryId)); const isBannerOpen = context.req.cookies['HIDE_BANNER'] !== `modal_category_${bannerCategoryId}` && bannersList.count !== 0; if (token && userType === 'STUDENT') { try { - const timetableFrameList = await queryClient.fetchQuery({ - queryKey: [TIMETABLE_FRAME_KEY + userSemester.year + userSemester.term], - queryFn: () => getTimetableFrame(token, userSemester), - }); + const timetableFrameList = await queryClient.fetchQuery( + timetableQueries.frameList(token, userSemester, { userType }), + ); const mainFrame = timetableFrameList.find((frame) => frame.is_main); const activeMainFrameId = mainFrame?.id; if (typeof activeMainFrameId === 'number') { - await queryClient.prefetchQuery({ - queryKey: [TIMETABLE_INFO_LIST, activeMainFrameId], - queryFn: () => getTimetableLectureInfo(token, activeMainFrameId), - }); + await queryClient.prefetchQuery(timetableQueries.lectureInfo(token, activeMainFrameId)); } } catch (error) { if (isServerAuthError(error)) { diff --git a/src/pages/lost-item/[id]/index.tsx b/src/pages/lost-item/[id]/index.tsx index ab0f294e3..d7fb74778 100644 --- a/src/pages/lost-item/[id]/index.tsx +++ b/src/pages/lost-item/[id]/index.tsx @@ -1,7 +1,7 @@ import type { GetServerSidePropsContext } from 'next'; import { useRouter } from 'next/router'; -import { dehydrate, QueryClient } from '@tanstack/react-query'; -import { getSingleLostItemArticle, getLostItemArticles } from 'api/articles'; +import { dehydrate, QueryClient, useSuspenseQuery } from '@tanstack/react-query'; +import { articleQueries } from 'api/articles/queries'; import ChatIcon from 'assets/svg/Articles/chat.svg'; import ReportIcon from 'assets/svg/Articles/report.svg'; import HotArticles from 'components/Articles/components/HotArticle'; @@ -16,7 +16,6 @@ import LostItemSEO from 'components/Articles/LostItemDetailPage/components/LostI import ReportModal from 'components/Articles/LostItemDetailPage/components/ReportModal'; import usePostFoundLostItem from 'components/Articles/LostItemDetailPage/hooks/usePostFoundLostItem'; import usePostLostItemChatroom from 'components/Articles/LostItemDetailPage/hooks/usePostLostItemChatroom'; -import useSingleLostItemArticle from 'components/Articles/LostItemDetailPage/hooks/useSingleLostItemArticle'; import { SSRLayout } from 'components/layout'; import LoginRequiredModal from 'components/modal/LoginRequiredModal'; import ROUTES from 'static/routes'; @@ -41,15 +40,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => const latestLostItemParams = { limit: 10, sort: 'LATEST' as const }; await Promise.all([ - queryClient.prefetchQuery({ - queryKey: ['lostItem', 'detail', articleId], - queryFn: () => getSingleLostItemArticle(token ?? '', articleId), - }), - queryClient.prefetchInfiniteQuery({ - queryKey: ['lostItem', latestLostItemParams], - queryFn: () => getLostItemArticles(token ?? '', { ...latestLostItemParams, page: 1 }), - initialPageParam: 1, - }), + queryClient.prefetchQuery(articleQueries.lostItemDetail(token ?? '', articleId)), + queryClient.prefetchInfiniteQuery(articleQueries.lostItemInfiniteList(token ?? '', latestLostItemParams)), ]); return { @@ -72,7 +64,7 @@ export default function LostItemDetailPage({ articleId }: LostItemDetailPageProp const portalManager = useModalPortal(); const token = useTokenState(); - const { article } = useSingleLostItemArticle(articleId); + const { data: article } = useSuspenseQuery(articleQueries.lostItemDetail(token, articleId)); const { mutateAsync: searchChatroom } = usePostLostItemChatroom(); const { mutate: toggleFound, isPending: isToggling } = usePostFoundLostItem(articleId); const { diff --git a/src/pages/lost-item/index.tsx b/src/pages/lost-item/index.tsx index 003caebdd..3ac2fdf7b 100644 --- a/src/pages/lost-item/index.tsx +++ b/src/pages/lost-item/index.tsx @@ -1,17 +1,17 @@ import { useMemo } from 'react'; import type { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next'; import { useRouter } from 'next/router'; -import { dehydrate, QueryClient } from '@tanstack/react-query'; -import { getLostItemArticles } from 'api/articles'; +import { dehydrate, keepPreviousData, QueryClient, useQuery } from '@tanstack/react-query'; import { LostItemArticlesRequest } from 'api/articles/entity'; +import { articleQueries } from 'api/articles/queries'; import LostItemList from 'components/Articles/components/LostItemList'; import LostItemPageLayout from 'components/Articles/components/LostItemPageLayout'; import Pagination from 'components/Articles/components/Pagination'; -import useLostItemPagination from 'components/Articles/hooks/useLostItemPagination'; -import { useLostItemSearch } from 'components/Articles/hooks/useLostItemSearch'; import { LostItemParams, parseLostItemQuery } from 'components/Articles/utils/lostItemQuery'; +import { selectLostItemPaginationData } from 'components/Articles/utils/selectArticlesData'; import { SSRLayout } from 'components/layout'; import useMount from 'utils/hooks/state/useMount'; +import useTokenState from 'utils/hooks/state/useTokenState'; import { parseServerSideParams } from 'utils/ts/parseServerSideParams'; import styles from './LostItemArticleListPage.module.scss'; @@ -32,10 +32,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => const apiParams = toLostItemArticlesRequest(params); - await queryClient.prefetchQuery({ - queryKey: ['lostItemPagination', apiParams], - queryFn: () => getLostItemArticles(token ?? '', apiParams), - }); + await queryClient.prefetchQuery(articleQueries.lostItemList(token ?? '', apiParams)); return { props: { @@ -68,6 +65,7 @@ export default function LostItemArticleListPage({ initialParams, }: InferGetServerSidePropsType) { const router = useRouter(); + const token = useTokenState(); const params = useLostItemParams(initialParams); const apiParams = toLostItemArticlesRequest(params); @@ -77,12 +75,19 @@ export default function LostItemArticleListPage({ const isSearching = keyword.length > 0; - const { data: lostItemData } = useLostItemPagination(apiParams); + const { data: lostItemData } = useQuery({ + ...articleQueries.lostItemList(token, apiParams), + placeholderData: keepPreviousData, + select: selectLostItemPaginationData, + }); - const { data: searchData } = useLostItemSearch({ - query: keyword, - page: params.page, - limit: 10, + const { data: searchData } = useQuery({ + ...articleQueries.lostItemSearch({ + query: keyword.trim(), + page: params.page ?? 1, + limit: 10, + }), + enabled: keyword.length > 0, }); const articles = isSearching ? (searchData?.articles ?? []) : (lostItemData?.articles ?? []); diff --git a/src/pages/room/[id]/index.tsx b/src/pages/room/[id]/index.tsx index b63b07aa1..5ba92aa6f 100644 --- a/src/pages/room/[id]/index.tsx +++ b/src/pages/room/[id]/index.tsx @@ -1,7 +1,7 @@ import type { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next'; import Image from 'next/image'; import { dehydrate, QueryClient } from '@tanstack/react-query'; -import { getRoomDetailInfo } from 'api/room'; +import { roomQueries } from 'api/room/queries'; import { SSRLayout } from 'components/layout'; import RoomDetailImg from 'components/Room/components/RoomDetailImg'; import RoomDetailMap from 'components/Room/components/RoomDetailMap'; @@ -25,10 +25,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => const queryClient = new QueryClient(); - await queryClient.prefetchQuery({ - queryKey: ['roomDetail', id], - queryFn: () => getRoomDetailInfo(id), - }); + await queryClient.prefetchQuery(roomQueries.detail(id)); return { props: { diff --git a/src/pages/room/index.tsx b/src/pages/room/index.tsx index bf8401f1b..bb9e125b9 100644 --- a/src/pages/room/index.tsx +++ b/src/pages/room/index.tsx @@ -1,11 +1,10 @@ -import { QueryClient, dehydrate } from '@tanstack/react-query'; -import { getRoomList } from 'api/room'; +import { QueryClient, dehydrate, useQuery } from '@tanstack/react-query'; +import { roomQueries } from 'api/room/queries'; import { SSRLayout } from 'components/layout'; import RoomList from 'components/Room/components/RoomList'; import useMarker from 'components/Room/RoomPage/hooks/useMarker'; import useNaverMap from 'components/Room/RoomPage/hooks/useNaverMap'; import useNaverMapScript from 'components/Room/RoomPage/hooks/useNaverMapScript'; -import useRoomList from 'components/Room/RoomPage/hooks/useRoomList'; import useMediaQuery from 'utils/hooks/layout/useMediaQuery'; import useScrollToTop from 'utils/hooks/ui/useScrollToTop'; import styles from './RoomPage.module.scss'; @@ -15,10 +14,7 @@ const LOCATION = { latitude: 36.764617, longitude: 127.283154 }; export const getServerSideProps = async () => { const queryClient = new QueryClient(); - await queryClient.prefetchQuery({ - queryKey: ['roomList'], - queryFn: () => getRoomList(), - }); + await queryClient.prefetchQuery(roomQueries.list()); return { props: { @@ -29,7 +25,7 @@ export const getServerSideProps = async () => { function RoomPage() { const isMobile = useMediaQuery(); - const roomList = useRoomList(); + const { data: roomList } = useQuery(roomQueries.list()); const isMapLoaded = useNaverMapScript(); const { getMap } = useNaverMap(LOCATION.latitude, LOCATION.longitude, isMapLoaded); useMarker({ getMap, roomList }); diff --git a/src/pages/store/[id].tsx b/src/pages/store/[id].tsx index 9188d8c9c..0990bd425 100644 --- a/src/pages/store/[id].tsx +++ b/src/pages/store/[id].tsx @@ -3,8 +3,15 @@ import { GetServerSidePropsContext } from 'next'; import Image from 'next/image'; import { useRouter } from 'next/router'; import { cn } from '@bcsdlab/utils'; -import { dehydrate, HydrationBoundary, QueryClient, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'; -import { getReviewList, getStoreDetailInfo, getStoreDetailMenu, getStoreEventList } from 'api/store'; +import { + dehydrate, + HydrationBoundary, + QueryClient, + useQueryClient, + useSuspenseQuery, + type DehydratedState, +} from '@tanstack/react-query'; +import { storeQueries, storeQueryKeys } from 'api/store/queries'; import EmptyImageIcon from 'assets/svg/empty-thumbnail.svg'; import Phone from 'assets/svg/Review/phone.svg'; import Copy from 'assets/svg/Store/copy.svg'; @@ -44,24 +51,18 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }; } await Promise.all([ - queryClient.prefetchQuery({ - queryKey: ['storeDetail', storeId], - queryFn: () => getStoreDetailInfo(storeId), - }), - queryClient.prefetchQuery({ - queryKey: ['storeDetailMenu', storeId], - queryFn: () => getStoreDetailMenu(storeId), - }), - queryClient.prefetchQuery({ - queryKey: ['review', storeId, 'LATEST'], - queryFn: () => getReviewList(Number(storeId), 1, 'LATEST'), - }), + queryClient.prefetchQuery(storeQueries.detail(storeId)), + queryClient.prefetchQuery(storeQueries.detailMenu(storeId)), + queryClient.prefetchQuery( + storeQueries.reviewList({ + shopId: Number(storeId), + page: 1, + sorter: 'LATEST', + }), + ), ]); - await queryClient.prefetchQuery({ - queryKey: ['storeEventList', storeId], - queryFn: ({ queryKey }) => getStoreEventList(queryKey[1] ?? ''), - }); + await queryClient.prefetchQuery(storeQueries.eventList(storeId)); return { props: { @@ -81,21 +82,19 @@ function StoreDetailPage({ id }: Props) { const logger = useLogger(); // waterfall 현상 막기 const { data: parallelData } = useSuspenseQuery({ - queryKey: ['storeDetail', 'storeDetailMenu', 'review', id], + queryKey: storeQueryKeys.detailPage(id), queryFn: () => Promise.all([ - queryClient.fetchQuery({ - queryKey: ['storeDetail', id], - queryFn: () => getStoreDetailInfo(id), - }), - queryClient.fetchQuery({ - queryKey: ['storeDetailMenu', id], - queryFn: () => getStoreDetailMenu(id), - }), - queryClient.fetchQuery({ - queryKey: ['review', id, 'LATEST'], - queryFn: () => getReviewList(Number(id), 1, 'LATEST', token), - }), + queryClient.fetchQuery(storeQueries.detail(id)), + queryClient.fetchQuery(storeQueries.detailMenu(id)), + queryClient.fetchQuery( + storeQueries.reviewList({ + shopId: Number(id), + page: 1, + sorter: 'LATEST', + token, + }), + ), ]), }); @@ -477,12 +476,12 @@ function StoreDetailPage({ id }: Props) { ); } -function StoreDetail({ dehydratedstate, id }: { dehydratedstate: unknown; id: string }) { +function StoreDetail({ dehydratedState, id }: { dehydratedState: DehydratedState; id: string }) { const router = useRouter(); return ( router.push('/store')}> - + }> diff --git a/src/pages/store/index.tsx b/src/pages/store/index.tsx index ccc22d1e2..65ae4dddb 100644 --- a/src/pages/store/index.tsx +++ b/src/pages/store/index.tsx @@ -3,15 +3,14 @@ import { GetServerSidePropsContext } from 'next'; import Image from 'next/image'; import { useRouter } from 'next/router'; import { cn } from '@bcsdlab/utils'; -import { dehydrate, HydrationBoundary, QueryClient, useQuery } from '@tanstack/react-query'; -import { getStoreCategories, getStoreListV2, getAllEvent } from 'api/store'; +import { dehydrate, HydrationBoundary, QueryClient, useQuery, useSuspenseQuery, type DehydratedState } from '@tanstack/react-query'; +import { storeQueries } from 'api/store/queries'; import Close from 'assets/svg/close-icon-20x20.svg'; import DesktopStoreList from 'components/Store/StorePage/components/DesktopStoreList'; import EventCarousel from 'components/Store/StorePage/components/EventCarousel'; import MobileStoreList from 'components/Store/StorePage/components/MobileStoreList'; import SearchBar from 'components/Store/StorePage/components/SearchBar'; import SearchBarModal from 'components/Store/StorePage/components/SearchBarModal'; -import { useStoreCategories } from 'components/Store/StorePage/hooks/useCategoryList'; import { getCategoryDurationTime, initializeCategoryEntryTime } from 'components/Store/utils/durationTime'; import IntroToolTip from 'components/ui/IntroToolTip'; import ROUTES from 'static/routes'; @@ -69,8 +68,6 @@ const toggleNameLabel = { DELIVERY: 'delivery', } as const; -const CATEGORY_IS_UNDEFINED = -1; - const loggingCategoryToggleSorterValue = (toggleName: 'COUNT' | 'RATING', category: string | undefined) => `check_${toggleNameLabel[toggleName]}_${category || '전체보기'}`; @@ -81,8 +78,11 @@ const useStoreList = (sorter: StoreSorterType, filter: StoreFilterType[], params const selectedCategory = Number(params.category); const { data: storeList } = useQuery({ - queryKey: ['storeListV2', sorter, filter, selectedCategory], - queryFn: () => getStoreListV2(sorter, filter, params.storeName), + ...storeQueries.listV2({ + sorter, + filter, + query: params.storeName, + }), placeholderData: (previousData) => previousData, select: (data) => { if (!data || !data.shops) return []; @@ -101,25 +101,20 @@ const useStoreList = (sorter: StoreSorterType, filter: StoreFilterType[], params export async function getServerSideProps(context: GetServerSidePropsContext) { const queryClient = new QueryClient(); - const { category, storeName } = context.query; - - const selectedCategory = category ? Number(category) : CATEGORY_IS_UNDEFINED; + const { storeName } = context.query; const selectedStoreName = storeName ? String(storeName) : undefined; - await queryClient.prefetchQuery({ - queryKey: ['storeCategories'], - queryFn: getStoreCategories, - }); + await queryClient.prefetchQuery(storeQueries.categories()); - await queryClient.prefetchQuery({ - queryKey: ['storeListV2', 'NONE', [], selectedCategory], - queryFn: () => getStoreListV2('NONE', [], selectedStoreName), - }); + await queryClient.prefetchQuery( + storeQueries.listV2({ + sorter: 'NONE', + filter: [], + query: selectedStoreName, + }), + ); - await queryClient.prefetchQuery({ - queryKey: ['all-event'], - queryFn: () => getAllEvent(), - }); + await queryClient.prefetchQuery(storeQueries.allEvents()); return { props: { @@ -145,7 +140,7 @@ function Store() { const filteredTypeList = Object.entries(storeFilterList) .filter(([, value]) => value) .map(([key]) => key as StoreFilterType); - const { data: categories } = useStoreCategories(); + const { data: categories } = useSuspenseQuery(storeQueries.categories()); const storeList = useStoreList(storeSorter, filteredTypeList, router.query); const selectedCategory = router.query.category ? Number(router.query.category) : -1; @@ -371,9 +366,9 @@ function Store() { ); } -export default function StorePage({ dehydrateState }: { dehydrateState: unknown }) { +export default function StorePage({ dehydratedState }: { dehydratedState: DehydratedState }) { return ( - + ); diff --git a/src/pages/store/review/edit/[id]/[reviewid]/index.tsx b/src/pages/store/review/edit/[id]/[reviewid]/index.tsx index e22523f82..825bce405 100644 --- a/src/pages/store/review/edit/[id]/[reviewid]/index.tsx +++ b/src/pages/store/review/edit/[id]/[reviewid]/index.tsx @@ -1,13 +1,16 @@ import { useRouter } from 'next/router'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { reviewQueries } from 'api/review/queries'; import useStoreDetail from 'components/Store/StoreDetailPage/hooks/useStoreDetail'; import { useEditStoreReview } from 'components/Store/StoreReviewPage/hooks/useEditStoreReview'; -import { useGetStoreReview } from 'components/Store/StoreReviewPage/hooks/useGetStoreReview'; import ReviewForm from 'components/Store/StoreReviewPage/ReviewForm/ReviewForm'; +import useTokenState from 'utils/hooks/state/useTokenState'; function EditReviewComponent({ id, reviewId }: { id: string; reviewId: string }) { + const token = useTokenState(); const { storeDetail } = useStoreDetail(id); const { mutate } = useEditStoreReview(String(storeDetail.id), reviewId); - const initialData = useGetStoreReview(id, reviewId); + const { data: initialData } = useSuspenseQuery(reviewQueries.detail(token, id, reviewId)); return ; } diff --git a/src/pages/timetable/index.tsx b/src/pages/timetable/index.tsx index d60bf3ffb..2c06969a9 100644 --- a/src/pages/timetable/index.tsx +++ b/src/pages/timetable/index.tsx @@ -4,13 +4,10 @@ import dynamic from 'next/dynamic'; import { useRouter } from 'next/router'; import { isKoinError } from '@bcsdlab/koin'; import { dehydrate, QueryClient } from '@tanstack/react-query'; -import { getDeptList } from 'api/dept'; -import { getMySemester, getSemesterInfoList, getTimetableFrame, getTimetableLectureInfo } from 'api/timetable'; +import { deptQueries } from 'api/dept/queries'; +import { createDefaultTimetableFrameList, timetableQueries, timetableQueryKeys } from 'api/timetable/queries'; import { SSRLayout } from 'components/layout'; -import { MY_SEMESTER_INFO_KEY } from 'components/TimetablePage/hooks/useMySemester'; -import { SEMESTER_INFO_KEY } from 'components/TimetablePage/hooks/useSemesterOptionList'; -import useTimetableFrameList, { TIMETABLE_FRAME_KEY } from 'components/TimetablePage/hooks/useTimetableFrameList'; -import { TIMETABLE_INFO_LIST } from 'components/TimetablePage/hooks/useTimetableInfoList'; +import useTimetableFrameList from 'components/TimetablePage/hooks/useTimetableFrameList'; import DefaultPage from 'components/TimetablePage/MainTimetablePage/DefaultPage'; import useMediaQuery from 'utils/hooks/layout/useMediaQuery'; import useTokenState from 'utils/hooks/state/useTokenState'; @@ -37,39 +34,22 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { if (token) { try { - const mySemesterData = await queryClient.fetchQuery({ - queryKey: [MY_SEMESTER_INFO_KEY], - queryFn: () => getMySemester(token), - }); + const mySemesterData = await queryClient.fetchQuery(timetableQueries.mySemester(token)); const userSemester = mySemesterData?.semesters?.[0]; const semester = year && term ? { year, term } : userSemester || getRecentSemester(); - const timetableFrameList = await queryClient.fetchQuery({ - queryKey: [TIMETABLE_FRAME_KEY + semester.year + semester.term], - queryFn: () => getTimetableFrame(token, semester), - }); + const timetableFrameList = await queryClient.fetchQuery(timetableQueries.frameList(token, semester)); const mainFrame = timetableFrameList.find((frame) => frame.is_main); const currentFrameId = validatedFrameId ?? mainFrame?.id ?? null; const prefetchPromises = [ - queryClient.prefetchQuery({ - queryKey: [SEMESTER_INFO_KEY], - queryFn: getSemesterInfoList, - }), - queryClient.prefetchQuery({ - queryKey: ['dept'], - queryFn: () => getDeptList(), - }), + queryClient.prefetchQuery(timetableQueries.semesterInfo()), + queryClient.prefetchQuery(deptQueries.list()), ]; if (currentFrameId !== null) { - prefetchPromises.push( - queryClient.prefetchQuery({ - queryKey: [TIMETABLE_INFO_LIST, currentFrameId], - queryFn: () => getTimetableLectureInfo(token, currentFrameId), - }), - ); + prefetchPromises.push(queryClient.prefetchQuery(timetableQueries.lectureInfo(token, currentFrameId))); } await Promise.all(prefetchPromises); @@ -77,17 +57,11 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { if (!isServerAuthError(error) && !(isKoinError(error) && error.status === 403)) throw error; if (isServerAuthError(error)) clearServerAuthCookies(context); const semester = getRecentSemester(); - queryClient.setQueryData( - [TIMETABLE_FRAME_KEY + semester.year + semester.term], - [{ id: null, name: '기본 시간표', is_main: true }], - ); + queryClient.setQueryData(timetableQueryKeys.frameList(semester), createDefaultTimetableFrameList()); } } else { const semester = getRecentSemester(); - queryClient.setQueryData( - [TIMETABLE_FRAME_KEY + semester.year + semester.term], - [{ id: null, name: '기본 시간표', is_main: true }], - ); + queryClient.setQueryData(timetableQueryKeys.frameList(semester), createDefaultTimetableFrameList()); } return { diff --git a/src/pages/timetable/modify/index.tsx b/src/pages/timetable/modify/index.tsx index e507bc28d..7cd02ccbf 100644 --- a/src/pages/timetable/modify/index.tsx +++ b/src/pages/timetable/modify/index.tsx @@ -2,11 +2,8 @@ import type { GetServerSidePropsContext } from 'next'; import { useRouter } from 'next/router'; import { isKoinError } from '@bcsdlab/koin'; import { dehydrate, QueryClient } from '@tanstack/react-query'; -import { getLectureList, getMySemester, getTimetableLectureInfo } from 'api/timetable'; +import { timetableQueries } from 'api/timetable/queries'; import { SSRLayout } from 'components/layout'; -import { MY_SEMESTER_INFO_KEY } from 'components/TimetablePage/hooks/useMySemester'; -import { SEMESTER_INFO_KEY } from 'components/TimetablePage/hooks/useSemesterOptionList'; -import { TIMETABLE_INFO_LIST } from 'components/TimetablePage/hooks/useTimetableInfoList'; import ModifyTimetablePage from 'components/TimetablePage/ModifyTimetablePage'; import { COOKIE_KEY } from 'static/url'; import { getRecentSemester } from 'utils/timetable/semester'; @@ -23,24 +20,15 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { if (token && userType === 'STUDENT') { try { - const mySemesterData = await queryClient.fetchQuery({ - queryKey: [MY_SEMESTER_INFO_KEY], - queryFn: () => getMySemester(token), - }); + const mySemesterData = await queryClient.fetchQuery(timetableQueries.mySemester(token, { userType })); const year = Number(query.year); const term = query.term as Term; const userSemester = mySemesterData?.semesters?.[0]; const semester = year && term ? { year, term } : userSemester || getRecentSemester(); await Promise.all([ - queryClient.prefetchQuery({ - queryKey: [TIMETABLE_INFO_LIST, timetableFrameId], - queryFn: () => getTimetableLectureInfo(token, timetableFrameId), - }), - queryClient.prefetchQuery({ - queryKey: [SEMESTER_INFO_KEY, semester], - queryFn: () => (semester ? getLectureList(semester) : null), - }), + queryClient.prefetchQuery(timetableQueries.lectureInfo(token, timetableFrameId)), + queryClient.prefetchQuery(timetableQueries.lectureList(semester)), ]); } catch (error) { if (!isServerAuthError(error) && !(isKoinError(error) && error.status === 403)) throw error; diff --git a/src/utils/hooks/abTest/useABTestView.ts b/src/utils/hooks/abTest/useABTestView.ts index 29acc9dfd..777426056 100644 --- a/src/utils/hooks/abTest/useABTestView.ts +++ b/src/utils/hooks/abTest/useABTestView.ts @@ -1,20 +1,10 @@ import { useSuspenseQuery } from '@tanstack/react-query'; -import { abTestAssign } from 'api/abTest'; +import { abTestQueries } from 'api/abTest/queries'; export const useABTestView = (title: string, authorization?: string) => { const accessHistoryId = typeof window !== 'undefined' ? localStorage.getItem('access_history_id') : null; - const { data: abTestView } = useSuspenseQuery({ - queryKey: ['abTestView', title, accessHistoryId], - queryFn: async () => { - try { - const response = await abTestAssign(title, authorization || undefined, accessHistoryId); - return response; - } catch { - return { access_history_id: null, variable_name: 'default' }; - } - }, - }); + const { data: abTestView } = useSuspenseQuery(abTestQueries.assign(title, authorization, accessHistoryId)); // 최초 편입 시 if (abTestView.access_history_id) { diff --git a/src/utils/hooks/state/useUser.ts b/src/utils/hooks/state/useUser.ts index 7db931792..102280705 100644 --- a/src/utils/hooks/state/useUser.ts +++ b/src/utils/hooks/state/useUser.ts @@ -1,25 +1,18 @@ import { useSuspenseQuery } from '@tanstack/react-query'; -import { getGeneralUser, getUser } from 'api/auth'; import { GeneralUserResponse, UserResponse } from 'api/auth/entity'; +import { authQueries } from 'api/auth/queries'; import { UserType, useTokenStore } from 'utils/zustand/auth'; -export type UnionUserResponse = UserResponse | GeneralUserResponse; - -const getUserInfo = async (token: string, userType: UserType): Promise => { - if (userType === 'STUDENT') { - return getUser(token); - } - return getGeneralUser(token); +type GeneralUserWithAnonymousNickname = GeneralUserResponse & { + anonymous_nickname: string; }; +export type UnionUserResponse = UserResponse | GeneralUserWithAnonymousNickname; + export const useUser = () => { const { token, userType } = useTokenStore(); const { data, isError } = useSuspenseQuery({ - queryKey: ['userInfo', token, userType], - queryFn: () => { - if (!token) return null; - return getUserInfo(token, userType); - }, + ...authQueries.userInfo(token, userType as UserType), select: (rawData) => { if (!rawData) return null; diff --git a/src/utils/hooks/state/useUserAcademicInfo.ts b/src/utils/hooks/state/useUserAcademicInfo.ts deleted file mode 100644 index 093bf0f9d..000000000 --- a/src/utils/hooks/state/useUserAcademicInfo.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getUserAcademicInfo } from 'api/auth'; -import useTokenState from './useTokenState'; - -export default function useUserAcademicInfo() { - const token = useTokenState(); - - return useSuspenseQuery({ - queryKey: ['userAcademicinfo'], - - queryFn: () => (token ? getUserAcademicInfo(token) : null), - }); -} diff --git a/yarn.lock b/yarn.lock index a263d3ad0..cd0eba07e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4585,21 +4585,21 @@ __metadata: languageName: node linkType: hard -"@tanstack/query-core@npm:5.28.6": - version: 5.28.6 - resolution: "@tanstack/query-core@npm:5.28.6" - checksum: 10/e9ae8d80a86890899604b4816cdff76b1c6d496fe627f8b726f5515f1f48632233394c98567ea54ac18498f2f69d01f4307116d6d1329edabba920c73c21a3bf +"@tanstack/query-core@npm:5.90.20": + version: 5.90.20 + resolution: "@tanstack/query-core@npm:5.90.20" + checksum: 10/25e38f4382442bc15e0f6cce8d787e9df8d8822c61d3f3e9427e89e01b1e2506f848292e086dae29aeb55f8ce71b097c34221f3c5eda37fb4a688b5ceca5d1b3 languageName: node linkType: hard -"@tanstack/react-query@npm:^5.28.6": - version: 5.28.6 - resolution: "@tanstack/react-query@npm:5.28.6" +"@tanstack/react-query@npm:^5.90.21": + version: 5.90.21 + resolution: "@tanstack/react-query@npm:5.90.21" dependencies: - "@tanstack/query-core": "npm:5.28.6" + "@tanstack/query-core": "npm:5.90.20" peerDependencies: - react: ^18.0.0 - checksum: 10/f7706485f3c21fcd2316a921f4b82f354700a19b0d69e7d5d611e999b73f1a3aa4608cd1004121af11badc4fa64bd8f6fc7e5cefa542fd6815ff2ec356b92f9f + react: ^18 || ^19 + checksum: 10/5bb4b6be7ac34a7423aca60484f1e35a0c7f8b2f86ef6656a3b6757943417561a50d9a159e2bff9d97a57a82ecec920df067b507c2ec6488beb286635b25e518 languageName: node linkType: hard @@ -10020,7 +10020,7 @@ __metadata: "@sentry/cli": "npm:^2.45.0" "@sentry/nextjs": "npm:^10" "@svgr/webpack": "npm:^8.1.0" - "@tanstack/react-query": "npm:^5.28.6" + "@tanstack/react-query": "npm:^5.90.21" "@testing-library/jest-dom": "npm:^5.14.1" "@testing-library/react": "npm:^13.0.0" "@testing-library/user-event": "npm:^13.2.1"