diff --git a/package-lock.json b/package-lock.json index 7d84bdb..bb4489c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1661,6 +1661,7 @@ "version": "4.12.2", "resolved": "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.12.2.tgz", "integrity": "sha512-6PDYZGlhidt+Kc0ay890IU4HLNfIR7/OxPvcNxw+nJ4HQhMKd8pnGnPn4n2vqC/arRFCNWQhgJP8rpsYKsz0GQ==", + "license": "MIT", "dependencies": { "flairup": "1.0.0" }, diff --git a/src/Router.jsx b/src/Router.jsx index eb44d0a..56828b2 100644 --- a/src/Router.jsx +++ b/src/Router.jsx @@ -2,7 +2,7 @@ import { BrowserRouter, Routes, Route } from "react-router-dom"; import EditPage from "./pages/EditPage"; import HomePage from "./pages/HomePage"; -import ListPage from "./pages/ListPage"; +import ListPage from "./pages/ListPage/ListPage"; import MessagePage from "./pages/MessagePage"; import NotFoundPage from "./pages/NotFoundPage"; import PostItemPage from "./pages/PostItemPage"; diff --git a/src/components/CardList/CardList.jsx b/src/components/CardList/CardList.jsx index 7b0596a..14c855f 100644 --- a/src/components/CardList/CardList.jsx +++ b/src/components/CardList/CardList.jsx @@ -17,6 +17,11 @@ const CardList = ({ }) => { const backgroundImage = getBackgroundImage(backgroundColor); + const maxVisible = 3; + const visibleProfiles = profileSection.slice(0, maxVisible); + let extraCount = totalUsers - maxVisible; + if (extraCount < 0) extraCount = 0; + return (
{message}
-
{profileSection}
+
+ {visibleProfiles.map((profile, index) => ( +
+ {profile} +
+ ))} + {extraCount > 0 && ( +
+ +{extraCount} +
+ )} +
{totalUsers} @@ -41,7 +57,9 @@ const CardList = ({
{badges.map((badge, index) => ( -
+
+ {badge.text} {badge.count} +
))}
diff --git a/src/components/CardList/CardList.module.css b/src/components/CardList/CardList.module.css index b420f82..525634a 100644 --- a/src/components/CardList/CardList.module.css +++ b/src/components/CardList/CardList.module.css @@ -13,6 +13,7 @@ position: relative; background-size: cover; background-position: center; + padding: 1.875rem 1.5rem 1.25rem 1.5rem; } .cardBackground { @@ -31,10 +32,50 @@ .profileWrapper { display: flex; justify-content: flex-start; + align-items: center; + position: relative; +} + +.profileImage { + width: 1.9375rem; + height: 1.9375rem; + border-radius: 50%; + border: 0.09375rem solid white; + overflow: hidden; + position: relative; + z-index: 1; + margin-left: -0.8125rem; +} + +.profileImage img { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; +} + +.profileImage:first-child { + margin-left: 0; +} + +.extraCount { + width: 1.875rem; + height: 1.75rem; + display: flex; + justify-content: center; + align-items: center; + background-color: white; + color: #555555; + font-weight: var(--font-weight-regular); + font-size: 0.75rem; /* 전역 css에 없어서 이렇게 사용 */ + line-height: 1.125rem; + position: relative; + z-index: 2; + border-radius: 1.875rem; } .reactionsWrapper { - border-top: 1px solid rgba(0, 0, 0, 0.1); + border-top: 0.0625rem solid rgba(0, 0, 0, 0.1); padding-top: 1.3rem; margin-top: 0.75rem; display: flex; @@ -49,6 +90,36 @@ gap: 0.5rem; } +.badge { + width: 4.0625rem; + height: 2.25rem; + border-radius: 2rem; + background: rgba(0, 0, 0, 0.54); + display: flex; + justify-content: center; + align-items: center; + color: #ffffff; +} + +.userCount { + font-size: 1rem; + font-weight: var(--font-weight-regular); + color: var(--color-gray-700); + margin-top: 0.5rem; + margin-top: 1.5rem; + margin-top: 0.25rem; +} + +.userCountNumber { + font-weight: var(--font-weight-bold); + line-height: var(--line-height-16); +} + +.cardMessage { + font-size: var(--font-size-24); + font-weight: var(--font-weight-bold); +} + @media (max-width: 1024px) { .card-container { display: grid; @@ -64,6 +135,34 @@ grid-template-columns: repeat(2, 1fr); gap: 0.75rem; } + .card { + width: 13rem; + max-width: 100%; + height: 14.5rem; + border-radius: 1rem; + overflow: hidden; + cursor: pointer; + display: flex; + flex-direction: column; + gap: 0.5rem; + justify-content: space-between; + position: relative; + background-size: cover; + background-position: center; + padding: 1.875rem 1.5rem 1.25rem 1.5rem; + } + .reactionsContainer { + display: flex; + gap: 0.25rem; + } + .badge { + width: 3.4375rem; + height: 2rem; + } + .cardMessage { + font-size: var(--font-size-24); + font-weight: var(--font-weight-bold); + } } @media (max-width: 480px) { diff --git a/src/components/Modal/Modal.module.css b/src/components/Modal/Modal.module.css index 7fbaaa8..b159776 100644 --- a/src/components/Modal/Modal.module.css +++ b/src/components/Modal/Modal.module.css @@ -82,7 +82,7 @@ ::-webkit-scrollbar { width: 0.25rem; - height: 6.25rem; + height: 0.25rem; } ::-webkit-scrollbar-thumb { @@ -108,3 +108,29 @@ .button:hover { background-color: var(--color-purple-700); } + +@media (max-width: 768px) { + .modal { + width: 23.125rem; + height: 22rem; + padding: 1.5rem; + } + + .bodyText { + width: 100%; + height: 10rem; + overflow-y: auto; + } + + .img { + width: 2.5rem; + height: 2.5rem; + } + + .button { + margin-top: 1rem; + width: 6.25rem; + height: 2.25rem; + font-size: 0.875rem; + } +} diff --git a/src/pages/ListPage.jsx b/src/pages/ListPage.jsx deleted file mode 100644 index 466be0a..0000000 --- a/src/pages/ListPage.jsx +++ /dev/null @@ -1,5 +0,0 @@ -const ListPage = () => { - return
ListPage
; -}; - -export default ListPage; diff --git a/src/pages/ListPage/ListPage.jsx b/src/pages/ListPage/ListPage.jsx new file mode 100644 index 0000000..e03ff39 --- /dev/null +++ b/src/pages/ListPage/ListPage.jsx @@ -0,0 +1,257 @@ +import { useState, useMemo } from "react"; + +import { useNavigate } from "react-router-dom"; + +import ArrowButton from "../../components/ArrowButton/ArrowButton"; +import CardList from "../../components/CardList/CardList"; +import useFetchData from "../../hooks/useFetchData"; + +import styles from "./ListPage.module.css"; + +const ListPage = () => { + const navigate = useNavigate(); + + // 데이터, 로딩, 에러 상태 + const { + isLoading, + isError, + error, + data: recipients, + } = useFetchData(fetchRecipients); + + // 한 번에 보여줄 카드 개수 + const itemsToShow = 4; + + // 인기 캐러셀 인덱스 상태 + const [popularIndex, setPopularIndex] = useState(0); + // 최근 캐러셀 인덱스 상태 + const [recentIndex, setRecentIndex] = useState(0); + + // 인기 롤링 페이퍼 (messageCount, reactionCount 내림차순) + const popularRecipients = useMemo(() => { + return [...(recipients || [])].sort((a, b) => { + if (b.messageCount !== a.messageCount) { + return b.messageCount - a.messageCount; // 1순위: messageCount 내림차순 + } + return b.reactionCount - a.reactionCount; // 2순위: reactionCount 내림차순 + }); + }, [recipients]); + + // 최근 롤링 페이퍼 (createdAt 내림차순) + const recentRecipients = useMemo(() => { + return [...(recipients || [])].sort( + (a, b) => new Date(b.createdAt) - new Date(a.createdAt), + ); + }, [recipients]); + + // 인기 섹션 좌우 이동 핸들러 + const handlePopularPrev = () => { + if (popularIndex > 0) { + setPopularIndex(popularIndex - 1); + } + }; + const handlePopularNext = () => { + if (popularIndex + itemsToShow < popularRecipients.length) { + setPopularIndex(popularIndex + 1); + } + }; + + // 최근 섹션 좌우 이동 핸들러 + const handleRecentPrev = () => { + if (recentIndex > 0) { + setRecentIndex(recentIndex - 1); + } + }; + const handleRecentNext = () => { + if (recentIndex + itemsToShow < recentRecipients.length) { + setRecentIndex(recentIndex + 1); + } + }; + + //로딩 중 + if (isLoading) { + return ( +
+ {/* 인기 롤링 페이퍼 섹션 */} +
+
인기 롤링 페이퍼 🔥
+
+ {[...Array(1)].map((_, colIndex) => ( +
+
+
+ ))} +
+ + {/* 최근에 만든 롤링 페이퍼 */} +
최근에 만든 롤링 페이퍼 ⭐️
+
+ {[...Array(1)].map((_, colIndex) => ( +
+
+
+ ))} +
+
+
+ ); + } + + if (isError) return

오류 발생: {error?.message || "알 수 없는 오류"}

; + + // 카드 너비와 간격 (css와 일치시켜주세요) + const cardWidth = 275; // px + const cardGap = 20; // px + const totalCardWidth = cardWidth + cardGap; // 한 카드당 차지하는 전체 너비 + + return ( +
+ {/* 인기 롤링 페이퍼 섹션 */} +
인기 롤링 페이퍼 🔥
+
+ {/* 좌측/우측 버튼 */} +
+ {popularIndex === 0 ? null : ( + + )} +
+
+ {popularIndex + itemsToShow >= popularRecipients.length ? null : ( + + )} +
+ {/* 카드 리스트 래퍼 */} +
+
+ {popularRecipients.map((recipient) => { + const hexColor = getHexColor(recipient.backgroundColor); + // **동적 프로필 이미지**: recentMessages에서 profileImageURL 사용 + const dynamicProfileImages = + recipient.recentMessages?.map((msg) => ( + {msg.sender} + )) || []; + // topReactions 데이터를 이용해 동적 badges 생성 + const dynamicBadges = + recipient.topReactions?.map((reaction) => ({ + id: `badge-${reaction.id}`, + text: reaction.emoji, + count: reaction.count, + })) || []; + + return ( +
+ navigate(`/post/${recipient.id}`)} + /> +
+ ); + })} +
+
+
+ + {/* 최근 롤링 페이퍼 섹션 */} +
최근에 만든 롤링 페이퍼 ⭐️
+
+
+ {recentIndex === 0 ? null : ( + + )} +
+
+ {recentIndex + itemsToShow >= recentRecipients.length ? null : ( + + )} +
+
+
+ {recentRecipients.map((recipient) => { + const hexColor = getHexColor(recipient.backgroundColor); + // 동적 프로필 이미지: profileImageURL 사용 + const dynamicProfileImages = + recipient.recentMessages?.map((msg) => ( + {msg.sender} + )) || []; + // topReactions 데이터를 이용해 동적 badges 생성 + const dynamicBadges = + recipient.topReactions?.map((reaction) => ({ + id: `badge-${reaction.id}`, + text: reaction.emoji, + count: reaction.count, + })) || []; + + return ( +
+ navigate(`/post/${recipient.id}`)} + /> +
+ ); + })} +
+
+
+ + {/* 하단 버튼 (새 롤링 페이퍼 생성) */} +
+ +
+
+ ); +}; + +export default ListPage; + +// 배경색 매핑 +const colorMapping = { + beige: "#FFE2AD", + purple: "#ECD9FF", + blue: "#B1E4FF", + green: "#D0F5C3", +}; + +const getHexColor = (apiColor) => { + return colorMapping[apiColor] || "#FFE2AD"; +}; + +const fetchRecipients = async () => { + const res = await fetch("https://rolling-api.vercel.app/14-6/recipients/"); + if (!res.ok) throw new Error("데이터 불러오기 실패"); + const data = await res.json(); + return data.results || []; +}; diff --git a/src/pages/ListPage/ListPage.module.css b/src/pages/ListPage/ListPage.module.css new file mode 100644 index 0000000..a9ec761 --- /dev/null +++ b/src/pages/ListPage/ListPage.module.css @@ -0,0 +1,240 @@ +.title { + font-weight: var(--font-weight-bold); + font-size: var(--font-size-24); + line-height: var(--line-height-24); + margin-top: 3.125rem; + margin-bottom: 1rem; +} + +.cardListWrapper { + overflow: hidden; + width: 72.5rem; + height: 16.25rem; +} + +.cardList { + display: flex; + transition: transform 0.5s ease-in-out; + will-change: transform; +} + +.card { + width: 17.1875rem; /* 카드의 너비 */ + margin-right: 1.25rem; /* 카드 사이의 간격 */ +} + +.button { + text-align: center; + margin-top: 4rem; +} + +.buttonChild { + width: 17.5rem; + height: 3.5rem; + background-color: var(--color-purple-600); + color: var(--color-white); + font-size: var(--font-size-18); + font-weight: var(--font-weight-bold); + border-radius: 0.75rem; + margin-bottom: 13.625rem; + cursor: pointer; +} + +.carouselContainer { + position: relative; + transition: transform 0.3s ease-in-out; +} + +.arrowLeftButton { + position: absolute; + z-index: 1; + top: 6.875rem; + left: -1.3125rem; +} + +.arrowRightButton { + position: absolute; + z-index: 1; + top: 6.875rem; + right: 1.3125rem; +} + +/* 스켈레톤 전체 컨테이너 */ +.skeletonContainer { + display: flex; + flex-direction: column; + gap: 1.25rem; + height: 100vh; /* 화면 중앙 정렬 */ +} + +/* 카드 리스트 전체를 감싸는 컨테이너 */ +.skeletonWrapper { + display: flex; + flex-direction: column; +} + +/* 개별 카드 리스트 */ +.skeletonCardListWrapper { + display: flex; + gap: 1.25rem; /* 카드 간격 추가 */ +} + +.titleIndependent { + position: relative; + right: 27.65625rem; + z-index: 1; +} + +/* 개별 스켈레톤 카드 */ +.skeletonCard { + width: 100%; + max-width: 72.5rem; + height: 16.25rem; + background-color: #e0e0e0; /* 기본 로딩 색상 */ + border-radius: 1rem; /* 둥근 테두리 */ + animation: skeleton-loading 1.5s infinite alternate; + position: relative; + display: flex; + justify-content: center; + align-items: center; + margin-right: 2.5rem; + margin-top: -1.25rem; + margin-bottom: -1.25rem; +} + +/* 스켈레톤 애니메이션 */ +@keyframes skeleton-loading { + 0% { + background-color: #e0e0e0; + } + 100% { + background-color: #f5f5f5; + } +} + +/* ✅ 카드 중앙에 위치하는 스피너 */ +.spinner { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); /* 정확한 중앙 정렬 */ + width: 2.5rem; + height: 2.5rem; + border: 0.25rem solid #e0e0e0; + border-top: 0.25rem solid #a64eff; /* 스피너 색상 */ + border-radius: 50%; + animation: spin 1s linear infinite; +} + +/* 스피너 애니메이션 */ +@keyframes spin { + 0% { + transform: translate(-50%, -50%) rotate(0deg); + } + 100% { + transform: translate(-50%, -50%) rotate(360deg); + } +} + +@media (max-width: 1280px) { + .arrowLeftButton, + .arrowRightButton { + display: none; + } + .cardListWrapper { + width: 100%; + overflow-x: auto; /* 터치 스크롤 가능하게 설정 */ + white-space: nowrap; /* 한 줄로 배치 */ + padding-bottom: 0.625rem; + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE & Edge */ + } +} + +/* Tablet (768px ~ 1024px) */ +@media (max-width: 1024px) { + .cardListWrapper { + width: 100%; + overflow-x: auto; /* 터치 스크롤 가능하게 설정 */ + white-space: nowrap; /* 한 줄로 배치 */ + padding-bottom: 0.625rem; + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE & Edge */ + } + + .cardListWrapper::-webkit-scrollbar { + display: none; /* Chrome, Safari */ + } + + .arrowLeftButton, + .arrowRightButton { + display: none; + } + + .buttonChild { + width: 100%; + height: 3.5rem; + background-color: var(--color-purple-600); + color: var(--color-white); + font-size: 1.125rem; + font-weight: bold; + border-radius: 0.75rem; + cursor: pointer; + padding: 1rem 0; + } + + .skeletonCard { + max-width: 54.1875rem; + } +} + +/* Mobile (Max 767px) */ +@media (max-width: 767px) { + .cardListWrapper { + width: 100%; + overflow-x: auto; /* 터치 스크롤 가능하게 설정 */ + white-space: nowrap; /* 한 줄로 배치 */ + padding-bottom: 0.625rem; + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE & Edge */ + } + + .cardListWrapper::-webkit-scrollbar { + display: none; /* Chrome, Safari */ + } + + .arrowLeftButton, + .arrowRightButton { + display: none; + } + + .buttonChild { + width: 100%; + height: 3.5rem; + background-color: var(--color-purple-600); + color: var(--color-white); + font-size: 1.125rem; + font-weight: bold; + border-radius: 0.75rem; + cursor: pointer; + padding: 1rem 0; + } + .skeletonCard { + max-width: 41.875rem; + } +} + +@media (max-width: 500px) { + .skeletonCard { + width: 27.25rem; /* 모바일에서는 전체 너비 */ + height: 14.5rem; /* 높이도 조정 */ + } + + .skeletonCard { + max-width: 27.25rem; + } + + .cardListWrapper { + height: 14.5rem; + } +}