diff --git a/.github/workflows/deploy_prod.yml b/.github/workflows/deploy_prod.yml index 636ff37..799de89 100644 --- a/.github/workflows/deploy_prod.yml +++ b/.github/workflows/deploy_prod.yml @@ -3,6 +3,9 @@ on: pull_request: branches: - dev + push: + branches: + - deploytest env: S3_BUCKET_NAME: itracker-front diff --git a/src/app/(iTracker)/products/[categoryName]/layout.tsx b/src/app/(iTracker)/products/[categoryName]/layout.tsx index d0c4cea..bdf23c6 100644 --- a/src/app/(iTracker)/products/[categoryName]/layout.tsx +++ b/src/app/(iTracker)/products/[categoryName]/layout.tsx @@ -4,16 +4,6 @@ import { MacbookProductListSkeleton } from '@/features/product/components/macboo import { Text } from '@/shared/components/shadcn/Text'; import React, { Suspense } from 'react'; -// export function generateStaticParams() { -// // const categoryData = await getCategoryList(); - -// const categories = ['macbook_air', 'macbook_pro']; - -// return categories.map((category: string) => ({ -// categoryName: category, -// })); -// } - export default function Layout({ params, children, @@ -24,8 +14,8 @@ export default function Layout({ const categoryName = categoryMap[params.categoryName]; return ( -
- +
+ {categoryName} 오늘의 할인율 TOP5 diff --git a/src/app/(iTracker)/products/[categoryName]/search/page.tsx b/src/app/(iTracker)/products/[categoryName]/search/page.tsx index 08e505b..2d17c9f 100644 --- a/src/app/(iTracker)/products/[categoryName]/search/page.tsx +++ b/src/app/(iTracker)/products/[categoryName]/search/page.tsx @@ -27,6 +27,7 @@ export default function SearchResult({ params }: { params: { categoryName: Categ ref.current.scrollIntoView({ behavior: 'smooth' }); // 스무스 스크롤링 효과 추가 } }); + return (
@@ -45,7 +46,7 @@ export default function SearchResult({ params }: { params: { categoryName: Categ 불러오는 중;
}> - +
); } diff --git a/src/features/product/components/macbook/Item/Skeleton.tsx b/src/features/product/components/macbook/Item/Skeleton.tsx index 73ec4ce..af325d1 100644 --- a/src/features/product/components/macbook/Item/Skeleton.tsx +++ b/src/features/product/components/macbook/Item/Skeleton.tsx @@ -2,12 +2,18 @@ import { Skeleton } from '@/shared/components/shadcn/ui/skeleton'; export const ProductItemSkeleton = () => { return ( -
  • -
    - - +
  • +
    + + + + + +
    + +
  • ); diff --git a/src/features/product/components/macbook/Item/index.tsx b/src/features/product/components/macbook/Item/index.tsx index 0cd215f..8d7263d 100644 --- a/src/features/product/components/macbook/Item/index.tsx +++ b/src/features/product/components/macbook/Item/index.tsx @@ -1,6 +1,7 @@ import { categoryMap } from '@/features/category/constants'; import { Macbook } from '@/features/product/api/getProductList'; import { Badge } from '@/shared/components/Badge'; +import DiscountBadge from '@/shared/components/DiscountBadge'; import { Text } from '@/shared/components/shadcn/Text'; import { convertToLocalFormat } from '@/shared/utils'; import Image from 'next/image'; @@ -16,7 +17,7 @@ export const MacbookProductListItem = ({ productItem }: ProductItemProps) => { return (
  • -
    +
    {productItem.title} { {categoryName} - {productItem.title} + {productItem.title}
    -
    - {productItem.chip} +
    + + {productItem.chip} +
    - - {productItem.cpu} - - - {productItem.gpu} - - - {productItem.storage} - - - {productItem.memory} - - - {productItem.color} - + {productItem.cpu} + {productItem.gpu} + {productItem.storage} + {productItem.memory} + {productItem.color}
    -
    - - {productItem.discountPercentage}% - -
    - {convertToLocalFormat(productItem.currentPrice)}원 + + + {convertToLocalFormat(productItem.currentPrice)}원 +
    diff --git a/src/features/product/components/macbook/Skeleton.tsx b/src/features/product/components/macbook/Skeleton.tsx index d55fff8..c97c7cb 100644 --- a/src/features/product/components/macbook/Skeleton.tsx +++ b/src/features/product/components/macbook/Skeleton.tsx @@ -4,8 +4,8 @@ export const MacbookProductListSkeleton = () => { const items = Array.from({ length: 5 }, (_, i) => ); return ( -
    -
      {items}
    +
    +
      {items}
    ); }; diff --git a/src/features/product/components/macbook/index.tsx b/src/features/product/components/macbook/index.tsx index 64b10c4..bc49851 100644 --- a/src/features/product/components/macbook/index.tsx +++ b/src/features/product/components/macbook/index.tsx @@ -9,7 +9,7 @@ export const MacbookProductList = ({ category }: { category: CategoryType }) => return (
    -
      +
        {productData?.data.map((productItem) => { return ; })} diff --git a/src/features/search/api/getSearchResult.ts b/src/features/search/api/getSearchResult.ts index c045732..4c2b8b0 100644 --- a/src/features/search/api/getSearchResult.ts +++ b/src/features/search/api/getSearchResult.ts @@ -8,18 +8,25 @@ export type GetSearchResultResponse = { pageInfo: { currentPage: number; lastPage: number; + elementSize: number; }; }; export const getSearchResultUrl = (category: string) => `${API_BASE_URL}/api/v1/products/${category}/search`; -export const getSearchResult = async (category: string, property: FilterProperty): Promise => { +export const getSearchResult = async ( + category: string, + property: FilterProperty, + pageParam: number, +): Promise => { const queryParams = queryString.stringify(property, { arrayFormat: 'comma' }); + const pageParams = queryString.stringify({ page: pageParam }, { arrayFormat: 'comma' }); - const url = `${getSearchResultUrl(category)}?${queryParams}`; + const url = `${getSearchResultUrl(category)}?${queryParams}&${pageParams}`; const response = await fetch(url); const data = (await response.json()) as GetSearchResultResponse; + return data; }; diff --git a/src/features/search/components/filter/index.tsx b/src/features/search/components/filter/index.tsx index c58af2c..8d00e63 100644 --- a/src/features/search/components/filter/index.tsx +++ b/src/features/search/components/filter/index.tsx @@ -21,8 +21,9 @@ import { Label } from '@radix-ui/react-label'; import { useRouter } from 'next/navigation'; import queryString from 'query-string'; -export const Filter = ({ title, category }: { title: string; category: CategoryType }) => { +export const Filter = ({ title, category, variant }: { title: string; category: CategoryType; variant: boolean }) => { const categoryName = categoryMap[category]; + const [open, setOpen] = useState(false); const [initialFilters, setInitialFilters] = useState({}); const [selectedFilters, setSelectedFilters] = useState({}); @@ -55,26 +56,21 @@ export const Filter = ({ title, category }: { title: string; category: CategoryT }; const handleSubmitProperty = () => { - // const serchUrl = (category: string) => `${API_BASE_URL}/api/v1/products/${category}/search`; - const queryParams = queryString.stringify(selectedFilters, { arrayFormat: 'comma' }); - - // const url = `${serchUrl(category)}?${queryParams}`; - - // const response = await fetch(url).then((res) => res.json()); - - // console.log(response); - - // console.log(selectedFilters); + setOpen(false); router.push(`/products/${category}/search?${queryParams}`); }; return ( - + - + - + {categoryName} 상품 필터 원하는 옵션을 선택 후 적용하기 버튼을 눌러주세요. diff --git a/src/features/search/components/searchResult/Item/Skeleton.tsx b/src/features/search/components/searchResult/Item/Skeleton.tsx index 57acdce..e31d4c3 100644 --- a/src/features/search/components/searchResult/Item/Skeleton.tsx +++ b/src/features/search/components/searchResult/Item/Skeleton.tsx @@ -2,7 +2,7 @@ import { Skeleton } from '@/shared/components/shadcn/ui/skeleton'; export const ProductItemSkeleton = () => { return ( -
      • +
      • diff --git a/src/features/search/components/searchResult/Item/index.tsx b/src/features/search/components/searchResult/Item/index.tsx index 7e9fa42..1892f58 100644 --- a/src/features/search/components/searchResult/Item/index.tsx +++ b/src/features/search/components/searchResult/Item/index.tsx @@ -1,6 +1,7 @@ import { categoryMap } from '@/features/category/constants'; import { Macbook } from '@/features/product/api/getProductList'; import { Badge } from '@/shared/components/Badge'; +import DiscountBadge from '@/shared/components/DiscountBadge'; import { Text } from '@/shared/components/shadcn/Text'; import { convertToLocalFormat } from '@/shared/utils'; import Image from 'next/image'; @@ -14,9 +15,9 @@ export const SearchResultItem = ({ productItem }: ProductItemProps) => { const categoryName = categoryMap[productItem.category]; return ( -
      • +
      • -
        +
        {productItem.title} { {categoryName} - {productItem.title} + {productItem.title}
        - {productItem.chip} + + {productItem.chip} +
        - - {productItem.cpu} - - - {productItem.gpu} - - - {productItem.storage} - - - {productItem.memory} - - - {productItem.color} - + {productItem.cpu} + {productItem.gpu} + {productItem.storage} + {productItem.memory} + {productItem.color}
        -
        - - {productItem.discountPercentage}% - -
        - {convertToLocalFormat(productItem.currentPrice)}원 + + + {convertToLocalFormat(productItem.currentPrice)}원 +
        diff --git a/src/features/search/components/searchResult/index.tsx b/src/features/search/components/searchResult/index.tsx index cdd02fc..8c208ad 100644 --- a/src/features/search/components/searchResult/index.tsx +++ b/src/features/search/components/searchResult/index.tsx @@ -1,10 +1,12 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ 'use client'; import { CategoryType } from '@/features/category/constants'; import { useGetSearchResult } from '../../hooks/useSearchResult'; import { FilterProperty } from '../../api/getFilterProperty'; -import queryString from 'query-string'; import { SearchResultItem } from './Item'; +import { Button } from '@/shared/components/shadcn/ui/button'; +import { Text } from '@/shared/components/shadcn/Text'; interface SearchResultProps { category: CategoryType; @@ -12,15 +14,33 @@ interface SearchResultProps { } export const SearchResultList = ({ category, params }: SearchResultProps) => { - const { data: productData } = useGetSearchResult(category, params); + const { data, hasNextPage, fetchNextPage } = useGetSearchResult(category, params); + const productData = data.pages.map((page) => page.data).flat(); + + const handleClickNextPage = () => { + fetchNextPage(); + }; + + console.log(hasNextPage); return ( -
        -
          - {productData?.data.map((productItem) => { +
          +
            + {productData.map((productItem) => { return ; })}
          + {hasNextPage ? ( + + ) : ( +
          + + 마지막 페이지입니다. + +
          + )}
          ); }; diff --git a/src/features/search/hooks/useSearchResult.ts b/src/features/search/hooks/useSearchResult.ts index 6a0cee8..44e03f5 100644 --- a/src/features/search/hooks/useSearchResult.ts +++ b/src/features/search/hooks/useSearchResult.ts @@ -1,4 +1,4 @@ -import { useSuspenseQuery, UseSuspenseQueryResult } from '@tanstack/react-query'; +import { InfiniteData, useSuspenseInfiniteQuery, UseSuspenseInfiniteQueryResult } from '@tanstack/react-query'; import { FilterProperty } from '../api/getFilterProperty'; import { CategoryType } from '@/features/category/constants'; import { getSearchResult, GetSearchResultResponse } from '../api/getSearchResult'; @@ -6,9 +6,17 @@ import { getSearchResult, GetSearchResultResponse } from '../api/getSearchResult export const useGetSearchResult = ( category: CategoryType, property: FilterProperty, -): UseSuspenseQueryResult => { - return useSuspenseQuery({ +): UseSuspenseInfiniteQueryResult> => { + return useSuspenseInfiniteQuery({ queryKey: ['search', category, property], - queryFn: () => getSearchResult(category, property), + queryFn: ({ pageParam }) => getSearchResult(category, property, pageParam), + initialPageParam: 1, + getNextPageParam: (lastPage) => { + if (lastPage.pageInfo.lastPage === lastPage.pageInfo.currentPage) { + return undefined; + } + + return lastPage.pageInfo.currentPage + 1; + }, }); }; diff --git a/src/shared/assets/Icons.tsx b/src/shared/assets/Icons.tsx new file mode 100644 index 0000000..8d1e49f --- /dev/null +++ b/src/shared/assets/Icons.tsx @@ -0,0 +1,12 @@ +import { SVGProps } from 'react'; + +export function Triangle(props: SVGProps) { + return ( + + + + ); +} diff --git a/src/shared/components/DiscountBadge/index.tsx b/src/shared/components/DiscountBadge/index.tsx new file mode 100644 index 0000000..ec96862 --- /dev/null +++ b/src/shared/components/DiscountBadge/index.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { Text } from '@/shared/components/shadcn/Text'; +import { Triangle } from '@/shared/assets/Icons'; + +const DiscountBadge = ({ discountPercentage }: { discountPercentage: number }) => { + const getBadgeContent = () => { + switch (true) { + case discountPercentage < 0: + return ( +
          + + + {discountPercentage}% + +
          + ); + case discountPercentage > 0: + return ( +
          + + + +{discountPercentage}% + +
          + ); + + case discountPercentage === 0: + return ( +
          + 변동없음 +
          + ); + default: + return null; + } + }; + + return <>{getBadgeContent()}; +}; + +export default DiscountBadge; diff --git a/src/shared/components/shadcn/ui/label.tsx b/src/shared/components/shadcn/ui/label.tsx index f27c69a..44401f6 100644 --- a/src/shared/components/shadcn/ui/label.tsx +++ b/src/shared/components/shadcn/ui/label.tsx @@ -1,26 +1,19 @@ -"use client" +'use client'; -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from 'react'; +import * as LabelPrimitive from '@radix-ui/react-label'; +import { cva, type VariantProps } from 'class-variance-authority'; -import { cn } from "@/shared/lib/tailwind/utils" +import { cn } from '@/shared/lib/tailwind/utils'; -const labelVariants = cva( - "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" -) +const labelVariants = cva('text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'); const Label = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef & - VariantProps + React.ComponentPropsWithoutRef & VariantProps >(({ className, ...props }, ref) => ( - -)) -Label.displayName = LabelPrimitive.Root.displayName + +)); +Label.displayName = LabelPrimitive.Root.displayName; -export { Label } +export { Label }; diff --git a/src/shared/components/shadcn/ui/toggle.tsx b/src/shared/components/shadcn/ui/toggle.tsx index ce3ccc5..94c6971 100644 --- a/src/shared/components/shadcn/ui/toggle.tsx +++ b/src/shared/components/shadcn/ui/toggle.tsx @@ -7,12 +7,12 @@ import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/shared/lib/tailwind/utils'; const toggleVariants = cva( - 'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-30', + 'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-primary hover:text-primary-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-30', { variants: { variant: { default: 'bg-transparent', - outline: 'border border-input bg-transparent hover:bg-accent hover:text-accent-foreground', + outline: 'border border-input bg-transparent hover:bg-primary hover:text-white', }, size: { default: 'h-10 px-3', diff --git a/tailwind.config.ts b/tailwind.config.ts index 0703a42..0c3e325 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -18,6 +18,7 @@ const config = { extend: { colors: { badge: '#F45151', + badgeBackground: '#F9F2F2', border: 'hsl(var(--border))', input: 'hsl(var(--input))', ring: 'hsl(var(--ring))',