Skip to content

Commit

Permalink
게시글 조회 이미지 기능 (#167)
Browse files Browse the repository at this point in the history
* feat: 게시글 이미지 렌더링 컴포넌트 추가

GET /ideas/{id} api에 맞게 수정

* feat: 게시글 이미지 클릭 시 캐러셀 보여주는 기능 추가

- react-responsive-carousel 이용
- useFeedDetailQuery는 임시

* feat: 아이폰 SE 환경에서 이미지 리스트 좌우 스크롤바 제거

* style: 이미지 리스트 원래 비율 유지하도록 수정

---------

Co-authored-by: semnil5202 <[email protected]>
  • Loading branch information
yogjin and semnil5202 authored Jun 22, 2024
1 parent 4e5d997 commit f93c708
Show file tree
Hide file tree
Showing 9 changed files with 259 additions and 3 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
11 changes: 11 additions & 0 deletions src/pages/FeedDetail/FeedDetail.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -39,6 +40,7 @@ const FeedDetailPage = () => {
owner,
ownerScrap,
ownerLike,
imageResponses,
} = useFeedDetailQuery(feedId);
const openConfirm = useConfirm();
const { deleteIdea } = useDeleteIdea();
Expand Down Expand Up @@ -95,6 +97,15 @@ const FeedDetailPage = () => {
</HyperLinkText>
</Box>

{imageResponses.length > 0 && (
<>
<Divider color="l3" />
<Spacer size={22} />
<IdeaImageList imageResponses={imageResponses} />
<Spacer size={22} />
</>
)}

<Divider color="bg1" height={8} />

<Box padding="30px 22px 0 22px">
Expand Down
3 changes: 3 additions & 0 deletions src/pages/FeedDetail/assets/leftArrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/pages/FeedDetail/assets/rightArrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
64 changes: 64 additions & 0 deletions src/pages/FeedDetail/components/IdeaImageList.tsx
Original file line number Diff line number Diff line change
@@ -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<number | null>(null);

const handleImageClick = (index: number) => {
setSelectedIndex(index);
};

const handleClose = () => {
setSelectedIndex(null);
};

return (
<Box margin="0 0 0 22px">
<Wrapper>
{imageResponses.map(({ id, imageUrl }, index) => (
<Item key={id} onClick={() => handleImageClick(index)}>
<Image src={imageUrl} alt={`Thumbnail ${index}`} />
</Item>
))}
</Wrapper>
{selectedIndex !== null && (
<ImageCarousel
imageUrls={imageResponses.map((response) => response.imageUrl)}
initialIndex={selectedIndex}
onClose={handleClose}
/>
)}
</Box>
);
};

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;
`;
115 changes: 115 additions & 0 deletions src/pages/FeedDetail/components/ImageCarousel.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<ModalOverlay onClick={onClose}>
<Header>
<Header.Item>
<button onClick={onClose}>
<SVGHeaderClose24 />
</button>
</Header.Item>
<Header.Item>
<Flex>
<Text font="suit16sb" color="b4">
이미지 상세보기
</Text>
(
<Text font="suit16sb" color="c1">
{currentIndex + 1}
</Text>
<Text font="suit16sb" color="b4">
/{imageUrls.length}
</Text>
)
</Flex>
</Header.Item>
<Spacer size={24} />
</Header>
<ModalContent onClick={(e) => e.stopPropagation()}>
<Carousel
selectedItem={initialIndex}
showIndicators={false}
showThumbs={false}
showStatus={false}
onChange={handleChange}
renderArrowPrev={(onClickHandler, hasPrev, label) =>
hasPrev && (
<button type="button" onClick={onClickHandler} title={label} style={{ ...arrowStyles, left: 0 }}>
<SVGLeftArrow />
</button>
)
}
renderArrowNext={(onClickHandler, hasNext, label) =>
hasNext && (
<button type="button" onClick={onClickHandler} title={label} style={{ ...arrowStyles, right: 0 }}>
<SVGRightArrow />
</button>
)
}
>
{imageUrls.map((imageUrl, index) => (
<div key={index}>
<img src={imageUrl} alt={`${index}번째 이미지`} />
</div>
))}
</Carousel>
</ModalContent>
</ModalOverlay>
);
};

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);
`;
19 changes: 18 additions & 1 deletion src/pages/FeedDetail/hooks/queries/useFeedDetailQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions src/pages/FeedDetail/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export interface ImageResponse {
id: number;
ideaId: number;
imageUrl: string;
}

export interface FeedDetailResponse {
memberId: number;
imageUrl: string;
Expand All @@ -18,6 +24,7 @@ export interface FeedDetailResponse {
owner: boolean;
ownerScrap: boolean;
ownerLike: boolean;
imageResponses: ImageResponse[];
}

export interface CommentParentResponse {
Expand Down
39 changes: 37 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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==
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -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==
Expand All @@ -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"
Expand Down

0 comments on commit f93c708

Please sign in to comment.