diff --git a/src/api/service/follower-service/index.ts b/src/api/service/follower-service/index.ts index 473ee604..ffeff2f2 100644 --- a/src/api/service/follower-service/index.ts +++ b/src/api/service/follower-service/index.ts @@ -1,11 +1,16 @@ import { api } from '@/api/core'; -import { GetFollowerResponse } from '@/types/service/follow'; +import { GetFollowerParams, GetFollowerResponse } from '@/types/service/follow'; import { FollowPathParams } from '@/types/service/user'; export const followerServiceRemote = () => ({ // 팔로워 목록 조회 - getFollowers: async ({ userId }: { userId: number }) => { - return api.get(`/users/${userId}/follow`); + getFollowers: async ({ userId, cursor, size = 20 }: GetFollowerParams) => { + return api.get(`/users/${userId}/follow`, { + params: { + cursor, + size, + }, + }); }, // 팔로워 등록 diff --git a/src/app/message/page.tsx b/src/app/message/page.tsx index ffc54176..988e359a 100644 --- a/src/app/message/page.tsx +++ b/src/app/message/page.tsx @@ -6,9 +6,12 @@ import { useEffect, useState } from 'react'; import Cookies from 'js-cookie'; +import { API } from '@/api'; import { Chat, FollowingList, FollowingNone, FollowingSearch } from '@/components/pages/message'; import { TabNavigation } from '@/components/shared'; -import { useGetFollowers } from '@/hooks/use-follower/use-follower-get'; +import { useInfiniteScroll } from '@/hooks/use-group/use-group-infinite-list'; +import { useIntersectionObserver } from '@/hooks/use-intersection-observer'; +import { INTERSECTION_OBSERVER_THRESHOLD } from '@/lib/constants/group-list'; const SOCIAL_TABS = [ { label: '팔로잉', value: 'following' }, @@ -17,16 +20,47 @@ const SOCIAL_TABS = [ export default function FollowingPage() { const [userId, setUserId] = useState(0); - const { data: followers } = useGetFollowers({ userId }, { enabled: !!userId }); + const params = useSearchParams(); + const tab = params.get('tab') || 'following'; useEffect(() => { const id = Cookies.get('userId'); // eslint-disable-next-line react-hooks/set-state-in-effect setUserId(Number(id)); }, []); - console.log(followers); - const params = useSearchParams(); - const tab = params.get('tab') || 'following'; + + // 1. 무한 스크롤 훅 호출 + const { + items: followers, + error, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + completedMessage, + } = useInfiniteScroll({ + queryFn: async ({ cursor, size }) => { + return await API.followerService.getFollowers({ + userId, + cursor, + size, + }); + }, + queryKey: ['followers', userId], + completedMessage: '모든 팔로잉을 불러왔습니다.', + enabled: !!userId, + }); + + // 2. IntersectionObserver로 스크롤 감지 + const sentinelRef = useIntersectionObserver({ + onIntersect: () => { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }, + enabled: hasNextPage && error === null, + threshold: INTERSECTION_OBSERVER_THRESHOLD, + }); + return (
@@ -35,13 +69,33 @@ export default function FollowingPage() { {tab === 'following' && ( <> + {!error && followers && followers.length > 0 ? ( + <> + + + {/* 3. Sentinel 요소 (필수!) */} + {hasNextPage &&
} + + {/* 4. 다음 페이지 로딩 중 */} + {isFetchingNextPage && ( +
+ 더 불러오는 중... +
+ )} - {followers && followers.items.length > 0 ? ( - + {/* 5. 완료 메시지 */} + {!hasNextPage && ( +
+ {completedMessage} +
+ )} + ) : ( -
- -
+ !error && ( +
+ +
+ ) )} )} diff --git a/src/components/pages/message/message-following-card/index.tsx b/src/components/pages/message/message-following-card/index.tsx index d6aeebfc..2b1e433b 100644 --- a/src/components/pages/message/message-following-card/index.tsx +++ b/src/components/pages/message/message-following-card/index.tsx @@ -32,9 +32,6 @@ export const FollowingCard = ({ className='flex cursor-pointer items-center gap-3 bg-white p-5' onClick={handleClick} > - {/*
- -
*/}
{ }); test('모든 아이템이 렌더링 되는지 테스트', () => { - render(); + render(); TEST_ITEMS.items.forEach((item) => { expect(screen.getByText(item.nickname)).toBeInTheDocument(); diff --git a/src/components/pages/message/message-following-list/index.tsx b/src/components/pages/message/message-following-list/index.tsx index 1f45e09d..9c113c51 100644 --- a/src/components/pages/message/message-following-list/index.tsx +++ b/src/components/pages/message/message-following-list/index.tsx @@ -1,17 +1,17 @@ 'use client'; -import { GetFollowerResponse } from '@/types/service/follow'; +import { Follower } from '@/types/service/follow'; import { FollowingCard } from '../message-following-card'; interface FollowingListProps { - items: GetFollowerResponse; + items: Follower[]; } export const FollowingList = ({ items }: FollowingListProps) => { return (
- {items.items.map((item) => ( + {items.map((item) => ( { - return
아직 팔로우 한 사람이 없어요.
; + return ( +
+ 아직 팔로우 한 사람이 없어요. + 마음에 드는 유저를 팔로우 해보세요! +
+ ); }; diff --git a/src/components/pages/message/message-following-search/index.tsx b/src/components/pages/message/message-following-search/index.tsx index 657adec4..22b10221 100644 --- a/src/components/pages/message/message-following-search/index.tsx +++ b/src/components/pages/message/message-following-search/index.tsx @@ -8,34 +8,59 @@ import { useAddFollowers } from '@/hooks/use-follower'; const FollowerModal = ({ userId }: { userId: number }) => { const { close } = useModal(); const [nickname, setNickname] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); const { mutate: addFollower } = useAddFollowers({ userId }); const handleConfirm = () => { - addFollower({ followNickname: nickname }); - close(); + if (!nickname.trim()) { + setErrorMessage('닉네임을 입력해주세요.'); + return; + } + + setErrorMessage(''); // 에러 메세지 초기화. + + addFollower( + { followNickname: nickname }, + { + onSuccess: () => { + close(); + }, + onError: () => { + setErrorMessage('존재하지 않는 유저입니다.'); + }, + }, + ); }; const handleChange = (e: React.ChangeEvent) => { const value = e.target.value; setNickname(value); + if (errorMessage) { + setErrorMessage(''); + } }; // 모달 모양 바뀌면 적용하기! return ( - + 팔로우 할 닉네임을 입력하세요 - } - placeholder='nickname' - onChange={handleChange} - onKeyDown={(e) => { - if (e.key === 'Enter') { - handleConfirm(); +
+ } - }} - /> - + placeholder='nickname' + value={nickname} + onChange={handleChange} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleConfirm(); + } + }} + /> + {errorMessage &&

{errorMessage}

} +