diff --git a/src/app.jsx b/src/app.jsx index b0509de..e056436 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -57,7 +57,7 @@ function App() { /> - } /> + } /> } /> diff --git a/src/components/image/card-background.jsx b/src/components/image/card-background.jsx new file mode 100644 index 0000000..6f9cb15 --- /dev/null +++ b/src/components/image/card-background.jsx @@ -0,0 +1,55 @@ +import React from "react"; +import styled from "styled-components"; +import SpinnerOverlay from "../loading/loading"; +import { useImageLodeChecker } from "../../hooks/use-image-loader"; + +const backgroundColors = { + beige: "#FFE2AD", + purple: "#ECD9FF", + green: "#D0F5C3", + blue: "#B1E4FF", +}; + +const getBackground = ($imageURL, $color, $overlayOn) => { + return $imageURL + ? `${ + $overlayOn ? "linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.4)), " : "" + }url(${$imageURL}) center/cover no-repeat` + : backgroundColors[$color] || "white"; +}; + +const CardContainer = styled.div` + position: relative; + width: 100%; + height: 100%; +`; + +const CardImage = styled.div` + background: ${({ $imageURL, $color, $overlayOn }) => + getBackground($imageURL, $color, $overlayOn)}; + z-index: 0; +`; + +const CardBackground = ({ + backgroundImageURL, + backgroundColor, + overlayOn = false, + ...props +}) => { + const noNeedToLoad = !backgroundImageURL ? true : false; + const isImageLoaded = useImageLodeChecker(backgroundImageURL, noNeedToLoad); + + return ( + + {!isImageLoaded && } + + + ); +}; + +export default CardBackground; diff --git a/src/components/loading/loading.jsx b/src/components/loading/loading.jsx new file mode 100644 index 0000000..bca7802 --- /dev/null +++ b/src/components/loading/loading.jsx @@ -0,0 +1,29 @@ +import React from "react"; +import styled, { keyframes } from "styled-components"; + +const spin = keyframes` + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +`; + +const Spinner = styled.div` + width: ${({ size }) => size || "50px"}; + height: ${({ size }) => size || "50px"}; + border: ${({ thickness }) => thickness || "4px"} solid #f3f3f313; + border-top: ${({ thickness }) => thickness || "4px"} solid + var(--color-purple-700); + + border-radius: 50%; + animation: ${spin} 1s linear infinite; + position: absolute; + justify-self: anchor-center; + align-self: anchor-center; + transform: translate(-50%, -50%); + z-index: 9999; +`; + +const SpinnerOverlay = ({ size, thickness }) => { + return ; +}; + +export default SpinnerOverlay; diff --git a/src/features/rolling-paper/api/rollingPaperList.js b/src/features/rolling-paper/api/rolling-paper-list.js similarity index 100% rename from src/features/rolling-paper/api/rollingPaperList.js rename to src/features/rolling-paper/api/rolling-paper-list.js diff --git a/src/features/rolling-paper/components/rolling-paper-list.jsx b/src/features/rolling-paper/components/rolling-paper-list.jsx index f411023..d273e32 100644 --- a/src/features/rolling-paper/components/rolling-paper-list.jsx +++ b/src/features/rolling-paper/components/rolling-paper-list.jsx @@ -1,17 +1,12 @@ import ArrowButton from "../../../components/button/arrow-button"; import ARROW_BUTTON_DIRECTION from "../../../components/button/arrow-button-direction"; import { media } from "../../../utils/media"; -import React, { useEffect, useState } from "react"; -import styled, { keyframes, css } from "styled-components"; +import React, { useMemo } from "react"; +import styled, { css } from "styled-components"; import Avatar from "../../../components/avatar/avatar"; import AVATAR_SIZE from "../../../components/avatar/avatar-size"; - -const backgroundColors = { - beige: "#FFE2AD", - purple: "#ECD9FF", - green: "#D0F5C3", - blue: "#B1E4FF", -}; +import CardBackground from "../../../components/image/card-background"; +import { useImageListLodeChecker } from "../../../hooks/use-image-loader"; const CardContainer = styled.div` display: grid; @@ -53,9 +48,9 @@ const CardContainer = styled.div` } `; -const CardItem = styled.div` +const CardItem = styled(CardBackground)` width: 275px; - min-height: 260px; + height: 260px; border-radius: 16px; text-align: left; padding: 30px 24px 20px 24px; @@ -67,12 +62,8 @@ const CardItem = styled.div` flex-direction: column; position: relative; overflow: hidden; - background: ${(props) => { - if (props.$backgroundImageURL) { - return `linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.4)), url(${props.$backgroundImageURL}) center/cover no-repeat`; - } - return backgroundColors[props.$backgroundColor] || "white"; - }}; + + justify-content: space-between; ${media.tablet} { flex-shrink: 0; @@ -82,28 +73,27 @@ const CardItem = styled.div` ${media.mobile} { width: 208px; height: 232px; - padding: 30px 15px 20px 15px; + padding: 25px 15px 15px 15px; + gap: 8px; } &::before { content: ""; position: absolute; - ${({ $backgroundImageURL, $backgroundColor }) => { - return $backgroundImageURL ? "" : polygonStyle[$backgroundColor]; + ${({ $backgroundImageURLForStyle, $backgroundColorForStyle }) => { + return $backgroundImageURLForStyle + ? "" + : polygonStyle[$backgroundColorForStyle]; }} } - - & > * { - position: relative; - } `; const ellipseStyle = css` width: 336px; height: 169px; - background-color: ${({ $backgroundColor }) => - $backgroundColor === "purple" - ? "rgba(220,185,255,0.4)" + background-color: ${({ $backgroundColorForStyle }) => + $backgroundColorForStyle === "purple" + ? "rgba(220, 185, 255, 0.4)" : "rgba(155, 226, 130, 0.3)"}; border-radius: 90.5px; top: 124px; @@ -179,7 +169,7 @@ const ProfileContainer = styled.div` `; const CardProfile = styled.div` - margin-left: ${($messageIndex) => ($messageIndex === 0 ? "0" : "-12px")}; + margin-left: ${({ $messageIndex }) => ($messageIndex === 0 ? "0" : "-12px")}; `; const OverProfile = styled.div` @@ -206,14 +196,22 @@ const MessageCountText = styled.span` `; const CardEmojiBox = styled.div` - border-top: 1px solid rgba(0, 0, 0, 0.1); - padding-top: 17px; + ${(props) => + props.$haveEmoji && + ` + border-top: 1px solid rgba(0, 0, 0, 0.1); + `} + padding-top: 13px; margin-top: auto; display: flex; flex-wrap: wrap; row-gap: 5px; z-index: 2; + + ${media.mobile} { + padding-top: 10px; + } `; const CardEmoji = styled.span` @@ -278,130 +276,31 @@ const PreviewButtonWrapper = styled.div` z-index: 10; `; -const spin = keyframes` - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -`; - -const Spinner = styled.div` - width: ${(props) => props.size || "40px"}; - height: ${(props) => props.size || "40px"}; - border: ${(props) => props.thickness || "4px"} solid - ${(props) => props.trackColor || "#f3f3f313"}; - border-top: ${(props) => props.thickness || "4px"} solid - var(--color-purple-700); - border-radius: 50%; - animation: ${spin} 1s linear infinite; - - ${(props) => - props.centered && - css` - margin: 0 auto; - display: block; - `} - - position: absolute; - justify-self: anchor-center; - align-self: anchor-center; -`; - function RollingPaperList({ cardData, totalPages, currentPage, onTurnCards }) { - const [imageLoadStates, setImageLoadStates] = useState({}); - const [profileLoadStates, setProfileLoadStates] = useState({}); - - useEffect(() => { - const loadImages = async () => { - const loadStates = {}; - - cardData.forEach((card) => { - if (card.backgroundImageURL) { - loadStates[card.id] = false; - } - }); - - setImageLoadStates(loadStates); - - const imagePromises = cardData.map((card) => { - if (!card.backgroundImageURL) return Promise.resolve(); - - return new Promise((resolve) => { - const img = new Image(); - img.onload = () => { - setImageLoadStates((prev) => ({ - ...prev, - [card.id]: true, - })); - resolve(); - }; - img.onerror = () => { - setImageLoadStates((prev) => ({ - ...prev, - [card.id]: false, - })); - resolve(); - }; - img.src = card.backgroundImageURL; - }); - }); - - await Promise.all(imagePromises); - }; - - loadImages(); - }, [cardData]); - - useEffect(() => { - const loadProfileImages = async () => { - const initialStates = {}; - cardData.forEach((card) => { - card.recentMessages.slice(0, 3).forEach((msg) => { - initialStates[msg.id] = false; - }); - }); - setProfileLoadStates(initialStates); - - const promises = []; - cardData.forEach((card) => { - card.recentMessages.slice(0, 3).forEach((msg) => { - if (!msg.profileImageURL) return; - promises.push( - new Promise((resolve) => { - const img = new Image(); - img.src = msg.profileImageURL; - img.onload = () => { - setProfileLoadStates((prev) => ({ ...prev, [msg.id]: true })); - resolve(); - }; - img.onerror = () => { - setProfileLoadStates((prev) => ({ ...prev, [msg.id]: false })); - resolve(); - }; - }) - ); - }); - }); - - await Promise.all(promises); - }; - - loadProfileImages(); - }, [cardData]); + const profileImages = useMemo( + () => + cardData.flatMap((card) => + card.recentMessages.slice(0, 3).map((msg) => ({ + id: msg.id, + backgroundImageURL: msg.profileImageURL, + })) + ), + [cardData] + ); + + const profileLoadStates = useImageListLodeChecker(profileImages); return ( {cardData.map((card) => ( - {card.backgroundImageURL && !imageLoadStates[card.id] && ( - - )} @@ -411,7 +310,7 @@ function RollingPaperList({ cardData, totalPages, currentPage, onTurnCards }) { {card.recentMessages .slice(0, 3) .map((messageCard, messageIndex) => ( - + {card.messageCount}명이 작성했어요! - + 0}> {card.topReactions.map((emoji, index) => { const countLength = emoji.count.toString().length; const isLongCount = countLength > 2; diff --git a/src/hooks/use-image-loader.jsx b/src/hooks/use-image-loader.jsx new file mode 100644 index 0000000..8a58963 --- /dev/null +++ b/src/hooks/use-image-loader.jsx @@ -0,0 +1,67 @@ +import { useState, useEffect } from "react"; + +function useImageLodeChecker(imageURL, noNeedToLoad = false) { + const [isLode, setIsLode] = useState(!imageURL || noNeedToLoad); + + useEffect(() => { + if (noNeedToLoad || !imageURL) return; + const img = new Image(); + img.src = imageURL; + + const handleLoad = () => setIsLode(true); + const handleError = () => setIsLode(false); + + img.addEventListener("load", handleLoad); + img.addEventListener("error", handleError); + + return () => { + img.removeEventListener("load", handleLoad); + img.removeEventListener("error", handleError); + }; + }, [imageURL, noNeedToLoad]); + + return isLode; +} + +function useImageListLodeChecker(imageList = []) { + const [imageLoadStates, setImageLoadStates] = useState({}); + + useEffect(() => { + if (!imageList.length) return; + + setImageLoadStates((prev) => { + const nextStates = { ...prev }; + imageList.forEach(({ id }) => { + if (nextStates[id] === undefined) { + nextStates[id] = false; + } + }); + return nextStates; + }); + + imageList.forEach(({ id, backgroundImageURL }) => { + setImageLoadStates((prev) => { + if (prev[id]) return prev; + + if (!backgroundImageURL) { + return { ...prev, [id]: false }; + } + + const img = new Image(); + img.src = backgroundImageURL; + img.onload = () => { + setImageLoadStates((p) => ({ ...p, [id]: true })); + }; + img.onerror = () => { + setImageLoadStates((p) => ({ ...p, [id]: false })); + }; + + return prev; + }); + }); + }, [imageList]); + + return imageLoadStates; +} + +export { useImageLodeChecker, useImageListLodeChecker }; diff --git a/src/pages/rolling-paper-list-page.jsx b/src/pages/rolling-paper-list-page.jsx index 8c1da0b..425f9cc 100644 --- a/src/pages/rolling-paper-list-page.jsx +++ b/src/pages/rolling-paper-list-page.jsx @@ -1,16 +1,25 @@ import { PrimaryButton } from "../components/button/button"; import BUTTON_SIZE from "../components/button/button-size"; -import { useNavigate } from "react-router"; import React, { useEffect, useState, useMemo } from "react"; -import { getRollingPaperList } from "../features/rolling-paper/api/rollingPaperList"; +import { useNavigate } from "react-router"; import styled from "styled-components"; import RollingPaperList from "../features/rolling-paper/components/rolling-paper-list"; import { media } from "../utils/media"; import { useMedia } from "../hooks/use-media"; +import { apiClient } from "../api/client"; const TopContainer = styled.div` text-align: center; margin-top: 50px; + min-height: calc(100vh - 64px); + display: flex; + flex-direction: column; +`; + +const CardBox = styled.article` + ${media.tablet} { + flex: 1; + } `; const CardSection = styled.section` @@ -37,6 +46,10 @@ const CardTitle = styled.h2` } `; +const ButtonFooter = styled.footer` + position: relative; +`; + const MakingButton = styled(PrimaryButton)` margin-top: 64px; font-weight: 400; @@ -44,12 +57,9 @@ const MakingButton = styled(PrimaryButton)` ${media.tablet} { justify-self: anchor-center; - margin-left: 24px; - margin-right: 24px; width: calc(100% - 48px); padding: 14px 20px; - position: relative; - bottom: 24px; + margin: 24px; } `; @@ -65,7 +75,7 @@ function getCachedImage(url) { function ShowMessageList() { const navigate = useNavigate(); - const [testData, setTestData] = useState([]); + const [recipientsData, setRecipientsData] = useState([]); const [popularDataList, setPopularDataList] = useState([]); const [recentDataList, setRecentDataList] = useState([]); const [popularCurrentPage, setPopularCurrentPage] = useState(0); @@ -82,33 +92,37 @@ function ShowMessageList() { }; useEffect(() => { - isDesktop ? setCardCount(4) : setCardCount(null); - }, [isDesktop]); - - - useEffect(() => { - getRollingPaperList().then(setTestData); + apiClient + .get("/recipients/") + .then((res) => { + setRecipientsData(res.data.results); + }) + .catch((err) => { + console.error("오류:", err); + }); }, []); useEffect(() => { - testData.forEach((data) => { + recipientsData.forEach((data) => { getCachedImage(data.imageURL); }); - }, [testData]); + }, [recipientsData]); useEffect(() => { - const sortedPopular = testData + const sortedPopular = recipientsData .slice() .sort((a, b) => b.messageCount - a.messageCount); setPopularDataList(sortedPopular); - const sortedRecent = testData + const sortedRecent = recipientsData .slice() .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); setRecentDataList(sortedRecent); - }, [testData]); + }, [recipientsData]); - const totalPages = cardCount ? Math.ceil(testData.length / cardCount) : 1; + const totalPages = cardCount + ? Math.ceil(recipientsData.length / cardCount) + : 1; const popularShowCards = useMemo(() => { if (!cardCount) return popularDataList; @@ -142,7 +156,7 @@ function ShowMessageList() { return ( -
+ 인기 롤링 페이퍼 🔥 handleTurnCards(direction, "recent")} /> -
- + + + +
); }