diff --git a/package.json b/package.json index 8a5de93e..9c5b9924 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "test": "vitest", + "test:ui": "vitest --ui", "chromatic": "npx chromatic --project-token=chpt_228d999e438e234" }, "dependencies": { @@ -28,7 +29,7 @@ "react-dom": "^18", "react-hook-form": "^7.49.2", "react-redux": "^9.0.4", - "swiper": "^11" + "react-slick": "^0.29.0" }, "devDependencies": { "@storybook/addon-essentials": "^7.6.6", @@ -47,9 +48,11 @@ "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "@types/react-slick": "^0.23.13", "@typescript-eslint/eslint-plugin": "^6.15.0", "@typescript-eslint/parser": "^6.15.0", "@vitejs/plugin-react": "^4.2.1", + "@vitest/ui": "^1.2.1", "chromatic": "^10.2.0", "eslint": "^8", "eslint-config-airbnb": "^19.0.4", @@ -62,6 +65,7 @@ "jsdom": "^23.2.0", "msw": "^2.0.13", "sass": "^1.69.6", + "slick-carousel": "^1.8.1", "storybook": "^7.6.6", "storybook-react-context": "^0.6.0", "stylelint": "^16.1.0", @@ -75,4 +79,4 @@ "msw": { "workerDirectory": "public" } -} \ No newline at end of file +} diff --git a/public/assets/icons/star.svg b/public/assets/icons/star.svg new file mode 100644 index 00000000..c402ff4a --- /dev/null +++ b/public/assets/icons/star.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/product.png b/public/assets/product.png new file mode 100644 index 00000000..590c698e Binary files /dev/null and b/public/assets/product.png differ diff --git a/public/assets/recommandItem1.png b/public/assets/recommendItem1.png similarity index 100% rename from public/assets/recommandItem1.png rename to public/assets/recommendItem1.png diff --git a/public/assets/recommandItem2.png b/public/assets/recommendItem2.png similarity index 100% rename from public/assets/recommandItem2.png rename to public/assets/recommendItem2.png diff --git a/public/assets/recommandItem3.png b/public/assets/recommendItem3.png similarity index 100% rename from public/assets/recommandItem3.png rename to public/assets/recommendItem3.png diff --git a/public/assets/recommandItem4.png b/public/assets/recommendItem4.png similarity index 100% rename from public/assets/recommandItem4.png rename to public/assets/recommendItem4.png diff --git a/src/app/globals.css b/src/app/globals.css index b1e55284..930516a9 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,3 +1,6 @@ +@import "~slick-carousel/slick/slick.css"; +@import "~slick-carousel/slick/slick-theme.css"; + :root { --primary: #0075FF; --secondary: #B2D6FF; diff --git a/src/app/page.module.scss b/src/app/page.module.scss index 07f5d293..edb0f304 100644 --- a/src/app/page.module.scss +++ b/src/app/page.module.scss @@ -11,12 +11,7 @@ padding: 0 24px; } - .bannerWrapper { - max-height: 140px; - overflow-y: hidden; - } - - .recommandTextWrapper { + .recommendTextWrapper { padding: 0 24px; } diff --git a/src/app/page.tsx b/src/app/page.tsx index f083de34..49a07985 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,19 +1,18 @@ import classNames from 'classnames/bind'; -import dynamic from 'next/dynamic'; -import BottomNav from '@components/shared/bottom-nav/BottomNav'; -import Flex from '@components/shared/flex/Flex'; -import Header from '@components/shared/header/Header'; -import ProductArticle from '@components/shared/product-article/ProductArticle'; -import Radio from '@components/shared/radio/Radio'; -import Spacing from '@components/shared/spacing/Spacing'; -import Text from '@components/shared/text/Text'; +import BottomNav from '@shared/bottom-nav/BottomNav'; +import Banner from '@shared/carousel/Banner'; +import RecommendList from '@shared/carousel/RecommendList'; +import Flex from '@shared/flex/Flex'; +import Header from '@shared/header/Header'; +import ProductArticle from '@shared/product-article/ProductArticle'; +import Radio from '@shared/radio/Radio'; +import SearchBar from '@shared/search-bar/SearchBar'; +import Spacing from '@shared/spacing/Spacing'; +import Text from '@shared/text/Text'; import styles from './page.module.scss'; -const Banner = dynamic(() => { return import('@components/shared/carousel/Banner'); }); -const RecommandList = dynamic(() => { return import('@components/shared/carousel/RecommandList'); }); - const cx = classNames.bind(styles); const bannerData = [ @@ -43,25 +42,25 @@ const bannerData = [ }, ]; -const recommandListData = [ +const recommendListData = [ { id: 1, link: '/', - src: '/assets/recommandItem1.png', + src: '/assets/recommendItem1.png', alt: '그림', productName: '카샴푸', }, { id: 2, link: '/', - src: '/assets/recommandItem2.png', + src: '/assets/recommendItem2.png', alt: '그림', productName: '휠 클리너', }, { id: 3, link: '/', - src: '/assets/recommandItem3.png', + src: '/assets/recommendItem3.png', alt: '그림', productName: '타올', @@ -69,7 +68,7 @@ const recommandListData = [ { id: 4, link: '/', - src: '/assets/recommandItem4.png', + src: '/assets/recommendItem4.png', alt: '그림', productName: '먼지털이개', @@ -77,7 +76,7 @@ const recommandListData = [ { id: 5, link: '/', - src: '/assets/recommandItem4.png', + src: '/assets/recommendItem4.png', alt: '그림', productName: '먼지털이개', @@ -85,7 +84,7 @@ const recommandListData = [ { id: 6, link: '/', - src: '/assets/recommandItem4.png', + src: '/assets/recommendItem4.png', alt: '그림', productName: '먼지털이개', @@ -137,15 +136,13 @@ export default function Home() { -
- -
+ -
+
추천 세차용품
- +
WashPedia 랭킹 @@ -153,7 +150,7 @@ export default function Home() { - + diff --git a/src/app/product/[id]/page.module.scss b/src/app/product/[id]/page.module.scss new file mode 100644 index 00000000..3f56f6c3 --- /dev/null +++ b/src/app/product/[id]/page.module.scss @@ -0,0 +1,7 @@ +.product { + padding: 0 24px; +} + +.productInfo { + padding: 16px 24px; +} diff --git a/src/app/product/[id]/page.tsx b/src/app/product/[id]/page.tsx new file mode 100644 index 00000000..d91fce7d --- /dev/null +++ b/src/app/product/[id]/page.tsx @@ -0,0 +1,43 @@ +import classNames from 'classnames/bind'; +import Image from 'next/image'; + +import Star from '@components/icons/Star'; +import Flex from '@shared/flex/Flex'; +import Header from '@shared/header/Header'; +import Radio from '@shared/radio/Radio'; +import Spacing from '@shared/spacing/Spacing'; +import Text from '@shared/text/Text'; + +import styles from './page.module.scss'; + +const cx = classNames.bind(styles); + +function SuppliesPage() { + return ( + <> +
+ 상품 이미지 + + 카믹스 + 아머올 세차용품 스피드 왁스 스프레이 + + 코팅제 + + + + + + 4.5 + + (20) + + + + + + + + ); +} + +export default SuppliesPage; diff --git a/src/components/icons/Star.tsx b/src/components/icons/Star.tsx new file mode 100644 index 00000000..c67a08b0 --- /dev/null +++ b/src/components/icons/Star.tsx @@ -0,0 +1,10 @@ +function Star({ size }: { size: number }) { + return ( + + + + + ); +} + +export default Star; diff --git a/src/components/shared/accordion/Accordion.stories.tsx b/src/components/shared/accordion/Accordion.stories.tsx index eb32ca3e..affded8f 100644 --- a/src/components/shared/accordion/Accordion.stories.tsx +++ b/src/components/shared/accordion/Accordion.stories.tsx @@ -1,11 +1,13 @@ -/* eslint-disable react/jsx-closing-tag-location */ -/* eslint-disable max-len */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta } from '@storybook/react'; import Minus from '@components/icons/Minus'; import Plus from '@components/icons/Plus'; +import Text from '@shared/text/Text'; import Accordion from './Accordion'; +import AccordionBody from './body/AccordionBody'; +import AccordionHeader from './header/AccordionHeader'; +import AccordionItem from './item/AccordionItem'; const meta = { title: 'Shared/Accordion', @@ -18,35 +20,41 @@ const meta = { } satisfies Meta; export default meta; -type Story = StoryObj; -export const Horizontal: Story = { - args: { - children: <> - - } closeIcon={}>Accordion Item #1 - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do - eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad - minim veniam, quis nostrud exercitation ullamco laboris nisi ut - aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla - pariatur. Excepteur sint occaecat cupidatat non proident, sunt in - culpa qui officia deserunt mollit anim id est laborum. - - - - } closeIcon={}>Accordion Item #1 - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do - eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad - minim veniam, quis nostrud exercitation ullamco laboris nisi ut - aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla - pariatur. Excepteur sint occaecat cupidatat non proident, sunt in - culpa qui officia deserunt mollit anim id est laborum. - - - , +export const AccordionStory = { + render: () => { + return ( + + + } closeIcon={}> + + 목록1 + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad + minim veniam, quis nostrud exercitation ullamco laboris nisi ut + + + + + + + 목록2 + + + + + aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in + culpa qui officia deserunt mollit anim id est laborum. + + + + + ); }, }; diff --git a/src/components/shared/accordion/Accordion.tsx b/src/components/shared/accordion/Accordion.tsx index 39606173..a130fcbc 100644 --- a/src/components/shared/accordion/Accordion.tsx +++ b/src/components/shared/accordion/Accordion.tsx @@ -1,35 +1,37 @@ -import { useCallback, useMemo, useState } from 'react'; +import { + forwardRef, useCallback, useMemo, useState, +} from 'react'; -import AccordionContext from '@contexts/AccordionContext'; +import AccordionContext from '@/contexts/AccordionContext'; -import AccordionBody from './body'; -import AccordionHeader from './header'; -import AccordionItem from './item'; +import { AccordionProps } from './type/accordion.type'; -function Accordion({ children }: { children: React.ReactNode | React.ReactNode[] }) { - const [activeItem, setActiveItem] = useState(''); +// eslint-disable-next-line max-len +const Accordion = forwardRef(({ defaultActiveItems = [], children, ...props }, ref) => { + const [activeItems, setActiveItems] = useState(defaultActiveItems); - const changeActiveItem = useCallback((value: string) => { - if (activeItem !== value) setActiveItem(value); - if (activeItem === value) setActiveItem(''); - }, [setActiveItem, activeItem]); + const handleSetActiveItem = useCallback((item: string) => { + if (activeItems?.includes(item)) { + setActiveItems(activeItems.filter((activeItem) => { return activeItem !== item; })); + } else { + setActiveItems([...activeItems, item]); + } + }, [activeItems]); const values = useMemo(() => { return { - activeItem, - changeSelectedItem: changeActiveItem, + activeItems, + setActiveItem: handleSetActiveItem, }; - }, [activeItem, changeActiveItem]); + }, [activeItems, handleSetActiveItem]); return ( - {children} +
+ {children} +
); -} - -Accordion.Item = AccordionItem; -Accordion.Header = AccordionHeader; -Accordion.Body = AccordionBody; +}); export default Accordion; diff --git a/src/components/shared/accordion/body/AccordionBody.module.scss b/src/components/shared/accordion/body/AccordionBody.module.scss new file mode 100644 index 00000000..6894b165 --- /dev/null +++ b/src/components/shared/accordion/body/AccordionBody.module.scss @@ -0,0 +1,9 @@ +.container { + width: 100%; + overflow: hidden; + transition: height 0.3s ease; + + & > div[data-name="body-inner"] { + padding: 16px; + } +} diff --git a/src/components/shared/accordion/body/AccordionBody.tsx b/src/components/shared/accordion/body/AccordionBody.tsx new file mode 100644 index 00000000..8c766983 --- /dev/null +++ b/src/components/shared/accordion/body/AccordionBody.tsx @@ -0,0 +1,46 @@ +import { + forwardRef, useEffect, useState, useRef, +} from 'react'; + +import classNames from 'classnames/bind'; + +import { useAccordion } from '@/contexts/AccordionContext'; + +import { AccordionBodyProps } from '../type/accordion.type'; + +import styles from './AccordionBody.module.scss'; + +const cx = classNames.bind(styles); + +const AccordionBody = forwardRef(({ + itemName = '', children, className, ...props +}, ref) => { + const innerRef = useRef(null); + + const { activeItems } = useAccordion(); + const isActive = activeItems.includes(itemName); + + const [currentBodyHeight, setCurrentBodyHeight] = useState(); + + useEffect(() => { + if (innerRef.current == null) return; + + setCurrentBodyHeight(isActive ? `${innerRef.current.clientHeight}px` : '0'); + }, [isActive]); + + return ( +
+
+ {children} +
+
+ ); +}); + +export default AccordionBody; diff --git a/src/components/shared/accordion/body/index.module.scss b/src/components/shared/accordion/body/index.module.scss deleted file mode 100644 index 7e378242..00000000 --- a/src/components/shared/accordion/body/index.module.scss +++ /dev/null @@ -1,12 +0,0 @@ -.container { - box-sizing: border-box; - max-height: 0; - overflow: hidden; - transition: all 0.3s ease-in-out; - - &.showItem { - max-height: 350px; - padding: 16px; - background-color: var(--gray); - } -} diff --git a/src/components/shared/accordion/body/index.tsx b/src/components/shared/accordion/body/index.tsx deleted file mode 100644 index 11e685bb..00000000 --- a/src/components/shared/accordion/body/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import classNames from 'classnames/bind'; - -import { useAccordion } from '@contexts/AccordionContext'; - -import styles from './index.module.scss'; - -const cx = classNames.bind(styles); - -interface AccordionBodyProps { - children: React.ReactNode; - label?: string; - className?: string; -} - -function AccordionBody({ children, label, className }: AccordionBodyProps) { - const { activeItem } = useAccordion(); - - return ( -
- {children} -
- ); -} - -export default AccordionBody; diff --git a/src/components/shared/accordion/header/index.module.scss b/src/components/shared/accordion/header/AccordionHeader.module.scss similarity index 90% rename from src/components/shared/accordion/header/index.module.scss rename to src/components/shared/accordion/header/AccordionHeader.module.scss index db665823..d0b5e751 100644 --- a/src/components/shared/accordion/header/index.module.scss +++ b/src/components/shared/accordion/header/AccordionHeader.module.scss @@ -2,6 +2,7 @@ display: flex; align-items: center; justify-content: space-between; + width: 100%; padding: 16px; border-bottom: 1px solid var(--gray-100); } diff --git a/src/components/shared/accordion/header/AccordionHeader.tsx b/src/components/shared/accordion/header/AccordionHeader.tsx new file mode 100644 index 00000000..eaf3eb7a --- /dev/null +++ b/src/components/shared/accordion/header/AccordionHeader.tsx @@ -0,0 +1,35 @@ +import { + forwardRef, useCallback, +} from 'react'; + +import classNames from 'classnames/bind'; + +import { useAccordion } from '@contexts/AccordionContext'; + +import { AccordionHeaderProps } from '../type/accordion.type'; + +import styles from './AccordionHeader.module.scss'; + +const cx = classNames.bind(styles); + +const AccordionHeader = forwardRef(({ + itemName = '', children, onClick, className, openIcon, closeIcon, ...props +}, ref) => { + const { setActiveItem, activeItems } = useAccordion(); + + const handleClick = useCallback((event: React.MouseEvent) => { + setActiveItem(itemName); + onClick?.(event); + }, [itemName, onClick, setActiveItem]); + + const isActive = activeItems.includes(itemName); + + return ( + + ); +}); + +export default AccordionHeader; diff --git a/src/components/shared/accordion/header/index.tsx b/src/components/shared/accordion/header/index.tsx deleted file mode 100644 index 5e9355c4..00000000 --- a/src/components/shared/accordion/header/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useCallback } from 'react'; - -import classNames from 'classnames/bind'; - -import { useAccordion } from '@contexts/AccordionContext'; - -import styles from './index.module.scss'; - -const cx = classNames.bind(styles); - -interface AccordionHeaderProps { - children: React.ReactNode - label?: string - className?: string - openIcon?: React.ReactNode - closeIcon?: React.ReactNode -} - -function AccordionHeader({ - label, children, className, openIcon, closeIcon, -}: AccordionHeaderProps) { - const { changeSelectedItem, activeItem } = useAccordion(); - - const handleClickAccordionHeader = useCallback(() => { - changeSelectedItem(label || ''); - }, [changeSelectedItem, label]); - - return ( -
- {children} - {label === activeItem ? closeIcon : openIcon} -
- ); -} - -export default AccordionHeader; diff --git a/src/components/shared/accordion/item/AccordionItem.module.scss b/src/components/shared/accordion/item/AccordionItem.module.scss new file mode 100644 index 00000000..6ec3262a --- /dev/null +++ b/src/components/shared/accordion/item/AccordionItem.module.scss @@ -0,0 +1,8 @@ +.container { + width: 100%; + border-top: 1px solid var(--gray); + + &:last-of-type { + border-bottom: 1px solid var(--gray); + } +} diff --git a/src/components/shared/accordion/item/AccordionItem.tsx b/src/components/shared/accordion/item/AccordionItem.tsx new file mode 100644 index 00000000..d08682e0 --- /dev/null +++ b/src/components/shared/accordion/item/AccordionItem.tsx @@ -0,0 +1,33 @@ +import { + Children, cloneElement, forwardRef, isValidElement, +} from 'react'; + +import classNames from 'classnames/bind'; + +import { AccordionItemProps } from '../type/accordion.type'; + +import styles from './AccordionItem.module.scss'; + +const cx = classNames.bind(styles); + +const AccordionItem = forwardRef(({ + itemName, children, className, ...props +}, ref) => { + const childrenWithProps = Children.toArray(children); + + const accordionItemChildren = childrenWithProps.map((child) => { + if (isValidElement(child)) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + return cloneElement(child, { ...child.props, itemName }); + } + + return null; + }); + return ( +
+ {accordionItemChildren} +
+ ); +}); + +export default AccordionItem; diff --git a/src/components/shared/accordion/item/index.tsx b/src/components/shared/accordion/item/index.tsx deleted file mode 100644 index 9caadddb..00000000 --- a/src/components/shared/accordion/item/index.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -import { Children, cloneElement, isValidElement } from 'react'; - -interface AccordionItemProps { - children: React.ReactNode[] - label: string - className?: string -} - -function AccordionItem({ children, label, className }: AccordionItemProps) { - const childrenArray = Children.toArray(children); - - const accordionItemChildren = childrenArray.map((child) => { - if (isValidElement(child)) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return cloneElement(child as any, { ...child.props, label }); - } - return null; - }); - - return
{accordionItemChildren}
; -} - -export default AccordionItem; diff --git a/src/components/shared/accordion/type/accordion.type.ts b/src/components/shared/accordion/type/accordion.type.ts new file mode 100644 index 00000000..1df80dc9 --- /dev/null +++ b/src/components/shared/accordion/type/accordion.type.ts @@ -0,0 +1,19 @@ +export type AccordionProps = { + defaultActiveItems?: string[] + children: React.ReactNode | React.ReactNode[] +} & Omit, 'children'>; + +export type AccordionItemProps = { + children: React.ReactNode[] + itemName: string +} & Omit, 'children'>; + +export type AccordionHeaderProps = { + itemName?: string + openIcon?: React.ReactNode + closeIcon?: React.ReactNode +} & React.ButtonHTMLAttributes; + +export type AccordionBodyProps = { + itemName?: string +} & React.HTMLAttributes; diff --git a/src/components/shared/carousel/Banner.scss b/src/components/shared/carousel/Banner.scss new file mode 100644 index 00000000..78e32547 --- /dev/null +++ b/src/components/shared/carousel/Banner.scss @@ -0,0 +1,38 @@ +.container { + position: relative; + + img { + width: auto; + margin: 0 auto; + } + + .dotsCustom { + position: absolute; + bottom: 5px; + left: 50%; + transform: translateX(-50%); + } + + .dotsCustom li { + display: inline-block; + margin: 0 6px; + cursor: pointer; + } + + .dotsCustom li button { + display: block; + width: 16px; + height: 8px; + border-radius: 8px; + background: var(--gray-100); + color: transparent; + cursor: pointer; + } + + /* stylelint-disable-next-line selector-class-pattern */ + .dotsCustom li.slick-active button { + width: 24px; + border-radius: 8px; + background-color: var(--primary); + } +} diff --git a/src/components/shared/carousel/Banner.tsx b/src/components/shared/carousel/Banner.tsx index 1ec250bb..9d8ec36f 100644 --- a/src/components/shared/carousel/Banner.tsx +++ b/src/components/shared/carousel/Banner.tsx @@ -1,32 +1,32 @@ 'use client'; +import Slider from 'react-slick'; + import Image from 'next/image'; import Link from 'next/link'; -import { Autoplay, Pagination } from 'swiper/modules'; -import { Swiper, SwiperSlide } from 'swiper/react'; -import 'swiper/scss'; -import 'swiper/scss/autoplay'; -import 'swiper/scss/pagination'; -import { IBannerdata } from './types/carousel.type'; +import './Banner.scss'; +import { IBannerData } from './types/carousel.type'; + +const settings = { + dots: true, + infinite: true, + speed: 2000, + slidesToShow: 1, + slidesToScroll: 1, + autoplay: true, + adaptiveHeight: true, + autoplaySpeed: 5000, + arrows: false, +}; -function Banner({ bannerData }: { bannerData: IBannerdata[] }) { +function Banner({ bannerData }: { bannerData: IBannerData[] }) { return ( - - {bannerData.map((slide) => { - return ( - - +
+ + {bannerData.map((slide) => { + return ( + {slide.alt} - - ); - })} - + ); + })} + +
); } diff --git a/src/components/shared/carousel/RecommandList.tsx b/src/components/shared/carousel/RecommandList.tsx deleted file mode 100644 index 9269fabd..00000000 --- a/src/components/shared/carousel/RecommandList.tsx +++ /dev/null @@ -1,47 +0,0 @@ -'use client'; - -import Image from 'next/image'; -import Link from 'next/link'; -import { Autoplay } from 'swiper/modules'; -import { Swiper, SwiperSlide } from 'swiper/react'; - -import 'swiper/scss'; -import 'swiper/scss/autoplay'; -import 'swiper/scss/pagination'; -import Flex from '@shared/flex/Flex'; -import Text from '@shared/text/Text'; - -import { IRecommandList } from './types/carousel.type'; - -function RecommandList({ recommandListData }: { recommandListData: IRecommandList[] }) { - return ( - - {recommandListData.map((slide) => { - return ( - - - - {slide.alt} - - {slide.productName} - - - ); - })} - - ); -} - -export default RecommandList; diff --git a/src/components/shared/carousel/RecommandList.stories.tsx b/src/components/shared/carousel/RecommendList.stories.tsx similarity index 67% rename from src/components/shared/carousel/RecommandList.stories.tsx rename to src/components/shared/carousel/RecommendList.stories.tsx index 7f4b96bc..d5afd231 100644 --- a/src/components/shared/carousel/RecommandList.stories.tsx +++ b/src/components/shared/carousel/RecommendList.stories.tsx @@ -1,44 +1,44 @@ import type { Meta, StoryObj } from '@storybook/react'; -import RecommandList from './RecommandList'; +import RecommendList from './RecommendList'; const meta = { - title: 'Shared/RecommandList', - component: RecommandList, + title: 'Shared/RecommendList', + component: RecommendList, parameters: { }, tags: ['autodocs'], argTypes: { - recommandListData: { + recommendListData: { control: 'object', }, }, -} satisfies Meta; +} satisfies Meta; export default meta; type Story = StoryObj; export const AdBanner: Story = { args: { - recommandListData: [ + recommendListData: [ { id: 1, link: '/', - src: '/assets/recommandItem1.png', + src: '/assets/recommendItem1.png', alt: '그림', productName: '카샴푸', }, { id: 2, link: '/', - src: '/assets/recommandItem2.png', + src: '/assets/recommendItem2.png', alt: '그림', productName: '휠 클리너', }, { id: 3, link: '/', - src: '/assets/recommandItem3.png', + src: '/assets/recommendItem3.png', alt: '그림', productName: '타올', @@ -46,7 +46,7 @@ export const AdBanner: Story = { { id: 4, link: '/', - src: '/assets/recommandItem4.png', + src: '/assets/recommendItem4.png', alt: '그림', productName: '먼지털이개', @@ -54,7 +54,7 @@ export const AdBanner: Story = { { id: 5, link: '/', - src: '/assets/recommandItem4.png', + src: '/assets/recommendItem4.png', alt: '그림', productName: '먼지털이개', @@ -62,7 +62,7 @@ export const AdBanner: Story = { { id: 6, link: '/', - src: '/assets/recommandItem4.png', + src: '/assets/recommendItem4.png', alt: '그림', productName: '먼지털이개', diff --git a/src/components/shared/carousel/RecommendList.tsx b/src/components/shared/carousel/RecommendList.tsx new file mode 100644 index 00000000..24dc3ac1 --- /dev/null +++ b/src/components/shared/carousel/RecommendList.tsx @@ -0,0 +1,47 @@ +'use client'; + +import Slider from 'react-slick'; + +import Image from 'next/image'; +import Link from 'next/link'; + +import Flex from '@shared/flex/Flex'; +import Text from '@shared/text/Text'; + +import { IRecommendList } from './types/carousel.type'; + +const settings = { + dots: false, + infinite: true, + speed: 2000, + slidesToShow: 4, + slidesToScroll: 1, + autoplay: true, + adaptiveHeight: true, + autoplaySpeed: 5000, + arrows: false, +}; + +function RecommendList({ recommendListData }: { recommendListData: IRecommendList[] }) { + return ( + + {recommendListData?.map((slide) => { + return ( + + + {slide.alt} + + {slide.productName} + + ); + })} + + ); +} + +export default RecommendList; diff --git a/src/components/shared/carousel/types/carousel.type.ts b/src/components/shared/carousel/types/carousel.type.ts index f1ccef2d..68c0fb5c 100644 --- a/src/components/shared/carousel/types/carousel.type.ts +++ b/src/components/shared/carousel/types/carousel.type.ts @@ -1,10 +1,10 @@ -export interface IBannerdata { +export interface IBannerData { id: number link: string src: string alt: string } -export interface IRecommandList extends IBannerdata { +export interface IRecommendList extends IBannerData { productName: string } diff --git a/src/components/shared/radio/Radio.module.scss b/src/components/shared/radio/Radio.module.scss index 0a5a8838..94a35a03 100644 --- a/src/components/shared/radio/Radio.module.scss +++ b/src/components/shared/radio/Radio.module.scss @@ -64,3 +64,11 @@ background-color: var(--primary); color: var(--white); } + +.label.product { + height: 36px; +} + +.input[type="radio"]:checked + .label.product { + border-bottom: 1px solid var(--black); +} diff --git a/src/components/shared/radio/Radio.tsx b/src/components/shared/radio/Radio.tsx index 44a916cf..57ef2b05 100644 --- a/src/components/shared/radio/Radio.tsx +++ b/src/components/shared/radio/Radio.tsx @@ -5,7 +5,7 @@ import classNames from 'classnames/bind'; import styles from './Radio.module.scss'; interface RadioProps extends InputHTMLAttributes { - type: 'gender' | 'ageGroup' | 'additionalInfo' | 'filter' + type: 'gender' | 'ageGroup' | 'additionalInfo' | 'filter' | 'product' label: string value: string | number } diff --git a/src/contexts/AccordionContext.tsx b/src/contexts/AccordionContext.tsx index 6faac792..89aed14a 100644 --- a/src/contexts/AccordionContext.tsx +++ b/src/contexts/AccordionContext.tsx @@ -1,13 +1,13 @@ import { createContext, useContext } from 'react'; interface AccordionContextValue { - activeItem: string - changeSelectedItem: (item: string) => void + activeItems: string[] + setActiveItem: (item: string) => void } const AccordionContext = createContext({ - activeItem: '', - changeSelectedItem: () => { }, + activeItems: [], + setActiveItem: () => { }, }); export function useAccordion() { diff --git a/yarn.lock b/yarn.lock index 983af32c..4220a2c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1808,6 +1808,11 @@ schema-utils "^3.0.0" source-map "^0.7.3" +"@polka/url@^1.0.0-next.24": + version "1.0.0-next.24" + resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.24.tgz#58601079e11784d20f82d0585865bb42305c4df3" + integrity sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ== + "@radix-ui/number@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-1.0.1.tgz#644161a3557f46ed38a042acf4a770e826021674" @@ -3557,6 +3562,13 @@ dependencies: "@types/react" "*" +"@types/react-slick@^0.23.13": + version "0.23.13" + resolved "https://registry.yarnpkg.com/@types/react-slick/-/react-slick-0.23.13.tgz#037434e73a58063047b121e08565f7185d811f36" + integrity sha512-bNZfDhe/L8t5OQzIyhrRhBr/61pfBcWaYJoq6UDqFtv5LMwfg4NsVDD2J8N01JqdAdxLjOt66OZEp6PX+dGs/A== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@>=16", "@types/react@^18": version "18.2.45" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.45.tgz#253f4fac288e7e751ab3dc542000fb687422c15c" @@ -3846,6 +3858,19 @@ dependencies: tinyspy "^2.2.0" +"@vitest/ui@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@vitest/ui/-/ui-1.2.1.tgz#dbf5eafd90aa52a2cd08bdbcdd81244ca1aeb9e0" + integrity sha512-5kyEDpH18TB13Keutk5VScWG+LUDfPJOL2Yd1hqX+jv6+V74tp4ZYcmTgx//WDngiZA5PvX3qCHQ5KrhGzPbLg== + dependencies: + "@vitest/utils" "1.2.1" + fast-glob "^3.3.2" + fflate "^0.8.1" + flatted "^3.2.9" + pathe "^1.1.1" + picocolors "^1.0.0" + sirv "^2.0.4" + "@vitest/utils@0.34.7", "@vitest/utils@^0.34.6": version "0.34.7" resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-0.34.7.tgz#46d0d27cd0f6ca1894257d4e141c5c48d7f50295" @@ -3865,6 +3890,16 @@ loupe "^2.3.7" pretty-format "^29.7.0" +"@vitest/utils@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-1.2.1.tgz#ad798cb13ec9e9e97b13be65d135e9e8e3c586aa" + integrity sha512-bsH6WVZYe/J2v3+81M5LDU8kW76xWObKIURpPrOXm2pjBniBu2MERI/XP60GpS4PHU3jyK50LUutOwrx4CyHUg== + dependencies: + diff-sequences "^29.6.3" + estree-walker "^3.0.3" + loupe "^2.3.7" + pretty-format "^29.7.0" + "@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" @@ -4877,7 +4912,7 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: inherits "^2.0.1" safe-buffer "^5.0.1" -classnames@^2.5.1: +classnames@^2.2.5, classnames@^2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== @@ -5710,6 +5745,11 @@ enhanced-resolve@^5.12.0, enhanced-resolve@^5.15.0, enhanced-resolve@^5.7.0: graceful-fs "^4.2.4" tapable "^2.2.0" +enquire.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/enquire.js/-/enquire.js-2.1.6.tgz#3e8780c9b8b835084c3f60e166dbc3c2a3c89814" + integrity sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw== + entities@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" @@ -6423,6 +6463,11 @@ fetch-retry@^5.0.2: resolved "https://registry.yarnpkg.com/fetch-retry/-/fetch-retry-5.0.6.tgz#17d0bc90423405b7a88b74355bf364acd2a7fa56" integrity sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ== +fflate@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.1.tgz#1ed92270674d2ad3c73f077cd0acf26486dae6c9" + integrity sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ== + figures@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -7848,6 +7893,13 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +json2mq@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/json2mq/-/json2mq-0.2.0.tgz#b637bd3ba9eabe122c83e9720483aeb10d2c904a" + integrity sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA== + dependencies: + string-convert "^0.2.0" + json5@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" @@ -8351,6 +8403,11 @@ mri@^1.2.0: resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== +mrmime@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-2.0.0.tgz#151082a6e06e59a9a39b46b3e14d5cfe92b3abb4" + integrity sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -9503,6 +9560,17 @@ react-remove-scroll@2.5.5: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" +react-slick@^0.29.0: + version "0.29.0" + resolved "https://registry.yarnpkg.com/react-slick/-/react-slick-0.29.0.tgz#0bed5ea42bf75a23d40c0259b828ed27627b51bb" + integrity sha512-TGdOKE+ZkJHHeC4aaoH85m8RnFyWqdqRfAGkhd6dirmATXMZWAxOpTLmw2Ll/jPTQ3eEG7ercFr/sbzdeYCJXA== + dependencies: + classnames "^2.2.5" + enquire.js "^2.1.6" + json2mq "^0.2.0" + lodash.debounce "^4.0.8" + resize-observer-polyfill "^1.5.0" + react-style-singleton@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" @@ -9755,6 +9823,11 @@ reselect@^5.0.1: resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.0.1.tgz#587cdaaeb4e0e8927cff80ebe2bbef05f74b1648" integrity sha512-D72j2ubjgHpvuCiORWkOUxndHJrxDaSolheiz5CO+roz8ka97/4msh2E8F5qay4GawR5vzBt5MkbDHT+Rdy/Wg== +resize-observer-polyfill@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -10153,6 +10226,15 @@ simple-update-notifier@^2.0.0: dependencies: semver "^7.5.3" +sirv@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/sirv/-/sirv-2.0.4.tgz#5dd9a725c578e34e449f332703eb2a74e46a29b0" + integrity sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ== + dependencies: + "@polka/url" "^1.0.0-next.24" + mrmime "^2.0.0" + totalist "^3.0.0" + sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" @@ -10172,6 +10254,11 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" +slick-carousel@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/slick-carousel/-/slick-carousel-1.8.1.tgz#a4bfb29014887bb66ce528b90bd0cda262cc8f8d" + integrity sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA== + "source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" @@ -10327,6 +10414,11 @@ strict-event-emitter@^0.5.0, strict-event-emitter@^0.5.1: resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz#1602ece81c51574ca39c6815e09f1a3e8550bd93" integrity sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ== +string-convert@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/string-convert/-/string-convert-0.2.1.tgz#6982cc3049fbb4cd85f8b24568b9d9bf39eeff97" + integrity sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A== + string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -10615,11 +10707,6 @@ swc-loader@^0.2.3: resolved "https://registry.yarnpkg.com/swc-loader/-/swc-loader-0.2.3.tgz#6792f1c2e4c9ae9bf9b933b3e010210e270c186d" integrity sha512-D1p6XXURfSPleZZA/Lipb3A8pZ17fP4NObZvFCDjK/OKljroqDpPmsBdTraWhVBqUNpcWBQY1imWdoPScRlQ7A== -swiper@^11: - version "11.0.5" - resolved "https://registry.yarnpkg.com/swiper/-/swiper-11.0.5.tgz#6ed1ad06e6906ba42fd4b93d4988f0626a49046e" - integrity sha512-rhCwupqSyRnWrtNzWzemnBLMoyYuoDgGgspAm/8iBD3jCvAWycPLH4Z3TB0O5520DHLzMx94yUMH/B9Efpa48w== - symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" @@ -10850,6 +10937,11 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +totalist@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8" + integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ== + tough-cookie@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf"