From c5d3eff467ae85f4e935c29640a4d01708bb3be0 Mon Sep 17 00:00:00 2001 From: l-lyun Date: Tue, 19 Aug 2025 21:17:43 +0900 Subject: [PATCH 1/7] =?UTF-8?q?fix:=20getreviewlists=20=EC=98=A4=ED=83=80?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/reviews/client.ts | 2 +- src/apis/reviews/query.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/apis/reviews/client.ts b/src/apis/reviews/client.ts index 0fd53bc..d47c279 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, diff --git a/src/apis/reviews/query.ts b/src/apis/reviews/query.ts index 557138d..1306f41 100644 --- a/src/apis/reviews/query.ts +++ b/src/apis/reviews/query.ts @@ -1,5 +1,5 @@ import {useInfiniteQuery, useMutation} from '@tanstack/react-query'; -import {getReveiewLists, postReviewReply} from './client'; +import {getReviewLists, postReviewReply} from './client'; import {ReviewReplyCreateRequest} from './model'; export const useReviewList = (marketId: number, enabled: boolean) => { @@ -7,7 +7,7 @@ export const useReviewList = (marketId: number, enabled: boolean) => { queryKey: ['marketList', marketId], queryFn: ({pageParam = 0}) => // FIXME: 페이징 단위 size - getReveiewLists({cursorId: pageParam, size: 5, marketId}), + getReviewLists({cursorId: pageParam, size: 5, marketId}), initialPageParam: 0, getNextPageParam: lastPage => { return lastPage?.hasNext From 968bd2f5ece82047a750ca4e2a5b73ca2426fefa Mon Sep 17 00:00:00 2001 From: l-lyun Date: Tue, 19 Aug 2025 21:19:51 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=EB=8C=80=EB=8C=93=EA=B8=80?= =?UTF-8?q?=EC=9D=84=20=EB=8B=AC=EC=A7=80=20=EC=95=8A=EC=9D=80=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20get=20api=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/reviews/client.ts | 21 +++++++++++++++++++++ src/apis/reviews/query.ts | 22 +++++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/apis/reviews/client.ts b/src/apis/reviews/client.ts index d47c279..3035583 100644 --- a/src/apis/reviews/client.ts +++ b/src/apis/reviews/client.ts @@ -26,6 +26,27 @@ export const getReviewLists = 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 1306f41..99d5eaa 100644 --- a/src/apis/reviews/query.ts +++ b/src/apis/reviews/query.ts @@ -1,5 +1,9 @@ import {useInfiniteQuery, useMutation} from '@tanstack/react-query'; -import {getReviewLists, postReviewReply} from './client'; +import { + getReviewLists, + postReviewReply, + getUnRepliedReviewLists, +} from './client'; import {ReviewReplyCreateRequest} from './model'; export const useReviewList = (marketId: number, enabled: boolean) => { @@ -18,6 +22,22 @@ export const useReviewList = (marketId: number, enabled: boolean) => { }); }; +export const useUnRepliedReviewList = (marketId: number, enabled: boolean) => { + return useInfiniteQuery({ + queryKey: ['marketList', marketId], + queryFn: ({pageParam = 0}) => + // FIXME: 페이징 단위 size + getUnRepliedReviewLists({cursorId: pageParam, size: 5, marketId}), + initialPageParam: 0, + getNextPageParam: lastPage => { + return lastPage?.hasNext + ? lastPage.reviews[lastPage.reviews.length - 1].id + : undefined; + }, + enabled: enabled, + }); +}; + export const useCreateReviewReply = () => { return useMutation({ mutationKey: ['reviewReply'], From e817179e3552dd5f66dcb8295e2f4ea656b205aa Mon Sep 17 00:00:00 2001 From: l-lyun Date: Tue, 19 Aug 2025 21:34:19 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20=EB=A6=AC=EB=B7=B0=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=B0=20=EB=82=B4=20=ED=83=AD=EB=B0=94=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/screens/ReviewScreen/ReviewScreen.style.tsx | 16 ++++++++++++++++ src/screens/ReviewScreen/index.tsx | 14 ++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/screens/ReviewScreen/ReviewScreen.style.tsx b/src/screens/ReviewScreen/ReviewScreen.style.tsx index 42343a2..fd27090 100644 --- a/src/screens/ReviewScreen/ReviewScreen.style.tsx +++ b/src/screens/ReviewScreen/ReviewScreen.style.tsx @@ -1,10 +1,26 @@ import styled from '@emotion/native'; +import {ToggleButton, ToggleButtonGroup} from '@/components/common'; const S = { Container: styled.View` flex: 1; background-color: white; `, + + 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..3b1483e 100644 --- a/src/screens/ReviewScreen/index.tsx +++ b/src/screens/ReviewScreen/index.tsx @@ -1,5 +1,4 @@ -// 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'; @@ -12,6 +11,7 @@ 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 { @@ -60,6 +60,16 @@ const ReviewScreen = () => { } return ( + + setSelected('no-reply')}> + 댓글을 달지 않은 리뷰 + + setSelected('every')}> + 전체 리뷰 + + Date: Tue, 19 Aug 2025 22:04:21 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20=EB=A6=AC=EB=B7=B0=20=EC=BB=A8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=84=88=20refresh=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/review/ReviewContainerLists.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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(); From 869e44fbd3c570334b71ec2866c7313840d945f2 Mon Sep 17 00:00:00 2001 From: l-lyun Date: Tue, 19 Aug 2025 22:04:47 +0900 Subject: [PATCH 5/7] =?UTF-8?q?fix:=20review=20get=20hooks=20querykey=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/reviews/query.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/apis/reviews/query.ts b/src/apis/reviews/query.ts index 99d5eaa..64f22c1 100644 --- a/src/apis/reviews/query.ts +++ b/src/apis/reviews/query.ts @@ -8,9 +8,9 @@ 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 + // TODO: 페이징 단위 size getReviewLists({cursorId: pageParam, size: 5, marketId}), initialPageParam: 0, getNextPageParam: lastPage => { @@ -24,9 +24,9 @@ export const useReviewList = (marketId: number, enabled: boolean) => { export const useUnRepliedReviewList = (marketId: number, enabled: boolean) => { return useInfiniteQuery({ - queryKey: ['marketList', marketId], + queryKey: ['marketList', 'review', 'no-reply', marketId], queryFn: ({pageParam = 0}) => - // FIXME: 페이징 단위 size + // TODO: 페이징 단위 size getUnRepliedReviewLists({cursorId: pageParam, size: 5, marketId}), initialPageParam: 0, getNextPageParam: lastPage => { From 3e37a1b51111bb53251e26639881a1fd49f44754 Mon Sep 17 00:00:00 2001 From: l-lyun Date: Tue, 19 Aug 2025 22:10:05 +0900 Subject: [PATCH 6/7] =?UTF-8?q?feat:=20=ED=83=AD=EB=B0=94=20=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20=EB=A6=AC=EB=B7=B0=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReviewScreen/ReviewScreen.style.tsx | 5 ++ src/screens/ReviewScreen/index.tsx | 77 ++++++++++++++----- 2 files changed, 63 insertions(+), 19 deletions(-) diff --git a/src/screens/ReviewScreen/ReviewScreen.style.tsx b/src/screens/ReviewScreen/ReviewScreen.style.tsx index fd27090..b9b7bdd 100644 --- a/src/screens/ReviewScreen/ReviewScreen.style.tsx +++ b/src/screens/ReviewScreen/ReviewScreen.style.tsx @@ -7,6 +7,11 @@ const S = { background-color: white; `, + EmptyWrapper: styled.View` + flex: 1; + align-items: center; + `, + ToggleText: styled.Text` color: ${props => props.theme.colors.tertiary}; `, diff --git a/src/screens/ReviewScreen/index.tsx b/src/screens/ReviewScreen/index.tsx index 3b1483e..0baafd3 100644 --- a/src/screens/ReviewScreen/index.tsx +++ b/src/screens/ReviewScreen/index.tsx @@ -1,7 +1,7 @@ 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'; @@ -15,13 +15,28 @@ const ReviewScreen = () => { 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,16 +63,41 @@ 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 ( @@ -71,13 +111,12 @@ const ReviewScreen = () => { ); }; - export default ReviewScreen; From 78eebf57c6b947562eeca9e516a6884db0d80a16 Mon Sep 17 00:00:00 2001 From: l-lyun Date: Tue, 19 Aug 2025 22:12:23 +0900 Subject: [PATCH 7/7] =?UTF-8?q?feat:=20=EB=A6=AC=EB=B7=B0=20=EB=8C=80?= =?UTF-8?q?=EB=8C=93=EA=B8=80=20=EC=9E=91=EC=84=B1=EC=8B=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EC=BF=BC=EB=A6=AC=ED=82=A4=20=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/screens/ReviewReplyScreen/index.tsx | 5 +++++ 1 file changed, 5 insertions(+) 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'], + }); }, }, );