Skip to content

Commit

Permalink
이미지 상세 모달을 제작한다. (SWYP-team-2th#122)
Browse files Browse the repository at this point in the history
* 이미지 상세 모달을 제작한다.

* 이미지 모달 연결

* 버튼 변경 및 로딩 추가
  • Loading branch information
YOOJS1205 committed Feb 28, 2025
1 parent bd9095c commit 50b7996
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 24 deletions.
15 changes: 9 additions & 6 deletions src/api/useGetVoteDetail.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { useSuspenseQuery } from '@tanstack/react-query';
import { request } from './config';

export interface Image {
id: number;
imageName: string;
imageUrl: string;
thumbnailUrl: string;
voted: boolean;
}

interface VoteDetailType {
id: number;
author: {
Expand All @@ -9,12 +17,7 @@ interface VoteDetailType {
profileUrl: string;
};
description: string;
images: {
id: number;
imageName: string;
imageUrl: string;
voted: boolean;
}[];
images: Image[];
shareUrl: string;
createdAt: string;
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/Dialog/DialogProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const DialogProvider = ({ children }: { children: React.ReactNode }) => {
{createPortal(
currentDialog && (
<Overlay onClose={closeDialog}>
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full">
{currentDialog}
</div>
</Overlay>
Expand Down
55 changes: 55 additions & 0 deletions src/components/vote-detail/ImageDetailModal/ImageDetailModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import useImageDetailModal from './hooks';
import { Image } from '@/api/useGetVoteDetail';
import Icon from '@/components/common/Icon';

interface ImageDetailModalProps {
images: Image[];
selectedImageId: number;
}

export default function ImageDetailModal({
images,
selectedImageId,
}: ImageDetailModalProps) {
const { scrollContainerRef, currentIndex, handleScroll, closeDialog } =
useImageDetailModal({
images,
selectedImageId,
});

return (
<div className="bg-gray-700 w-full h-[100dvh] max-w-[480px] flex flex-col">
<header className="h-[57px] flex items-center justify-between px-4 text-white z-10">
<button onClick={closeDialog}>
<Icon name="ArrowLeft" size="medium" />
</button>
<div className="text-white">
<span className="font-medium">{currentIndex + 1}</span>
<span className="mx-1">/</span>
<span>{images.length}</span>
</div>
<div className="w-[24px] h-full"></div>
</header>

<div
ref={scrollContainerRef}
className="flex-1 flex overflow-x-auto snap-x snap-mandatory"
style={{ scrollBehavior: 'smooth' }}
onScroll={handleScroll}
>
{images.map((image) => (
<div
key={image.id}
className="min-w-full h-full flex-shrink-0 snap-center flex items-center justify-center"
>
<img
src={image.imageUrl}
alt={`image-${image.id}`}
className="w-full h-auto max-h-full object-cover"
/>
</div>
))}
</div>
</div>
);
}
50 changes: 50 additions & 0 deletions src/components/vote-detail/ImageDetailModal/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useEffect, useRef, useState } from 'react';
import { Image } from '@/api/useGetVoteDetail';
import { useDialog } from '@/components/common/Dialog/hooks';

interface UseImageDetailModalOptions {
images: Image[];
selectedImageId: number;
}

export default function useImageDetailModal({
images,
selectedImageId,
}: UseImageDetailModalOptions) {
const { closeDialog } = useDialog();

const [currentImageId, setCurrentImageId] = useState(
selectedImageId || images[0]?.id,
);

const scrollContainerRef = useRef<HTMLDivElement>(null);

const currentIndex = images.findIndex((img) => img.id === currentImageId);

const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
const container = e.currentTarget;
const itemWidth = container.clientWidth;

const visibleIndex = Math.round(container.scrollLeft / itemWidth);

if (images[visibleIndex] && images[visibleIndex].id !== currentImageId) {
setCurrentImageId(images[visibleIndex].id);
}
};

useEffect(() => {
if (scrollContainerRef.current && currentIndex !== -1) {
scrollContainerRef.current.scrollLeft =
currentIndex * scrollContainerRef.current.clientWidth;
}
}, [selectedImageId]);

return {
scrollContainerRef,
imageNum: images.length,
images,
currentIndex,
handleScroll,
closeDialog,
};
}
1 change: 1 addition & 0 deletions src/components/vote-detail/ImageDetailModal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './ImageDetailModal';
29 changes: 14 additions & 15 deletions src/components/vote-detail/Vote/VoteCard/VoteCardItem.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
import useVote from '@/api/useVote';
import { HTMLAttributes } from 'react';
import Icon from '@/components/common/Icon';
import { Label } from '@/components/common/Label/Label';
import { cn } from '@/utils/cn';

interface VoteCardItemProps {
interface VoteCardItemProps extends HTMLAttributes<HTMLButtonElement> {
image: {
id: number;
imageName: string;
imageUrl: string;
thumbnailUrl: string;
voted: boolean;
};
postId: number;
handleVote: (e: React.MouseEvent<HTMLButtonElement>) => void;
}

export default function VoteCardItem({ image, postId }: VoteCardItemProps) {
const { mutate: voteMutate } = useVote(postId);

const handleVote = () => {
voteMutate(image.id);
};

export default function VoteCardItem({
image,
onClick,
handleVote,
}: VoteCardItemProps) {
return (
// 추후에 사진 클릭 시 사진 확대 로직 들어가야함.
<div
<button
className={cn(
'relative w-full rounded-2xl overflow-hidden',
image.voted
? 'bg-gray-100 shadow-[0_0_0_3px_#FFFFFF,0_0_0_6px_#FFB300]' // 흰색 + 노란색 테두리 shadow로 처리할 수 있네
? 'bg-gray-100 shadow-[0_0_0_3px_#FFFFFF,0_0_0_6px_#FFB300]'
: 'bg-gray-100',
)}
onClick={onClick}
>
<div className="relative w-full aspect-[7/9] rounded-xl overflow-hidden bg-gray-100">
<img src={image.imageUrl} className="w-full h-full object-cover" />
<img src={image.thumbnailUrl} className="w-full h-full object-cover" />
</div>

<div className="absolute bottom-0 left-0 w-full bg-gray-900/40 px-3 py-2 flex justify-between items-center">
Expand All @@ -56,6 +55,6 @@ export default function VoteCardItem({ image, postId }: VoteCardItemProps) {
</Label>
</div>
)}
</div>
</button>
);
}
32 changes: 30 additions & 2 deletions src/components/vote-detail/Vote/VoteCard/VoteCardList.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,43 @@
import { useParams } from 'react-router-dom';
import VoteCardItem from './VoteCardItem';
import ImageDetailModal from '../../ImageDetailModal';
import useVote from '@/api/useVote';
import { useDialog } from '@/components/common/Dialog/hooks';
import Loading from '@/components/common/Loading';
import useVoteDetail from '@/components/vote-detail/Vote/VoteCard/hooks';

export default function VoteCardList() {
const { postId } = useParams<{ postId: string }>();
const { voteDetail } = useVoteDetail(Number(postId));
const { mutate: voteMutate, isPending } = useVote(Number(postId));
const { openDialog } = useDialog();

const handleClickVoteCardItem = (id: number) => {
openDialog(
<ImageDetailModal images={voteDetail.images} selectedImageId={id} />,
);
};

const handleVote =
(id: number) => (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
voteMutate(id);
};

return (
<div className="flex space-x-6 my-[15px] px-[12px]">
<div className="flex w-full space-x-6 my-[15px] px-[12px] relative">
{isPending && (
<div className="absolute w-full inset-0 z-10 bg-gray-100/50">
<Loading />
</div>
)}
{voteDetail.images.map((image) => (
<VoteCardItem key={image.id} image={image} postId={Number(postId)} />
<VoteCardItem
key={image.id}
image={image}
onClick={() => handleClickVoteCardItem(image.id)}
handleVote={handleVote(image.id)}
/>
))}
</div>
);
Expand Down
2 changes: 2 additions & 0 deletions src/components/vote-detail/Vote/VoteSection/VoteSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export default function VoteSection() {
<>
<Suspense fallback={<Loading />}>
<VoteCardList />
</Suspense>
<Suspense fallback={<Loading />}>
<VoteResultList />
</Suspense>
</>
Expand Down

0 comments on commit 50b7996

Please sign in to comment.