From d6bc8d79ba20aeb79f6c120e46a2877e41606dd9 Mon Sep 17 00:00:00 2001 From: caadiq Date: Sun, 11 Jan 2026 23:15:56 +0900 Subject: [PATCH] =?UTF-8?q?=EB=AA=A8=EB=B0=94=EC=9D=BC=20=EC=95=A8?= =?UTF-8?q?=EB=B2=94=20=EA=B0=A4=EB=9F=AC=EB=A6=AC=20UI=20=EB=8C=80?= =?UTF-8?q?=ED=8F=AD=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Swiper 라이브러리로 ViewPager 스타일 라이트박스 구현 - LightboxIndicator 컴포넌트에 width prop 추가 (모바일 120px) - 2열 지그재그 Masonry 그리드 레이아웃 - 바텀시트 정보 표시 (드래그 핸들 지원) - 뒤로가기 처리 (라이트박스/다이얼로그 닫기) - 앨범 조회 API: folder_name 또는 title로 검색 (PC/모바일 호환) --- backend/routes/albums.js | 9 +- .../components/common/LightboxIndicator.jsx | 7 +- .../src/pages/mobile/public/AlbumDetail.jsx | 240 ++++++------ .../src/pages/mobile/public/AlbumGallery.jsx | 368 ++++++++++++++---- 4 files changed, 417 insertions(+), 207 deletions(-) diff --git a/backend/routes/albums.js b/backend/routes/albums.js index 29c56c2..99d1bba 100644 --- a/backend/routes/albums.js +++ b/backend/routes/albums.js @@ -81,13 +81,14 @@ router.get("/", async (req, res) => { } }); -// 앨범 folder_name으로 조회 +// 앨범 folder_name 또는 title로 조회 router.get("/by-name/:name", async (req, res) => { try { - const folderName = decodeURIComponent(req.params.name); + const name = decodeURIComponent(req.params.name); + // folder_name 또는 title로 검색 (PC는 title, 모바일은 folder_name 사용) const [albums] = await pool.query( - "SELECT * FROM albums WHERE folder_name = ?", - [folderName] + "SELECT * FROM albums WHERE folder_name = ? OR title = ?", + [name, name] ); if (albums.length === 0) { diff --git a/frontend/src/components/common/LightboxIndicator.jsx b/frontend/src/components/common/LightboxIndicator.jsx index d317282..b0f7ba1 100644 --- a/frontend/src/components/common/LightboxIndicator.jsx +++ b/frontend/src/components/common/LightboxIndicator.jsx @@ -5,11 +5,12 @@ import { memo } from 'react'; * 이미지 갤러리에서 현재 위치를 표시하는 슬라이딩 점 인디케이터 * CSS transition 사용으로 GPU 가속 */ -const LightboxIndicator = memo(function LightboxIndicator({ count, currentIndex, goToIndex }) { - const translateX = -(currentIndex * 18) + 100 - 6; +const LightboxIndicator = memo(function LightboxIndicator({ count, currentIndex, goToIndex, width = 200 }) { + const halfWidth = width / 2; + const translateX = -(currentIndex * 18) + halfWidth - 6; return ( -
+
{/* 양옆 페이드 그라데이션 */}
{ - if (lightbox.images.length <= 1) return; - setImageLoaded(false); - setLightbox(prev => ({ - ...prev, - index: (prev.index - 1 + prev.images.length) % prev.images.length - })); - }, [lightbox.images.length]); - - const goToNext = useCallback(() => { - if (lightbox.images.length <= 1) return; - setImageLoaded(false); - setLightbox(prev => ({ - ...prev, - index: (prev.index + 1) % prev.images.length - })); - }, [lightbox.images.length]); + // 라이트박스 열기 - 히스토리 추가 + const openLightbox = useCallback((images, index, options = {}) => { + setLightbox({ open: true, images, index, showNav: options.showNav !== false, teasers: options.teasers }); + window.history.pushState({ lightbox: true }, ''); + }, []); const closeLightbox = useCallback(() => { setLightbox(prev => ({ ...prev, open: false })); }, []); + // 앨범 소개 열기 - 히스토리 추가 + const openDescriptionModal = useCallback(() => { + setShowDescriptionModal(true); + window.history.pushState({ description: true }, ''); + }, []); + + const closeDescriptionModal = useCallback(() => { + setShowDescriptionModal(false); + }, []); + + // 뒤로가기 처리 + useEffect(() => { + const handlePopState = () => { + if (showDescriptionModal) { + setShowDescriptionModal(false); + } else if (lightbox.open) { + setLightbox(prev => ({ ...prev, open: false })); + } + }; + + window.addEventListener('popstate', handlePopState); + return () => window.removeEventListener('popstate', handlePopState); + }, [showDescriptionModal, lightbox.open]); + // 이미지 다운로드 const downloadImage = useCallback(async () => { const imageUrl = lightbox.images[lightbox.index]; @@ -140,12 +155,11 @@ function MobileAlbumDetail() { initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="w-44 h-44 rounded-2xl overflow-hidden shadow-xl mb-4" - onClick={() => setLightbox({ - open: true, - images: [album.cover_original_url || album.cover_medium_url], - index: 0, - showNav: false - })} + onClick={() => openLightbox( + [album.cover_original_url || album.cover_medium_url], + 0, + { showNav: false } + )} > setShowDescriptionModal(true)} + onClick={openDescriptionModal} className="mt-3 flex items-center gap-1.5 mx-auto px-3 py-1.5 text-xs text-gray-500 bg-white/80 rounded-full shadow-sm" > @@ -209,13 +223,11 @@ function MobileAlbumDetail() { {album.teasers.map((teaser, index) => (
setLightbox({ - open: true, - images: album.teasers.map(t => t.original_url), + onClick={() => openLightbox( + album.teasers.map(t => t.original_url), index, - teasers: album.teasers, - showNav: true - })} + { teasers: album.teasers, showNav: true } + )} className="w-24 h-24 flex-shrink-0 bg-gray-100 rounded-2xl overflow-hidden relative shadow-sm" > {teaser.media_type === 'video' ? ( @@ -303,12 +315,11 @@ function MobileAlbumDetail() { {allPhotos.slice(0, 6).map((photo, idx) => (
setLightbox({ - open: true, - images: [photo.original_url], - index: 0, - showNav: false - })} + onClick={() => openLightbox( + [photo.original_url], + 0, + { showNav: false } + )} className="aspect-square bg-gray-100 rounded-xl overflow-hidden shadow-sm" > setShowDescriptionModal(false)} + onClick={() => window.history.back()} > { - if (info.offset.y > 200 || info.velocity.y > 500) { - setShowDescriptionModal(false); + if (info.offset.y > 100 || info.velocity.y > 300) { + window.history.back(); } }} className="bg-white rounded-t-3xl w-full max-h-[80vh] overflow-hidden" @@ -366,7 +377,7 @@ function MobileAlbumDetail() {

앨범 소개

- + {/* 상단 헤더 - 3등분 */} +
+
+ +
+ {lightbox.showNav && lightbox.images.length > 1 && ( + + {lightbox.index + 1} / {lightbox.images.length} + + )} +
+ +
- {/* 이전 버튼 - showNav가 true일 때만 */} + {/* Swiper */} + { swiperRef.current = swiper; }} + onSlideChange={(swiper) => setLightbox(prev => ({ ...prev, index: swiper.activeIndex }))} + className="w-full h-full" + spaceBetween={0} + slidesPerView={1} + resistance={true} + resistanceRatio={0.5} + > + {lightbox.images.map((url, index) => ( + +
+ {lightbox.teasers?.[index]?.media_type === 'video' ? ( +
+
+ ))} +
+ + {/* 모바일용 인디케이터 */} {lightbox.showNav && lightbox.images.length > 1 && ( - - )} - - {/* 로딩 스피너 */} - {!imageLoaded && ( -
-
-
- )} - - {/* 이미지/비디오 */} - {lightbox.teasers?.[lightbox.index]?.media_type === 'video' ? ( -