Skip to content

Commit

Permalink
feat: 커뮤니티 조회수 구현 (#1235)
Browse files Browse the repository at this point in the history
* feat: 조회수 카운터 추가

* feat: 조회수 로직 변경

* refactor: 불필요 컴포넌트 제거

* feat: 조회수 추가

* feat: 클린업 추가

* feat: 조회수 요청 보장
  • Loading branch information
Tekiter authored Dec 1, 2023
1 parent b4592f6 commit 9640ec0
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 107 deletions.
14 changes: 5 additions & 9 deletions src/api/endpoint/feed/postPostHit.ts
Original file line number Diff line number Diff line change
@@ -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),
});
};
39 changes: 39 additions & 0 deletions src/components/feed/common/hooks/useHit.ts
Original file line number Diff line number Diff line change
@@ -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<string>());
const timeoutTokenRef = useRef<null | ReturnType<typeof setTimeout>>(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,
};
};
172 changes: 89 additions & 83 deletions src/components/feed/list/FeedCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<BaseProps>) => {
return (
<Flex
css={{
backgroundColor: colors.gray950,
padding: '16px',
gap: 8,
borderBottom: `1px solid ${colors.gray800}`,
}}
>
{isBlindWriter || profileImage == null ? (
<div css={{ flexShrink: 0 }}>
<IconMember />
</div>
) : (
<Link href={playgroundLink.memberDetail(memberId)}>
<ProfileImage width={32} height={32} src={profileImage} alt='profileImage' />
</Link>
)}
<Flex direction='column' css={{ minWidth: 0, gap: '8px', width: '100%' }}>
<Stack gutter={title ? 8 : 4}>
<Flex justify='space-between'>
{isBlindWriter ? (
<Top align='center'>
<Text typography='SUIT_14_SB' lineHeight={20}>
익명
</Text>
<InfoText typography='SUIT_14_R' lineHeight={20} color={colors.gray400}>
{isShowInfo && info}
</InfoText>
</Top>
) : (
<Link href={playgroundLink.memberDetail(memberId)}>
const Base = forwardRef<HTMLDivElement, PropsWithChildren<BaseProps>>(
(
{
profileImage,
name,
info,
title,
content,
createdAt,
isBlindWriter = false,
isQuestion = false,
commentLength,
hits,
children,
rightIcon,
memberId,
isShowInfo,
},
ref,
) => {
return (
<Flex
ref={ref}
css={{
backgroundColor: colors.gray950,
padding: '16px',
gap: 8,
borderBottom: `1px solid ${colors.gray800}`,
}}
>
{isBlindWriter || profileImage == null ? (
<div css={{ flexShrink: 0 }}>
<IconMember />
</div>
) : (
<Link href={playgroundLink.memberDetail(memberId)}>
<ProfileImage width={32} height={32} src={profileImage} alt='profileImage' />
</Link>
)}
<Flex direction='column' css={{ minWidth: 0, gap: '8px', width: '100%' }}>
<Stack gutter={title ? 8 : 4}>
<Flex justify='space-between'>
{isBlindWriter ? (
<Top align='center'>
<Text typography='SUIT_14_SB' lineHeight={20}>
{name}
익명
</Text>
<InfoText typography='SUIT_14_R' lineHeight={20} color={colors.gray400}>
{info}
{isShowInfo && info}
</InfoText>
</Top>
</Link>
)}
<Stack.Horizontal gutter={4} align='center'>
<Text typography='SUIT_14_R' lineHeight={20} color={colors.gray400}>
{getRelativeTime(createdAt)}
) : (
<Link href={playgroundLink.memberDetail(memberId)}>
<Top align='center'>
<Text typography='SUIT_14_SB' lineHeight={20}>
{name}
</Text>
<InfoText typography='SUIT_14_R' lineHeight={20} color={colors.gray400}>
{info}
</InfoText>
</Top>
</Link>
)}
<Stack.Horizontal gutter={4} align='center'>
<Text typography='SUIT_14_R' lineHeight={20} color={colors.gray400}>
{getRelativeTime(createdAt)}
</Text>
{rightIcon}
</Stack.Horizontal>
</Flex>
<Stack gutter={8}>
{title && (
<Title typography='SUIT_17_SB'>
{isQuestion && <QuestionBadge>질문</QuestionBadge>}
{title}
</Title>
)}
<Text
typography='SUIT_15_L'
lineHeight={22}
css={{
whiteSpace: 'pre-wrap',
}}
>
{renderContent(content)}
</Text>
{rightIcon}
</Stack.Horizontal>
</Flex>
<Stack gutter={8}>
{title && (
<Title typography='SUIT_17_SB'>
{isQuestion && <QuestionBadge>질문</QuestionBadge>}
{title}
</Title>
)}
<Text
typography='SUIT_15_L'
lineHeight={22}
css={{
whiteSpace: 'pre-wrap',
}}
>
{renderContent(content)}
</Text>
</Stack>
</Stack>
</Stack>
{children}
<Bottom gutter={2}>
<Text typography='SUIT_14_R'>{`댓글 ${commentLength}개 ∙ 조회수 ${hits}회`}</Text>
</Bottom>
{children}
<Bottom gutter={2}>
<Text typography='SUIT_14_R'>{`댓글 ${commentLength}개 ∙ 조회수 ${hits}회`}</Text>
</Bottom>
</Flex>
</Flex>
</Flex>
);
};
);
},
);

const ProfileImage = styled.img`
flex-shrink: 0;
Expand Down
9 changes: 7 additions & 2 deletions src/components/feed/list/FeedList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<FeedListProps> = ({ renderFeedDetailLink }) => {
const FeedList: FC<FeedListProps> = ({ renderFeedDetailLink, onScrollChange }) => {
const [categoryId] = useCategoryParam({ defaultValue: '' });
const { data: categoryData } = useQuery({
queryKey: getCategory.cacheKey(),
Expand Down Expand Up @@ -47,7 +48,11 @@ const FeedList: FC<FeedListProps> = ({ renderFeedDetailLink }) => {
</div>
)}
>
<FeedListItems categoryId={categoryId} renderFeedDetailLink={renderFeedDetailLink} />
<FeedListItems
categoryId={categoryId}
renderFeedDetailLink={renderFeedDetailLink}
onScrollChange={onScrollChange}
/>
</ErrorBoundary>
</HeightSpacer>
<LoggingClick eventKey='feedUploadButton'>
Expand Down
4 changes: 3 additions & 1 deletion src/components/feed/list/FeedListItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<FeedListItemsProps> = ({ categoryId, renderFeedDetailLink }) => {
const FeedListItems: FC<FeedListItemsProps> = ({ categoryId, renderFeedDetailLink, onScrollChange }) => {
const { data, refetch, fetchNextPage, isLoading, isError } = useGetPostsInfiniteQuery({
categoryId,
});
Expand Down Expand Up @@ -77,6 +78,7 @@ const FeedListItems: FC<FeedListItemsProps> = ({ categoryId, renderFeedDetailLin
endReached={() => {
fetchNextPage();
}}
isScrolling={onScrollChange}
itemContent={(_, post) => {
return renderFeedDetailLink({
feedId: `${post.id}`,
Expand Down
29 changes: 17 additions & 12 deletions src/components/feed/page/FeedHomePage.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 !== '';

Expand All @@ -22,12 +25,13 @@ const CommunityPage: FC = () => {
listSlot={
<FeedList
renderFeedDetailLink={({ children, feedId }) => (
// TODO: to @tekiter 조회수 구현 시 변경해주세욤
<LoggingImpression areaThreshold={0.5} eventKey='feedCard' param={{ feedId }}>
<LoggingClick eventKey='feedCard' param={{ feedId }}>
<FeedDetailLink feedId={feedId}>{children}</FeedDetailLink>
</LoggingClick>
</LoggingImpression>
<ImpressionArea onImpressionStart={() => queueHit(feedId)}>
<LoggingImpression areaThreshold={0.5} eventKey='feedCard' param={{ feedId }}>
<LoggingClick eventKey='feedCard' param={{ feedId }}>
<FeedDetailLink feedId={feedId}>{children}</FeedDetailLink>
</LoggingClick>
</LoggingImpression>
</ImpressionArea>
)}
/>
}
Expand All @@ -54,12 +58,13 @@ const CommunityPage: FC = () => {
listSlot={
<FeedList
renderFeedDetailLink={({ children, feedId }) => (
// TODO: to @tekiter 조회수 구현 시 변경해주세욤
<LoggingImpression areaThreshold={0.5} eventKey='feedCard' param={{ feedId }}>
<LoggingClick eventKey='feedCard' param={{ feedId }}>
<FeedDetailLink feedId={feedId}>{children}</FeedDetailLink>
</LoggingClick>
</LoggingImpression>
<ImpressionArea onImpressionStart={() => queueHit(feedId)}>
<LoggingImpression areaThreshold={0.5} eventKey='feedCard' param={{ feedId }}>
<LoggingClick eventKey='feedCard' param={{ feedId }}>
<FeedDetailLink feedId={feedId}>{children}</FeedDetailLink>
</LoggingClick>
</LoggingImpression>
</ImpressionArea>
)}
/>
}
Expand Down

0 comments on commit 9640ec0

Please sign in to comment.