Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 4 additions & 25 deletions src/pages/feed/Feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { getTotalFeeds } from '@/api/feeds/getTotalFeed';
import { getMyFeeds } from '@/api/feeds/getMyFeed';
import { useSocialLoginToken } from '@/hooks/useSocialLoginToken';
import { useInifinieScroll } from '@/hooks/useInifinieScroll';
import LoadingSpinner from '@/components/common/LoadingSpinner';
import { Container, SkeletonWrapper } from './Feed.styled';
import type { PostData } from '@/types/post';

Expand Down Expand Up @@ -77,29 +78,7 @@ const Feed = () => {
}, [activeTab]);

const currentFeed = activeTab === '피드' ? totalFeed : myFeed;

useEffect(() => {
const loadFeedsWithToken = async () => {
await waitForToken();

setTabLoading(true);

try {
const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));

if (activeTab === '피드') {
await Promise.all([loadTotalFeeds(), minLoadingTime]);
} else if (activeTab === '내 피드') {
await Promise.all([loadMyFeeds(), minLoadingTime]);
}
} finally {
setTabLoading(false);
setInitialLoading(false);
}
};

loadFeedsWithToken();
}, [activeTab, waitForToken, loadTotalFeeds, loadMyFeeds]);
const showInitialLoading = currentFeed.isLoading && currentFeed.items.length === 0;

return (
<Container>
Expand All @@ -109,7 +88,7 @@ const Feed = () => {
rightButtonClick={handleNoticeButton}
/>
<TabBar tabs={tabs} activeTab={activeTab} onTabClick={setActiveTab} />
{initialLoading || tabLoading ? (
{showInitialLoading ? (
activeTab === '내 피드' ? (
<OtherFeedSkeleton showFollowButton={false} paddingTop={136} />
) : (
Expand All @@ -126,7 +105,7 @@ const Feed = () => {
<TotalFeed
showHeader={true}
posts={totalFeed.items}
isMyFeed={false}
isTotalFeed={true}
isLast={totalFeed.isLast}
/>
</>
Expand Down
8 changes: 1 addition & 7 deletions src/pages/feed/FeedDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,8 @@ const FeedDetailPage = () => {

try {
setLoading(true);

const feedResponse = await getFeedDetail(Number(feedId));
const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));
const [feedResponse, commentsResponse] = await Promise.all([
getFeedDetail(Number(feedId)),
getComments(Number(feedId), { postType: 'FEED' }),
]);
await minLoadingTime;
const [feedResponse] = await Promise.all([getFeedDetail(Number(feedId)), minLoadingTime]);

setFeedData(feedResponse.data);
setError(null);
Expand Down
66 changes: 24 additions & 42 deletions src/pages/feed/FollowerListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,52 +19,23 @@ const FollowerListPage = () => {

const [totalCount, setTotalCount] = useState(0);

const handleBackClick = () => {
navigate(-1);
};

const loadUserList = useCallback(
async (cursor?: string) => {
if (loading) return;

try {
setLoading(true);
setError(null);
let response;

const minLoadingTime = !cursor ? new Promise(resolve => setTimeout(resolve, 500)) : null;

if (type === 'followerlist') {
if (!userId) {
setError('사용자 ID가 없습니다.');
return;
}
const [data] = await Promise.all([
getFollowerList(userId, { size: 10, cursor: cursor || null }),
]);
response = data;
} else {
const [data] = await Promise.all([
getFollowingList({ size: 10, cursor: cursor || null }),
]);
response = data;
}
await minLoadingTime;
const userList = useInifinieScroll<FollowData>({
enabled: !!type,
reloadKey: `${type}-${userId ?? ''}`,
fetchPage: async cursor => {
const minLoadingTime = cursor ? null : new Promise(resolve => setTimeout(resolve, 500));

let userData: FollowData[] = [];
if (type === 'followerlist') {
userData = (response.data as { followers: FollowData[] })?.followers || [];
} else {
userData = (response.data as { followings: FollowData[] })?.followings || [];
if (type === 'followerlist') {
if (!userId) {
throw new Error('사용자 ID가 없습니다.');
}

if (!response || !response.data) {
setError('API 응답이 없습니다.');
return;
}
const response = await getFollowerList(userId, { size: 10, cursor });
if (minLoadingTime) await minLoadingTime;

const total = response.data.totalFollowerCount;
if (typeof total === 'number') setTotalCount(total);

return {
items: response.data.followers || [],
nextCursor: response.data.nextCursor || null,
Expand All @@ -73,8 +44,11 @@ const FollowerListPage = () => {
}

const response = await getFollowingList({ size: 10, cursor });
if (minLoadingTime) await minLoadingTime;

const total = response.data.totalFollowingCount;
if (typeof total === 'number') setTotalCount(total);

return {
items: response.data.followings || [],
nextCursor: response.data.nextCursor || null,
Expand All @@ -89,11 +63,18 @@ const FollowerListPage = () => {
window.scrollTo(0, 0);
}, [type, userId]);

const showInitialSkeleton = userList.isLoading && userList.items.length === 0;

return (
<Wrapper>
<TitleHeader leftIcon={<img src={leftArrow} alt="뒤로가기" />} onLeftClick={handleBackClick} title={title} />
<TitleHeader
leftIcon={<img src={leftArrow} alt="뒤로가기" />}
onLeftClick={() => navigate(-1)}
title={title}
/>
<TotalBar>전체 {totalCount}</TotalBar>
{loading && userList.length === 0 ? (

{showInitialSkeleton ? (
<UserProfileList>
{Array.from({ length: 5 }).map((_, i) => (
<UserProfileItemSkeleton key={i} type={type as UserProfileType} />
Expand All @@ -116,6 +97,7 @@ const FollowerListPage = () => {
isMyself={user.isMyself}
/>
))}

{!userList.isLast && <div ref={userList.sentinelRef} style={{ height: 20 }} />}
{userList.isLoadingMore && (
<div style={{ display: 'flex', justifyContent: 'center', padding: '16px 0' }}>
Expand Down
131 changes: 16 additions & 115 deletions src/pages/groupSearch/GroupSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@ import rightChevron from '../../assets/common/right-Chevron.svg';
import { useState, useEffect, useCallback } from 'react';
import RecentSearchTabs from '@/components/search/RecentSearchTabs';
import GroupSearchResult from '@/components/search/GroupSearchResult';
import LoadingSpinner from '@/components/common/LoadingSpinner';
import { getRecentSearch, type RecentSearchData } from '@/api/recentsearch/getRecentSearch';
import { deleteRecentSearch } from '@/api/recentsearch/deleteRecentSearch';
import { getSearchRooms } from '@/api/rooms/getSearchRooms';
import { useNavigate, useLocation } from 'react-router-dom';
import { AllRoomsButton, LoadingMessage } from './GroupSearch.styled';
import { AllRoomsButton } from './GroupSearch.styled';
import { useInifinieScroll } from '@/hooks/useInifinieScroll';
import { GroupCardSkeleton, RecentSearchTabsSkeleton } from '@/shared/ui/Skeleton';
import { Content } from '@/components/search/GroupSearchResult.styled';
Expand All @@ -25,20 +24,16 @@ const GroupSearch = () => {

const [searchTerm, setSearchTerm] = useState('');
const [searchStatus, setSearchStatus] = useState<SearchStatus>('idle');

const [selectedFilter, setSelectedFilter] = useState<string>('마감임박순');
const toSortKey = useCallback(
(f: string): SortKey => (f === '인기순' ? 'memberCount' : 'deadline'),
[],
);
const [category, setCategory] = useState<string>('');
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');

const [recentSearches, setRecentSearches] = useState<RecentSearchData[]>([]);

const [isLoadingRecentSearches, setIsLoadingRecentSearches] = useState(true);
const [searchTimeoutId, setSearchTimeoutId] = useState<NodeJS.Timeout | null>(null);

const [showTabs, setShowTabs] = useState(false);

useEffect(() => {
Expand All @@ -52,7 +47,7 @@ const GroupSearch = () => {
}
}, [searchStatus]);

const fetchRecentSearches = async () => {
const fetchRecentSearches = useCallback(async () => {
try {
setIsLoadingRecentSearches(true);
const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));
Expand All @@ -63,62 +58,14 @@ const GroupSearch = () => {
} finally {
setIsLoadingRecentSearches(false);
}
};

const searchFirstPage = useCallback(
async (
term: string,
sortKey: SortKey,
status: 'searching' | 'searched',
categoryParam: string,
isAllCategory: boolean = false,
keepPrevious: boolean = false,
) => {
setIsLoading(true);
setError(null);
if (!keepPrevious) {
setRooms([]);
setNextCursor(null);
setIsLast(true);
}

try {
const isFinalized = status === 'searched';
const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));
const [res] = await Promise.all([
getSearchRooms(
term.trim(),
sortKey,
undefined,
isFinalized,
categoryParam,
isAllCategory,
),
]);
await minLoadingTime;
if (res.isSuccess) {
const { roomList, nextCursor: nc, isLast: last } = res.data;

setRooms(roomList);
setNextCursor(nc);
setIsLast(last);
} else {
setError(res.message || '검색 실패');
}
} catch {
setError('네트워크 오류가 발생했습니다.');
} finally {
setIsLoading(false);
}
},
[],
);
}, []);

useEffect(() => {
if (location.state?.allRooms) {
navigate(location.pathname, { replace: true });

setSearchTerm('');
setDebouncedSearchTerm('');
setSearchStatus('searched');
setShowTabs(true);
setCategory('');
Expand All @@ -141,7 +88,7 @@ const GroupSearch = () => {
setSearchStatus('searching');
setShowTabs(false);
const id = setTimeout(() => {
searchFirstPage(trimmed, toSortKey(selectedFilter), 'searching', category, false, true);
setDebouncedSearchTerm(trimmed);
}, 300);
setSearchTimeoutId(id);
};
Expand Down Expand Up @@ -180,81 +127,35 @@ const GroupSearch = () => {
setCategory('');
};

const searchStatusRef = useRef(searchStatus);
const categoryRef = useRef(category);
const selectedFilterRef = useRef(selectedFilter);
const searchTermRef = useRef(searchTerm);

useEffect(() => {
searchStatusRef.current = searchStatus;
categoryRef.current = category;
selectedFilterRef.current = selectedFilter;
searchTermRef.current = searchTerm;
});

useEffect(() => {
if (searchStatus !== 'searched') return;

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, true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedFilter, category]);

useEffect(() => {
if (searchStatus === 'searched') {
setDebouncedSearchTerm(searchTerm.trim());
}
}, [searchStatus, searchTerm]);

const id = setTimeout(() => {
const currentCategory = categoryRef.current;
searchFirstPage(term, toSortKey(selectedFilter), 'searching', currentCategory, false, true);
}, 300);
setSearchTimeoutId(id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchTerm, searchStatus, selectedFilter]);
const queryTerm = searchStatus === 'searched' ? searchTerm.trim() : debouncedSearchTerm.trim();
const isAllCategory = !queryTerm && category === '';

const loadMore = useCallback(async () => {
const trimmedTerm = searchTerm.trim();
const isAllCategory = !trimmedTerm && category === '';
if ((!isAllCategory && !trimmedTerm) || !nextCursor || isLast || isLoadingMore) return;
try {
setIsLoadingMore(true);
const isFinalized = searchStatus === 'searched';
const isAllCategory = !queryTerm && category === '';
const searchResult = useInifinieScroll({
enabled: searchStatus !== 'idle' && (searchStatus === 'searched' || !!queryTerm),
reloadKey: `${searchStatus}-${queryTerm}-${selectedFilter}-${category}`,
fetchPage: async cursor => {
if (searchStatus === 'searching' && !queryTerm) {
return { items: [], nextCursor: null, isLast: true };
}

const minLoadingTime = cursor ? null : new Promise(resolve => setTimeout(resolve, 500));
const res = await getSearchRooms(
queryTerm,
toSortKey(selectedFilter),
cursor ?? undefined,
isFinalized,
searchStatus === 'searched',
category,
isAllCategory,
);

if (minLoadingTime) await minLoadingTime;

if (!res.isSuccess) {
throw new Error(res.message || '검색 실패');
}
Expand Down Expand Up @@ -317,7 +218,7 @@ const GroupSearch = () => {

{searchStatus !== 'idle' ? (
<>
{isLoading && rooms.length === 0 ? (
{searchResult.isLoading && searchResult.items.length === 0 ? (
<Content>
{Array.from({ length: 5 }).map((_, i) => (
<GroupCardSkeleton key={i} type="search" />
Expand Down
Loading