From 40af2153b87c84ba6e06ce4e681863e530c04666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A5=98=EC=A0=95=EC=9A=B0?= <88191233+jw-r@users.noreply.github.com> Date: Thu, 5 Dec 2024 20:07:06 +0900 Subject: [PATCH] feat: bookmark collection (#299) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: bookmark collection * feat: 컬렉션 즐겨찾기 --- src/actions/fetchers/user/get-user/index.ts | 1 + src/features/category/config/index.ts | 11 ++ .../collection/components/collection.tsx | 38 ++++-- .../collection/components/exploration.tsx | 17 ++- .../collection/components/my-collection.tsx | 117 +++++++++++------- .../components/start-quiz-drawer.tsx | 34 ++--- src/requests/collection/hooks.ts | 56 ++++++++- src/types/collection.d.ts | 26 ++-- 8 files changed, 207 insertions(+), 93 deletions(-) diff --git a/src/actions/fetchers/user/get-user/index.ts b/src/actions/fetchers/user/get-user/index.ts index 193085f0..bc996aca 100644 --- a/src/actions/fetchers/user/get-user/index.ts +++ b/src/actions/fetchers/user/get-user/index.ts @@ -4,6 +4,7 @@ import { apiClient } from '@/actions/api-client' import { API_ENDPOINT } from '@/actions/endpoints' export interface GetUserResponse { + id: number name: string email: string role: 'ROLE_USER' | 'ROLE_ADMIN' diff --git a/src/features/category/config/index.ts b/src/features/category/config/index.ts index 49f35d09..fa362ae4 100644 --- a/src/features/category/config/index.ts +++ b/src/features/category/config/index.ts @@ -1,47 +1,58 @@ type Category = { code: Collection.Field name: string + emoji: string } export const CATEGORIES: Category[] = [ { code: 'IT', name: 'IT·프로그래밍', + emoji: '🤖', }, { code: 'LAW', name: '법률·정치', + emoji: '📖', }, { code: 'BUSINESS_ECONOMY', name: '경제·경영', + emoji: '💰', }, { code: 'SOCIETY_POLITICS', name: '사회·정치', + emoji: '⚖️', }, { code: 'LANGUAGE', name: '언어', + emoji: '💬', }, { code: 'MEDICINE_PHARMACY', name: '의학·약학', + emoji: '🩺', }, { code: 'ART', name: '예술', + emoji: '🎨', }, { code: 'SCIENCE_ENGINEERING', name: '과학·공학', + emoji: '🔬', }, { code: 'HISTORY_PHILOSOPHY', name: '역사·철학', + emoji: '📜', }, { code: 'OTHER', name: '기타', + emoji: '♾️', }, ] diff --git a/src/features/collection/components/collection.tsx b/src/features/collection/components/collection.tsx index 0b548e0a..720edd0b 100644 --- a/src/features/collection/components/collection.tsx +++ b/src/features/collection/components/collection.tsx @@ -12,7 +12,8 @@ interface Props extends HTMLAttributes { category: string problemCount: number lastUpdated: string - isBookMarked: boolean + isBookMarked?: boolean + isOwner?: boolean bookMarkCount: number } @@ -23,7 +24,8 @@ const Collection = ({ category, problemCount, lastUpdated, - isBookMarked, + isBookMarked = false, + isOwner = false, bookMarkCount, className, }: Props) => { @@ -33,23 +35,35 @@ const Collection = ({
- {isBookMarked ? ( + {isOwner && ( { e.stopPropagation() - bookmarkMutate({ collectionId, isBookMarked: true }) - }} - /> - ) : ( - { - e.stopPropagation() - bookmarkMutate({ collectionId, isBookMarked: false }) }} /> )} + {!isOwner && + (isBookMarked ? ( + { + e.stopPropagation() + bookmarkMutate({ collectionId, isBookMarked: true }) + }} + /> + ) : ( + { + e.stopPropagation() + bookmarkMutate({ collectionId, isBookMarked: false }) + }} + /> + ))} {bookMarkCount} diff --git a/src/features/collection/components/exploration.tsx b/src/features/collection/components/exploration.tsx index 276e6d50..4de98875 100644 --- a/src/features/collection/components/exploration.tsx +++ b/src/features/collection/components/exploration.tsx @@ -7,12 +7,14 @@ import Text from '@/shared/components/ui/text' import StartQuizDrawer from './start-quiz-drawer' import { useBookmarkedCollections, useCollections } from '@/requests/collection/hooks' import Loading from '@/shared/components/custom/loading' +import { useUser } from '@/shared/hooks/use-user' const controlButtons = ['분야', '퀴즈 유형', '문제 수'] const Exploration = () => { const { data: collectionsData, isLoading } = useCollections() const { data: bookmarkedCollections, isLoading: isBookmarkedLoading } = useBookmarkedCollections() + const { user } = useUser() return ( <> @@ -43,17 +45,23 @@ const Exploration = () => { (bookmarkedCollection) => bookmarkedCollection.id === collection.id ) ) + const multipleChoiceCount = + collection.quizzes?.filter((quiz) => quiz.quizType === 'MULTIPLE_CHOICE').length ?? 0 + const oxCount = + collection.quizzes?.filter((quiz) => quiz.quizType === 'MIX_UP').length ?? 0 + return ( { emoji={collection.emoji} title={collection.name} category={collection.collectionField} - problemCount={collection.quizCount} + problemCount={multipleChoiceCount + oxCount} lastUpdated="2일 전" + isOwner={user?.id === collection.member.creatorId} isBookMarked={isBookmarked} bookMarkCount={collection.bookmarkCount} /> diff --git a/src/features/collection/components/my-collection.tsx b/src/features/collection/components/my-collection.tsx index e3ccc9d4..8d24e61e 100644 --- a/src/features/collection/components/my-collection.tsx +++ b/src/features/collection/components/my-collection.tsx @@ -25,6 +25,7 @@ const MyCollection = () => { const { data: myCollectionsData, isLoading: isMyCollectionLoading } = useMyCollections() const { data: bookmarkedCollectionsData, isLoading: isBookmarkedCollectionLoading } = useBookmarkedCollections() + const { data: bookmarkedCollections, isLoading: isBookmarkedLoading } = useBookmarkedCollections() return ( <> @@ -62,64 +63,86 @@ const MyCollection = () => { 만들기 - {myCollectionsData?.collections.map((collection) => ( - - } - /> - ))} + {myCollectionsData?.collections.map((collection) => { + const multipleChoiceCount = + collection.quizzes?.filter((quiz) => quiz.quizType === 'MULTIPLE_CHOICE') + .length ?? 0 + const oxCount = + collection.quizzes?.filter((quiz) => quiz.quizType === 'MIX_UP').length ?? 0 + + return ( + + } + /> + ) + })} ), 'save-collection': isBookmarkedCollectionLoading ? ( ) : ( - {bookmarkedCollectionsData?.collections.map((collection) => ( - { + const isBookmarked = Boolean( + bookmarkedCollections?.collections.some( + (bookmarkedCollection) => bookmarkedCollection.id === collection.id + ) + ) + const multipleChoiceCount = + collection.quizzes?.filter((quiz) => quiz.quizType === 'MULTIPLE_CHOICE') + .length ?? 0 + const oxCount = + collection.quizzes?.filter((quiz) => quiz.quizType === 'MIX_UP').length ?? 0 + + return ( + + } /> - } - /> - ))} + ) + })} ), }} diff --git a/src/features/collection/components/start-quiz-drawer.tsx b/src/features/collection/components/start-quiz-drawer.tsx index 6a314bb9..543a9d67 100644 --- a/src/features/collection/components/start-quiz-drawer.tsx +++ b/src/features/collection/components/start-quiz-drawer.tsx @@ -17,7 +17,8 @@ interface StartQuizDrawerProps { multipleChoiceCount: number oxCount: number description: string - isBookMarked: boolean + isBookMarked?: boolean + isOwner?: boolean bookMarkCount: number } @@ -30,7 +31,8 @@ const StartQuizDrawer = ({ multipleChoiceCount, oxCount, description, - isBookMarked, + isBookMarked = false, + isOwner = false, bookMarkCount, }: StartQuizDrawerProps) => { const { mutate: bookmarkMutate } = useBookmarkMutation() @@ -56,19 +58,21 @@ const StartQuizDrawer = ({
- {isBookMarked ? ( - bookmarkMutate({ collectionId, isBookMarked: true })} - /> - ) : ( - bookmarkMutate({ collectionId, isBookMarked: false })} - /> - )} + {isOwner && } + {!isOwner && + (isBookMarked ? ( + bookmarkMutate({ collectionId, isBookMarked: true })} + /> + ) : ( + bookmarkMutate({ collectionId, isBookMarked: false })} + /> + ))} {bookMarkCount} diff --git a/src/requests/collection/hooks.ts b/src/requests/collection/hooks.ts index 95ef317f..7e0b9007 100644 --- a/src/requests/collection/hooks.ts +++ b/src/requests/collection/hooks.ts @@ -58,10 +58,22 @@ export const useBookmarkMutation = () => { return createBookmark(collectionId) }, onMutate: async ({ collectionId, isBookMarked }) => { - await queryClient.cancelQueries({ queryKey: ['collections'] }) + // 진행 중인 쿼리들 취소 + await Promise.all([ + queryClient.cancelQueries({ queryKey: ['collections'] }), + queryClient.cancelQueries({ queryKey: ['bookmarkedCollections'] }), + ]) - const previousCollections = queryClient.getQueryData(['collections']) + // 이전 데이터 스냅샷 저장 + const previousCollections = queryClient.getQueryData([ + 'collections', + ]) + const previousBookmarkedCollections = + queryClient.getQueryData([ + 'bookmarkedCollections', + ]) + // 전체 컬렉션 데이터 낙관적 업데이트 queryClient.setQueryData(['collections'], (old: Collection.Response.GetAllCollections) => { const newCollections = { ...old, @@ -69,7 +81,9 @@ export const useBookmarkMutation = () => { if (collection.id === collectionId) { return { ...collection, - isBookMarked: !isBookMarked, + bookmarkCount: isBookMarked + ? collection.bookmarkCount - 1 + : collection.bookmarkCount + 1, } } return collection @@ -78,12 +92,46 @@ export const useBookmarkMutation = () => { return newCollections }) - return { previousCollections } + // 북마크된 컬렉션 데이터 낙관적 업데이트 + queryClient.setQueryData( + ['bookmarkedCollections'], + (old: Collection.Response.GetBookmarkedCollections) => { + if (isBookMarked) { + // 북마크 제거 시 해당 컬렉션을 목록에서 제거 + return { + ...old, + collections: old.collections.filter((collection) => collection.id !== collectionId), + } + } else { + // 북마크 추가 시 해당 컬렉션을 목록에 추가 + // collections 쿼리에서 해당 컬렉션 정보를 가져옴 + const collectionsData = queryClient.getQueryData( + ['collections'] + ) + const collectionToAdd = collectionsData?.collections.find( + (collection) => collection.id === collectionId + ) + + if (collectionToAdd) { + return { + ...old, + collections: [...old.collections, collectionToAdd], + } + } + return old + } + } + ) + + return { previousCollections, previousBookmarkedCollections } }, onError: (err, variables, context) => { + // 에러 발생 시 이전 데이터로 복구 queryClient.setQueryData(['collections'], context?.previousCollections) + queryClient.setQueryData(['bookmarkedCollections'], context?.previousBookmarkedCollections) }, onSettled: async () => { + // 작업 완료 후 캐시 무효화 await Promise.all([ queryClient.invalidateQueries({ queryKey: ['collections'] }), queryClient.invalidateQueries({ queryKey: ['bookmarkedCollections'] }), diff --git a/src/types/collection.d.ts b/src/types/collection.d.ts index 7aea283e..77253ff8 100644 --- a/src/types/collection.d.ts +++ b/src/types/collection.d.ts @@ -28,11 +28,23 @@ type QuizWithType = BaseQuiz & { type Collection = { id: number name: string + description: string emoji: string bookmarkCount: number collectionField: CollectionField - memberName: string - quizCount: number + solvedMemberCount: number + member: { + creatorId: number + creatorName: string + } + quizzes: { + id: number + question: string + answer: string + explanation: string + options: string[] + quizType: QuizType + }[] } type CollectionQuizResult = { @@ -64,15 +76,7 @@ interface CollectionRecordResponse { } /** GET /api/v2/collections/{collection_id}/collection_info */ -interface CollectionInfoResponse { - id: number - name: string - emoji: string - description: string - solvedCount: number - bookmarkCount: number - quizzes: QuizWithType[] -} +type CollectionInfoResponse = Collection /** GET /api/v2/collections/my-collections */ interface MyCollectionsResponse {