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"