diff --git a/src/apis/reviews/client.ts b/src/apis/reviews/client.ts index 0fd53bc..3035583 100644 --- a/src/apis/reviews/client.ts +++ b/src/apis/reviews/client.ts @@ -5,7 +5,7 @@ import { ReviewReplyCreateRequest, } from './model'; -export const getReveiewLists = async ({ +export const getReviewLists = async ({ marketId, cursorId, size, @@ -26,6 +26,27 @@ export const getReveiewLists = async ({ return null; } }; +export const getUnRepliedReviewLists = async ({ + marketId, + cursorId, + size, +}: ReviewListsRequest): Promise => { + try { + const res = await apiClient.get( + `/owner/review/market/${marketId}/no-reply`, + { + params: { + cursorId, + size, + }, + }, + ); + return res; + } catch (error) { + console.error('리뷰 페치 에러', error); + return null; + } +}; export const postReviewReply = async ({ reviewId, diff --git a/src/apis/reviews/query.ts b/src/apis/reviews/query.ts index 557138d..64f22c1 100644 --- a/src/apis/reviews/query.ts +++ b/src/apis/reviews/query.ts @@ -1,13 +1,33 @@ import {useInfiniteQuery, useMutation} from '@tanstack/react-query'; -import {getReveiewLists, postReviewReply} from './client'; +import { + getReviewLists, + postReviewReply, + getUnRepliedReviewLists, +} from './client'; import {ReviewReplyCreateRequest} from './model'; export const useReviewList = (marketId: number, enabled: boolean) => { return useInfiniteQuery({ - queryKey: ['marketList', marketId], + queryKey: ['marketList', 'review', 'every', marketId], queryFn: ({pageParam = 0}) => - // FIXME: 페이징 단위 size - getReveiewLists({cursorId: pageParam, size: 5, marketId}), + // TODO: 페이징 단위 size + getReviewLists({cursorId: pageParam, size: 5, marketId}), + initialPageParam: 0, + getNextPageParam: lastPage => { + return lastPage?.hasNext + ? lastPage.reviews[lastPage.reviews.length - 1].id + : undefined; + }, + enabled: enabled, + }); +}; + +export const useUnRepliedReviewList = (marketId: number, enabled: boolean) => { + return useInfiniteQuery({ + queryKey: ['marketList', 'review', 'no-reply', marketId], + queryFn: ({pageParam = 0}) => + // TODO: 페이징 단위 size + getUnRepliedReviewLists({cursorId: pageParam, size: 5, marketId}), initialPageParam: 0, getNextPageParam: lastPage => { return lastPage?.hasNext diff --git a/src/components/review/ReviewContainerLists.tsx b/src/components/review/ReviewContainerLists.tsx index 033ddf6..8056412 100644 --- a/src/components/review/ReviewContainerLists.tsx +++ b/src/components/review/ReviewContainerLists.tsx @@ -4,6 +4,8 @@ import S from './ReviewContainerLists.style'; import {FlatList} from 'react-native'; import {ActivityIndicator} from 'react-native-paper'; import ReviewContainer from './ReviewContainer'; +import {useQueryClient} from '@tanstack/react-query'; +import usePullDownRefresh from '@/hooks/usePullDownRefresh'; type ReviewContainerListsProps = { reviews: ReviewType[]; @@ -18,12 +20,22 @@ const ReviewContainerLists = ({ isFetchingNextPage, fetchNextPage, }: ReviewContainerListsProps) => { + const queryClient = useQueryClient(); + + const {onRefresh, refreshing} = usePullDownRefresh(async () => { + await queryClient.invalidateQueries({ + queryKey: ['marketList', 'review'], + }); + }); + return ( item.id.toString()} renderItem={({item}) => } + refreshing={refreshing} + onRefresh={onRefresh} onEndReached={() => { if (hasNextPage && !isFetchingNextPage) { fetchNextPage(); diff --git a/src/screens/ReviewReplyScreen/index.tsx b/src/screens/ReviewReplyScreen/index.tsx index 19f3b1f..a2d0a4e 100644 --- a/src/screens/ReviewReplyScreen/index.tsx +++ b/src/screens/ReviewReplyScreen/index.tsx @@ -7,6 +7,7 @@ import {ActivityIndicator} from 'react-native-paper'; import {useNavigation} from '@react-navigation/native'; import {Keyboard, TouchableWithoutFeedback, Platform} from 'react-native'; import TextInput from '@/components/common/TextInput/TextInput'; +import {useQueryClient} from '@tanstack/react-query'; type ReviewReplyScreenProps = StackScreenProps< ReviewStackParamList, @@ -19,6 +20,7 @@ const ReviewReplyScreen = ({route}: ReviewReplyScreenProps) => { const {mutate: createReviewReply, isPending} = useCreateReviewReply(); const navigation = useNavigation(); + const queryClient = useQueryClient(); const handleReplySubmit = () => { if (!replyContent.trim()) return; @@ -28,6 +30,9 @@ const ReviewReplyScreen = ({route}: ReviewReplyScreenProps) => { onSuccess: () => { setReplyContent(''); navigation.goBack(); + queryClient.invalidateQueries({ + queryKey: ['marketList', 'review'], + }); }, }, ); diff --git a/src/screens/ReviewScreen/ReviewScreen.style.tsx b/src/screens/ReviewScreen/ReviewScreen.style.tsx index 42343a2..b9b7bdd 100644 --- a/src/screens/ReviewScreen/ReviewScreen.style.tsx +++ b/src/screens/ReviewScreen/ReviewScreen.style.tsx @@ -1,10 +1,31 @@ import styled from '@emotion/native'; +import {ToggleButton, ToggleButtonGroup} from '@/components/common'; const S = { Container: styled.View` flex: 1; background-color: white; `, + + EmptyWrapper: styled.View` + flex: 1; + align-items: center; + `, + + ToggleText: styled.Text` + color: ${props => props.theme.colors.tertiary}; + `, + + NavbarGroup: styled(ToggleButtonGroup)` + display: flex; + flex-direction: row; + justify-content: space-between; + margin: 10px; + `, + + ToggleButton: styled(ToggleButton)` + width: 100%; + `, }; export default S; diff --git a/src/screens/ReviewScreen/index.tsx b/src/screens/ReviewScreen/index.tsx index 381a361..0baafd3 100644 --- a/src/screens/ReviewScreen/index.tsx +++ b/src/screens/ReviewScreen/index.tsx @@ -1,8 +1,7 @@ -// ReviewScreen.tsx -import React from 'react'; +import React, {useState} from 'react'; import {ActivityIndicator, Text} from 'react-native'; import S from './ReviewScreen.style'; -import {useReviewList} from '@/apis/reviews'; +import {useReviewList, useUnRepliedReviewList} from '@/apis/reviews'; import useProfile from '@/hooks/useProfile'; import ReviewContainerLists from '@/components/review/ReviewContainerLists'; import NonRegister from '@/components/common/NonRegister'; @@ -12,16 +11,32 @@ import useMarket from '@/hooks/useMarket'; const ReviewScreen = () => { const {profile} = useProfile(); const {marketInfo} = useMarket(); + const [selected, setSelected] = useState<'every' | 'no-reply'>('no-reply'); const marketId = profile?.marketId; const { - data: reviewLists, - hasNextPage, - fetchNextPage, - isFetchingNextPage, - status, - // error, - } = useReviewList(marketId!, Boolean(marketId)); + data: allPages, + hasNextPage: hasNextAll, + fetchNextPage: fetchNextAll, + isFetchingNextPage: isFetchingNextAll, + status: statusAll, + } = useReviewList(marketId!, !!marketId); + + const { + data: unrepliedPages, + hasNextPage: hasNextUn, + fetchNextPage: fetchNextUn, + isFetchingNextPage: isFetchingNextUn, + status: statusUn, + } = useUnRepliedReviewList(marketId!, !!marketId); + + const isPending = + (selected === 'every' && statusAll === 'pending') || + (selected === 'no-reply' && statusUn === 'pending'); + + const isError = + (selected === 'every' && statusAll === 'error') || + (selected === 'no-reply' && statusUn === 'error'); if (!profile) { return ; @@ -31,7 +46,7 @@ const ReviewScreen = () => { return ; } - if (status === 'pending') { + if (isPending) { return ( @@ -40,7 +55,7 @@ const ReviewScreen = () => { } // TODO: error 로직 개선 - if (status === 'error') { + if (isError) { return ( Error @@ -48,26 +63,60 @@ const ReviewScreen = () => { ); } - const reviews = - reviewLists?.pages.flatMap(page => (page ? page.reviews : [])) || []; + const allReviews = allPages?.pages.flatMap(p => (p ? p.reviews : [])) ?? []; - if (reviews.length === 0 || !reviews) { + const unrepliedReviews = + unrepliedPages?.pages.flatMap(p => (p ? p.reviews : [])) ?? []; + + const currentReviews = selected === 'every' ? allReviews : unrepliedReviews; + const currentHasNext = selected === 'every' ? hasNextAll : hasNextUn; + const currentFetchNext = selected === 'every' ? fetchNextAll : fetchNextUn; + const currentIsFetchingNext = + selected === 'every' ? isFetchingNextAll : isFetchingNextUn; + + if (!currentReviews || currentReviews.length === 0) { return ( - 등록된 리뷰가 없어요! + + setSelected('no-reply')}> + 댓글을 달지 않은 리뷰 + + setSelected('every')}> + 전체 리뷰 + + + + + {selected === 'every' + ? '등록된 리뷰가 없어요!' + : '미답변 리뷰가 없어요!'} + + ); } + return ( + + setSelected('no-reply')}> + 댓글을 달지 않은 리뷰 + + setSelected('every')}> + 전체 리뷰 + + ); }; - export default ReviewScreen;