Skip to content
Open
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
20 changes: 9 additions & 11 deletions sprint-mission/src/App.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
import { Routes, Route } from "react-router-dom";
import RouteLayout from "./shared/ui/RouteLayout";
import RootLayout from "./shared/ui/RootLayout";
import { LandingPage } from "./pages/LandingPage/LandingPage";
import { ItemsPage } from "./pages/ItemsPage/ItemsPage";
import { RegistrationItemsPage } from "./pages/RegistrationItemsPage/RegistrationItemsPage";

function App() {
return (
<Routes>
{/* TODO: "/" element <LandingPage />로 수정 */}
{/* TODO: <ItemsPage /> path "/items"로 수정 */}
<Route
path="/"
element={
<RouteLayout>
<ItemsPage />
</RouteLayout>
}
/>
<Route path="/" element={<RootLayout />}>
<Route index element={<LandingPage />} />
<Route path="items" element={<ItemsPage />} />
{/* <Route path="items/:id" element={<ItemDetailPage />} /> */}
<Route path="registration" element={<RegistrationItemsPage />} />
</Route>
</Routes>
);
}
Expand Down
8 changes: 8 additions & 0 deletions sprint-mission/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ body {
margin: 0 auto;
}

input:focus {
outline: 1px solid var(--primary100);
}

textarea:focus {
outline: 1px solid var(--primary100);
}

/* 태블릿 사이즈 미디어쿼리 */
@media (max-width: 1199px) {
.content {
Expand Down
56 changes: 1 addition & 55 deletions sprint-mission/src/pages/ItemsPage/ItemsPage.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,61 +9,7 @@
padding-bottom: 140px;
}

/* BestItems/OnSaleItems 섹션 동일, 반응형 변화 없어서 부모 컴포넌트 css에서 한번에 관리 */
/* BestItems/OnSaleItems 섹션 동일, 반응형 변화 없어서 부모 컴포넌트 css에서 한번에 적용 */
.section-title {
color: var(--gray900);
}

.item-card {
cursor: pointer;
text-decoration: none;
}

.item-card-text {
display: flex;
flex-direction: column;
gap: 6px;
color: var(--gray800);
}

.fav-count-box {
display: flex;
gap: 4px;
align-items: center;
}

.fav-heart-icon {
width: 16px;
height: 16px;
}

.fav-count {
color: var(--gray600);
}

/* SkeletonCard */
.skeleton-card-text {
width: 100%;
display: flex;
flex-direction: column;
gap: 6px;
}

.skeleton-name {
height: 24px;
background-color: var(--gray200);
border-radius: 4px;
}

.skeleton-price {
height: 26px;
background-color: var(--gray200);
border-radius: 4px;
}

.skeleton-fav {
width: 42px;
height: 18px;
background-color: var(--gray200);
border-radius: 4px;
}
33 changes: 21 additions & 12 deletions sprint-mission/src/pages/ItemsPage/section/BestItems/BestItems.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import "./BestItems.css";
import { ItemCard } from "../ui/ItemCard";
import { ItemCard } from "../common/ui/ItemCard";
import { ORDER_BY } from "../../../../utils/APIs/getItemsListAPI";
import { Typo, typoStyles } from "../../../../shared/Typo/Typo";
import { useMediaQuery } from "../../../../shared/store/useScreenSizeStore";
import { useItemsFetch } from "../hooks/useItemsFetch";
import { useMediaQuery } from "../../../../shared/hooks/mediaQueryHook";
import { useItemsFetch } from "../common/hooks/itemsFetchHook";
import { useState, useEffect } from "react";
import { SkeletonCard } from "../common/ui/SkeletonCard";

//sizeConfig
const SCREEN_SIZES_TO_PAGE_SIZE = {
Expand All @@ -15,30 +16,38 @@ const SCREEN_SIZES_TO_PAGE_SIZE = {

export function BestItems() {
const screenSize = useMediaQuery();
const pageSize = SCREEN_SIZES_TO_PAGE_SIZE[screenSize];
const limit = SCREEN_SIZES_TO_PAGE_SIZE[screenSize];
const [params, setParams] = useState({
pageSize, //현재 screenSize에 해당하는 pageSize 쿼리로 전달
orderBy: ORDER_BY.FAVORITE.value, //정렬 기준: 좋아요순
limit, //현재 screenSize에 해당하는 limit 쿼리로 전달
sort: ORDER_BY.FAVORITE.value, //정렬 기준: 좋아요순
});

//screenSize가 변경될 때 쿼리의 pageSize만 업데이트
//screenSize가 변경될 때 쿼리의 limit만 업데이트
useEffect(() => {
setParams((prev) => ({ ...prev, pageSize }));
}, [screenSize]);
setParams((prev) => ({ ...prev, limit }));
}, [limit]);

//api호출
const { productList, isLoading } = useItemsFetch(params);

//XXX: 스켈레톤 ui는 일부러 시간을 지연시키고 보여주기도 함(useItemsFetch에서 최소 지연 시간 설정)
//XXX: https://tech.kakaopay.com/post/skeleton-ui-idea/
const isShowSkeleton = isLoading || !productList.length;

return (
<section id="best-items">
<Typo
className={`${typoStyles.textXlBold} section-title`}
content="베스트 상품"
/>
<div className="cards-box">
{productList.map((product, idx) => (
<ItemCard product={product} key={idx} isLoading={isLoading} />
))}
{isShowSkeleton
? Array.from({ length: limit }).map((_, idx) => (
<SkeletonCard key={idx} />
))
: productList.map((product, idx) => (
<ItemCard product={product} key={idx} />
))}
</div>
</section>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
@import "../../../../shared/styles/color.css";

/* FIXME: 여기도 컴포넌트단위로 나눌지 고민 */
#on-sale-items {
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
}

.section-top {
#on-sale-items .section-top {
width: 100%;
display: flex;
justify-content: space-between;
Expand Down Expand Up @@ -45,10 +46,6 @@ input::placeholder {
color: var(--gray400);
}

input:focus {
outline: 1px solid var(--primary100);
}

#item-search-icon {
width: 15px;
height: 15px;
Expand Down Expand Up @@ -189,7 +186,7 @@ input:focus {
/* 모바일 사이즈 미디어쿼리 */
@media (max-width: 743px) {
/* 상단 그리드 변경 */
.section-top {
#on-sale-items .section-top {
display: block;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import "./OnSaleItems.css";
import { SearchItems } from "./ui/SearchItems";
import { PostItems } from "./ui/PostItems";
import { SortItems } from "./ui/SortItems";
import { ItemCard } from "../ui/ItemCard";
import { ItemCard } from "../common/ui/ItemCard";
import { PaginationItems } from "./ui/PaginationItems";
import { Typo, typoStyles } from "../../../../shared/Typo/Typo";
import { useMediaQuery } from "../../../../shared/store/useScreenSizeStore";
import { useItemsFetch } from "../hooks/useItemsFetch";
import { useMediaQuery } from "../../../../shared/hooks/mediaQueryHook";
import { useItemsFetch } from "../common/hooks/itemsFetchHook";
import { useCallback, useEffect, useState } from "react";
import { SkeletonCard } from "../common/ui/SkeletonCard";

//sizeConfig
const SCREEN_SIZES_TO_PAGE_SIZE = {
Expand All @@ -18,15 +19,15 @@ const SCREEN_SIZES_TO_PAGE_SIZE = {

export function OnSaleItems() {
const screenSize = useMediaQuery();
const pageSize = SCREEN_SIZES_TO_PAGE_SIZE[screenSize];
const limit = SCREEN_SIZES_TO_PAGE_SIZE[screenSize];
const [params, setParams] = useState({
pageSize, //현재 screenSize에 해당하는 pageSize 쿼리로 전달
limit, //현재 screenSize에 해당하는 limit 쿼리로 전달
});

//screenSize가 변경될 때 쿼리의 pageSize만 업데이트
//screenSize가 변경될 때 쿼리의 limit 업데이트
useEffect(() => {
setParams((prev) => ({ ...prev, pageSize }));
}, [screenSize]);
setParams((prev) => ({ ...prev, limit }));
}, [limit]);

/**
* 파라미터 업데이트
Expand All @@ -37,8 +38,11 @@ export function OnSaleItems() {
}, []);

//api 호출
const { productList, totalCount, isLoading } = useItemsFetch(params);
const totalPageCount = Math.ceil(totalCount / params.pageSize); //페이지네이션에 필요한 전체 페이지 수 계산
const { productList, totalPages, isLoading } = useItemsFetch(params);
const totalPageCount = totalPages; //백엔드에서 계산해둔 전체 페이지 수 받아오기

//FIXME: 스크린사이즈 바뀔때 기존 데이터 보여주다가 스켈레톤보여주다가 다시 새로운 데이터 불러옴. 이것도 개선할수있을지.
const isShowSkeleton = isLoading || !productList.length;

return (
<section id="on-sale-items">
Expand All @@ -50,14 +54,18 @@ export function OnSaleItems() {
<div className="utility-box">
<SearchItems onSearch={(keyword) => updateParams({ keyword })} />
<PostItems />
<SortItems onSortChange={(orderBy) => updateParams({ orderBy })} />
<SortItems onSortChange={(sort) => updateParams({ sort })} />
</div>
</div>

<div className="cards-box">
{productList.map((product, idx) => (
<ItemCard product={product} key={idx} isLoading={isLoading} />
))}
{isShowSkeleton
? Array.from({ length: limit }).map((_, idx) => (
<SkeletonCard key={idx} />
))
: productList.map((product, idx) => (
<ItemCard product={product} key={idx} />
))}
</div>

<PaginationItems
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import arrowLeft from "../../../../../shared/assets/arrow_left.png";
import arrowRight from "../../../../../shared/assets/arrow_right.png";
import { typoStyles } from "../../../../../shared/Typo/Typo";
import { usePagination } from "../hooks/usePagination";
import { usePagination } from "../../../../../shared/hooks/paginationHook";

export function PaginationItems({
currentPage = 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import { Typo, typoStyles } from "../../../../../shared/Typo/Typo";

export function PostItems() {
return (
//TODO: 링크 수정하기 "/registration"
<Link id="post-item-btn" to="/">
<Link id="post-item-btn" to="/registration">
<Typo className={typoStyles.textLgSemibold} content="상품 등록하기" />
</Link>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import sortIcon from "../../../../../shared/assets/sort_icon.png";
import mobileSortIcon from "../../../../../shared/assets/mobile_sort_icon.png";
import { useOrderBy } from "../hooks/useOrderBy";
import { useResponseSort } from "../../../../../shared/hooks/responseSortHook";
import { ORDER_BY } from "../../../../../utils/APIs/getItemsListAPI";
import { typoStyles } from "../../../../../shared/Typo/Typo";
import { useMediaQuery } from "../../../../../shared/store/useScreenSizeStore";

import { useMediaQuery } from "../../../../../shared/hooks/mediaQueryHook";
//ORDER_BY의 값만 배열로 가져오기
const ORDER_BY_VALUE_ARR = Object.values(ORDER_BY);

export function SortItems({ onSortChange }) {
const screenSize = useMediaQuery();
const { selectedName, showDropdown, handleSelectSort, toggleDropdown } =
useOrderBy(ORDER_BY.RECENT); //초기값은 "최신순"으로 설정
useResponseSort(ORDER_BY.RECENT); //초기값은 "최신순"으로 설정

const dropdownMenuClassName = showDropdown ? "show" : "";
const sortItemsLabelClassName = screenSize === "MOBILE" ? "mobile" : "";
Expand All @@ -38,7 +37,7 @@ export function SortItems({ onSortChange }) {
className="dropdown-option"
onClick={() => {
handleSelectSort(item);
onSortChange(item.value); //orderBy 파라미터 업데이트
onSortChange(item.value); //sort 파라미터 업데이트
}}
key={idx}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
import { useEffect, useCallback, useState } from "react";
import { getItemsListAPI } from "../../../../utils/APIs/getItemsListAPI";
import { getItemsListAPI } from "../../../../../utils/APIs/getItemsListAPI";

/**
* params 변경시 getItemsDataAPI 호출
* @param {object} params {page: int, pageSize: int, orderBy: string, keyword: string}
* @returns {object} { productList: [], totalCount: 0, isLoading: boolean }
* @param {object} params {page: int, limit: int, sort: string, keyword: string}
* @returns {object} { productList: [], totalPages: 0, isLoading: boolean }
* @description productList는 각 상품별 이름, 가격, 이미지 링크 등 데이터가 담긴 객체 배열
*/
export const useItemsFetch = (params) => {
const [isLoading, setIsLoading] = useState(true);
const [data, setData] = useState({ list: [], totalCount: 0 });
const [data, setData] = useState({
ProductList: [],
totalPages: 1,
});

const fetchItems = useCallback(async () => {
try {
setIsLoading(true);
const startTime = Date.now();

console.log("쿼리 파라미터: ", params);
const response = await getItemsListAPI(params);
setData(response);

// 경과시간 계산
const elTime = Date.now() - startTime;
const remainingTime = Math.max(500 - elTime, 0);

// 최소 시간 지연(스켈레톤 보여주는 로딩 최소 시간)
await new Promise((resolve) => setTimeout(resolve, remainingTime));
} catch (error) {
console.error("상품 목록 불러오기 오류:", error);
console.error("상품 목록 불러오기 오류: ", error);
} finally {
setIsLoading(false); //API실행이 종료되면 섹션 로딩 상태 종료
}
Expand All @@ -30,7 +42,7 @@ export const useItemsFetch = (params) => {

return {
isLoading,
productList: data.list,
totalCount: data.totalCount,
productList: data.ProductList,
totalPages: data.totalPages,
};
};
Loading