diff --git a/package.json b/package.json index 6d2efb0b..4fa821ae 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-helmet-async": "^2.0.4", + "react-responsive-carousel": "^3.2.23", "react-router-dom": "^6.15.0", "react-use": "^17.5.0", "zustand": "^4.4.1" diff --git a/src/pages/FeedDetail/FeedDetail.page.tsx b/src/pages/FeedDetail/FeedDetail.page.tsx index 0d84af5e..1001da9d 100644 --- a/src/pages/FeedDetail/FeedDetail.page.tsx +++ b/src/pages/FeedDetail/FeedDetail.page.tsx @@ -3,6 +3,7 @@ import { Badge, Divider, Header, Spacer, Text, TextDivider, Flex, Box } from 'co import { useNavigate, useParams } from 'react-router-dom'; import Comments from './components/Comments'; +import IdeaImageList from './components/IdeaImageList'; import ModifyDropdown from './components/ModifyDropdown'; import ProfileInfo from './components/ProfileInfo'; import ReactionBar from './components/ReactionBar'; @@ -39,6 +40,7 @@ const FeedDetailPage = () => { owner, ownerScrap, ownerLike, + imageResponses, } = useFeedDetailQuery(feedId); const openConfirm = useConfirm(); const { deleteIdea } = useDeleteIdea(); @@ -95,6 +97,15 @@ const FeedDetailPage = () => { + {imageResponses.length > 0 && ( + <> + + + + + + )} + diff --git a/src/pages/FeedDetail/assets/leftArrow.svg b/src/pages/FeedDetail/assets/leftArrow.svg new file mode 100644 index 00000000..dfedea2e --- /dev/null +++ b/src/pages/FeedDetail/assets/leftArrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/pages/FeedDetail/assets/rightArrow.svg b/src/pages/FeedDetail/assets/rightArrow.svg new file mode 100644 index 00000000..7826ab67 --- /dev/null +++ b/src/pages/FeedDetail/assets/rightArrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/pages/FeedDetail/components/IdeaImageList.tsx b/src/pages/FeedDetail/components/IdeaImageList.tsx new file mode 100644 index 00000000..1d1181bd --- /dev/null +++ b/src/pages/FeedDetail/components/IdeaImageList.tsx @@ -0,0 +1,64 @@ +import styled from '@emotion/styled'; +import { Box } from 'concept-be-design-system'; +import { useState } from 'react'; + +import ImageCarousel from './ImageCarousel'; +import { ImageResponse } from '../types'; + +interface Props { + imageResponses: ImageResponse[]; +} + +const IdeaImageList = ({ imageResponses }: Props) => { + const [selectedIndex, setSelectedIndex] = useState(null); + + const handleImageClick = (index: number) => { + setSelectedIndex(index); + }; + + const handleClose = () => { + setSelectedIndex(null); + }; + + return ( + + + {imageResponses.map(({ id, imageUrl }, index) => ( + handleImageClick(index)}> + {`Thumbnail + + ))} + + {selectedIndex !== null && ( + response.imageUrl)} + initialIndex={selectedIndex} + onClose={handleClose} + /> + )} + + ); +}; + +export default IdeaImageList; + +const Wrapper = styled.div` + display: flex; + overflow-x: auto; + flex-wrap: nowrap; + gap: 8px; + + &::-webkit-scrollbar { + display: none; + } +`; + +const Item = styled.div` + flex: 0 0 auto; +`; + +const Image = styled.img` + object-fit: cover; + width: 120px; + height: 120px; +`; diff --git a/src/pages/FeedDetail/components/ImageCarousel.tsx b/src/pages/FeedDetail/components/ImageCarousel.tsx new file mode 100644 index 00000000..8ecc93d1 --- /dev/null +++ b/src/pages/FeedDetail/components/ImageCarousel.tsx @@ -0,0 +1,115 @@ +import styled from '@emotion/styled'; +import { Flex, Header, SVGHeaderClose24, Spacer, Text } from 'concept-be-design-system'; +import { CSSProperties, useState } from 'react'; +import { Carousel } from 'react-responsive-carousel'; +import 'react-responsive-carousel/lib/styles/carousel.min.css'; +import { useLockBodyScroll } from 'react-use'; + +import { ReactComponent as SVGLeftArrow } from '../assets/leftArrow.svg'; +import { ReactComponent as SVGRightArrow } from '../assets/rightArrow.svg'; + +interface Props { + imageUrls: string[]; + initialIndex: number; + onClose: () => void; +} +const arrowStyles: CSSProperties = { + position: 'absolute', + zIndex: 2, + top: 'calc(50% - 20px)', + width: 40, + height: 48, + cursor: 'pointer', + background: 'rgba(0, 0, 0, 0.15)', +}; + +const ImageCarousel = ({ imageUrls, initialIndex, onClose }: Props) => { + const [currentIndex, setCurrentIndex] = useState(initialIndex); + + const handleChange = (index: number) => { + setCurrentIndex(index); + }; + + useLockBodyScroll(); + + return ( + +
+ + + + + + + 이미지 상세보기 + + ( + + {currentIndex + 1} + + + /{imageUrls.length} + + ) + + + +
+ e.stopPropagation()}> + + hasPrev && ( + + ) + } + renderArrowNext={(onClickHandler, hasNext, label) => + hasNext && ( + + ) + } + > + {imageUrls.map((imageUrl, index) => ( +
+ {`${index}번째 +
+ ))} +
+
+
+ ); +}; + +export default ImageCarousel; + +const ModalOverlay = styled.div` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + z-index: 10; + background: rgba(0, 0, 0, 0.6); +`; + +const ModalContent = styled.div` + display: flex; + position: relative; + align-items: center; + max-width: 420px; + height: 100%; + background: rgba(0, 0, 0, 1); +`; diff --git a/src/pages/FeedDetail/hooks/queries/useFeedDetailQuery.ts b/src/pages/FeedDetail/hooks/queries/useFeedDetailQuery.ts index af9a0a6e..75421a33 100644 --- a/src/pages/FeedDetail/hooks/queries/useFeedDetailQuery.ts +++ b/src/pages/FeedDetail/hooks/queries/useFeedDetailQuery.ts @@ -6,7 +6,24 @@ const useFeedDetailQuery = (id: string) => { const { data: feedDetail } = useSuspenseQuery({ queryKey: ['feed', 'detail', id], queryFn: () => getFeedDetail(id), - select: (data) => ({ ...data }), + select: (data) => ({ + ...data, + imageResponses: [ + { + id: 1, + imageUrl: 'https://www.contestkorea.com/admincenter/files/meet/202207070917022079123.jpg', + }, + { + id: 2, + imageUrl: 'https://news.nateimg.co.kr/orgImg/sh/2022/11/18/6812837_996935_3331.jpg', + }, + { + id: 3, + imageUrl: + 'https://www.syu.ac.kr/wp-content/uploads/2020/07/%EA%B3%B5%EB%AA%A8%EC%A0%84-%ED%8F%AC%EC%8A%A4%ED%84%B0-scaled.jpg', + }, + ], + }), }); return feedDetail; diff --git a/src/pages/FeedDetail/types/index.ts b/src/pages/FeedDetail/types/index.ts index de8aa3fb..d7c82269 100644 --- a/src/pages/FeedDetail/types/index.ts +++ b/src/pages/FeedDetail/types/index.ts @@ -1,3 +1,9 @@ +export interface ImageResponse { + id: number; + ideaId: number; + imageUrl: string; +} + export interface FeedDetailResponse { memberId: number; imageUrl: string; @@ -18,6 +24,7 @@ export interface FeedDetailResponse { owner: boolean; ownerScrap: boolean; ownerLike: boolean; + imageResponses: ImageResponse[]; } export interface CommentParentResponse { diff --git a/yarn.lock b/yarn.lock index 0dc10111..e5b8f668 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1175,6 +1175,11 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +classnames@^2.2.5: + version "2.5.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" @@ -2248,7 +2253,7 @@ lodash@^4.17.21: resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -loose-envify@^1.0.0, loose-envify@^1.1.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -2362,6 +2367,11 @@ node-releases@^2.0.13: resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz" integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + object-inspect@^1.12.3, object-inspect@^1.9.0: version "1.12.3" resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz" @@ -2514,6 +2524,15 @@ prettier@^3.0.3: resolved "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz" integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg== +prop-types@^15.5.8: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" @@ -2537,6 +2556,13 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.0" +react-easy-swipe@^0.0.21: + version "0.0.21" + resolved "https://registry.yarnpkg.com/react-easy-swipe/-/react-easy-swipe-0.0.21.tgz#ce9384d576f7a8529dc2ca377c1bf03920bac8eb" + integrity sha512-OeR2jAxdoqUMHIn/nS9fgreI5hSpgGoL5ezdal4+oO7YSSgJR8ga+PkYGJrSrJ9MKlPcQjMQXnketrD7WNmNsg== + dependencies: + prop-types "^15.5.8" + react-fast-compare@^3.2.2: version "3.2.2" resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz" @@ -2551,7 +2577,7 @@ react-helmet-async@^2.0.4: react-fast-compare "^3.2.2" shallowequal "^1.1.0" -react-is@^16.7.0: +react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -2561,6 +2587,15 @@ react-refresh@^0.14.0: resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz" integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ== +react-responsive-carousel@^3.2.23: + version "3.2.23" + resolved "https://registry.yarnpkg.com/react-responsive-carousel/-/react-responsive-carousel-3.2.23.tgz#4c0016ff54603e604bb5c1f9e7ef2d1eda133f1d" + integrity sha512-pqJLsBaKHWJhw/ItODgbVoziR2z4lpcJg+YwmRlSk4rKH32VE633mAtZZ9kDXjy4wFO+pgUZmDKPsPe1fPmHCg== + dependencies: + classnames "^2.2.5" + prop-types "^15.5.8" + react-easy-swipe "^0.0.21" + react-router-dom@^6.15.0: version "6.15.0" resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.15.0.tgz"