베스트 상품
-
- {items.map((item) => {
- return
;
- })}
+
+ {loading
+ ? Array.from({ length: getSkeletonCount(deviceType) }).map(
+ (_, idx) =>
+ )
+ : items.map((item) => )}
>
diff --git a/src/pages/Market/components/Item/Item.css b/src/pages/Market/components/Item/Item.css
index a74c47ed..cee80f16 100644
--- a/src/pages/Market/components/Item/Item.css
+++ b/src/pages/Market/components/Item/Item.css
@@ -1,28 +1,97 @@
.Item {
- box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 16px;
+ transition: transform 0.2s ease;
}
-.Item.best-four {
- width: 282px;
+.Item:hover {
+ transform: scale(1.03);
}
-.Item.best-two {
- max-width: calc((100% - 10px) / 2);
+.item-skeleton {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ border-radius: 16px;
+}
+
+.skeleton-image {
+ width: 100%;
+ aspect-ratio: 1/1;
+ background-color: #e0e0e0;
+ border-radius: 8px;
+}
+
+.text-section {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
}
-.Item.best-one {
- max-width: calc(100%);
+.skeleton-text {
+ width: 100%;
+ height: 14px;
+ background-color: #e0e0e0;
+ border-radius: 4px;
+ margin: 2px 0;
}
-.Item .item-img {
+.skeleton-text.name {
+ width: 70%;
+ height: 20px;
+}
+
+.skeleton-text.price {
+ width: 50%;
+ height: 22px;
+}
+
+.skeleton-text.favorite {
+ width: 15%;
+ height: 14px;
+}
+
+.image-wrapper {
+ position: relative;
+ width: 100%;
+ aspect-ratio: 1 / 1;
+ overflow: hidden;
+}
+
+.image-skeleton {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background: #e0e0e0;
+ animation: pulse 1.2s infinite ease-in-out;
+ border-radius: 16px;
+}
+
+.item-img.hidden {
+ display: none;
+}
+
+.item-img.visible {
+ display: block;
+ width: 100%;
border-radius: 16px;
aspect-ratio: 1 / 1;
object-fit: cover;
}
+@keyframes pulse {
+ 0% {
+ background-color: #e0e0e0;
+ }
+ 50% {
+ background-color: #f0f0f0;
+ }
+ 100% {
+ background-color: #e0e0e0;
+ }
+}
+
.Item .text-section {
display: flex;
flex-direction: column;
@@ -32,12 +101,14 @@
.Item .item-name {
font-size: 14px;
font-weight: 500;
+ line-height: 24px;
color: var(--gray800);
}
.Item .item-price {
font-size: 16px;
font-weight: 700;
+ line-height: 26px;
color: var(--gray800);
}
@@ -49,5 +120,6 @@
.Item .favorite-count {
font-size: 12px;
font-weight: 500;
+ line-height: 18px;
color: var(--gray600);
}
diff --git a/src/pages/Market/components/Item/Item.jsx b/src/pages/Market/components/Item/Item.jsx
index c7a9676e..a33f3fea 100644
--- a/src/pages/Market/components/Item/Item.jsx
+++ b/src/pages/Market/components/Item/Item.jsx
@@ -1,21 +1,43 @@
+import { useState } from "react";
import "./Item.css";
import heart from "../../../../../src/assets/images/heart.svg";
import fallback from "../../../../../src/assets/images/fallback.png";
-const Item = ({ item, size }) => {
+const Item = ({ item, isLoading = false }) => {
+ if (isLoading) {
+ return (
+
+ );
+ }
+
const { images, name, price, favoriteCount } = item;
+ const [imageLoaded, setImageLoaded] = useState(false);
+
+ const imageSrc = images && images.length > 0 ? images[0] : fallback;
return (
-
-

0 ? images[0] : fallback}
- alt={name}
- className="item-img"
- onError={(e) => {
- e.target.src = fallback;
- }}
- />
+
+
+ {!imageLoaded &&
}
+

setImageLoaded(true)}
+ onError={(e) => {
+ e.target.src = fallback;
+ setImageLoaded(true);
+ }}
+ />
+
{name}
-
{price}원
+
{price.toLocaleString()}원
{favoriteCount}
diff --git a/src/pages/Market/components/Pagination/Pagination.css b/src/pages/Market/components/Pagination/Pagination.css
index 0e378a5d..78031fe0 100644
--- a/src/pages/Market/components/Pagination/Pagination.css
+++ b/src/pages/Market/components/Pagination/Pagination.css
@@ -20,6 +20,11 @@
color: var(--gray500);
}
+.Pagination button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
.Pagination .active {
background-color: #2f80ed;
color: var(--gray100);
diff --git a/src/pages/Market/components/Pagination/Pagination.jsx b/src/pages/Market/components/Pagination/Pagination.jsx
index 2764b808..7026cb99 100644
--- a/src/pages/Market/components/Pagination/Pagination.jsx
+++ b/src/pages/Market/components/Pagination/Pagination.jsx
@@ -1,5 +1,4 @@
import { useState, useEffect } from "react";
-import { getProducts } from "../../../../api";
import arrow_left from "../../../../assets/images/arrow_left.svg";
import arrow_right from "../../../../assets/images/arrow_right.svg";
import "./Pagination.css";
@@ -17,7 +16,6 @@ const Pagination = ({ currentPage, setCurrentPage, totalCount, pageSize }) => {
}, [currentPage]);
const changePage = (page) => {
- if (page < 1 || page > totalPages) return;
setCurrentPage(page);
};
@@ -38,8 +36,8 @@ const Pagination = ({ currentPage, setCurrentPage, totalCount, pageSize }) => {
return (
<>
>