Skip to content
95 changes: 63 additions & 32 deletions src/pages/ListPage/ListPage.jsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,99 @@
import { useEffect, useState } from 'react';
import Slider from './components/Slider';
import styles from './ListPage.module.scss';
import Button from '../../components/Button/Button';
import { Link } from 'react-router-dom';
import Button from '@/components/Button/Button';

import { listRecipients } from '../../apis/recipientsApi';
import { useApi } from '../../hooks/useApi';
import { listRecipients } from '@/apis/recipientsApi';
import { useApi } from '@/hooks/useApi';

const ListPage = () => {
/* 무한스크롤: Api 요청 데이터에서 next값이 있는지 확인, true 일때만 데이터를 불러옴 */
const hasNext = false;
/* 무한스크롤: 추가 데이터 로드 */
const loadMore = () => {};
// 인기/최신 각각 오프셋·hasNext 관리
const [popularOffset, setPopularOffset] = useState(0);
const [recentOffset, setRecentOffset] = useState(0);
const [popularHasNext, setPopularHasNext] = useState(false);
const [recentHasNext, setRecentHasNext] = useState(false);

// 1) useApi로 전체 Recipient 목록(fetch) 요청
// useApi 훅으로 인기순 데이터 페칭
const {
data: listData,
// refetch 필요 시 사용 가능
data: popularData,
loading: popularLoading,
refetch: getPopularList,
} = useApi(
listRecipients,
{ limit: 20, offset: 0 },
{ limit: 20, offset: popularOffset, sortLike: true },
{
errorMessage: '롤링페이퍼 목록을 불러오는 데 실패했습니다.',
errorMessage: '인기 롤링페이퍼 목록을 불러오는 데 실패했습니다.',
retry: 1,
immediate: true, // 마운트 시 자동 호출
immediate: true,
},
);

// 2) 정렬된 두 배열을 상태로 관리
const [popularCards, setPopularCards] = useState([]); // reactionCount 내림차순
const [recentCards, setRecentCards] = useState([]); // createdAt 최신순
// useApi 훅으로 최신순 데이터 페칭
const {
data: recentData,
loading: recentLoading,
refetch: getRecentList,
} = useApi(
listRecipients,
{ limit: 20, offset: recentOffset, sortLike: false },
{
errorMessage: '최근 롤링페이퍼 목록을 불러오는 데 실패했습니다.',
retry: 1,
immediate: true,
},
);

// 인기Cards 상태
const [popularCards, setPopularCards] = useState([]);
useEffect(() => {
if (popularLoading || !popularData) return;
const { results, next } = popularData;
setPopularCards((prev) => (popularOffset === 0 ? results : [...prev, ...results]));
setPopularHasNext(!!next);
}, [popularData, popularLoading, popularOffset]);

// 3) listData.results가 들어오는 시점에 한 번만 정렬
// 최근Cards 상태
const [recentCards, setRecentCards] = useState([]);
useEffect(() => {
if (!listData || !Array.isArray(listData.results)) {
return;
}
const arr = listData.results;
if (recentLoading || !recentData) return;
const { results, next } = recentData;
setRecentCards((prev) => (recentOffset === 0 ? results : [...prev, ...results]));
setRecentHasNext(!!next);
}, [recentData, recentLoading, recentOffset]);

// 인기순 (reactionCount 내림차순)
const byPopular = [...arr].sort((a, b) => b.reactionCount - a.reactionCount);
setPopularCards(byPopular);
// 인기 무한스크롤 로드
const loadMorePopular = () => {
if (popularLoading || !popularHasNext) return;
const newOffset = popularOffset + 20;
setPopularOffset(newOffset);
getPopularList({ limit: 20, offset: newOffset, sortLike: true });
};

// 최신순 (createdAt 최신순)
const byRecent = [...arr].sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
setRecentCards(byRecent);
}, [listData]);
// 최신 무한스크롤 로드
const loadMoreRecent = () => {
if (recentLoading || !recentHasNext) return;
const newOffset = recentOffset + 20;
setRecentOffset(newOffset);
getRecentList({ limit: 20, offset: newOffset, sortLike: false });
};

return (
<div className={styles['list-page']}>
{/* 인기 롤링 페이퍼 🔥 */}
<section className={styles['list-page__section']}>
<h2 className={styles['list-page__title']}>인기 롤링 페이퍼 🔥</h2>
<Slider cards={popularCards} hasNext={hasNext} loadMore={loadMore} />
<Slider cards={popularCards} hasNext={popularHasNext} loadMore={loadMorePopular} />
</section>

{/* 최근에 만든 롤링 페이퍼 ⭐️ */}
<section className={styles['list-page__section']}>
<h2 className={styles['list-page__title']}>최근에 만든 롤링 페이퍼 ⭐️</h2>
<Slider className={styles['list-page_slider']} cards={recentCards} />
<Slider cards={recentCards} hasNext={recentHasNext} loadMore={loadMoreRecent} />
</section>

<Link to='/post' style={{ textDecoration: 'none', textAlign: 'center' }}>
<button className={styles['list-page__createButton']}>나도 만들어보기</button>
<Link to='/post'>
<Button className={styles['list-page__createButton']}>나도 만들어보기</Button>
</Link>
</div>
);
Expand Down
3 changes: 3 additions & 0 deletions src/pages/ListPage/ListPage.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@
font-size: 18px;
line-height: 28px;
color: var(--color-white);
align-self: center;
display: block; // inline → block으로 바꿔서 margin:auto 적용
margin: 0 auto;

@media screen and (min-width: 355px) and (max-width: 1199px) {
width: 100%;
Expand Down
33 changes: 26 additions & 7 deletions src/pages/ListPage/components/Slider.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useRef } from 'react';
import styles from './Slider.module.scss';
import ItemCard from './ItemCard';
import ArrowButton from '../../../components/Button/ArrowButton';
import HorizontalScrollContainer from '../../../components/HorizontalScrollContainer/HorizontalScrollContainer';
import ArrowButton from '@/components/Button/ArrowButton';
import HorizontalScrollContainer from '@/components/HorizontalScrollContainer/HorizontalScrollContainer';
import { useSliderPaging } from '@/hooks/useSliderPaging';
import InfinityScrollWrapper from '@/components/InfinityScrollWrapper/InfinityScrollWrapper';

Expand All @@ -11,9 +11,7 @@ const GAP = 16;
const PAGE_SIZE = 4;

const Slider = ({ cards, hasNext, loadMore }) => {
/* 무한 스크롤: 스크롤 감지 ref 요소 전달 */
const scrollObserverRef = useRef(null);

const { wrapperRef, isDesktop, pageIndex, canPrev, canNext, goPrev, goNext } = useSliderPaging({
totalItems: cards.length,
pageSize: PAGE_SIZE,
Expand All @@ -27,6 +25,22 @@ const Slider = ({ cards, hasNext, loadMore }) => {
? cards.slice(pageIndex * PAGE_SIZE, pageIndex * PAGE_SIZE + PAGE_SIZE)
: cards;

// 오른쪽 화살표 클릭 핸들러
const handleNext = () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수연님! 프리뷰 링크에서 확인해보았는데, 태블릿 사이즈 이하에서 무한 스크롤시에는 데이터가 순차적으로 잘 불러와지고 있고,
웹에서는 초기에 데이터가 여러번 호출되어 마지막 리스트에서 버튼을 클릭하지 않아도 데이터를 불러오고 있는 것 같습니다. 해당 부분 확인 부탁드립니다!

if (isDesktop) {
const totalPage = Math.max(0, Math.ceil(cards.length / PAGE_SIZE) - 1);
if (pageIndex < totalPage) {
goNext();
} else if (pageIndex === totalPage && hasNext) {
// 마지막 페이지이면서 서버에 더 있으면 추가 로드
loadMore();
}
} else {
// 모바일에서는 그냥 loadMore()로 충분
loadMore();
}
};

return (
<div className={styles.slider}>
{isDesktop && canPrev && (
Expand All @@ -37,7 +51,12 @@ const Slider = ({ cards, hasNext, loadMore }) => {

<div ref={wrapperRef} className={styles['slider__container']}>
<HorizontalScrollContainer ref={scrollObserverRef} hideScroll={false}>
<InfinityScrollWrapper hasNext={hasNext} callback={loadMore} isHorizontal>
<InfinityScrollWrapper
hasNext={hasNext}
callback={loadMore}
isHorizontal
scrollObserverRef={scrollObserverRef}
>
<div className={styles['slider__track']}>
{visibleCards.map((c) => (
<ItemCard key={c.id} data={c} />
Expand All @@ -47,9 +66,9 @@ const Slider = ({ cards, hasNext, loadMore }) => {
</HorizontalScrollContainer>
</div>

{isDesktop && canNext && (
{isDesktop && (canNext || hasNext) && (
<div className={styles['slider__arrow--right']}>
<ArrowButton direction='right' onClick={goNext} />
<ArrowButton direction='right' onClick={handleNext} />
</div>
)}
</div>
Expand Down
Loading