Skip to content

Commit

Permalink
feat: 북마크 낙관적 업데이트
Browse files Browse the repository at this point in the history
  • Loading branch information
jw-r committed Dec 23, 2024
1 parent b3a436e commit 1de016c
Show file tree
Hide file tree
Showing 4 changed files with 4,642 additions and 4,676 deletions.
1 change: 1 addition & 0 deletions src/features/collection/components/collection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const Collection = ({
name="book-mark"
className="size-[24px] cursor-pointer"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
bookmarkMutate({ collectionId, isBookMarked: false })
}}
Expand Down
41 changes: 37 additions & 4 deletions src/features/collection/components/detail-info/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
'use client'

import QuizCard from '@/features/quiz/components/quiz-card'
import { useCollectionInfo } from '@/requests/collection/hooks'
import { useBookmarkMutation, useCollectionInfo } from '@/requests/collection/hooks'
import { useCollectionQuizzesInfo } from '@/requests/quiz/hooks'
import CategoryTag from '@/shared/components/custom/category-tag'
import FixedBottom from '@/shared/components/custom/fixed-bottom'
import Icon from '@/shared/components/custom/icon'
import Loading from '@/shared/components/custom/loading'
import { Button } from '@/shared/components/ui/button'
import Text from '@/shared/components/ui/text'
Expand All @@ -23,6 +24,7 @@ const DetailInfo = ({ id }: Props) => {
const { user } = useUser()
const { data: collectionData } = useCollectionInfo(id)
const { mutate: createQuizSetMutate } = useCollectionQuizzesInfo()
const { mutate: bookmarkMutate } = useBookmarkMutation()

const quizCounts = useMemo(() => {
if (!collectionData?.quizzes) return { multiple: 0, ox: 0 }
Expand Down Expand Up @@ -61,6 +63,8 @@ const DetailInfo = ({ id }: Props) => {
/** TODO: Spinner로 대체 */
if (!collectionData) return <Loading center size="xs" />

const isOwner = user?.id === collectionData.member.creatorId

return (
<>
<div>
Expand All @@ -76,9 +80,38 @@ const DetailInfo = ({ id }: Props) => {
<div className="h-px w-full bg-border-divider" />
<div className="p-[24px_16px_64px_16px]">
<div>
<Text typography="subtitle1-bold" className="text-text-primary">
{quizCounts.multiple + quizCounts.ox} 문제
</Text>
<div className="flex items-center justify-between">
<Text typography="subtitle1-bold" className="text-text-primary">
{quizCounts.multiple + quizCounts.ox} 문제
</Text>
<div className="flex flex-col items-center gap-[4px]">
{isOwner && (
<Icon name="book-mark-fill" className="size-[24px] text-icon-disabled" />
)}
{!isOwner &&
(collectionData.bookmarked ? (
<Icon
name="book-mark-fill"
className="size-[24px] cursor-pointer"
onClick={() => {
bookmarkMutate({ collectionId: collectionData.id, isBookMarked: true })
}}
/>
) : (
<Icon
name="book-mark"
className="size-[24px] cursor-pointer"
onClick={() => {
bookmarkMutate({ collectionId: collectionData.id, isBookMarked: false })
}}
/>
))}
<Text typography="text2-medium" className="text-text-caption">
{collectionData.bookmarkCount}
</Text>
</div>
</div>

<div className="mt-[8px] flex items-center gap-[8px]">
<Text typography="text1-medium" className="text-text-sub">
객관식 {quizCounts.multiple}
Expand Down
159 changes: 106 additions & 53 deletions src/requests/collection/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,80 +84,133 @@ export const useBookmarkMutation = () => {
await Promise.all([
queryClient.cancelQueries({ queryKey: ['collections'] }),
queryClient.cancelQueries({ queryKey: ['bookmarkedCollections'] }),
queryClient.cancelQueries({ queryKey: ['collectionInfo', collectionId] }),
])

// 이전 데이터 스냅샷 저장
const previousCollections = queryClient.getQueryData<Collection.Response.GetAllCollections>([
'collections',
])
const collectionsQueries = queryClient.getQueriesData<Collection.Response.GetAllCollections>({
queryKey: ['collections'],
})

const previousDataMap = new Map()

// 모든 collections 쿼리에 대해 낙관적 업데이트 수행
collectionsQueries.forEach(([queryKey, data]) => {
if (data?.collections) {
// 이전 데이터 저장
previousDataMap.set(queryKey, data)

// 낙관적 업데이트
queryClient.setQueryData(queryKey, {
...data,
collections: data.collections.map((collection) => {
if (collection.id === collectionId) {
return {
...collection,
bookmarked: !isBookMarked,
bookmarkCount: isBookMarked
? collection.bookmarkCount - 1
: collection.bookmarkCount + 1,
}
}
return collection
}),
})
}
})

// 북마크된 컬렉션 데이터 처리
const previousBookmarkedCollections =
queryClient.getQueryData<Collection.Response.GetBookmarkedCollections>([
'bookmarkedCollections',
])

// 전체 컬렉션 데이터 낙관적 업데이트
queryClient.setQueryData(['collections'], (old: Collection.Response.GetAllCollections) => {
const newCollections = {
...old,
collections: old.collections.map((collection) => {
if (collection.id === collectionId) {
return {
...collection,
isBookmarked: !collection.bookmarked,
bookmarkCount: isBookMarked
? collection.bookmarkCount - 1
: collection.bookmarkCount + 1,
}
if (previousBookmarkedCollections?.collections) {
if (isBookMarked) {
// 북마크 제거 시
queryClient.setQueryData<Collection.Response.GetBookmarkedCollections>(
['bookmarkedCollections'],
{
...previousBookmarkedCollections,
collections: previousBookmarkedCollections.collections.filter(
(collection) => collection.id !== collectionId
),
}
return collection
}),
}
return newCollections
})

// 북마크된 컬렉션 데이터 낙관적 업데이트
queryClient.setQueryData(
['bookmarkedCollections'],
(old: Collection.Response.GetBookmarkedCollections) => {
if (isBookMarked) {
// 북마크 제거 시 해당 컬렉션을 목록에서 제거
return {
...old,
collections: old.collections.filter((collection) => collection.id !== collectionId),
)
} else {
// 북마크 추가 시
// 가장 최신의 collection 데이터를 찾기 위해 모든 collections 쿼리를 확인
let collectionToAdd = null
for (const [, data] of collectionsQueries) {
const found = data?.collections.find((collection) => collection.id === collectionId)
if (found) {
collectionToAdd = found
break
}
} else {
// 북마크 추가 시 해당 컬렉션을 목록에 추가
// collections 쿼리에서 해당 컬렉션 정보를 가져옴
const collectionsData = queryClient.getQueryData<Collection.Response.GetAllCollections>(
['collections']
)
const collectionToAdd = collectionsData?.collections.find(
(collection) => collection.id === collectionId
)
}

if (collectionToAdd) {
return {
...old,
collections: [...old.collections, collectionToAdd],
if (collectionToAdd) {
queryClient.setQueryData<Collection.Response.GetBookmarkedCollections>(
['bookmarkedCollections'],
{
...previousBookmarkedCollections,
collections: [
...previousBookmarkedCollections.collections,
{
...collectionToAdd,
bookmarked: true,
bookmarkCount: collectionToAdd.bookmarkCount + 1,
},
],
}
}
return old
)
}
}
)
}

// 컬렉션 상세 낙관적 업데이트
const previousCollectionInfo =
queryClient.getQueryData<Collection.Response.GetCollectionInfo>([
'collectionInfo',
collectionId,
])

return { previousCollections, previousBookmarkedCollections }
if (previousCollectionInfo) {
queryClient.setQueryData<Collection.Response.GetCollectionInfo>(
['collectionInfo', collectionId],
{
...previousCollectionInfo,
bookmarked: !isBookMarked,
bookmarkCount: isBookMarked
? previousCollectionInfo.bookmarkCount - 1
: previousCollectionInfo.bookmarkCount + 1,
}
)
}

return { previousDataMap, previousBookmarkedCollections, previousCollectionInfo }
},
onError: (err, variables, context) => {
onError: (_, __, context) => {
// 에러 발생 시 이전 데이터로 복구
queryClient.setQueryData(['collections'], context?.previousCollections)
queryClient.setQueryData(['bookmarkedCollections'], context?.previousBookmarkedCollections)
if (context?.previousDataMap) {
context.previousDataMap.forEach((data, queryKey) => {
queryClient.setQueryData(queryKey, data)
})
}
if (context?.previousBookmarkedCollections) {
queryClient.setQueryData(['bookmarkedCollections'], context.previousBookmarkedCollections)
}
if (context?.previousCollectionInfo) {
queryClient.setQueryData(
['collectionInfo', context.previousCollectionInfo.id],
context.previousCollectionInfo
)
}
},
onSettled: async () => {
// 작업 완료 후 캐시 무효화
await Promise.all([
queryClient.invalidateQueries({ queryKey: ['collections'] }),
queryClient.invalidateQueries({ queryKey: ['bookmarkedCollections'] }),
queryClient.invalidateQueries({ queryKey: ['collectionInfo'] }),
])
},
})
Expand Down
Loading

0 comments on commit 1de016c

Please sign in to comment.