diff --git a/.storybook/preview.ts b/.storybook/preview.tsx similarity index 68% rename from .storybook/preview.ts rename to .storybook/preview.tsx index afac7ead..eca38681 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.tsx @@ -1,4 +1,6 @@ import type { Preview } from '@storybook/react'; +import { OverlayProvider } from '../src/hooks/useOverlay/OverlayProvider'; +import React from 'react'; import '../styles/globals.css' const preview: Preview = { @@ -20,6 +22,13 @@ const preview: Preview = { }, }, }, + decorators: [ + (Story) => ( + + + + ), + ], }; export default preview; diff --git a/package.json b/package.json index d9c21b9b..133ffb4c 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@tanstack/react-query": "^4.10.3", "axios": "^1.1.3", "dayjs": "^1.11.6", + "embla-carousel-react": "^8.0.0-rc13", "nanoid": "^4.0.0", "next": "12.3.1", "openapi-fetch": "^0.7.1", diff --git a/src/components/feed/FeedPostViewer/FeedPostViewer.stories.ts b/src/components/feed/FeedPostViewer/FeedPostViewer.stories.ts index 99924b47..d240e08b 100644 --- a/src/components/feed/FeedPostViewer/FeedPostViewer.stories.ts +++ b/src/components/feed/FeedPostViewer/FeedPostViewer.stories.ts @@ -20,7 +20,12 @@ export const Default: Story = { title: '하트시그널 시즌 1부터 시즌 4까지 중에 가장 인기가 많았던 출연자는?', contents: `1번 김현우\n2번 오영주\n3번 임현주\n4번 박지현\n5번 기타`, updatedDate: '2시간 전', - images: ['https://via.placeholder.com/600/92c952'], + images: [ + 'https://via.placeholder.com/600/92c952', + 'https://via.placeholder.com/600/771796', + 'https://via.placeholder.com/600/92c952', + 'https://via.placeholder.com/600/771796', + ], user: { id: 1, name: '김지민', diff --git a/src/components/feed/FeedPostViewer/FeedPostViewer.tsx b/src/components/feed/FeedPostViewer/FeedPostViewer.tsx index 99d38a8a..f98cfcdd 100644 --- a/src/components/feed/FeedPostViewer/FeedPostViewer.tsx +++ b/src/components/feed/FeedPostViewer/FeedPostViewer.tsx @@ -8,6 +8,8 @@ import LikeFillIcon from 'public/assets/svg/like_fill.svg'; import SendIcon from 'public/assets/svg/send.svg'; import { styled } from 'stitches.config'; import { Box } from '@components/box/Box'; +import { useOverlay } from '@hooks/useOverlay/Index'; +import ImageCarouselModal from '@components/modal/ImageCarouselModal'; interface FeedPostViewerProps { post: paths['/post/v1/{postId}']['get']['responses']['200']['content']['application/json']; @@ -15,6 +17,14 @@ interface FeedPostViewerProps { } export default function FeedPostViewer({ post, Actions }: FeedPostViewerProps) { + const overlay = useOverlay(); + + const handleClickImage = (images: string[], startIndex: number) => () => { + overlay.open(({ isOpen, close }) => ( + + )); + }; + return ( @@ -45,11 +55,12 @@ export default function FeedPostViewer({ post, Actions }: FeedPostViewerProps) { {post.images && ( {post.images.length === 1 ? ( - + ) : ( {post.images.map((image, index) => ( - + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ))} )} @@ -146,6 +157,7 @@ const BigImage = styled('img', { height: '453px', objectFit: 'cover', borderRadius: '10px', + cursor: 'pointer', }); const ImageListWrapper = styled('div', { display: 'grid', @@ -157,6 +169,7 @@ const ImageListItem = styled('img', { height: '136px', objectFit: 'cover', borderRadius: '8px', + cursor: 'pointer', }); const ViewCount = styled('span', { mt: '$16', diff --git a/src/components/modal/ImageCarouselModal.tsx b/src/components/modal/ImageCarouselModal.tsx new file mode 100644 index 00000000..7ace15d6 --- /dev/null +++ b/src/components/modal/ImageCarouselModal.tsx @@ -0,0 +1,124 @@ +import { Box } from '@components/box/Box'; +import { styled } from 'stitches.config'; +import ModalContainer from './ModalContainer'; +import useEmblaCarousel from 'embla-carousel-react'; +import ArrowLeft from 'public/assets/svg/arrow_big_left.svg'; +import ArrowRight from 'public/assets/svg/arrow_big_right.svg'; +import CloseIcon from 'public/assets/svg/x_big.svg'; +import { useEffect, useState } from 'react'; + +interface ImageCarouselModalProps { + isOpen: boolean; + close: () => void; + images: string[]; + startIndex?: number; +} + +export default function ImageCarouselModal({ isOpen, close, images, startIndex = 0 }: ImageCarouselModalProps) { + const [emblaRef, api] = useEmblaCarousel({ startIndex, duration: 0 }); + const [currentIndex, setCurrentIndex] = useState(startIndex + 1); + + useEffect(() => { + if (!api) return; + api.on('select', () => setCurrentIndex(api.selectedScrollSnap() + 1)); + + return () => { + api.destroy(); + }; + }, [api]); + + return ( + + + + {/* top 고정 요소 */} + {/* eslint-disable-next-line prettier/prettier */} + {currentIndex}/{images.length} + + + + + api?.scrollPrev()}> + + + + + {images.map(image => ( + + + + ))} + + + api?.scrollNext()}> + + + + + + ); +} +const ModalWrapper = styled(Box, { + position: 'fixed', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + flexType: 'center', + zIndex: '$2', + backgroundColor: '#181818', + width: '100%', + height: '100vh', +}); +const Container = styled('div', { + position: 'relative', + width: '100%', + height: '100%', + color: 'white', + flexType: 'verticalCenter', + justifyContent: 'space-between', +}); +const Counter = styled('div', { + position: 'absolute', + top: '28px', + left: '48px', + color: 'white', + fontStyle: 'T1', +}); +const CloseButton = styled('button', { + position: 'absolute', + top: '32px', + right: '48px', + width: '24px', + height: '24px', + border: 'none', + outline: 'none', +}); +const CarouselContainer = styled('div', { + overflow: 'hidden', +}); +const CarouselScrollContainer = styled('div', { + display: 'flex', + maxWidth: '1280px', + height: '100vh', + maxHeight: '600px', +}); +const CarouselItem = styled('div', { + flexType: 'center', + width: '100%', + height: '100%', + flex: '0 0 100%', + minWidth: 0, +}); +const Image = styled('img', { + width: '100%', + height: '100%', + objectFit: 'contain', +}); +const ArrowButton = styled('button', { + flexType: 'center', + width: '72px', + height: '72px', + flexShrink: 0, + borderRadius: '20px', + background: '$black80', +}); diff --git a/yarn.lock b/yarn.lock index 1ed9558d..e9cda528 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6969,6 +6969,24 @@ elliptic@^6.5.3: minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" +embla-carousel-react@^8.0.0-rc13: + version "8.0.0-rc13" + resolved "https://registry.yarnpkg.com/embla-carousel-react/-/embla-carousel-react-8.0.0-rc13.tgz#faf388744d8261b0dda0dfe33bf3cd4b84ac891e" + integrity sha512-Yq8TXVtTTPIF5dmLzedamS2h+gDEY56x1kp6W4g+phL7DyLV2KJKHCUvfeRlVsbG90Q0if27IIUUNKKNLfN7mQ== + dependencies: + embla-carousel "8.0.0-rc13" + embla-carousel-reactive-utils "8.0.0-rc13" + +embla-carousel-reactive-utils@8.0.0-rc13: + version "8.0.0-rc13" + resolved "https://registry.yarnpkg.com/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.0.0-rc13.tgz#383271c9cd20b20fb71328ddd3aa546978a7a542" + integrity sha512-sH8JRUYTjSAeEJw01YWPLjBOHOCnl9GuqLJKWqMxkYJAluUbB/koshjeewa78und6pZkWUsHUYv/dm0YJ4ZdHg== + +embla-carousel@8.0.0-rc13: + version "8.0.0-rc13" + resolved "https://registry.yarnpkg.com/embla-carousel/-/embla-carousel-8.0.0-rc13.tgz#2b3e7100f86492f3eae1409ee8bbb343de448931" + integrity sha512-77U2nJRl5YtKCXiVuUqD4U8FQSqDjwVdNPO3KoOgi5WfVHnKgMn0bVOLOd3ehCPGyB/r1Mqd1Gbcwhip7bedlw== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz"