diff --git a/package.json b/package.json index 4eb3df58..1bff14ca 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "postcss": "^8.4.27", "postcss-syntax": "^0.36.2", "prettier": "2.7.1", + "react-intersection-observer": "^9.5.3", "storybook": "^7.5.1", "stylelint": "^15.10.2", "stylelint-config-standard": "^34.0.0", diff --git a/src/apis/post/apis.ts b/src/apis/post/apis.ts index 372318c9..c0009c61 100644 --- a/src/apis/post/apis.ts +++ b/src/apis/post/apis.ts @@ -1,5 +1,10 @@ -import type { GetCategoriesRes } from './types' +import type { GetCategoriesRes, PostReq, PostDataInfoRes } from './types' + import { http } from '@utils/http' export const getCategories = () => http.get('/categories') + +export const getPostList = (param: PostReq) => { + return http.get('/posts', param) +} diff --git a/src/apis/post/queries.ts b/src/apis/post/queries.ts index 9b0bd05e..07c25595 100644 --- a/src/apis/post/queries.ts +++ b/src/apis/post/queries.ts @@ -1,8 +1,20 @@ -import { useQuery } from '@tanstack/react-query' -import { getCategories } from './apis' +import { useInfiniteQuery, useQuery } from '@tanstack/react-query' +import { getCategories, getPostList } from './apis' +import type { PostReq, PostDataInfoRes } from './types' export const useGetCategoriesQuery = () => useQuery({ queryKey: ['getCategories'], queryFn: getCategories }) + +export const useGetPostListQuery = (param: PostReq) => + useInfiniteQuery({ + queryKey: ['postList'], + queryFn: () => getPostList(param), + initialPageParam: null, + getNextPageParam: lastPage => + lastPage?.hasNext + ? lastPage.posts[lastPage.posts.length - 1].id + : undefined + }) diff --git a/src/assets/images/category_book.svg b/src/assets/images/category_books.svg similarity index 100% rename from src/assets/images/category_book.svg rename to src/assets/images/category_books.svg diff --git a/src/assets/images/category_ditigaldevice.svg b/src/assets/images/category_digital.svg similarity index 100% rename from src/assets/images/category_ditigaldevice.svg rename to src/assets/images/category_digital.svg diff --git a/src/assets/images/category_mangoods.svg b/src/assets/images/category_menfashion.svg similarity index 100% rename from src/assets/images/category_mangoods.svg rename to src/assets/images/category_menfashion.svg diff --git a/src/assets/images/category_more.svg b/src/assets/images/category_other.svg similarity index 100% rename from src/assets/images/category_more.svg rename to src/assets/images/category_other.svg diff --git a/src/assets/images/category_petgoods.svg b/src/assets/images/category_pet.svg similarity index 100% rename from src/assets/images/category_petgoods.svg rename to src/assets/images/category_pet.svg diff --git a/src/assets/images/category_womangoods.svg b/src/assets/images/category_womanfashion.svg similarity index 100% rename from src/assets/images/category_womangoods.svg rename to src/assets/images/category_womanfashion.svg diff --git a/src/components/home/CategorySlider/index.tsx b/src/components/home/CategorySlider/index.tsx index 8b7748b7..9a963b7f 100644 --- a/src/components/home/CategorySlider/index.tsx +++ b/src/components/home/CategorySlider/index.tsx @@ -2,70 +2,7 @@ import { useMedia } from '@offer-ui/react' import { useState, useEffect, useRef, useCallback } from 'react' import type { ReactElement, TouchEventHandler } from 'react' import { Styled } from './styled' -import { IMAGE } from '@constants' - -const CATEGORY_LIST = [ - { - imageUrl: `${IMAGE.CATEGORY_MAN_GOODS}`, - title: '남성패션/잡화', - url: 'string' - }, - { - imageUrl: `${IMAGE.CATEGORY_WOMAN_GOODS}`, - title: '여성패션/잡화', - url: 'string' - }, - { - imageUrl: `${IMAGE.CATEGORY_GAME}`, - title: '게임', - url: 'string' - }, - { - imageUrl: `${IMAGE.CATEGORY_SPORTS}`, - title: '스포츠/레저', - url: 'string' - }, - { - imageUrl: `${IMAGE.CATEGORY_TOY}`, - title: '장난감/취미', - url: 'string' - }, - { - imageUrl: `${IMAGE.CATEGORY_DIGITAL_DEVICE}`, - title: '디지털기기', - url: 'string' - }, - { - imageUrl: `${IMAGE.CATEGORY_CAR}`, - title: '자동차/공구', - url: 'string' - }, - { - imageUrl: `${IMAGE.CATEGORY_APPLIANCE}`, - title: '생활가전', - url: 'string' - }, - { - imageUrl: `${IMAGE.CATEGORY_DIGITAL_FURNITURE}`, - title: '가구/인테리어', - url: 'string' - }, - { - imageUrl: `${IMAGE.CATEGORY_BOOK}`, - title: '도서/티켓/음반', - url: 'string' - }, - { - imageUrl: `${IMAGE.CATEGORY_PET_GOODS}`, - title: '반려동물용품', - url: 'string' - }, - { - imageUrl: `${IMAGE.CATEGORY_MORE}`, - title: '더보기', - url: 'string' - } -] +import { useGetCategoriesQuery } from '@apis/post' const CategorySlider = (): ReactElement => { const containerRef = useRef(null) @@ -77,6 +14,8 @@ const CategorySlider = (): ReactElement => { const [isMoveFromArrowButton, setIsMoveArrowButton] = useState(0) const isFirstCategory = containerRef.current?.scrollLeft === 0 + const { data: categories } = useGetCategoriesQuery() + useEffect(() => { if (desktop) { setIsDesktop(true) @@ -169,24 +108,26 @@ const CategorySlider = (): ReactElement => { )} - {CATEGORY_LIST.map(cateGory => ( - { - alert(cateGory.title) - }}> - - - - {cateGory.title} - - ))} + {categories?.map(cateGory => { + return ( + { + alert(cateGory.name) + }}> + + + + {cateGory.name} + + ) + })} diff --git a/src/components/home/CategorySlider/styled.ts b/src/components/home/CategorySlider/styled.ts index 92ed016b..41c52aa4 100644 --- a/src/components/home/CategorySlider/styled.ts +++ b/src/components/home/CategorySlider/styled.ts @@ -37,7 +37,7 @@ export const CateGoryBoxWrapper = styled.div` width: 100%; max-width: 1200px; - height: 118px; + height: fit-content; cursor: pointer; diff --git a/src/components/home/ProductItem/index.tsx b/src/components/home/ProductItem/index.tsx index 75abf12b..66da4976 100644 --- a/src/components/home/ProductItem/index.tsx +++ b/src/components/home/ProductItem/index.tsx @@ -1,6 +1,7 @@ import type { ReactElement } from 'react' import { Styled } from './styled' import type { ProductItemProps } from './types' +import { getTimeDiffText } from '@utils/format' const ProductItem = ({ productItem }: ProductItemProps): ReactElement => { return ( @@ -10,8 +11,8 @@ const ProductItem = ({ productItem }: ProductItemProps): ReactElement => { { {productItem.price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')}원 - {productItem.tradeArea} 방금전 + {productItem.location} {getTimeDiffText(productItem.createdAt)} diff --git a/src/components/home/ProductItem/types.ts b/src/components/home/ProductItem/types.ts index 684202c3..51dda5f3 100644 --- a/src/components/home/ProductItem/types.ts +++ b/src/components/home/ProductItem/types.ts @@ -1,19 +1,11 @@ export type ProductItemProps = { productItem: { id: number - mainImageUrl: string title: string price: number - tradeArea: string - tradeStatus: { - code: number - name: string - } - createdDate: string - modifiedDate: string - isLiked: boolean - likeCount: number - isReviewed: boolean - sellerNickName: string + thumnail: string + location: string + createdAt: string + liked: boolean } } diff --git a/src/components/home/ProductList/index.tsx b/src/components/home/ProductList/index.tsx index b78b37ba..801808ec 100644 --- a/src/components/home/ProductList/index.tsx +++ b/src/components/home/ProductList/index.tsx @@ -1,17 +1,45 @@ import type { ReactElement } from 'react' +import { useEffect, useState } from 'react' +import { useInView } from 'react-intersection-observer' import { Styled } from './styled' import type { ProductListProps } from './types' import { ProductItem } from '../ProductItem' -const ProductList = ({ productList }: ProductListProps): ReactElement => { +const ProductList = ({ + postData, + hasNextPage, + fetchNextPage +}: ProductListProps): ReactElement => { + const [isFirstRender, setIsFirstRender] = useState(false) + const { ref: isLastPrdRef, inView } = useInView({ + threshold: 1 + }) + + const shouldFetchNextPage = inView && hasNextPage + + useEffect(() => { + if (!shouldFetchNextPage) { + return + } + if (!isFirstRender) { + setIsFirstRender(true) + return + } + + fetchNextPage && fetchNextPage() + }, [fetchNextPage, shouldFetchNextPage, isFirstRender]) + return ( <> 새로운 상품 - {productList.map(item => ( - - ))} + {postData?.map(page => + page?.posts?.map(item => ( + + )) + )} + ) } diff --git a/src/components/home/ProductList/styled.ts b/src/components/home/ProductList/styled.ts index 1d7ec9b2..7cced8fe 100644 --- a/src/components/home/ProductList/styled.ts +++ b/src/components/home/ProductList/styled.ts @@ -36,7 +36,12 @@ const ProductListWrapper = styled.div` } ` +const LastFooter = styled.div` + height: 20px; +` + export const Styled = { NewProductTitle, - ProductListWrapper + ProductListWrapper, + LastFooter } diff --git a/src/components/home/ProductList/types.ts b/src/components/home/ProductList/types.ts index 6776e583..19a45d83 100644 --- a/src/components/home/ProductList/types.ts +++ b/src/components/home/ProductList/types.ts @@ -1,19 +1,17 @@ +import type { + FetchNextPageOptions, + InfiniteData, + InfiniteQueryObserverResult +} from '@tanstack/react-query' +import type { PostReq, PostDataInfoRes } from '@apis/post' + export type ProductListProps = { - productList: { - id: number - mainImageUrl: string - title: string - price: number - tradeArea: string - tradeStatus: { - code: number - name: string - } - createdDate: string - modifiedDate: string - isLiked: boolean - likeCount: number - isReviewed: boolean - sellerNickName: string - }[] + postData?: PostDataInfoRes[] + filterOption?: Pick + hasNextPage?: boolean + fetchNextPage?( + options?: FetchNextPageOptions + ): Promise< + InfiniteQueryObserverResult, Error> + > } diff --git a/src/constants/images.ts b/src/constants/images.ts index c228a560..71474705 100644 --- a/src/constants/images.ts +++ b/src/constants/images.ts @@ -1,15 +1,15 @@ import CATEGORY_APPLIANCE from '@assets/images/category_appliance.svg' -import CATEGORY_BOOK from '@assets/images/category_book.svg' +import CATEGORY_BOOKS from '@assets/images/category_books.svg' import CATEGORY_CAR from '@assets/images/category_car.svg' -import CATEGORY_DIGITAL_DEVICE from '@assets/images/category_ditigaldevice.svg' -import CATEGORY_DIGITAL_FURNITURE from '@assets/images/category_furniture.svg' +import CATEGORY_DIGITAL from '@assets/images/category_digital.svg' +import CATEGORY_FURNITURE from '@assets/images/category_furniture.svg' import CATEGORY_GAME from '@assets/images/category_game.svg' -import CATEGORY_MAN_GOODS from '@assets/images/category_mangoods.svg' -import CATEGORY_MORE from '@assets/images/category_more.svg' -import CATEGORY_PET_GOODS from '@assets/images/category_petgoods.svg' +import CATEGORY_MEN_FASHION from '@assets/images/category_menfashion.svg' +import CATEGORY_OTHER from '@assets/images/category_other.svg' +import CATEGORY_PET from '@assets/images/category_pet.svg' import CATEGORY_SPORTS from '@assets/images/category_sports.svg' import CATEGORY_TOY from '@assets/images/category_toy.svg' -import CATEGORY_WOMAN_GOODS from '@assets/images/category_womangoods.svg' +import CATEGORY_WOMEN_FASHION from '@assets/images/category_womanfashion.svg' import CHECKBOARD from '@assets/images/checkboard.svg' import LOGO from '@assets/images/logo.svg' import MAIL from '@assets/images/mail.svg' @@ -19,17 +19,17 @@ type ImageKey = keyof typeof IMAGE export const IMAGE = { CATEGORY_APPLIANCE, - CATEGORY_BOOK, + CATEGORY_BOOKS, CATEGORY_CAR, - CATEGORY_DIGITAL_DEVICE, - CATEGORY_DIGITAL_FURNITURE, + CATEGORY_DIGITAL, + CATEGORY_FURNITURE, CATEGORY_GAME, - CATEGORY_MAN_GOODS, - CATEGORY_MORE, - CATEGORY_PET_GOODS, + CATEGORY_MEN_FASHION, + CATEGORY_OTHER, + CATEGORY_PET, CATEGORY_SPORTS, CATEGORY_TOY, - CATEGORY_WOMAN_GOODS, + CATEGORY_WOMEN_FASHION, CHECKBOARD, LOGO, MAIL, diff --git a/src/mocks/fixture.ts b/src/mocks/fixture.ts index 9aa5b883..30284454 100644 --- a/src/mocks/fixture.ts +++ b/src/mocks/fixture.ts @@ -10,3 +10,66 @@ export const myProfile: MyProfile = { reviewCount: 0, likeProductCount: 0 } + +export const categories: GetCategoriesRes = [ + { + code: 'MEN_FASHION', + name: '남성패션/잡화', + imageUrl: '' + }, + { + code: 'WOMEN_FASHION', + name: '여성패션/잡화', + imageUrl: '' + }, + { + code: 'GAME', + name: '게임', + imageUrl: '' + }, + { + code: 'SPORTS', + name: '스포츠/레저', + imageUrl: '' + }, + { + code: 'TOY', + name: '장난감/취미', + imageUrl: '' + }, + { + code: 'DIGITAL', + name: '디지털기기', + imageUrl: '' + }, + { + code: 'CAR', + name: '자동차/공구', + imageUrl: '' + }, + { + code: 'APPLIANCE', + name: '생활가전', + imageUrl: '' + }, + { + code: 'FURNITURE', + name: '가구/인테리어', + imageUrl: '' + }, + { + code: 'BOOKS', + name: '도서/티켓/음반', + imageUrl: '' + }, + { + code: 'PET', + name: '반려동물용품', + imageUrl: '' + }, + { + code: 'OTHER', + name: '기타 중고물품', + imageUrl: '' + } +] diff --git a/src/pages/index.tsx b/src/pages/index.tsx index b17cebab..4f6b497e 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,113 +1,29 @@ import styled from '@emotion/styled' import type { NextPage } from 'next' import { ProductList } from '../components/home/ProductList' +import { useGetPostListQuery } from '@apis/post' import { CategorySlider, HomeBanner } from '@components' -const apiRes = { - elements: [ - { - id: 5, - mainImageUrl: 'string', - title: 'string', - price: 8000, - tradeArea: '서울시 강남구', - tradeStatus: { - code: 4, - name: '판매중' - }, - createdDate: '2021-12-10T14:23:53', - modifiedDate: '2021-12-10T14:23:53', - isLiked: false, - likeCount: 0, - isReviewed: false, - sellerNickName: 'hypeboy' - }, - { - id: 4, - mainImageUrl: 'string', - title: 'string', - price: 8000, - tradeArea: '서울시 강남구', - tradeStatus: { - code: 4, - name: '판매중' - }, - createdDate: '2021-12-10T14:23:53', - modifiedDate: '2021-12-10T14:23:53', - isLiked: false, - likeCount: 0, - isReviewed: false, - sellerNickName: 'hypeboy' - }, - { - id: 3, - mainImageUrl: 'string', - title: 'string', - price: 8000, - tradeArea: '서울시 강남구', - tradeStatus: { - code: 4, - name: '판매중' - }, - createdDate: '2021-12-10T14:23:53', - modifiedDate: '2021-12-10T14:23:53', - isLiked: false, - likeCount: 0, - isReviewed: false, - sellerNickName: 'hypeboy' - }, - { - id: 2, - mainImageUrl: 'string', - title: 'string', - price: 36500, - tradeArea: '서울시 강남구', - tradeStatus: { - code: 4, - name: '판매중' - }, - createdDate: '2021-12-10T14:25:30', - modifiedDate: '2021-12-10T14:25:30', - isLiked: false, - likeCount: 0, - isReviewed: false, - sellerNickName: 'hypeboy' - }, - { - id: 1, - mainImageUrl: 'string', - title: 'string', - price: 8000, - tradeArea: '서울시 강남구', - tradeStatus: { - code: 4, - name: '판매중' - }, - createdDate: '2021-12-10T14:23:53', - modifiedDate: '2021-12-10T14:23:53', - isLiked: false, - likeCount: 0, - isReviewed: false, - sellerNickName: 'hypeboy' - } - ], - pageInfo: { - currentPageNumber: 1, - lastPageNumber: 1, - sizePerPage: 2, - totalElementCount: 2, - isFirstPage: true, - isLastPage: true - } -} - const Home: NextPage = () => { + const { + data: postList, + fetchNextPage, + hasNextPage + } = useGetPostListQuery({ + lastId: null, + limit: 8 + }) + return ( - + ) diff --git a/src/pages/result/index.tsx b/src/pages/result/index.tsx index b33bf922..3003bfe0 100644 --- a/src/pages/result/index.tsx +++ b/src/pages/result/index.tsx @@ -22,104 +22,6 @@ const Result: NextPage = () => { } }, [desktop]) - const apiRes = { - elements: [ - { - id: 5, - mainImageUrl: 'string', - title: 'string', - price: 8000, - tradeArea: '서울시 강남구', - tradeStatus: { - code: 4, - name: '판매중' - }, - createdDate: '2021-12-10T14:23:53', - modifiedDate: '2021-12-10T14:23:53', - isLiked: false, - likeCount: 0, - isReviewed: false, - sellerNickName: 'hypeboy' - }, - { - id: 4, - mainImageUrl: 'string', - title: 'string', - price: 8000, - tradeArea: '서울시 강남구', - tradeStatus: { - code: 4, - name: '판매중' - }, - createdDate: '2021-12-10T14:23:53', - modifiedDate: '2021-12-10T14:23:53', - isLiked: false, - likeCount: 0, - isReviewed: false, - sellerNickName: 'hypeboy' - }, - { - id: 3, - mainImageUrl: 'string', - title: 'string', - price: 8000, - tradeArea: '서울시 강남구', - tradeStatus: { - code: 4, - name: '판매중' - }, - createdDate: '2021-12-10T14:23:53', - modifiedDate: '2021-12-10T14:23:53', - isLiked: false, - likeCount: 0, - isReviewed: false, - sellerNickName: 'hypeboy' - }, - { - id: 2, - mainImageUrl: 'string', - title: 'string', - price: 36500, - tradeArea: '서울시 강남구', - tradeStatus: { - code: 4, - name: '판매중' - }, - createdDate: '2021-12-10T14:25:30', - modifiedDate: '2021-12-10T14:25:30', - isLiked: false, - likeCount: 0, - isReviewed: false, - sellerNickName: 'hypeboy' - }, - { - id: 1, - mainImageUrl: 'string', - title: 'string', - price: 8000, - tradeArea: '서울시 강남구', - tradeStatus: { - code: 4, - name: '판매중' - }, - createdDate: '2021-12-10T14:23:53', - modifiedDate: '2021-12-10T14:23:53', - isLiked: false, - likeCount: 0, - isReviewed: false, - sellerNickName: 'hypeboy' - } - ], - pageInfo: { - currentPageNumber: 1, - lastPageNumber: 1, - sizePerPage: 2, - totalElementCount: 2, - isFirstPage: true, - isLastPage: true - } - } - const { checkFilterList, onCheckItem, @@ -168,7 +70,7 @@ const Result: NextPage = () => { sortPriceItems={sortPriceItems} tradePeriodItems={tradePeriodItems} /> - + diff --git a/src/utils/format/index.ts b/src/utils/format/index.ts index 86373ec8..bf521e8b 100644 --- a/src/utils/format/index.ts +++ b/src/utils/format/index.ts @@ -13,6 +13,41 @@ export const formatDate = ( format: keyof typeof DATE_FORMAT ) => dayjs(date).format(format) +export const getTimeDiffText = (date: string | Date) => { + const currentDate = dayjs() + const targetDate = dayjs(date) + + const diffMinutes = currentDate.diff(targetDate, 'minute') + const diffHours = currentDate.diff(targetDate, 'hour') + const diffDays = currentDate.diff(targetDate, 'day') + const diffMonths = currentDate.diff(targetDate, 'month') + const diffYears = currentDate.diff(targetDate, 'year') + + if (diffMinutes <= 10) { + return '방금 전' + } + if (diffHours < 1) { + return `${diffMinutes}분 전` + } + if (diffHours < 2) { + return '한 시간 전' + } + if (diffHours < 24) { + return `${diffHours}시간 전` + } + if (diffDays < 2) { + return '하루 전' + } + if (diffMonths < 1) { + return `${diffDays}일 전` + } + if (diffYears < 1) { + return `${diffMonths}달 전` + } + + return `${diffYears}년 전` +} + export const toLocaleCurrency = (value: number): string => { return value.toLocaleString('kr') } diff --git a/yarn.lock b/yarn.lock index e6ad298b..8c29bd66 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10126,6 +10126,11 @@ react-inspector@^6.0.0: resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-6.0.2.tgz#aa3028803550cb6dbd7344816d5c80bf39d07e9d" integrity sha512-x+b7LxhmHXjHoU/VrFAzw5iutsILRoYyDq97EDYdFpPLcvqtEzk4ZSZSQjnFPbr5T57tLXnHcqFYoN1pI6u8uQ== +react-intersection-observer@^9.5.3: + version "9.5.3" + resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.5.3.tgz#f47a31ed3a0359cbbfdb91a53d7470ac2ab7b3c7" + integrity sha512-NJzagSdUPS5rPhaLsHXYeJbsvdpbJwL6yCHtMk91hc0ufQ2BnXis+0QQ9NBh6n9n+Q3OyjR6OQLShYbaNBkThQ== + react-is@18.1.0: version "18.1.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67"