feat: 라이트박스 UI 개선 - min-width/height 적용, body 스크롤 숨김, 이미지 크기 증가, 멤버 태그 개별 표시

This commit is contained in:
caadiq 2026-01-02 11:07:51 +09:00
parent ab92e3117e
commit 79fb58e2ee
2 changed files with 205 additions and 146 deletions

View file

@ -37,6 +37,21 @@ function AlbumDetail() {
setLightbox(prev => ({ ...prev, open: false })); 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 downloadImage = useCallback(async () => {
const imageUrl = lightbox.images[lightbox.index]; const imageUrl = lightbox.images[lightbox.index];
@ -373,90 +388,96 @@ function AlbumDetail() {
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
transition={{ duration: 0.2 }} 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"
> >
{/* 상단 버튼들 */} {/* 내부 컨테이너 - min-width, min-height 적용 */}
<div className="absolute top-6 right-6 flex gap-3 z-10"> <div className="min-w-[1200px] min-h-[800px] w-full h-full relative flex items-center justify-center">
{/* 다운로드 버튼 */} {/* 상단 버튼들 */}
<button <div className="absolute top-6 right-6 flex gap-3 z-10">
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 <button
key={i} className="text-white/70 hover:text-white transition-colors"
className={`w-2 h-2 rounded-full transition-colors ${i === lightbox.index ? 'bg-white' : 'bg-white/40'}`}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); 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> </div>
)}
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>

View file

@ -76,6 +76,21 @@ function AlbumGallery() {
setLightbox(prev => ({ ...prev, open: false })); 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(() => { const goToPrev = useCallback(() => {
if (photos.length <= 1) return; if (photos.length <= 1) return;
@ -225,80 +240,103 @@ function AlbumGallery() {
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
transition={{ duration: 0.2 }} 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"
> >
{/* 상단 버튼들 */} {/* 내부 컨테이너 - min-width, min-height 적용 (화면 줄여도 크기 유지, 스크롤) */}
<div className="absolute top-6 right-6 flex gap-3 z-10"> <div className="min-w-[1200px] min-h-[800px] w-full h-full relative flex items-center justify-center">
<button {/* 상단 버튼들 */}
className="text-white/70 hover:text-white transition-colors" <div className="absolute top-6 right-6 flex gap-3 z-10">
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>
</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' }}
/>
{/* 다음 버튼 */}
{photos.length > 1 && (
<button
className="absolute right-6 text-white/70 hover:text-white transition-colors z-10"
onClick={goToNext}
>
<ChevronRight size={48} />
</button>
)}
{/* 하단 점 인디케이터 */}
<div className="absolute bottom-6 flex gap-1.5 flex-wrap justify-center max-w-[80vw]">
{photos.map((_, i) => (
<button <button
key={i} className="text-white/70 hover:text-white transition-colors"
className={`w-2 h-2 rounded-full transition-colors ${i === lightbox.index ? 'bg-white' : 'bg-white/40'}`} onClick={downloadImage}
onClick={() => { >
setImageLoaded(false); <Download size={28} />
setLightbox({ ...lightbox, index: i }); </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>
{/* 이전 버튼 - 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>
)}
{/* 로딩 스피너 */}
{!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> </div>
</motion.div> </motion.div>
)} )}