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")}
/>
-
-
+
+
+
+
);
}