diff --git a/src/api/auth/authClientAPI.ts b/src/api/auth/authClientAPI.ts index dc17ff46..bbd87291 100644 --- a/src/api/auth/authClientAPI.ts +++ b/src/api/auth/authClientAPI.ts @@ -1,16 +1,16 @@ -import { EditInfoParams } from '@/features/profile/types'; import apiClient from '@/lib/utils/apiClient'; export const authClientAPI = { //회원정보 확인 //회원정보 수정 - editInfo: async ({ nickname, image, description }: EditInfoParams) => { - await apiClient.post('auths/user', { - nickname, - image, - description, + editInfo: async (formData: FormData) => { + const response = await apiClient.post('auths/user', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, }); + return response.data; }, //회원가입 diff --git a/src/api/auth/react-query/customHooks.ts b/src/api/auth/react-query/customHooks.ts index 74958a5a..58834975 100644 --- a/src/api/auth/react-query/customHooks.ts +++ b/src/api/auth/react-query/customHooks.ts @@ -2,12 +2,11 @@ import { useMutation } from '@tanstack/react-query'; import { showToast } from '@/components/toast/toast'; import { authClientAPI } from '../authClientAPI'; import { getUserInfo } from '@/features/auth/api/auth'; -import { EditInfoParams } from '@/features/profile/types'; //프로필 수정하기 -export function useEditInfo() { +export function useEditInfoMutation() { return useMutation({ - mutationFn: (data: EditInfoParams) => authClientAPI.editInfo(data), + mutationFn: (formData: FormData) => authClientAPI.editInfo(formData), onSuccess: () => { getUserInfo(); showToast({ message: '프로필 수정이 완료되었습니다.', type: 'success' }); diff --git a/src/api/book-club/bookClubMainAPI.ts b/src/api/book-club/bookClubMainAPI.ts index 12ad39cc..a37a09e5 100644 --- a/src/api/book-club/bookClubMainAPI.ts +++ b/src/api/book-club/bookClubMainAPI.ts @@ -22,7 +22,7 @@ export const bookClubMainAPI = { //유저가 참가한 북클럽 조회 userJoined: async (userId: number, params?: MyProfileParams) => { - const response = await apiClient.get(`/book-clubs/user/${userId}/joined`, { + const response = await apiClient.get(`/book-clubs/users/${userId}/joined`, { params, }); return response.data; @@ -30,9 +30,12 @@ export const bookClubMainAPI = { //유저가 만든 북클럽 조회 userCreated: async (userId: number, params?: MyProfileParams) => { - const response = await apiClient.get(`/book-clubs/user/${userId}/created`, { - params, - }); + const response = await apiClient.get( + `/book-clubs/users/${userId}/created`, + { + params, + }, + ); return response.data; }, diff --git a/src/api/book-club/react-query/customHooks.ts b/src/api/book-club/react-query/customHooks.ts index defec8ef..dcfdc23e 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,14 +108,22 @@ export function useLikeBookClub() { return useMutation, number>({ mutationFn: (id: number) => bookClubLikeAPI.like(id), - onSuccess: (_, id) => { - queryClient.invalidateQueries({ - queryKey: bookClubs.list().queryKey, - }); + + onMutate: async (id) => { + return likeOnMutate(queryClient, id, true); + }, + //TODO: 로직 확인 후 변경 필요 + onSuccess: () => { queryClient.invalidateQueries({ - queryKey: bookClubs.detail(id).queryKey, + queryKey: bookClubs._def, }); }, + + onError: (_error, id, context) => { + if (context) { + likeOnError(queryClient, id, context); + } + }, }); } @@ -120,13 +132,21 @@ export function useUnLikeBookClub() { return useMutation, number>({ mutationFn: (id: number) => bookClubLikeAPI.unlike(id), - onSuccess: (_, id) => { - queryClient.invalidateQueries({ - queryKey: bookClubs.list().queryKey, - }); + + onMutate: async (id) => { + return likeOnMutate(queryClient, id, false); + }, + //TODO: 로직 확인 후 변경 필요 + onSuccess: () => { queryClient.invalidateQueries({ - queryKey: bookClubs.detail(id).queryKey, + queryKey: bookClubs._def, }); }, + + 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..86a0a536 --- /dev/null +++ b/src/api/book-club/react-query/likeOptimisticUpdate.ts @@ -0,0 +1,65 @@ +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, + }); + } + + //TODO: 로직 확인 후 변경 필요 + queryClient.invalidateQueries({ + queryKey: bookClubs._def, + }); + + 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/app/chat/[id]/page.tsx b/src/app/chat/[id]/page.tsx index f3d7d151..74df14f6 100644 --- a/src/app/chat/[id]/page.tsx +++ b/src/app/chat/[id]/page.tsx @@ -39,7 +39,7 @@ function ChatRoomPage() { const [isConnected, setIsConnected] = useState(false); const { data } = useQuery( - bookClubs.my()._ctx.joined({ order: 'DESC', page: 1, size: 10 }), + bookClubs.my()._ctx.joined({ order: 'DESC', page: 1, size: 100 }), ); const bookClubDetail = data?.bookClubs?.find( @@ -64,17 +64,13 @@ function ChatRoomPage() { await new Promise((resolve, reject) => { const checkConnection = setInterval(() => { - console.log(`소켓 연결 시도 ${attempts + 1}회`); - if (client?.connected) { - console.log('소켓 연결 성공!'); clearInterval(checkConnection); resolve(true); } attempts++; if (attempts >= maxAttempts) { - console.log('소켓 연결 최대 시도 횟수 초과'); clearInterval(checkConnection); reject(new Error('소켓 연결 타임아웃')); } @@ -181,7 +177,7 @@ function ChatRoomPage() { }; return ( -
+
@@ -192,12 +188,12 @@ function ChatRoomPage() { className="bg-gray-light-02" />

채팅

- +
} - onClick={() => console.log('메뉴 열기 버튼 클릭')} + onClick={() => {}} className="bg-gray-light-02" />
@@ -226,7 +222,7 @@ function ChatRoomPage() { onProfileClick={() => {}} />
-
+
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/components/header/HeaderBar.tsx b/src/components/header/HeaderBar.tsx index 3cfba709..34a905db 100644 --- a/src/components/header/HeaderBar.tsx +++ b/src/components/header/HeaderBar.tsx @@ -7,6 +7,7 @@ import { usePathname, useRouter } from 'next/navigation'; import { useAuthStore } from '@/store/authStore'; import DropDown from '../drop-down/DropDown'; import { logout } from '@/features/auth/api/auth'; +import { showToast } from '../toast/toast'; function HeaderBar() { const pathname = usePathname(); @@ -17,6 +18,7 @@ function HeaderBar() { if (value === 'LOGOUT') { try { await logout(); + showToast({ message: '로그아웃 되었습니다 ', type: 'success' }); router.replace('/bookclub'); } catch (error) { console.error('로그아웃 실패:', error); diff --git a/src/constants/avatar.ts b/src/constants/avatar.ts index 509cb089..3105d6d9 100644 --- a/src/constants/avatar.ts +++ b/src/constants/avatar.ts @@ -5,6 +5,7 @@ export const AVATAR_SIZE = { lg: 'h-[56px] w-[56px]', xl: 'h-[74px] w-[71px]', max: 'h-[74px] w-[74px]', + profile: 'h-[80px] w-[80px]', } as const; export type AvatarSize = keyof typeof AVATAR_SIZE; diff --git a/src/constants/navigation.ts b/src/constants/navigation.ts index 52a930ff..4ef5d0c5 100644 --- a/src/constants/navigation.ts +++ b/src/constants/navigation.ts @@ -2,7 +2,7 @@ export const NAV_ITEMS = [ { id: 'bookco', href: '/bookclub', label: 'bookco' }, { id: 'bookclub', href: '/bookclub', label: '책 모임' }, { id: 'exchange', href: '/exchange', label: '책 교환' }, - { id: 'wish', href: '/wish', label: '찜 목록' }, + // { id: 'wish', href: '/wish', label: '찜 목록' }, { id: 'chat', href: '/chat', label: '채팅' }, ] as const; diff --git a/src/features/auth/api/auth.ts b/src/features/auth/api/auth.ts index 01abd80b..41db7216 100644 --- a/src/features/auth/api/auth.ts +++ b/src/features/auth/api/auth.ts @@ -30,7 +30,7 @@ export const login = async (data: LoginFormData) => { await getUserInfo(); const token = getCookie('auth_token'); - console.log('token', token); + if (token) { initializeSocket(token); } diff --git a/src/features/auth/api/refreshAccessToken.ts b/src/features/auth/api/refreshAccessToken.ts index 453566c9..f5b9c73d 100644 --- a/src/features/auth/api/refreshAccessToken.ts +++ b/src/features/auth/api/refreshAccessToken.ts @@ -15,7 +15,6 @@ export const refreshAccessToken = async (refreshToken: string) => { throw new Error('토큰 갱신 실패'); } - console.log('리프레시 성공'); return response.json(); } catch (error) { console.error('토큰 갱신 에러:', error); diff --git a/src/features/bookclub/components/BookClubMainPage.tsx b/src/features/bookclub/components/BookClubMainPage.tsx index 8d027d28..f434bbdf 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(); @@ -40,6 +33,7 @@ function BookClubMainPage() { } actionElement={