diff --git a/apps/client/src/pages/myBookmark/MyBookmark.tsx b/apps/client/src/pages/myBookmark/MyBookmark.tsx index 66c87de2..b92d1b92 100644 --- a/apps/client/src/pages/myBookmark/MyBookmark.tsx +++ b/apps/client/src/pages/myBookmark/MyBookmark.tsx @@ -50,8 +50,11 @@ const MyBookmark = () => { const handleDeleteArticle = (id: number) => { deleteArticle(id, { onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['bookmarkReadArticles'] }); - queryClient.invalidateQueries({ queryKey: ['bookmarkUnreadArticles'] }); + queryClient.invalidateQueries({ queryKey: ['bookmarkArticles'] }); + queryClient.invalidateQueries({ queryKey: ['bookmarkArticlesCount'] }); + queryClient.invalidateQueries({ + queryKey: ['categoryBookmarkArticlesCount'], + }); queryClient.invalidateQueries({ queryKey: ['categoryBookmarkArticles'], }); diff --git a/apps/client/src/pages/myBookmark/apis/axios.ts b/apps/client/src/pages/myBookmark/apis/axios.ts index 15446227..9c799bc1 100644 --- a/apps/client/src/pages/myBookmark/apis/axios.ts +++ b/apps/client/src/pages/myBookmark/apis/axios.ts @@ -1,34 +1,48 @@ +import { + BookmarkArticlesCountResponse, + BookmarkArticlesResponse, + CategoryBookmarkArticleResponse, +} from '@pages/myBookmark/types/api'; import apiRequest from '@shared/apis/setting/axiosInstance'; -export const getBookmarkArticles = async (page: number, size: number) => { +export const getBookmarkArticles = async ( + readStatus: boolean | null, + page: number, + size: number +): Promise => { + const readStatusQuery = + readStatus === null ? '' : `&read-status=${readStatus}`; const { data } = await apiRequest.get( - `/api/v1/articles?page=${page}&size=${size}` + `/api/v3/articles?page=${page}&size=${size}${readStatusQuery}` ); return data.data; }; -export const getBookmarkUnreadArticles = async (page: number, size: number) => { - const { data } = await apiRequest.get( - `/api/v1/articles/unread?page=${page}&size=${size}` - ); - return data.data; -}; +export const getBookmarkArticlesCount = + async (): Promise => { + const { data } = await apiRequest.get(`/api/v3/articles/count`); + return data.data; + }; export const getCategoryBookmarkArticles = async ( categoryId: string | null, readStatus: boolean | null, page: number, size: number -) => { - if (readStatus === null) { - const { data } = await apiRequest.get( - `/api/v1/articles/category?categoryId=${categoryId}&page=${page}&size=${size}` - ); - return data.data; - } else { - const { data } = await apiRequest.get( - `/api/v1/articles/category?categoryId=${categoryId}&read-status=${readStatus}&page=${page}&size=${size}` - ); - return data.data; - } +): Promise => { + const readStatusQuery = + readStatus === null ? '' : `&read-status=${readStatus}`; + const { data } = await apiRequest.get( + `/api/v3/articles/category?category-id=${categoryId}&page=${page}&size=${size}${readStatusQuery}` + ); + return data.data; +}; + +export const getCategoryBookmarkArticlesCount = async ( + categoryId: string +): Promise => { + const { data } = await apiRequest.get( + `/api/v3/articles/category/count?category-id=${categoryId}` + ); + return data.data; }; diff --git a/apps/client/src/pages/myBookmark/apis/queries.ts b/apps/client/src/pages/myBookmark/apis/queries.ts index 2d864ae3..9b722659 100644 --- a/apps/client/src/pages/myBookmark/apis/queries.ts +++ b/apps/client/src/pages/myBookmark/apis/queries.ts @@ -1,27 +1,33 @@ -import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; +import { + useQuery, + useSuspenseInfiniteQuery, + useSuspenseQuery, +} from '@tanstack/react-query'; import { getBookmarkArticles, - getBookmarkUnreadArticles, + getBookmarkArticlesCount, getCategoryBookmarkArticles, + getCategoryBookmarkArticlesCount, } from './axios'; +import { + CategoryBookmarkArticleResponse, +} from '../types/api'; -export const useGetBookmarkArticles = () => { +export const useGetBookmarkArticles = (readStatus: boolean | null) => { return useSuspenseInfiniteQuery({ - queryKey: ['bookmarkReadArticles'], - queryFn: ({ pageParam = 0 }) => getBookmarkArticles(pageParam, 20), + queryKey: ['bookmarkArticles', readStatus], + queryFn: ({ pageParam = 0 }) => + getBookmarkArticles(readStatus, Number(pageParam), 20), initialPageParam: 0, getNextPageParam: (lastPage, allPages) => lastPage.articles.length === 0 ? undefined : allPages.length, }); }; -export const useGetBookmarkUnreadArticles = () => { - return useSuspenseInfiniteQuery({ - queryKey: ['bookmarkUnreadArticles'], - queryFn: ({ pageParam = 0 }) => getBookmarkUnreadArticles(pageParam, 20), - initialPageParam: 0, - getNextPageParam: (lastPage, allPages) => - lastPage.articles.length === 0 ? undefined : allPages.length, +export const useGetBookmarkArticlesCount = () => { + return useSuspenseQuery({ + queryKey: ['bookmarkArticlesCount'], + queryFn: getBookmarkArticlesCount, }); }; @@ -31,10 +37,20 @@ export const useGetCategoryBookmarkArticles = ( ) => { return useSuspenseInfiniteQuery({ queryKey: ['categoryBookmarkArticles', readStatus, categoryId], - queryFn: ({ pageParam = 0 }) => { - if (!categoryId) return null; - return getCategoryBookmarkArticles(categoryId, readStatus, pageParam, 20); + if (!categoryId) + return Promise.resolve({ + totalArticleCount: 0, + unreadArticleCount: 0, + categoryName: '', + articles: [], + }); + return getCategoryBookmarkArticles( + categoryId, + readStatus, + Number(pageParam), + 20 + ); }, initialPageParam: 0, @@ -44,3 +60,13 @@ export const useGetCategoryBookmarkArticles = ( }, }); }; + +export const useGetCategoryBookmarkArticlesCount = ( + categoryId: string | null +) => { + return useQuery({ + queryKey: ['categoryBookmarkArticlesCount', categoryId], + queryFn: () => getCategoryBookmarkArticlesCount(categoryId!), + enabled: !!categoryId, + }); +}; diff --git a/apps/client/src/pages/myBookmark/components/fetchCard/FetchCard.tsx b/apps/client/src/pages/myBookmark/components/fetchCard/FetchCard.tsx deleted file mode 100644 index 1bacbf66..00000000 --- a/apps/client/src/pages/myBookmark/components/fetchCard/FetchCard.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { BookmarkArticle } from '@pages/myBookmark/types/api'; -import { Card } from '@pinback/design-system/ui'; -import { useGetPageMeta } from '@shared/apis/queries'; -import React from 'react'; - -interface FetchCardProps { - article: BookmarkArticle; - onClick: () => void; - onOptionsClick?: (e: React.MouseEvent) => void; -} - -const FetchCard = ({ article, onClick, onOptionsClick }: FetchCardProps) => { - const { data: meta, isPending, isError: error } = useGetPageMeta(article.url); - - if (isPending) { - return ( -
- ); - } - - const displayTitle = !error && meta.title ? meta.title : '제목 없음'; - const displayImageUrl = !error ? meta.image : undefined; - - return ( - - ); -}; - -export default FetchCard; diff --git a/apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx b/apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx index e987ff9d..64c752da 100644 --- a/apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx +++ b/apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx @@ -1,15 +1,8 @@ -import { - useGetBookmarkArticles, - useGetBookmarkUnreadArticles, - useGetCategoryBookmarkArticles, -} from '@pages/myBookmark/apis/queries'; - -import { useInfiniteScroll } from '@shared/hooks/useInfiniteScroll'; -import FetchCard from '@pages/myBookmark/components/fetchCard/FetchCard'; +import { useMyBookmarkContentData } from '@pages/myBookmark/hooks/useMyBookmarkContentData'; import NoArticles from '@pages/myBookmark/components/NoArticles/NoArticles'; import NoUnreadArticles from '@pages/myBookmark/components/noUnreadArticles/NoUnreadArticles'; import { MutableRefObject } from 'react'; -import { Badge } from '@pinback/design-system/ui'; +import { Badge, Card } from '@pinback/design-system/ui'; interface MyBookmarkContentProps { activeBadge: 'all' | 'notRead'; @@ -33,67 +26,22 @@ const MyBookmarkContent = ({ scrollContainerRef, }: MyBookmarkContentProps) => { const { - data: articlesData, - fetchNextPage: fetchNextArticles, - hasNextPage: hasNextArticles, - } = useGetBookmarkArticles(); - - const { - data: unreadArticlesData, - fetchNextPage: fetchNextUnreadArticles, - hasNextPage: hasNextUnreadArticles, - } = useGetBookmarkUnreadArticles(); - - const { - data: categoryArticlesData, - fetchNextPage: fetchNextCategoryArticles, - hasNextPage: hasNextCategoryArticles, - } = useGetCategoryBookmarkArticles( + view, + list, + counts, + pagination, + } = useMyBookmarkContentData({ + activeBadge, + category, categoryId, - activeBadge === 'notRead' ? false : null - ); - - const categoryList = - categoryId && categoryArticlesData?.pages - ? categoryArticlesData.pages.flatMap((page) => page.articles) - : []; - - const articlesToDisplay = category - ? categoryList - : activeBadge === 'all' - ? (articlesData?.pages.flatMap((page) => page.articles) ?? []) - : (unreadArticlesData?.pages.flatMap((page) => page.articles) ?? []); - - const totalArticle = category - ? categoryArticlesData?.pages?.[0]?.totalArticle - : articlesData?.pages?.[0]?.totalArticle; - - const totalUnread = category - ? categoryArticlesData?.pages?.[0]?.totalUnreadArticle - : articlesData?.pages?.[0]?.totalUnreadArticle; - - const hasNextPage = category - ? hasNextCategoryArticles - : activeBadge === 'all' - ? hasNextArticles - : hasNextUnreadArticles; - - const fetchNextPage = category - ? fetchNextCategoryArticles - : activeBadge === 'all' - ? fetchNextArticles - : fetchNextUnreadArticles; - - const observerRef = useInfiniteScroll({ - fetchNextPage, - hasNextPage, - root: scrollContainerRef, + scrollContainerRef, }); + const totalCount = counts.total ?? 0; /** Empty 상태 컴포넌트 */ const EmptyStateComponent = () => { - if (articlesToDisplay.length === 0) { - if (articlesData?.pages?.[0]?.totalArticle === 0) return ; + if (list.articles.length === 0) { + if (totalCount === 0) return ; return ; } return null; @@ -104,36 +52,51 @@ const MyBookmarkContent = ({
onBadgeChange('all')} isActive={activeBadge === 'all'} /> onBadgeChange('notRead')} isActive={activeBadge === 'notRead'} />
- {articlesToDisplay.length > 0 ? ( + {list.articles.length > 0 ? (
- {articlesToDisplay.map((article) => ( - ( + { window.open(article.url, '_blank'); updateToReadStatus(article.articleId, { onSuccess: () => { queryClient.invalidateQueries({ - queryKey: ['bookmarkReadArticles'], + queryKey: ['bookmarkArticles'], + }); + queryClient.invalidateQueries({ + queryKey: ['bookmarkArticlesCount'], }); queryClient.invalidateQueries({ - queryKey: ['bookmarkUnreadArticles'], + queryKey: ['categoryBookmarkArticlesCount'], }); queryClient.invalidateQueries({ queryKey: ['categoryBookmarkArticles'], @@ -152,7 +115,10 @@ const MyBookmarkContent = ({ /> ))} -
+
) : ( diff --git a/apps/client/src/pages/myBookmark/hooks/useMyBookmarkContentData.ts b/apps/client/src/pages/myBookmark/hooks/useMyBookmarkContentData.ts new file mode 100644 index 00000000..d81ecd28 --- /dev/null +++ b/apps/client/src/pages/myBookmark/hooks/useMyBookmarkContentData.ts @@ -0,0 +1,91 @@ +import { + useGetBookmarkArticles, + useGetBookmarkArticlesCount, + useGetCategoryBookmarkArticles, + useGetCategoryBookmarkArticlesCount, +} from '@pages/myBookmark/apis/queries'; +import { useInfiniteScroll } from '@shared/hooks/useInfiniteScroll'; +import { MutableRefObject } from 'react'; + +interface UseMyBookmarkContentDataParams { + activeBadge: 'all' | 'notRead'; + category: string | null; + categoryId: string | null; + scrollContainerRef: MutableRefObject; +} + +export const useMyBookmarkContentData = ({ + activeBadge, + category, + categoryId, + scrollContainerRef, +}: UseMyBookmarkContentDataParams) => { + const readStatus = activeBadge === 'notRead' ? false : null; + const isCategoryView = !!categoryId; + + const { + data: bookmarkArticlesData, + fetchNextPage: fetchNextBookmarkArticles, + hasNextPage: hasNextBookmarkArticles, + } = useGetBookmarkArticles(readStatus); + const { data: bookmarkCountData } = useGetBookmarkArticlesCount(); + const { data: categoryCountData } = useGetCategoryBookmarkArticlesCount( + categoryId + ); + + const { + data: categoryArticlesData, + fetchNextPage: fetchNextCategoryArticles, + hasNextPage: hasNextCategoryArticles, + } = useGetCategoryBookmarkArticles(categoryId, readStatus); + + const categoryList = + isCategoryView && categoryArticlesData?.pages + ? categoryArticlesData.pages.flatMap((page) => page.articles) + : []; + + const articlesToDisplay = isCategoryView + ? categoryList + : (bookmarkArticlesData?.pages.flatMap((page) => page.articles) ?? []); + + const totalArticle = isCategoryView + ? categoryCountData?.totalArticleCount + : bookmarkCountData?.totalArticleCount; + const totalUnread = isCategoryView + ? categoryCountData?.unreadArticleCount + : bookmarkCountData?.unreadArticleCount; + + const categoryNameFromResponse = isCategoryView + ? categoryArticlesData?.pages?.[0]?.categoryName || category || undefined + : undefined; + + const hasNextPage = isCategoryView + ? hasNextCategoryArticles + : hasNextBookmarkArticles; + const fetchNextPage = isCategoryView + ? fetchNextCategoryArticles + : fetchNextBookmarkArticles; + + const sentinelRef = useInfiniteScroll({ + fetchNextPage, + hasNextPage, + root: scrollContainerRef, + }); + + return { + view: { + isCategoryView, + categoryName: categoryNameFromResponse, + }, + list: { + articles: articlesToDisplay, + }, + counts: { + total: totalArticle, + unread: totalUnread, + }, + pagination: { + sentinelRef, + }, + }; +}; diff --git a/apps/client/src/pages/myBookmark/types/api.ts b/apps/client/src/pages/myBookmark/types/api.ts index ec05025c..bd128829 100644 --- a/apps/client/src/pages/myBookmark/types/api.ts +++ b/apps/client/src/pages/myBookmark/types/api.ts @@ -7,25 +7,31 @@ interface Category { export interface BookmarkArticle { articleId: number; url: string; - memo: string; + title: string | null; + thumbnailUrl: string | null; + memo: string | null; createdAt: string; isRead: boolean; - category: Category; + category?: Category; } -// 북마크 전체 조회 -export interface BookmarkArticleResponse { - totalArticle: number; - totalUnreadArticle: number; - isNewUser: boolean; +// 북마크 조회(v3) +export interface BookmarkArticlesResponse { + totalArticleCount: number; + unreadArticleCount: number; articles: BookmarkArticle[]; } -// 북마크 안 읽음 조회 -export interface UnreadBookmarkArticleResponse { - totalArticle: number; - totalUnreadArticle: number; +export interface CategoryBookmarkArticleResponse { + totalArticleCount: number; + unreadArticleCount: number; + categoryName: string; articles: BookmarkArticle[]; } -export type CategoryBookmarkArticleResponse = UnreadBookmarkArticleResponse; +// 나의 북마크 카운트 조회 +export interface BookmarkArticlesCountResponse { + totalArticleCount: number; + readArticleCount: number; + unreadArticleCount: number; +} diff --git a/apps/client/src/shared/components/cardEditModal/CardEditModal.tsx b/apps/client/src/shared/components/cardEditModal/CardEditModal.tsx index d2e6de21..0013428f 100644 --- a/apps/client/src/shared/components/cardEditModal/CardEditModal.tsx +++ b/apps/client/src/shared/components/cardEditModal/CardEditModal.tsx @@ -115,10 +115,13 @@ export default function CardEditModal({ queryKey: ['remindArticles'], }); queryClient.invalidateQueries({ - queryKey: ['bookmarkReadArticles'], + queryKey: ['bookmarkArticles'], }); queryClient.invalidateQueries({ - queryKey: ['bookmarkUnreadArticles'], + queryKey: ['bookmarkArticlesCount'], + }); + queryClient.invalidateQueries({ + queryKey: ['categoryBookmarkArticlesCount'], }); queryClient.invalidateQueries({ queryKey: ['categoryBookmarkArticles'],