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 }));
|
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,8 +388,10 @@ 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="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">
|
<div className="absolute top-6 right-6 flex gap-3 z-10">
|
||||||
{/* 다운로드 버튼 */}
|
{/* 다운로드 버튼 */}
|
||||||
|
|
@ -399,7 +416,7 @@ function AlbumDetail() {
|
||||||
{/* 이전 버튼 */}
|
{/* 이전 버튼 */}
|
||||||
{lightbox.images.length > 1 && (
|
{lightbox.images.length > 1 && (
|
||||||
<button
|
<button
|
||||||
className="absolute left-6 text-white/70 hover:text-white transition-colors z-10"
|
className="absolute left-6 p-2 text-white/70 hover:text-white transition-colors z-10"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
goToPrev();
|
goToPrev();
|
||||||
|
|
@ -417,22 +434,24 @@ function AlbumDetail() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 이미지 */}
|
{/* 이미지 */}
|
||||||
|
<div className="flex flex-col items-center mx-24">
|
||||||
<motion.img
|
<motion.img
|
||||||
key={lightbox.index}
|
key={lightbox.index}
|
||||||
src={lightbox.images[lightbox.index]}
|
src={lightbox.images[lightbox.index]}
|
||||||
alt="확대 이미지"
|
alt="확대 이미지"
|
||||||
className={`max-w-[90vw] max-h-[90vh] object-contain rounded-lg transition-opacity duration-200 ${imageLoaded ? 'opacity-100' : 'opacity-0'}`}
|
className={`max-w-[1100px] max-h-[75vh] object-contain rounded-lg transition-opacity duration-200 ${imageLoaded ? 'opacity-100' : 'opacity-0'}`}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onLoad={() => setImageLoaded(true)}
|
onLoad={() => setImageLoaded(true)}
|
||||||
initial={{ x: slideDirection * 100 }}
|
initial={{ x: slideDirection * 100 }}
|
||||||
animate={{ x: 0 }}
|
animate={{ x: 0 }}
|
||||||
transition={{ duration: 0.25, ease: 'easeOut' }}
|
transition={{ duration: 0.25, ease: 'easeOut' }}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 다음 버튼 */}
|
{/* 다음 버튼 */}
|
||||||
{lightbox.images.length > 1 && (
|
{lightbox.images.length > 1 && (
|
||||||
<button
|
<button
|
||||||
className="absolute right-6 text-white/70 hover:text-white transition-colors z-10"
|
className="absolute right-6 p-2 text-white/70 hover:text-white transition-colors z-10"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
goToNext();
|
goToNext();
|
||||||
|
|
@ -442,21 +461,23 @@ function AlbumDetail() {
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 인디케이터 - 이미지 2개 이상일 때만 표시 */}
|
{/* 인디케이터 */}
|
||||||
{lightbox.images.length > 1 && (
|
{lightbox.images.length > 1 && (
|
||||||
<div className="absolute bottom-6 flex gap-2">
|
<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) => (
|
{lightbox.images.map((_, i) => (
|
||||||
<button
|
<button
|
||||||
key={i}
|
key={i}
|
||||||
className={`w-2 h-2 rounded-full transition-colors ${i === lightbox.index ? 'bg-white' : 'bg-white/40'}`}
|
className={`w-2 h-2 rounded-full transition-colors flex-shrink-0 ${i === lightbox.index ? 'bg-white' : 'bg-white/40'}`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
setImageLoaded(false);
|
||||||
setLightbox({ ...lightbox, index: i });
|
setLightbox({ ...lightbox, index: i });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
|
||||||
|
|
@ -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,8 +240,10 @@ 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="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">
|
<div className="absolute top-6 right-6 flex gap-3 z-10">
|
||||||
<button
|
<button
|
||||||
|
|
@ -248,10 +265,10 @@ function AlbumGallery() {
|
||||||
{lightbox.index + 1} / {photos.length}
|
{lightbox.index + 1} / {photos.length}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 이전 버튼 */}
|
{/* 이전 버튼 - margin으로 이미지와 간격 */}
|
||||||
{photos.length > 1 && (
|
{photos.length > 1 && (
|
||||||
<button
|
<button
|
||||||
className="absolute left-6 text-white/70 hover:text-white transition-colors z-10"
|
className="absolute left-6 p-2 text-white/70 hover:text-white transition-colors z-10"
|
||||||
onClick={goToPrev}
|
onClick={goToPrev}
|
||||||
>
|
>
|
||||||
<ChevronLeft size={48} />
|
<ChevronLeft size={48} />
|
||||||
|
|
@ -265,34 +282,54 @@ function AlbumGallery() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 이미지 */}
|
{/* 이미지 + 컨셉 정보 - 양옆 margin으로 화살표와 간격 */}
|
||||||
|
<div className="flex flex-col items-center mx-24">
|
||||||
<motion.img
|
<motion.img
|
||||||
key={lightbox.index}
|
key={lightbox.index}
|
||||||
src={photos[lightbox.index]?.originalUrl}
|
src={photos[lightbox.index]?.originalUrl}
|
||||||
alt="확대 이미지"
|
alt="확대 이미지"
|
||||||
className={`max-w-[90vw] max-h-[85vh] object-contain rounded-lg transition-opacity duration-200 ${imageLoaded ? 'opacity-100' : 'opacity-0'}`}
|
className={`max-w-[1100px] max-h-[75vh] object-contain rounded-lg transition-opacity duration-200 ${imageLoaded ? 'opacity-100' : 'opacity-0'}`}
|
||||||
onLoad={() => setImageLoaded(true)}
|
onLoad={() => setImageLoaded(true)}
|
||||||
initial={{ x: slideDirection * 100 }}
|
initial={{ x: slideDirection * 100 }}
|
||||||
animate={{ x: 0 }}
|
animate={{ x: 0 }}
|
||||||
transition={{ duration: 0.25, ease: 'easeOut' }}
|
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 && (
|
{photos.length > 1 && (
|
||||||
<button
|
<button
|
||||||
className="absolute right-6 text-white/70 hover:text-white transition-colors z-10"
|
className="absolute right-6 p-2 text-white/70 hover:text-white transition-colors z-10"
|
||||||
onClick={goToNext}
|
onClick={goToNext}
|
||||||
>
|
>
|
||||||
<ChevronRight size={48} />
|
<ChevronRight size={48} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 하단 점 인디케이터 */}
|
{/* 하단 점 인디케이터 - 한 줄 고정, 스크롤바 숨김 */}
|
||||||
<div className="absolute bottom-6 flex gap-1.5 flex-wrap justify-center max-w-[80vw]">
|
<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) => (
|
{photos.map((_, i) => (
|
||||||
<button
|
<button
|
||||||
key={i}
|
key={i}
|
||||||
className={`w-2 h-2 rounded-full transition-colors ${i === lightbox.index ? 'bg-white' : 'bg-white/40'}`}
|
className={`w-2 h-2 rounded-full transition-colors flex-shrink-0 ${i === lightbox.index ? 'bg-white' : 'bg-white/40'}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setImageLoaded(false);
|
setImageLoaded(false);
|
||||||
setLightbox({ ...lightbox, index: i });
|
setLightbox({ ...lightbox, index: i });
|
||||||
|
|
@ -300,6 +337,7 @@ function AlbumGallery() {
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue