feat: 라이트박스 UI 개선 - min-width/height 적용, body 스크롤 숨김, 이미지 크기 증가, 멤버 태그 개별 표시
This commit is contained in:
parent
ab92e3117e
commit
79fb58e2ee
2 changed files with 205 additions and 146 deletions
|
|
@ -37,6 +37,21 @@ function AlbumDetail() {
|
|||
setLightbox(prev => ({ ...prev, open: false }));
|
||||
}, []);
|
||||
|
||||
// 라이트박스 열릴 때 body 스크롤 숨기기
|
||||
useEffect(() => {
|
||||
if (lightbox.open) {
|
||||
document.documentElement.style.overflow = 'hidden';
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.documentElement.style.overflow = '';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
return () => {
|
||||
document.documentElement.style.overflow = '';
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}, [lightbox.open]);
|
||||
|
||||
// 이미지 다운로드 함수
|
||||
const downloadImage = useCallback(async () => {
|
||||
const imageUrl = lightbox.images[lightbox.index];
|
||||
|
|
@ -373,90 +388,96 @@ function AlbumDetail() {
|
|||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed inset-0 bg-black/90 z-50 flex items-center justify-center"
|
||||
className="fixed inset-0 bg-black/95 z-50 overflow-auto"
|
||||
>
|
||||
{/* 상단 버튼들 */}
|
||||
<div className="absolute top-6 right-6 flex gap-3 z-10">
|
||||
{/* 다운로드 버튼 */}
|
||||
<button
|
||||
className="text-white/70 hover:text-white transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
downloadImage();
|
||||
}}
|
||||
>
|
||||
<Download size={28} />
|
||||
</button>
|
||||
{/* 닫기 버튼 */}
|
||||
<button
|
||||
className="text-white/70 hover:text-white transition-colors"
|
||||
onClick={closeLightbox}
|
||||
>
|
||||
<X size={32} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 이전 버튼 */}
|
||||
{lightbox.images.length > 1 && (
|
||||
<button
|
||||
className="absolute left-6 text-white/70 hover:text-white transition-colors z-10"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
goToPrev();
|
||||
}}
|
||||
>
|
||||
<ChevronLeft size={48} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 로딩 스피너 */}
|
||||
{!imageLoaded && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-4 border-white border-t-transparent"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 이미지 */}
|
||||
<motion.img
|
||||
key={lightbox.index}
|
||||
src={lightbox.images[lightbox.index]}
|
||||
alt="확대 이미지"
|
||||
className={`max-w-[90vw] max-h-[90vh] object-contain rounded-lg transition-opacity duration-200 ${imageLoaded ? 'opacity-100' : 'opacity-0'}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onLoad={() => setImageLoaded(true)}
|
||||
initial={{ x: slideDirection * 100 }}
|
||||
animate={{ x: 0 }}
|
||||
transition={{ duration: 0.25, ease: 'easeOut' }}
|
||||
/>
|
||||
|
||||
{/* 다음 버튼 */}
|
||||
{lightbox.images.length > 1 && (
|
||||
<button
|
||||
className="absolute right-6 text-white/70 hover:text-white transition-colors z-10"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
goToNext();
|
||||
}}
|
||||
>
|
||||
<ChevronRight size={48} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 인디케이터 - 이미지 2개 이상일 때만 표시 */}
|
||||
{lightbox.images.length > 1 && (
|
||||
<div className="absolute bottom-6 flex gap-2">
|
||||
{lightbox.images.map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
className={`w-2 h-2 rounded-full transition-colors ${i === lightbox.index ? 'bg-white' : 'bg-white/40'}`}
|
||||
{/* 내부 컨테이너 - min-width, min-height 적용 */}
|
||||
<div className="min-w-[1200px] min-h-[800px] w-full h-full relative flex items-center justify-center">
|
||||
{/* 상단 버튼들 */}
|
||||
<div className="absolute top-6 right-6 flex gap-3 z-10">
|
||||
{/* 다운로드 버튼 */}
|
||||
<button
|
||||
className="text-white/70 hover:text-white transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setLightbox({ ...lightbox, index: i });
|
||||
downloadImage();
|
||||
}}
|
||||
>
|
||||
<Download size={28} />
|
||||
</button>
|
||||
{/* 닫기 버튼 */}
|
||||
<button
|
||||
className="text-white/70 hover:text-white transition-colors"
|
||||
onClick={closeLightbox}
|
||||
>
|
||||
<X size={32} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 이전 버튼 */}
|
||||
{lightbox.images.length > 1 && (
|
||||
<button
|
||||
className="absolute left-6 p-2 text-white/70 hover:text-white transition-colors z-10"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
goToPrev();
|
||||
}}
|
||||
>
|
||||
<ChevronLeft size={48} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 로딩 스피너 */}
|
||||
{!imageLoaded && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-4 border-white border-t-transparent"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 이미지 */}
|
||||
<div className="flex flex-col items-center mx-24">
|
||||
<motion.img
|
||||
key={lightbox.index}
|
||||
src={lightbox.images[lightbox.index]}
|
||||
alt="확대 이미지"
|
||||
className={`max-w-[1100px] max-h-[75vh] object-contain rounded-lg transition-opacity duration-200 ${imageLoaded ? 'opacity-100' : 'opacity-0'}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onLoad={() => setImageLoaded(true)}
|
||||
initial={{ x: slideDirection * 100 }}
|
||||
animate={{ x: 0 }}
|
||||
transition={{ duration: 0.25, ease: 'easeOut' }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 다음 버튼 */}
|
||||
{lightbox.images.length > 1 && (
|
||||
<button
|
||||
className="absolute right-6 p-2 text-white/70 hover:text-white transition-colors z-10"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
goToNext();
|
||||
}}
|
||||
>
|
||||
<ChevronRight size={48} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 인디케이터 */}
|
||||
{lightbox.images.length > 1 && (
|
||||
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 flex gap-1.5 overflow-x-auto scrollbar-hide" style={{ maxWidth: '1000px' }}>
|
||||
{lightbox.images.map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
className={`w-2 h-2 rounded-full transition-colors flex-shrink-0 ${i === lightbox.index ? 'bg-white' : 'bg-white/40'}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setImageLoaded(false);
|
||||
setLightbox({ ...lightbox, index: i });
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
|
|
|||
|
|
@ -76,6 +76,21 @@ function AlbumGallery() {
|
|||
setLightbox(prev => ({ ...prev, open: false }));
|
||||
}, []);
|
||||
|
||||
// 라이트박스 열릴 때 body 스크롤 숨기기
|
||||
useEffect(() => {
|
||||
if (lightbox.open) {
|
||||
document.documentElement.style.overflow = 'hidden';
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.documentElement.style.overflow = '';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
return () => {
|
||||
document.documentElement.style.overflow = '';
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}, [lightbox.open]);
|
||||
|
||||
// 이전/다음 이미지
|
||||
const goToPrev = useCallback(() => {
|
||||
if (photos.length <= 1) return;
|
||||
|
|
@ -225,80 +240,103 @@ function AlbumGallery() {
|
|||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed inset-0 bg-black/95 z-50 flex items-center justify-center"
|
||||
className="fixed inset-0 bg-black/95 z-50 overflow-auto"
|
||||
>
|
||||
{/* 상단 버튼들 */}
|
||||
<div className="absolute top-6 right-6 flex gap-3 z-10">
|
||||
<button
|
||||
className="text-white/70 hover:text-white transition-colors"
|
||||
onClick={downloadImage}
|
||||
>
|
||||
<Download size={28} />
|
||||
</button>
|
||||
<button
|
||||
className="text-white/70 hover:text-white transition-colors"
|
||||
onClick={closeLightbox}
|
||||
>
|
||||
<X size={32} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 카운터 */}
|
||||
<div className="absolute top-6 left-6 text-white/70 text-sm z-10">
|
||||
{lightbox.index + 1} / {photos.length}
|
||||
</div>
|
||||
|
||||
{/* 이전 버튼 */}
|
||||
{photos.length > 1 && (
|
||||
<button
|
||||
className="absolute left-6 text-white/70 hover:text-white transition-colors z-10"
|
||||
onClick={goToPrev}
|
||||
>
|
||||
<ChevronLeft size={48} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 로딩 스피너 */}
|
||||
{!imageLoaded && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-4 border-white border-t-transparent"></div>
|
||||
{/* 내부 컨테이너 - min-width, min-height 적용 (화면 줄여도 크기 유지, 스크롤) */}
|
||||
<div className="min-w-[1200px] min-h-[800px] w-full h-full relative flex items-center justify-center">
|
||||
{/* 상단 버튼들 */}
|
||||
<div className="absolute top-6 right-6 flex gap-3 z-10">
|
||||
<button
|
||||
className="text-white/70 hover:text-white transition-colors"
|
||||
onClick={downloadImage}
|
||||
>
|
||||
<Download size={28} />
|
||||
</button>
|
||||
<button
|
||||
className="text-white/70 hover:text-white transition-colors"
|
||||
onClick={closeLightbox}
|
||||
>
|
||||
<X size={32} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 이미지 */}
|
||||
<motion.img
|
||||
key={lightbox.index}
|
||||
src={photos[lightbox.index]?.originalUrl}
|
||||
alt="확대 이미지"
|
||||
className={`max-w-[90vw] max-h-[85vh] object-contain rounded-lg transition-opacity duration-200 ${imageLoaded ? 'opacity-100' : 'opacity-0'}`}
|
||||
onLoad={() => setImageLoaded(true)}
|
||||
initial={{ x: slideDirection * 100 }}
|
||||
animate={{ x: 0 }}
|
||||
transition={{ duration: 0.25, ease: 'easeOut' }}
|
||||
/>
|
||||
{/* 카운터 */}
|
||||
<div className="absolute top-6 left-6 text-white/70 text-sm z-10">
|
||||
{lightbox.index + 1} / {photos.length}
|
||||
</div>
|
||||
|
||||
{/* 다음 버튼 */}
|
||||
{photos.length > 1 && (
|
||||
<button
|
||||
className="absolute right-6 text-white/70 hover:text-white transition-colors z-10"
|
||||
onClick={goToNext}
|
||||
>
|
||||
<ChevronRight size={48} />
|
||||
</button>
|
||||
)}
|
||||
{/* 이전 버튼 - margin으로 이미지와 간격 */}
|
||||
{photos.length > 1 && (
|
||||
<button
|
||||
className="absolute left-6 p-2 text-white/70 hover:text-white transition-colors z-10"
|
||||
onClick={goToPrev}
|
||||
>
|
||||
<ChevronLeft size={48} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 하단 점 인디케이터 */}
|
||||
<div className="absolute bottom-6 flex gap-1.5 flex-wrap justify-center max-w-[80vw]">
|
||||
{photos.map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
className={`w-2 h-2 rounded-full transition-colors ${i === lightbox.index ? 'bg-white' : 'bg-white/40'}`}
|
||||
onClick={() => {
|
||||
setImageLoaded(false);
|
||||
setLightbox({ ...lightbox, index: i });
|
||||
}}
|
||||
{/* 로딩 스피너 */}
|
||||
{!imageLoaded && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-4 border-white border-t-transparent"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 이미지 + 컨셉 정보 - 양옆 margin으로 화살표와 간격 */}
|
||||
<div className="flex flex-col items-center mx-24">
|
||||
<motion.img
|
||||
key={lightbox.index}
|
||||
src={photos[lightbox.index]?.originalUrl}
|
||||
alt="확대 이미지"
|
||||
className={`max-w-[1100px] max-h-[75vh] object-contain rounded-lg transition-opacity duration-200 ${imageLoaded ? 'opacity-100' : 'opacity-0'}`}
|
||||
onLoad={() => setImageLoaded(true)}
|
||||
initial={{ x: slideDirection * 100 }}
|
||||
animate={{ x: 0 }}
|
||||
transition={{ duration: 0.25, ease: 'easeOut' }}
|
||||
/>
|
||||
))}
|
||||
{/* 컨셉 정보 - 정보가 있을 때만 표시 */}
|
||||
{imageLoaded && photos[lightbox.index]?.title && (
|
||||
<div className="mt-6 flex flex-col items-center gap-2">
|
||||
<span className="px-4 py-2 bg-white/10 backdrop-blur-sm rounded-full text-white font-medium text-base">
|
||||
{photos[lightbox.index]?.title}
|
||||
</span>
|
||||
{/* 멤버가 있고 빈 문자열이 아닐 때만 표시, 쉼표로 분리해서 개별 태그 */}
|
||||
{photos[lightbox.index]?.members && String(photos[lightbox.index]?.members).trim() && (
|
||||
<div className="flex items-center gap-2">
|
||||
{String(photos[lightbox.index]?.members).split(',').map((member, idx) => (
|
||||
<span key={idx} className="px-3 py-1.5 bg-primary/80 rounded-full text-white text-sm">
|
||||
{member.trim()}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 다음 버튼 - margin으로 이미지와 간격 */}
|
||||
{photos.length > 1 && (
|
||||
<button
|
||||
className="absolute right-6 p-2 text-white/70 hover:text-white transition-colors z-10"
|
||||
onClick={goToNext}
|
||||
>
|
||||
<ChevronRight size={48} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 하단 점 인디케이터 - 한 줄 고정, 스크롤바 숨김 */}
|
||||
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 flex gap-1.5 overflow-x-auto scrollbar-hide" style={{ maxWidth: '1000px' }}>
|
||||
{photos.map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
className={`w-2 h-2 rounded-full transition-colors flex-shrink-0 ${i === lightbox.index ? 'bg-white' : 'bg-white/40'}`}
|
||||
onClick={() => {
|
||||
setImageLoaded(false);
|
||||
setLightbox({ ...lightbox, index: i });
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue