From 9640ec00eb75b10bfa213d7e506eb7e045b3f2de Mon Sep 17 00:00:00 2001 From: Tekiter Date: Sat, 2 Dec 2023 03:15:48 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=BB=A4=EB=AE=A4=EB=8B=88=ED=8B=B0=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EC=88=98=20=EA=B5=AC=ED=98=84=20(#1235)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 조회수 카운터 추가 * feat: 조회수 로직 변경 * refactor: 불필요 컴포넌트 제거 * feat: 조회수 추가 * feat: 클린업 추가 * feat: 조회수 요청 보장 --- src/api/endpoint/feed/postPostHit.ts | 14 +- src/components/feed/common/hooks/useHit.ts | 39 +++++ src/components/feed/list/FeedCard.tsx | 172 +++++++++++---------- src/components/feed/list/FeedList.tsx | 9 +- src/components/feed/list/FeedListItems.tsx | 4 +- src/components/feed/page/FeedHomePage.tsx | 29 ++-- 6 files changed, 160 insertions(+), 107 deletions(-) create mode 100644 src/components/feed/common/hooks/useHit.ts diff --git a/src/api/endpoint/feed/postPostHit.ts b/src/api/endpoint/feed/postPostHit.ts index 9a62c11c8..49bfe2c07 100644 --- a/src/api/endpoint/feed/postPostHit.ts +++ b/src/api/endpoint/feed/postPostHit.ts @@ -1,18 +1,14 @@ -import { useMutation } from '@tanstack/react-query'; import { z } from 'zod'; import { createEndpoint } from '@/api/typedAxios'; export const postPostHit = createEndpoint({ - request: (postId: string) => ({ + request: (postIdList: string[]) => ({ method: 'POST', - url: `api/v1/community/posts/${postId}/hit`, + url: `api/v1/community/posts/hit`, + data: { + postIdList: postIdList.map((id) => Number(id)).filter((value) => !Number.isNaN(value)), + }, }), serverResponseScheme: z.unknown(), }); - -export const usePostPostHitMutation = () => { - return useMutation({ - mutationFn: (postId: string) => postPostHit.request(postId), - }); -}; diff --git a/src/components/feed/common/hooks/useHit.ts b/src/components/feed/common/hooks/useHit.ts new file mode 100644 index 000000000..27da67fd1 --- /dev/null +++ b/src/components/feed/common/hooks/useHit.ts @@ -0,0 +1,39 @@ +import { useMutation } from '@tanstack/react-query'; +import { useEffect, useRef } from 'react'; + +import { postPostHit } from '@/api/endpoint/feed/postPostHit'; + +export const useHit = () => { + const { mutate } = useMutation({ + mutationFn: (ids: string[]) => postPostHit.request(ids), + }); + + const viewedSet = useRef(new Set()); + const timeoutTokenRef = useRef>(null); + + const handleFeedImpression = (feedId: string) => { + viewedSet.current.add(feedId); + scheduleProcess(); + }; + + const scheduleProcess = () => { + if (timeoutTokenRef.current != null) { + return; + } + + // 페이지를 벗어나도 조회수 요청이 나가도록 일부러 cleanup 때 정리 안함 + timeoutTokenRef.current = setTimeout(() => { + const ids = [...viewedSet.current.values()]; + if (ids.length > 0) { + mutate(ids); + } + + viewedSet.current.clear(); + timeoutTokenRef.current = null; + }, 5000); + }; + + return { + queueHit: handleFeedImpression, + }; +}; diff --git a/src/components/feed/list/FeedCard.tsx b/src/components/feed/list/FeedCard.tsx index 0754e408c..e38bed2be 100644 --- a/src/components/feed/list/FeedCard.tsx +++ b/src/components/feed/list/FeedCard.tsx @@ -3,7 +3,7 @@ import styled from '@emotion/styled'; import { colors } from '@sopt-makers/colors'; import { Flex, Stack } from '@toss/emotion-utils'; import Link from 'next/link'; -import { PropsWithChildren, ReactNode } from 'react'; +import { forwardRef, PropsWithChildren, ReactNode } from 'react'; import HorizontalScroller from '@/components/common/HorizontalScroller'; import Text from '@/components/common/Text'; @@ -29,97 +29,103 @@ interface BaseProps { isShowInfo: boolean; } -const Base = ({ - profileImage, - name, - info, - title, - content, - createdAt, - isBlindWriter = false, - isQuestion = false, - commentLength, - hits, - children, - rightIcon, - memberId, - isShowInfo, -}: PropsWithChildren) => { - return ( - - {isBlindWriter || profileImage == null ? ( -
- -
- ) : ( - - - - )} - - - - {isBlindWriter ? ( - - - 익명 - - - {isShowInfo && info} - - - ) : ( - +const Base = forwardRef>( + ( + { + profileImage, + name, + info, + title, + content, + createdAt, + isBlindWriter = false, + isQuestion = false, + commentLength, + hits, + children, + rightIcon, + memberId, + isShowInfo, + }, + ref, + ) => { + return ( + + {isBlindWriter || profileImage == null ? ( +
+ +
+ ) : ( + + + + )} + + + + {isBlindWriter ? ( - {name} + 익명 - {info} + {isShowInfo && info} - - )} - - - {getRelativeTime(createdAt)} + ) : ( + + + + {name} + + + {info} + + + + )} + + + {getRelativeTime(createdAt)} + + {rightIcon} + + + + {title && ( + + {isQuestion && <QuestionBadge>질문</QuestionBadge>} + {title} + + )} + + {renderContent(content)} - {rightIcon} - - - - {title && ( - - {isQuestion && <QuestionBadge>질문</QuestionBadge>} - {title} - - )} - - {renderContent(content)} - +
- - {children} - - {`댓글 ${commentLength}개 ∙ 조회수 ${hits}회`} - + {children} + + {`댓글 ${commentLength}개 ∙ 조회수 ${hits}회`} + +
- - ); -}; + ); + }, +); const ProfileImage = styled.img` flex-shrink: 0; diff --git a/src/components/feed/list/FeedList.tsx b/src/components/feed/list/FeedList.tsx index da4496e8e..c11f2760d 100644 --- a/src/components/feed/list/FeedList.tsx +++ b/src/components/feed/list/FeedList.tsx @@ -16,9 +16,10 @@ import { MOBILE_MEDIA_QUERY } from '@/styles/mediaQuery'; interface FeedListProps { renderFeedDetailLink: (props: { children: ReactNode; feedId: string }) => ReactNode; + onScrollChange?: (scrolling: boolean) => void; } -const FeedList: FC = ({ renderFeedDetailLink }) => { +const FeedList: FC = ({ renderFeedDetailLink, onScrollChange }) => { const [categoryId] = useCategoryParam({ defaultValue: '' }); const { data: categoryData } = useQuery({ queryKey: getCategory.cacheKey(), @@ -47,7 +48,11 @@ const FeedList: FC = ({ renderFeedDetailLink }) => { )} > - + diff --git a/src/components/feed/list/FeedListItems.tsx b/src/components/feed/list/FeedListItems.tsx index 4aa026c05..67f8e3f3b 100644 --- a/src/components/feed/list/FeedListItems.tsx +++ b/src/components/feed/list/FeedListItems.tsx @@ -22,9 +22,10 @@ import { textStyles } from '@/styles/typography'; interface FeedListItemsProps { categoryId: string | undefined; renderFeedDetailLink: (props: { children: ReactNode; feedId: string }) => ReactNode; + onScrollChange?: (scrolling: boolean) => void; } -const FeedListItems: FC = ({ categoryId, renderFeedDetailLink }) => { +const FeedListItems: FC = ({ categoryId, renderFeedDetailLink, onScrollChange }) => { const { data, refetch, fetchNextPage, isLoading, isError } = useGetPostsInfiniteQuery({ categoryId, }); @@ -77,6 +78,7 @@ const FeedListItems: FC = ({ categoryId, renderFeedDetailLin endReached={() => { fetchNextPage(); }} + isScrolling={onScrollChange} itemContent={(_, post) => { return renderFeedDetailLink({ feedId: `${post.id}`, diff --git a/src/components/feed/page/FeedHomePage.tsx b/src/components/feed/page/FeedHomePage.tsx index c52f0c6ac..83f84a896 100644 --- a/src/components/feed/page/FeedHomePage.tsx +++ b/src/components/feed/page/FeedHomePage.tsx @@ -1,8 +1,10 @@ +import { ImpressionArea } from '@toss/impression-area'; import { FC } from 'react'; import Responsive from '@/components/common/Responsive'; import { LoggingClick } from '@/components/eventLogger/components/LoggingClick'; import { LoggingImpression } from '@/components/eventLogger/components/LoggingImpression'; +import { useHit } from '@/components/feed/common/hooks/useHit'; import { CategoryLink, FeedDetailLink, useFeedDetailParam } from '@/components/feed/common/queryParam'; import FeedDetail from '@/components/feed/detail/FeedDetail'; import FeedList from '@/components/feed/list/FeedList'; @@ -11,6 +13,7 @@ import MobileCommunityLayout from '@/components/feed/page/layout/MobileCommunity const CommunityPage: FC = () => { const [postId] = useFeedDetailParam(); + const { queueHit } = useHit(); const isDetailOpen = postId != null && postId !== ''; @@ -22,12 +25,13 @@ const CommunityPage: FC = () => { listSlot={ ( - // TODO: to @tekiter 조회수 구현 시 변경해주세욤 - - - {children} - - + queueHit(feedId)}> + + + {children} + + + )} /> } @@ -54,12 +58,13 @@ const CommunityPage: FC = () => { listSlot={ ( - // TODO: to @tekiter 조회수 구현 시 변경해주세욤 - - - {children} - - + queueHit(feedId)}> + + + {children} + + + )} /> }