1+ import { ChevronLeftIcon , ChevronRightIcon } from '@shared/assets/icons' ;
12import { ROUTES } from '@shared/constants' ;
23import { Profile } from '@shared/ui/Profile' ;
4+ import { cn } from '@shared/utils/cn' ;
5+ import { useRef , useState } from 'react' ;
36import { Link } from 'react-router' ;
47
58type ProductImageSectionProps = {
6- image : string ;
9+ postId : string ;
10+ images : string [ ] ;
711 title : string ;
812 seller : {
9- id ?: string ;
1013 nickname : string ;
1114 profileImage ?: string ;
1215 } ;
1316} ;
1417
15- export const ProductImageSection = ( { image, title, seller } : ProductImageSectionProps ) => {
18+ export const ProductImageSection = ( { postId, images, title, seller } : ProductImageSectionProps ) => {
19+ const [ currentIndex , setCurrentIndex ] = useState ( 0 ) ;
20+ const touchStartX = useRef ( 0 ) ;
21+ const hasMultipleImages = images . length > 1 ;
22+
23+ const goToPrevious = ( ) => {
24+ if ( currentIndex > 0 ) {
25+ setCurrentIndex ( ( prev ) => prev - 1 ) ;
26+ }
27+ } ;
28+
29+ const goToNext = ( ) => {
30+ if ( currentIndex < images . length - 1 ) {
31+ setCurrentIndex ( ( prev ) => prev + 1 ) ;
32+ }
33+ } ;
34+
35+ const handleTouchStart = ( e : React . TouchEvent ) => {
36+ touchStartX . current = e . touches [ 0 ] . clientX ;
37+ } ;
38+
39+ const handleTouchEnd = ( e : React . TouchEvent ) => {
40+ const touchEndX = e . changedTouches [ 0 ] . clientX ;
41+ const diff = touchStartX . current - touchEndX ;
42+
43+ if ( Math . abs ( diff ) > 50 ) {
44+ if ( diff > 0 ) {
45+ goToNext ( ) ;
46+ } else {
47+ goToPrevious ( ) ;
48+ }
49+ }
50+ } ;
51+
1652 const sellerContent = (
1753 < >
1854 < Profile size = "chat" image = { seller . profileImage } alt = { `${ seller . nickname } 프로필` } className = "shrink-0" />
@@ -22,16 +58,72 @@ export const ProductImageSection = ({ image, title, seller }: ProductImageSectio
2258
2359 return (
2460 < div className = "flex w-full shrink-0 flex-col items-start gap-[30px] lg:w-[590px]" >
25- < div className = "aspect-square w-full overflow-hidden rounded-(--radius-s) bg-gray-50 lg:h-[568px] lg:w-[590px]" >
26- < img src = { image } alt = { title } className = "h-full w-full object-cover" />
61+ < div
62+ className = "group relative aspect-square w-full overflow-hidden rounded-(--radius-s) bg-gray-50 lg:h-[568px] lg:w-[590px]"
63+ onTouchStart = { handleTouchStart }
64+ onTouchEnd = { handleTouchEnd }
65+ >
66+ < div
67+ className = "flex h-full transition-transform duration-300 ease-out"
68+ style = { { transform : `translateX(-${ currentIndex * 100 } %)` } }
69+ >
70+ { images . map ( ( image , index ) => (
71+ < img
72+ key = { index }
73+ src = { image }
74+ alt = { `${ title } ${ index + 1 } ` }
75+ className = "h-full w-full shrink-0 object-cover"
76+ />
77+ ) ) }
78+ </ div >
79+
80+ { hasMultipleImages && (
81+ < >
82+ { currentIndex > 0 && (
83+ < button
84+ type = "button"
85+ onClick = { goToPrevious }
86+ className = "absolute top-1/2 left-4 flex h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full bg-white/80 text-gray-700 opacity-0 shadow-md transition-opacity group-hover:opacity-100"
87+ aria-label = "이전 이미지"
88+ >
89+ < ChevronLeftIcon className = "h-6 w-6" />
90+ </ button >
91+ ) }
92+ { currentIndex < images . length - 1 && (
93+ < button
94+ type = "button"
95+ onClick = { goToNext }
96+ className = "absolute top-1/2 right-4 flex h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full bg-white/80 text-gray-700 opacity-0 shadow-md transition-opacity group-hover:opacity-100"
97+ aria-label = "다음 이미지"
98+ >
99+ < ChevronRightIcon className = "h-6 w-6" />
100+ </ button >
101+ ) }
102+
103+ < div className = "absolute bottom-4 left-1/2 flex -translate-x-1/2 gap-2" >
104+ { images . map ( ( _ , index ) => (
105+ < button
106+ key = { index }
107+ type = "button"
108+ onClick = { ( ) => setCurrentIndex ( index ) }
109+ className = { cn (
110+ 'h-2 w-2 rounded-full transition-colors' ,
111+ index === currentIndex ? 'bg-green-500' : 'bg-white/60'
112+ ) }
113+ aria-label = { `${ index + 1 } 번째 이미지로 이동` }
114+ />
115+ ) ) }
116+ </ div >
117+ </ >
118+ ) }
27119 </ div >
28- { seller . id ? (
29- < Link to = { `${ ROUTES . SELLER_PROFILE } /${ seller . id } ` } className = "flex items-center gap-[23px]" >
30- { sellerContent }
31- </ Link >
32- ) : (
33- < div className = "flex items-center gap-[23px]" > { sellerContent } </ div >
34- ) }
120+ < Link
121+ to = { `${ ROUTES . SELLER_PROFILE } /${ postId } ` }
122+ state = { { nickname : seller . nickname , profileImage : seller . profileImage } }
123+ className = "flex items-center gap-[23px]"
124+ >
125+ { sellerContent }
126+ </ Link >
35127 </ div >
36128 ) ;
37129} ;
0 commit comments