diff --git a/src/components/search/GroupSearchResult.styled.ts b/src/components/search/GroupSearchResult.styled.ts index 0d06d573..f337879c 100644 --- a/src/components/search/GroupSearchResult.styled.ts +++ b/src/components/search/GroupSearchResult.styled.ts @@ -21,11 +21,13 @@ export const Tab = styled.button<{ selected?: boolean }>` cursor: pointer; `; -export const Content = styled.div` +export const Content = styled.div<{ isRefetching?: boolean }>` display: flex; flex-direction: column; overflow-y: auto; padding: 0 20px 60px; + opacity: ${({ isRefetching }) => (isRefetching ? 0.45 : 1)}; + transition: opacity 0.15s ease; &::-webkit-scrollbar { display: none; diff --git a/src/components/search/GroupSearchResult.tsx b/src/components/search/GroupSearchResult.tsx index 9082d502..a7ee1043 100644 --- a/src/components/search/GroupSearchResult.tsx +++ b/src/components/search/GroupSearchResult.tsx @@ -63,7 +63,10 @@ const GroupSearchResult = ({ onClickRoom, }: Props) => { const mapped = useMemo(() => rooms.map(mapToGroupCardModel), [rooms]); - const isEmpty = !isLoading && mapped.length === 0; + // searching 중에는 아직 debounce 대기 중일 수 있으므로 빈 결과 화면을 표시하지 않음 + const isEmpty = !isLoading && mapped.length === 0 && type !== 'searching'; + // 기존 결과를 유지한 채 재검색 중인 상태 (필터·카테고리 변경 시) + const isRefetching = isLoading && mapped.length > 0; return ( <> @@ -87,7 +90,8 @@ const GroupSearchResult = ({ {(showTabs || type === 'searched') && ( - 전체 {mapped.length} + {/* 재검색 중엔 이전 카운트를 유지하고, 초기 검색 완료 시 카운트를 표시 */} + {isLoading && !isRefetching ? '' : `전체 ${mapped.length}`} )} - + {error && {error}} {isEmpty ? ( diff --git a/src/components/search/RecentSearchTabs.tsx b/src/components/search/RecentSearchTabs.tsx index 5fad50a9..2112e9fb 100644 --- a/src/components/search/RecentSearchTabs.tsx +++ b/src/components/search/RecentSearchTabs.tsx @@ -6,18 +6,22 @@ interface RecentSearchTabsProps { recentSearches: string[]; handleDelete: (term: string) => void; handleRecentSearchClick: (term: string) => void; + isLoading?: boolean; } const RecentSearchTabs = ({ recentSearches, handleDelete, handleRecentSearchClick, + isLoading = false, }: RecentSearchTabsProps) => { return ( 최근 검색어 - {recentSearches.length === 0 ? ( + {isLoading ? ( + 최근 검색어를 불러오고 있습니다. + ) : recentSearches.length === 0 ? ( 최근 검색어가 아직 없어요. ) : ( recentSearches.map(recentSearch => ( diff --git a/src/pages/feed/MyFeedPage.styled.ts b/src/pages/feed/MyFeedPage.styled.ts index a20008f1..c086bfb7 100644 --- a/src/pages/feed/MyFeedPage.styled.ts +++ b/src/pages/feed/MyFeedPage.styled.ts @@ -1,7 +1,16 @@ import styled from '@emotion/styled'; +import { colors } from '@/styles/global/global'; export const Container = styled.div` min-width: 320px; max-width: 767px; margin: 0 auto; `; + +export const LoadingScreen = styled.div` + min-width: 320px; + max-width: 767px; + margin: 0 auto; + min-height: 100dvh; + background-color: ${colors.black.main}; +`; diff --git a/src/pages/feed/MyFeedPage.tsx b/src/pages/feed/MyFeedPage.tsx index 72a210a9..56a24ecc 100644 --- a/src/pages/feed/MyFeedPage.tsx +++ b/src/pages/feed/MyFeedPage.tsx @@ -8,7 +8,7 @@ import OtherFeed from '@/components/feed/OtherFeed'; import { getOtherFeed, type OtherFeedItem } from '@/api/feeds/getOtherFeed'; import { getOtherProfile } from '@/api/users/getOtherProfile'; import type { OtherProfileData } from '@/types/profile'; -import { Container } from './MyFeedPage.styled'; +import { Container, LoadingScreen } from './MyFeedPage.styled'; const MyFeedPage = () => { const navigate = useNavigate(); @@ -53,7 +53,14 @@ const MyFeedPage = () => { }, [userId]); if (loading) { - return <>; + return ( + + } + onLeftClick={handleBackClick} + /> + + ); } if (error) { diff --git a/src/pages/feed/OtherFeedPage.tsx b/src/pages/feed/OtherFeedPage.tsx index 7b6ce457..9613055c 100644 --- a/src/pages/feed/OtherFeedPage.tsx +++ b/src/pages/feed/OtherFeedPage.tsx @@ -8,7 +8,7 @@ import OtherFeed from '@/components/feed/OtherFeed'; import { getOtherFeed, type OtherFeedItem } from '@/api/feeds/getOtherFeed'; import { getOtherProfile } from '@/api/users/getOtherProfile'; import type { OtherProfileData } from '@/types/profile'; -import { Container } from './MyFeedPage.styled'; +import { Container, LoadingScreen } from './MyFeedPage.styled'; const OtherFeedPage = () => { const navigate = useNavigate(); @@ -53,7 +53,14 @@ const OtherFeedPage = () => { }, [userId]); if (loading) { - return <>; + return ( + + } + onLeftClick={handleBackClick} + /> + + ); } if (error) { diff --git a/src/pages/feed/UserSearch.styled.ts b/src/pages/feed/UserSearch.styled.ts index 13345ca7..538ec857 100644 --- a/src/pages/feed/UserSearch.styled.ts +++ b/src/pages/feed/UserSearch.styled.ts @@ -25,3 +25,12 @@ export const SearchBarContainer = styled.div` export const Content = styled.div` margin-top: 132px; `; + +export const LoadingMessage = styled.div` + display: flex; + justify-content: center; + align-items: center; + padding: 40px 20px; + color: ${colors.white}; + font-size: 16px; +`; diff --git a/src/pages/feed/UserSearch.tsx b/src/pages/feed/UserSearch.tsx index 80d5f3bf..c43c3fcb 100644 --- a/src/pages/feed/UserSearch.tsx +++ b/src/pages/feed/UserSearch.tsx @@ -9,7 +9,7 @@ import { useNavigate } from 'react-router-dom'; import { useUserSearch } from '@/hooks/useUserSearch'; import { getRecentSearch, type RecentSearchData } from '@/api/recentsearch/getRecentSearch'; import { deleteRecentSearch } from '@/api/recentsearch/deleteRecentSearch'; -import { Content, SearchBarContainer, Wrapper } from './UserSearch.styled'; +import { Content, SearchBarContainer, Wrapper, LoadingMessage } from './UserSearch.styled'; const UserSearch = () => { const navigate = useNavigate(); @@ -25,10 +25,18 @@ const UserSearch = () => { }); const [recentSearches, setRecentSearches] = useState([]); + const [isRecentLoading, setIsRecentLoading] = useState(false); const fetchRecentSearches = async () => { - const response = await getRecentSearch('USER'); - setRecentSearches(response.data.recentSearchList); + setIsRecentLoading(true); + try { + const response = await getRecentSearch('USER'); + setRecentSearches(response.isSuccess ? response.data.recentSearchList : []); + } catch { + setRecentSearches([]); + } finally { + setIsRecentLoading(false); + } }; useEffect(() => { @@ -100,7 +108,9 @@ const UserSearch = () => { {isSearching ? ( <> - {isSearched ? ( + {userList.length === 0 && (loading || !isSearched) ? ( + 검색 중... + ) : isSearched ? ( { recentSearches={recentSearches.map(item => item.searchTerm)} handleDelete={handleDeleteWrapper} handleRecentSearchClick={handleRecentSearchClick} + isLoading={isRecentLoading} /> )} diff --git a/src/pages/feed/UserSearchResult.tsx b/src/pages/feed/UserSearchResult.tsx index a6ee90f9..301ebf99 100644 --- a/src/pages/feed/UserSearchResult.tsx +++ b/src/pages/feed/UserSearchResult.tsx @@ -18,10 +18,7 @@ export function UserSearchResult({ hasMore, onLoadMore, }: UserSearchResultProps) { - const isEmptySearchedUserList = () => { - if (searchedUserList.length === 0) return true; - else return false; - }; + const isEmpty = searchedUserList.length === 0 && type !== 'searching'; const observerRef = useRef(null); @@ -47,8 +44,8 @@ export function UserSearchResult({ {type === 'searching' ? <> : 전체 {searchedUserList.length}} - {isEmptySearchedUserList() ? ( - {loading ? '사용자 찾는 중...' : '찾는 사용자가 없어요.'} + {isEmpty ? ( + 찾는 사용자가 없어요. ) : ( <> {searchedUserList.map((user, index) => ( diff --git a/src/pages/groupSearch/GroupSearch.tsx b/src/pages/groupSearch/GroupSearch.tsx index 95eb6d12..80f33be8 100644 --- a/src/pages/groupSearch/GroupSearch.tsx +++ b/src/pages/groupSearch/GroupSearch.tsx @@ -38,6 +38,7 @@ const GroupSearch = () => { const [category, setCategory] = useState(''); const [recentSearches, setRecentSearches] = useState([]); + const [isRecentLoading, setIsRecentLoading] = useState(false); const [searchTimeoutId, setSearchTimeoutId] = useState(null); const [showTabs, setShowTabs] = useState(false); @@ -46,11 +47,14 @@ const GroupSearch = () => { useEffect(() => { (async () => { + setIsRecentLoading(true); try { const response = await getRecentSearch('ROOM'); setRecentSearches(response.isSuccess ? response.data.recentSearchList : []); } catch { setRecentSearches([]); + } finally { + setIsRecentLoading(false); } })(); }, []); @@ -62,11 +66,14 @@ const GroupSearch = () => { }, [searchStatus]); const fetchRecentSearches = async () => { + setIsRecentLoading(true); try { const response = await getRecentSearch('ROOM'); setRecentSearches(response.isSuccess ? response.data.recentSearchList : []); } catch { setRecentSearches([]); + } finally { + setIsRecentLoading(false); } }; @@ -77,12 +84,15 @@ const GroupSearch = () => { status: 'searching' | 'searched', categoryParam: string, isAllCategory: boolean = false, + keepPrevious: boolean = false, ) => { setIsLoading(true); setError(null); - setRooms([]); - setNextCursor(null); - setIsLast(true); + if (!keepPrevious) { + setRooms([]); + setNextCursor(null); + setIsLast(true); + } try { const isFinalized = status === 'searched'; @@ -142,7 +152,7 @@ const GroupSearch = () => { setSearchStatus('searching'); setShowTabs(false); const id = setTimeout(() => { - searchFirstPage(trimmed, toSortKey(selectedFilter), 'searching', category); + searchFirstPage(trimmed, toSortKey(selectedFilter), 'searching', category, false, true); }, 300); setSearchTimeoutId(id); }; @@ -195,12 +205,30 @@ const GroupSearch = () => { useEffect(() => { if (searchStatus !== 'searched') return; - const term = searchTerm.trim(); + const term = searchTermRef.current.trim(); + const currentCategory = categoryRef.current; + const isAllCategory = !term && currentCategory === ''; + + searchFirstPage( + term, + toSortKey(selectedFilterRef.current), + 'searched', + currentCategory, + isAllCategory, + true, + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchStatus, searchTerm]); + + useEffect(() => { + if (searchStatusRef.current !== 'searched') return; + + const term = searchTermRef.current.trim(); const isAllCategory = !term && category === ''; - searchFirstPage(term, toSortKey(selectedFilter), 'searched', category, isAllCategory); + searchFirstPage(term, toSortKey(selectedFilter), 'searched', category, isAllCategory, true); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedFilter, category, searchStatus, searchTerm]); + }, [selectedFilter, category]); useEffect(() => { const term = searchTerm.trim(); @@ -213,7 +241,7 @@ const GroupSearch = () => { const id = setTimeout(() => { const currentCategory = categoryRef.current; - searchFirstPage(term, toSortKey(selectedFilter), 'searching', currentCategory); + searchFirstPage(term, toSortKey(selectedFilter), 'searching', currentCategory, false, true); }, 300); setSearchTimeoutId(id); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -320,7 +348,8 @@ const GroupSearch = () => { {searchStatus !== 'idle' ? ( <> - {isLoading && rooms.length === 0 ? ( + {(isLoading && rooms.length === 0) || + (searchStatus === 'searching' && rooms.length === 0) ? ( 검색 중... ) : ( { } }} handleRecentSearchClick={handleRecentSearchClick} + isLoading={isRecentLoading} />

전체 모임방 둘러보기