diff --git a/app/(protected)/(tabs)/_layout.tsx b/app/(protected)/(tabs)/_layout.tsx index 9b91c20..1257b12 100644 --- a/app/(protected)/(tabs)/_layout.tsx +++ b/app/(protected)/(tabs)/_layout.tsx @@ -1,5 +1,5 @@ import { Tabs } from "expo-router"; -import { Platform, Text, View } from "react-native"; +import { DeviceEventEmitter, Platform, Text, View } from "react-native"; import { HeaderWithBack, HeaderWithNotification } from "@/components/Header"; import useSubscribeNotification from "@/hooks/useSubscribeNotification"; @@ -81,6 +81,14 @@ export default function TabsLayout() { title: "Home", tabBarIcon: ({ color }) => , }} + listeners={({ navigation, route }) => ({ + tabPress: () => { + const state = navigation.getState(); + if (state.routes[state.index].name === route.name) { + DeviceEventEmitter.emit("SCROLL_HOME_TO_TOP"); + } + }, + })} /> , }} + listeners={({ navigation, route }) => ({ + tabPress: () => { + const rootState = navigation.getState(); + if (rootState.routes[rootState.index].name !== route.name) return; + + const nestedState = rootState.routes[rootState.index].state as { + index: number; + routeNames: string[]; + }; + if (!nestedState?.routeNames) { + DeviceEventEmitter.emit("SCROLL_FRIEND_TO_TOP"); + return; + } + + const topTabName = nestedState.routeNames[nestedState.index]; + const eventMap = { + index: "SCROLL_FRIEND_TO_TOP", + request: "SCROLL_REQUEST_TO_TOP", + }; + + const eventName = eventMap[topTabName as keyof typeof eventMap]; + if (eventName) { + DeviceEventEmitter.emit(eventName); + } + }, + })} /> , }} + listeners={({ navigation, route }) => ({ + tabPress: () => { + const state = navigation.getState(); + if (state.routes[state.index].name === route.name) { + DeviceEventEmitter.emit("SCROLL_MY_PAGE_TO_TOP"); + } + }, + })} /> diff --git a/app/(protected)/(tabs)/friend/index.tsx b/app/(protected)/(tabs)/friend/index.tsx index d8c945b..f27dd2c 100644 --- a/app/(protected)/(tabs)/friend/index.tsx +++ b/app/(protected)/(tabs)/friend/index.tsx @@ -1,5 +1,5 @@ import { useQueryClient } from "@tanstack/react-query"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import ErrorScreen from "@/components/ErrorScreen"; import { FriendItem } from "@/components/FriendItem"; @@ -23,6 +23,7 @@ export default function Friend() { isFetchingNextPage, error, loadMore, + refetch, } = useInfiniteLoad({ queryFn: getFriends(keyword), queryKey: ["friends", keyword], @@ -35,11 +36,13 @@ export default function Friend() { }, 200); // 친구창에 focus 들어올 때마다 친구목록 새로고침 (검색중일 때 제외) - useFocusEffect(() => { - if (!keyword && !isFetchingNextPage) { - queryClient.invalidateQueries({ queryKey: ["friends"] }); - } - }); + useFocusEffect( + useCallback(() => { + if (!keyword && !isFetchingNextPage) { + queryClient.invalidateQueries({ queryKey: ["friends"] }); + } + }, [queryClient, keyword, isFetchingNextPage]), + ); // 친구의 운동 정보가 바뀌면 쿼리 다시 패치하도록 정보 구독 useEffect(() => { @@ -80,6 +83,7 @@ export default function Friend() { return ( (null); // 유저의 친구 요청 정보 조회 const { @@ -28,6 +34,7 @@ export default function Request() { isFetchingNextPage, error, loadMore, + refetch, } = useInfiniteLoad({ queryFn: getFriendRequests, queryKey: ["friendRequests"], @@ -36,11 +43,13 @@ export default function Request() { const hasRequests = !!requestData?.pages[0].total; // 친구 요청창에 focus 들어올 때마다 친구목록 새로고침 - useFocusEffect(() => { - if (!isFetchingNextPage) { - queryClient.invalidateQueries({ queryKey: ["friendRequests"] }); - } - }); + useFocusEffect( + useCallback(() => { + if (!isFetchingNextPage) { + queryClient.invalidateQueries({ queryKey: ["friendRequests"] }); + } + }, [isFetchingNextPage, queryClient]), + ); // 친구 요청이 추가되면 쿼리 다시 패치하도록 정보 구독 useEffect(() => { @@ -60,6 +69,20 @@ export default function Request() { }; }, [queryClient.invalidateQueries]); + const handleScrollToTop = useCallback(() => { + flatListRef.current?.scrollToOffset({ offset: 0, animated: true }); + refetch(); + }, [refetch]); + + useEffect(() => { + const subscription = DeviceEventEmitter.addListener( + "SCROLL_REQUEST_TO_TOP", + handleScrollToTop, + ); + + return () => subscription.remove(); + }, [handleScrollToTop]); + // 에러 스크린 if (error) { return ; @@ -73,6 +96,7 @@ export default function Request() { return ( page.data) : []} keyExtractor={(request) => String(request.requestId)} diff --git a/app/(protected)/(tabs)/home.tsx b/app/(protected)/(tabs)/home.tsx index 3b78e6f..e046a11 100644 --- a/app/(protected)/(tabs)/home.tsx +++ b/app/(protected)/(tabs)/home.tsx @@ -11,9 +11,10 @@ import useRefresh from "@/hooks/useRefresh"; import { getPostLikes, getPosts } from "@/utils/supabase"; import { useRouter } from "expo-router"; import * as SecureStore from "expo-secure-store"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { ActivityIndicator, + DeviceEventEmitter, Dimensions, FlatList, Image, @@ -33,6 +34,7 @@ export default function Home() { const [selectedPostId, setSelectedPostId] = useState(null); const [selectedAuthorId, setSelectedAuthorId] = useState(null); const [isLikedModalVisible, setIsLikedModalVisible] = useState(false); + const flatListRef = useRef(null); const { openModal } = useModal(); const router = useRouter(); @@ -91,9 +93,24 @@ export default function Home() { handleLoadId(); }, []); + const handleScrollToTop = useCallback(() => { + flatListRef.current?.scrollToOffset({ offset: 0, animated: true }); + onRefresh(); + }, [onRefresh]); + + useEffect(() => { + const subscription = DeviceEventEmitter.addListener( + "SCROLL_HOME_TO_TOP", + handleScrollToTop, + ); + + return () => subscription.remove(); + }, [handleScrollToTop]); + return ( page.data) ?? []} keyExtractor={(item) => item.id.toString()} contentContainerStyle={{ gap: 10 }} diff --git a/app/(protected)/(tabs)/mypage.tsx b/app/(protected)/(tabs)/mypage.tsx index 8a6f0d9..29b1fb5 100644 --- a/app/(protected)/(tabs)/mypage.tsx +++ b/app/(protected)/(tabs)/mypage.tsx @@ -20,6 +20,7 @@ export default function MyPage() { data: posts, isLoading: isPostsLoading, isError: isPostsError, + refetch, } = useFetchData( ["userPosts"], () => getMyPosts(), @@ -43,6 +44,7 @@ export default function MyPage() { } /> ({ ...post, id: post.id.toString() })) diff --git a/components/PostGrid.tsx b/components/PostGrid.tsx index 9d8f1f7..b355748 100644 --- a/components/PostGrid.tsx +++ b/components/PostGrid.tsx @@ -1,6 +1,8 @@ import images from "@/constants/images"; import { useRouter } from "expo-router"; +import { useCallback, useEffect, useRef } from "react"; import { + DeviceEventEmitter, Dimensions, FlatList, Image, @@ -14,12 +16,28 @@ interface Post { } interface PostGridProps { + refetch: () => void; posts: Post[] | null; isError?: boolean; } -export default function PostGrid({ posts, isError }: PostGridProps) { +export default function PostGrid({ refetch, posts, isError }: PostGridProps) { const router = useRouter(); + const flatListRef = useRef(null); + + const handleScrollToTop = useCallback(() => { + flatListRef.current?.scrollToOffset({ offset: 0, animated: true }); + refetch(); + }, [refetch]); + + useEffect(() => { + const subscription = DeviceEventEmitter.addListener( + "SCROLL_MY_PAGE_TO_TOP", + handleScrollToTop, + ); + + return () => subscription.remove(); + }, [handleScrollToTop]); if (isError) { return ( @@ -52,6 +70,7 @@ export default function PostGrid({ posts, isError }: PostGridProps) { return ( { const size = Dimensions.get("window").width / 3; diff --git a/components/SearchLayout.tsx b/components/SearchLayout.tsx index 352aeca..e447d1f 100644 --- a/components/SearchLayout.tsx +++ b/components/SearchLayout.tsx @@ -1,8 +1,9 @@ import colors from "@/constants/colors"; import type { UserProfile } from "@/types/User.interface"; -import { useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { ActivityIndicator, + DeviceEventEmitter, FlatList, type ListRenderItemInfo, View, @@ -11,6 +12,7 @@ import { SafeAreaView } from "react-native-safe-area-context"; import SearchBar from "./SearchBar"; interface SearchLayoutProps { + refetch?: () => void; data: UserProfile[]; // 추후 검색 사용 범위 넓어지면 변경 가능 isFetchingNextPage?: boolean; onChangeKeyword: (newKeyword: string) => void; @@ -20,6 +22,7 @@ interface SearchLayoutProps { } export function SearchLayout({ + refetch, data, isFetchingNextPage, onChangeKeyword, @@ -28,10 +31,26 @@ export function SearchLayout({ emptyComponent, }: SearchLayoutProps) { const [keyword, setKeyword] = useState(""); + const flatListRef = useRef(null); + + const handleScrollToTop = useCallback(() => { + flatListRef.current?.scrollToOffset({ offset: 0, animated: true }); + refetch?.(); + }, [refetch]); + + useEffect(() => { + const subscription = DeviceEventEmitter.addListener( + "SCROLL_FRIEND_TO_TOP", + handleScrollToTop, + ); + + return () => subscription.remove(); + }, [handleScrollToTop]); return ( elem.id}