diff --git a/src/api/book-club/react-query/customHooks.ts b/src/api/book-club/react-query/customHooks.ts index defec8ef..76b5250f 100644 --- a/src/api/book-club/react-query/customHooks.ts +++ b/src/api/book-club/react-query/customHooks.ts @@ -9,6 +9,7 @@ import { } from '../index'; import { WriteReviewParams } from '../types'; import { AxiosError } from 'axios'; +import { likeOnError, likeOnMutate } from './likeOptimisticUpdate'; export function useBookClubCreateMutation() { const queryClient = useQueryClient(); @@ -89,6 +90,9 @@ export function useCancelBookClub() { return useMutation({ mutationFn: (id: number) => bookClubMainAPI.cancel(id), onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: bookClubs.list().queryKey, + }); queryClient.invalidateQueries({ queryKey: bookClubs.my()._ctx.created().queryKey, }); @@ -104,13 +108,15 @@ export function useLikeBookClub() { return useMutation, number>({ mutationFn: (id: number) => bookClubLikeAPI.like(id), - onSuccess: (_, id) => { - queryClient.invalidateQueries({ - queryKey: bookClubs.list().queryKey, - }); - queryClient.invalidateQueries({ - queryKey: bookClubs.detail(id).queryKey, - }); + + onMutate: async (id) => { + return likeOnMutate(queryClient, id, true); + }, + + onError: (_error, id, context) => { + if (context) { + likeOnError(queryClient, id, context); + } }, }); } @@ -120,13 +126,15 @@ export function useUnLikeBookClub() { return useMutation, number>({ mutationFn: (id: number) => bookClubLikeAPI.unlike(id), - onSuccess: (_, id) => { - queryClient.invalidateQueries({ - queryKey: bookClubs.list().queryKey, - }); - queryClient.invalidateQueries({ - queryKey: bookClubs.detail(id).queryKey, - }); + + onMutate: async (id) => { + return likeOnMutate(queryClient, id, false); + }, + + onError: (_error, id, context) => { + if (context) { + likeOnError(queryClient, id, context); + } }, }); } diff --git a/src/api/book-club/react-query/likeOptimisticUpdate.ts b/src/api/book-club/react-query/likeOptimisticUpdate.ts new file mode 100644 index 00000000..f509ca45 --- /dev/null +++ b/src/api/book-club/react-query/likeOptimisticUpdate.ts @@ -0,0 +1,60 @@ +import { QueryClient } from '@tanstack/react-query'; +import { BookClub } from '@/types/bookclubs'; +import { bookClubs } from './queries'; +import { DEFAULT_FILTERS } from '@/lib/constants/filters'; + +export const likeOnMutate = async ( + queryClient: QueryClient, + id: number, + isLiked: boolean, +) => { + const listQueryKey = bookClubs.list(DEFAULT_FILTERS).queryKey; + const detailQueryKey = bookClubs.detail(id).queryKey; + + const previousBookClubs = queryClient.getQueryData<{ + bookClubs: BookClub[]; + }>(listQueryKey); + + const previousDetail = queryClient.getQueryData(detailQueryKey); + + // 목록 캐시 업데이트 + if (previousBookClubs) { + queryClient.setQueryData(listQueryKey, { + ...previousBookClubs, + bookClubs: previousBookClubs.bookClubs.map((club) => + club.id === id ? { ...club, isLiked } : club, + ), + }); + } + + // 상세 캐시 업데이트 + if (previousDetail) { + queryClient.setQueryData(detailQueryKey, { + ...previousDetail, + isLiked, + }); + } + + return { previousBookClubs, previousDetail }; +}; + +export const likeOnError = ( + queryClient: QueryClient, + id: number, + context: { + previousBookClubs?: { bookClubs: BookClub[] }; + previousDetail?: BookClub; + }, +) => { + const listQueryKey = bookClubs.list(DEFAULT_FILTERS).queryKey; + const detailQueryKey = bookClubs.detail(id).queryKey; + + // 이전 상태 복구 + if (context.previousBookClubs) { + queryClient.setQueryData(listQueryKey, context.previousBookClubs); + } + + if (context.previousDetail) { + queryClient.setQueryData(detailQueryKey, context.previousDetail); + } +}; diff --git a/src/components/common-layout/FilterBar.tsx b/src/components/common-layout/FilterBar.tsx index 3f650976..e992ebc1 100644 --- a/src/components/common-layout/FilterBar.tsx +++ b/src/components/common-layout/FilterBar.tsx @@ -3,24 +3,14 @@ import { SearchSection, FilterSection, } from '@/components/common-layout'; -import { BookClub, BookClubParams } from '@/types/bookclubs'; -import { Dispatch, SetStateAction } from 'react'; +import { BookClubParams } from '@/types/bookclubs'; interface FilterBarProps { filters: BookClubParams; handleFilterChange: (newFilter: Partial) => void; - bookClubs: BookClub[]; - initialBookClubs: BookClub[]; - setBookClubs: Dispatch>; } -function FilterBar({ - filters, - handleFilterChange, - bookClubs, - initialBookClubs, - setBookClubs, -}: FilterBarProps) { +function FilterBar({ filters, handleFilterChange }: FilterBarProps) { return (
@@ -30,12 +20,7 @@ function FilterBar({ handleFilterChange({ searchKeyword: value }) } /> - +
); } diff --git a/src/components/common-layout/FilterSection.tsx b/src/components/common-layout/FilterSection.tsx index 2dfa8c8c..28fc3851 100644 --- a/src/components/common-layout/FilterSection.tsx +++ b/src/components/common-layout/FilterSection.tsx @@ -2,45 +2,23 @@ import DropDown from '@/components/drop-down/DropDown'; import FilterCheckbox from '@/components/filter-checkbox/FilterCheckbox'; -import { ChangeEvent, Dispatch, SetStateAction, useState } from 'react'; +import { ChangeEvent, useState } from 'react'; import SortingButton from '@/components/sorting-button/SortingButton'; -import { BookClub, BookClubParams } from '../../types/bookclubs'; +import { BookClubParams } from '../../types/bookclubs'; import { getMeetingType, getMemberLimit } from '@/lib/utils/filterUtils'; -import { clubStatus } from '@/lib/utils/clubUtils'; interface CategoryTabsProps { - bookClubs: BookClub[]; - initialBookClubs: BookClub[]; - setBookClubs: Dispatch>; onFilterChange: (newFilters: Partial) => void; } -function FilterSection({ - bookClubs, - initialBookClubs, - setBookClubs, - onFilterChange, -}: CategoryTabsProps) { +function FilterSection({ onFilterChange }: CategoryTabsProps) { const [showAvailableOnly, setShowAvailableOnly] = useState(false); // 신청가능 const toggleAvailableOnly = (e: ChangeEvent) => { const isChecked = e.target.checked; setShowAvailableOnly(isChecked); - const filteredBookClubs = isChecked - ? bookClubs.filter( - (club) => - club.memberCount < club.memberLimit && - clubStatus( - club.memberCount, - club.memberLimit, - club.endDate, - new Date(), - ) !== 'closed', - ) - : initialBookClubs; - - setBookClubs(filteredBookClubs); + onFilterChange({ isAvailable: isChecked }); }; const updateMemberLimitFilter = (selectedValue: string | undefined) => { diff --git a/src/features/bookclub/components/BookClubMainPage.tsx b/src/features/bookclub/components/BookClubMainPage.tsx index 8d027d28..4cdc7632 100644 --- a/src/features/bookclub/components/BookClubMainPage.tsx +++ b/src/features/bookclub/components/BookClubMainPage.tsx @@ -9,14 +9,7 @@ import { useRouter } from 'next/navigation'; import Loading from '@/components/loading/Loading'; function BookClubMainPage() { - const { - clubList, - initialBookClubs, - setClubList, - isLoading, - filters, - updateFilters, - } = useBookClubList(); + const { clubList, isLoading, filters, updateFilters } = useBookClubList(); const router = useRouter(); @@ -48,13 +41,7 @@ function BookClubMainPage() { /> } /> - + {isLoading ? (
diff --git a/src/features/bookclub/hooks/useFetchBookClubList.ts b/src/features/bookclub/hooks/useFetchBookClubList.ts index 974374af..8d72f6a1 100644 --- a/src/features/bookclub/hooks/useFetchBookClubList.ts +++ b/src/features/bookclub/hooks/useFetchBookClubList.ts @@ -1,34 +1,19 @@ -import { useState, useEffect } from 'react'; -import { BookClub, BookClubParams } from '@/types/bookclubs'; +import { useState } from 'react'; +import { BookClubParams } from '@/types/bookclubs'; import { useQuery } from '@tanstack/react-query'; import { bookClubs } from '@/api/book-club/react-query'; +import { DEFAULT_FILTERS } from '@/lib/constants/filters'; const useBookClubList = () => { - // TODO: 신청 가능 필터 param 추가시 clubList, initialBookClubs 상태 관리x - const [clubList, setClubList] = useState([]); - const [initialBookClubs, setInitialBookClubs] = useState([]); - const [filters, setFilters] = useState({ - bookClubType: 'ALL', - meetingType: 'ALL', - order: 'DESC', - page: 1, - size: 10, - searchKeyword: '', - }); + const [filters, setFilters] = useState(DEFAULT_FILTERS); const { data, isLoading, error } = useQuery({ ...bookClubs.list(filters), }); - const clubInfo = data?.bookClubs; + const clubList = data?.bookClubs; - // TODO: param 추가시, useEffect 대신 clubInfo 직접 사용 - useEffect(() => { - if (clubInfo) { - setClubList(clubInfo); - setInitialBookClubs(clubInfo); // 초기 데이터 설정 - } - }, [clubInfo]); + // console.log('useQuery 데이터:', clubList); const updateFilters = (newFilters: Partial) => { setFilters((prevFilters) => ({ ...prevFilters, ...newFilters })); @@ -36,8 +21,6 @@ const useBookClubList = () => { return { clubList, - initialBookClubs, - setClubList, isLoading, error, filters, diff --git a/src/features/club-details/components/ReviewSummary.tsx b/src/features/club-details/components/ReviewSummary.tsx index 3abb9e6c..3c61c75e 100644 --- a/src/features/club-details/components/ReviewSummary.tsx +++ b/src/features/club-details/components/ReviewSummary.tsx @@ -7,6 +7,10 @@ import { } from '@/features/club-details/utils/rating'; function ReviewSummary({ reviewInfo }: { reviewInfo: ClubReviewResponse }) { + const validAverageScore = isNaN(reviewInfo.averageScore) + ? 0 + : reviewInfo.averageScore; + return (

@@ -20,10 +24,10 @@ function ReviewSummary({ reviewInfo }: { reviewInfo: ClubReviewResponse }) { >
- {reviewInfo.averageScore} + {validAverageScore} / 5
- +
{[5, 4, 3, 2, 1].map((score) => ( diff --git a/src/features/club-wish/components/WishPage.tsx b/src/features/club-wish/components/WishPage.tsx index 272aa81b..d9a165fe 100644 --- a/src/features/club-wish/components/WishPage.tsx +++ b/src/features/club-wish/components/WishPage.tsx @@ -18,13 +18,7 @@ function WishPage() { } /> - {}} - bookClubs={mockBookClubs} - setBookClubs={() => {}} - initialBookClubs={[]} - /> + {}} /> ); diff --git a/src/lib/constants/filters.ts b/src/lib/constants/filters.ts new file mode 100644 index 00000000..0ee948a3 --- /dev/null +++ b/src/lib/constants/filters.ts @@ -0,0 +1,10 @@ +import { BookClubParams } from '@/types/bookclubs'; + +export const DEFAULT_FILTERS: BookClubParams = { + bookClubType: 'ALL', + meetingType: 'ALL', + order: 'DESC', + page: 1, + size: 10, + searchKeyword: '', +}; diff --git a/src/types/bookclubs.ts b/src/types/bookclubs.ts index 45d1426f..81b9c090 100644 --- a/src/types/bookclubs.ts +++ b/src/types/bookclubs.ts @@ -10,6 +10,7 @@ export interface BookClubParams { searchKeyword?: string; memberLimitMin?: number; memberLimitMax?: number; + isAvailable?: boolean; } export interface MyProfileParams {