Skip to content
Merged
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
66 changes: 20 additions & 46 deletions src/hooks/useSliderPaging.jsx
Original file line number Diff line number Diff line change
@@ -1,60 +1,34 @@
// src/hooks/useSliderPaging.js
import { useState, useRef, useEffect, useCallback } from 'react';
import { useState, useEffect, useMemo, useCallback } from 'react';

export function useSliderPaging({ totalItems, pageSize, cardWidth, gap, breakpoint = 1200 }) {
const wrapperRef = useRef(null);

// 1) 뷰포트가 데스크톱 모드인지
export function useSliderPaging({ totalItems, pageSize, breakpoint = 1200 }) {
// 1) 뷰포트 모드(데스크톱/모바일) 감지
const [isDesktop, setIsDesktop] = useState(window.innerWidth >= breakpoint);
useEffect(() => {
const onResize = () => setIsDesktop(window.innerWidth >= breakpoint);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
const handler = () => setIsDesktop(window.innerWidth >= breakpoint);
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, [breakpoint]);

// 2) 페이지 인덱스
// 2) 페이지 인덱스 관리
const [pageIndex, setPageIndex] = useState(0);
const totalPage = Math.max(0, Math.ceil(totalItems / pageSize) - 1);

// 3) 한 페이지당 스크롤 픽셀
const offset = pageSize * (cardWidth + gap);

// 4) 데스크톱: 버튼 클릭 시 페이지 이동
const slideTo = useCallback(
(idx) => {
const el = wrapperRef.current;
if (!el) return;
el.scrollTo({ left: idx * offset, behavior: 'smooth' });
setPageIndex(idx);
},
[offset],
const totalPage = useMemo(
() => Math.max(0, Math.ceil(totalItems / pageSize) - 1),
[totalItems, pageSize],
);

// 3) 이전/다음 버튼 활성화 여부
const canPrev = pageIndex > 0;
const canNext = pageIndex < totalPage;
const goPrev = () => canPrev && slideTo(pageIndex - 1);
const goNext = () => canNext && slideTo(pageIndex + 1);

// 5) 모바일·태블릿: 직접 스크롤 → 페이지 인덱스 동기화
useEffect(() => {
if (isDesktop) return;
const el = wrapperRef.current;
if (!el) return;
const onScroll = () => {
const idx = Math.round(el.scrollLeft / offset);
setPageIndex(Math.min(Math.max(idx, 0), totalPage));
};
el.addEventListener('scroll', onScroll, { passive: true });
return () => el.removeEventListener('scroll', onScroll);
}, [isDesktop, offset, totalPage]);
// 4) 페이지 이동 함수
const goPrev = useCallback(() => {
if (canPrev) setPageIndex((idx) => idx - 1);
}, [canPrev]);

const goNext = useCallback(() => {
if (canNext) setPageIndex((idx) => idx + 1);
}, [canNext]);

return {
wrapperRef,
isDesktop,
pageIndex,
canPrev,
canNext,
goPrev,
goNext,
};
return { isDesktop, pageIndex, totalPage, canPrev, canNext, goPrev, goNext };
}
4 changes: 2 additions & 2 deletions src/pages/ListPage/ListPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,15 @@ const ListPage = () => {
setRecentHasNext(!!next);
}, [recentData, recentLoading, recentOffset]);

// 인기 무한스크롤 로드
// 인기 카드 추가 로드 함수
const loadMorePopular = () => {
if (popularLoading || !popularHasNext) return;
const newOffset = popularOffset + 20;
setPopularOffset(newOffset);
getPopularList({ limit: 20, offset: newOffset, sortLike: true });
};

// 최신 무한스크롤 로드
// 최신 카드 추가 로드 함수
const loadMoreRecent = () => {
if (recentLoading || !recentHasNext) return;
const newOffset = recentOffset + 20;
Expand Down
1 change: 1 addition & 0 deletions src/pages/ListPage/ListPage.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@

@media screen and (min-width: 355px) and (max-width: 1199px) {
width: 100%;
margin-bottom: 20px;
}
}
}
45 changes: 16 additions & 29 deletions src/pages/ListPage/components/ItemCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,32 @@
import React from 'react';
import { Link } from 'react-router-dom';
import styles from './ItemCard.module.scss';
import { COLOR_STYLES } from '@/constants/colorThemeStyle';
import ShowAvatars from './ShowAvatars';
import ShowEmoji from './ShowEmoji';
import { getBackgroundStylesFromPostData } from '@/utils/getBackgroundStylesFromPostData';
import { getContentStylesFromPostData } from '@/utils/getContentStylesFromPostData';

const ItemCard = ({
data: {
id,
name,
backgroundColor,
backgroundImageURL,
messageCount,
recentMessages = [],
topReactions = [],
},
id,
name,
backgroundColor,
backgroundImageURL,
messageCount,
recentMessages = [],
topReactions = [],
}) => {
const { primary, border, accent } = COLOR_STYLES[backgroundColor] || {};
const backgroundStyle = backgroundImageURL
? {
backgroundImage: `url(${backgroundImageURL})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}
: { backgroundColor: primary, borderColor: border };
// 배경 설정: 이미지가 있으면 이미지, 없으면 컬러
const backgroundStyle = getBackgroundStylesFromPostData({ backgroundColor, backgroundImageURL });

const contentClass = backgroundImageURL
? `${styles['item-card__content']} ${styles['item-card__content--image']}`
: styles['item-card__content'];
// 밝은 색: 검정 텍스트
// 어두운 색: 하얀색 텍스트
// 이미지: 하얀색 텍스트 / 어두운색 overlay 적용
const contentStyle = getContentStylesFromPostData(backgroundImageURL);

return (
<Link to={`/post/${id}`} className={styles['item-card__link']}>
<div className={styles['item-card']} style={backgroundStyle}>
{backgroundImageURL && (
<div
className={styles['item-card__overlay']}
style={{ background: `linear-gradient(180deg, ${accent} 0%, rgba(0,0,0,0) 100%)` }}
/>
)}

<div className={contentClass}>
<div className={styles['item-card__content']} style={contentStyle}>
<h3 className={styles['item-card__title']}>To. {name}</h3>
<p className={styles['item-card__meta']}>{messageCount}명이 작성했어요!</p>

Expand Down
67 changes: 40 additions & 27 deletions src/pages/ListPage/components/Slider.jsx
Original file line number Diff line number Diff line change
@@ -1,74 +1,87 @@
import { useRef } from 'react';
import React, { 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 { useSliderPaging } from '@/hooks/useSliderPaging';
import InfinityScrollWrapper from '@/components/InfinityScrollWrapper/InfinityScrollWrapper';

const CARD_WIDTH = 275;
const GAP = 16;
const PAGE_SIZE = 4;

const Slider = ({ cards, hasNext, loadMore }) => {
const scrollObserverRef = useRef(null);
const { wrapperRef, isDesktop, pageIndex, canPrev, canNext, goPrev, goNext } = useSliderPaging({
const { isDesktop, pageIndex, canPrev, canNext, goPrev, goNext } = useSliderPaging({
totalItems: cards.length,
pageSize: PAGE_SIZE,
cardWidth: CARD_WIDTH,
gap: GAP,
breakpoint: 1200,
});

// 데스크톱 전용: 현재 페이지에 해당하는 카드만
// 데스크탑: 현재 페이지*4 ~ 페이지*4+4 슬라이스
// 모바일: 전체 cards (무한 스크롤)
const visibleCards = isDesktop
? cards.slice(pageIndex * PAGE_SIZE, pageIndex * PAGE_SIZE + PAGE_SIZE)
: cards;

// 오른쪽 화살표 클릭 핸들러
const handleNext = () => {
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()로 충분
if (canNext) {
goNext();
} else if (hasNext) {
goNext();
loadMore();
}
};

return (
<div className={styles.slider}>
{isDesktop && canPrev && (
{isDesktop && (
<div className={styles['slider__arrow--left']}>
<ArrowButton direction='left' onClick={goPrev} />
<ArrowButton direction='left' onClick={goPrev} disabled={!canPrev} />
</div>
)}

<div ref={wrapperRef} className={styles['slider__container']}>
<HorizontalScrollContainer ref={scrollObserverRef} hideScroll={false}>
<div className={styles['slider__container']}>
{isDesktop ? (
<div className={styles['slider__track']}>
{visibleCards.map((card) => (
<ItemCard
key={card.id}
id={card.id}
name={card.name}
backgroundColor={card.backgroundColor}
backgroundImageURL={card.backgroundImageURL}
messageCount={card.messageCount}
recentMessages={card.recentMessages}
topReactions={card.topReactions}
/>
))}
</div>
) : (
<InfinityScrollWrapper
hasNext={hasNext}
callback={loadMore}
isHorizontal
scrollObserverRef={scrollObserverRef}
>
<div className={styles['slider__track']}>
{visibleCards.map((c) => (
<ItemCard key={c.id} data={c} />
{visibleCards.map((card) => (
<ItemCard
key={card.id}
id={card.id}
name={card.name}
backgroundColor={card.backgroundColor}
backgroundImageURL={card.backgroundImageURL}
messageCount={card.messageCount}
recentMessages={card.recentMessages}
topReactions={card.topReactions}
/>
))}
</div>
</InfinityScrollWrapper>
</HorizontalScrollContainer>
)}
</div>

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