라이트박스 뒤로가기 버튼 처리 추가

- PC/모바일 앨범 상세, 갤러리 페이지에 뒤로가기 처리
- PC/모바일 X 일정 상세 페이지에 뒤로가기 처리
- 라이트박스 열릴 때 history.pushState 호출
- popstate 이벤트로 라이트박스 닫기

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-01-21 12:05:36 +09:00
parent d8055c00e5
commit 67e9992cf1
4 changed files with 92 additions and 30 deletions

View file

@ -315,6 +315,7 @@ function XSection({ schedule }) {
const openLightbox = (index) => { const openLightbox = (index) => {
setLightboxIndex(index); setLightboxIndex(index);
setLightboxOpen(true); setLightboxOpen(true);
window.history.pushState({ lightbox: true }, '');
}; };
const goToPrev = () => { const goToPrev = () => {
@ -341,6 +342,18 @@ function XSection({ schedule }) {
}; };
}, [lightboxOpen]); }, [lightboxOpen]);
//
useEffect(() => {
const handlePopState = () => {
if (lightboxOpen) {
setLightboxOpen(false);
}
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, [lightboxOpen]);
// ( ) // ( )
const linkDecorator = (href, text, key) => ( const linkDecorator = (href, text, key) => (
<a <a
@ -478,12 +491,12 @@ function XSection({ schedule }) {
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
className="fixed inset-0 bg-black z-50 flex items-center justify-center" className="fixed inset-0 bg-black z-50 flex items-center justify-center"
onClick={() => setLightboxOpen(false)} onClick={() => window.history.back()}
> >
{/* 닫기 버튼 */} {/* 닫기 버튼 */}
<button <button
className="absolute top-4 right-4 p-2 text-white/70 z-10" className="absolute top-4 right-4 p-2 text-white/70 z-10"
onClick={() => setLightboxOpen(false)} onClick={() => window.history.back()}
> >
<X size={28} /> <X size={28} />
</button> </button>

View file

@ -45,10 +45,30 @@ function AlbumDetail() {
})); }));
}, [lightbox.images.length]); }, [lightbox.images.length]);
// -
const openLightbox = useCallback((images, index, options = {}) => {
setLightbox({ open: true, images, index, teasers: options.teasers });
window.history.pushState({ lightbox: true }, '');
}, []);
const closeLightbox = useCallback(() => { const closeLightbox = useCallback(() => {
setLightbox(prev => ({ ...prev, open: false })); setLightbox(prev => ({ ...prev, open: 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]);
// body // body
useEffect(() => { useEffect(() => {
if (lightbox.open) { if (lightbox.open) {
@ -98,7 +118,7 @@ function AlbumDetail() {
goToNext(); goToNext();
break; break;
case 'Escape': case 'Escape':
closeLightbox(); window.history.back();
break; break;
default: default:
break; break;
@ -202,11 +222,10 @@ function AlbumDetail() {
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.4 }} transition={{ duration: 0.4 }}
className="w-80 h-80 flex-shrink-0 rounded-2xl overflow-hidden shadow-lg cursor-pointer group" className="w-80 h-80 flex-shrink-0 rounded-2xl overflow-hidden shadow-lg cursor-pointer group"
onClick={() => setLightbox({ onClick={() => openLightbox(
open: true, [album.cover_original_url || album.cover_medium_url],
images: [album.cover_original_url || album.cover_medium_url], 0
index: 0 )}
})}
> >
<img <img
src={album.cover_medium_url || album.cover_original_url} src={album.cover_medium_url || album.cover_original_url}
@ -253,6 +272,7 @@ function AlbumDetail() {
<button <button
onClick={() => { onClick={() => {
setShowDescriptionModal(true); setShowDescriptionModal(true);
window.history.pushState({ description: true }, '');
setShowMenu(false); setShowMenu(false);
}} }}
className="w-full flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 transition-colors" className="w-full flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
@ -291,16 +311,15 @@ function AlbumDetail() {
<p className="text-xs text-gray-400 mb-2">티저 이미지</p> <p className="text-xs text-gray-400 mb-2">티저 이미지</p>
<div className="flex gap-2"> <div className="flex gap-2">
{album.teasers.map((teaser, index) => ( {album.teasers.map((teaser, index) => (
<div <div
key={index} key={index}
onClick={() => setLightbox({ onClick={() => openLightbox(
open: true, album.teasers.map(t =>
images: album.teasers.map(t =>
t.media_type === 'video' ? (t.video_url || t.original_url) : t.original_url t.media_type === 'video' ? (t.video_url || t.original_url) : t.original_url
), ),
index, index,
teasers: album.teasers // media_type { teasers: album.teasers }
})} )}
className="w-24 h-24 bg-gray-200 rounded-lg overflow-hidden cursor-pointer transition-all duration-300 ease-out hover:scale-105 hover:shadow-xl hover:z-10 relative" className="w-24 h-24 bg-gray-200 rounded-lg overflow-hidden cursor-pointer transition-all duration-300 ease-out hover:scale-105 hover:shadow-xl hover:z-10 relative"
> >
<> <>
@ -397,9 +416,9 @@ function AlbumDetail() {
</div> </div>
<div className="grid grid-cols-4 gap-4"> <div className="grid grid-cols-4 gap-4">
{previewPhotos.map((photo, idx) => ( {previewPhotos.map((photo, idx) => (
<div <div
key={photo.id} key={photo.id}
onClick={() => setLightbox({ open: true, images: [photo.original_url], index: 0 })} onClick={() => openLightbox([photo.original_url], 0)}
className="aspect-square bg-gray-200 rounded-xl overflow-hidden cursor-pointer transition-all duration-300 ease-out hover:scale-[1.03] hover:shadow-xl hover:z-10" className="aspect-square bg-gray-200 rounded-xl overflow-hidden cursor-pointer transition-all duration-300 ease-out hover:scale-[1.03] hover:shadow-xl hover:z-10"
> >
<img <img
@ -445,9 +464,9 @@ function AlbumDetail() {
<Download size={28} /> <Download size={28} />
</button> </button>
{/* 닫기 버튼 */} {/* 닫기 버튼 */}
<button <button
className="text-white/70 hover:text-white transition-colors" className="text-white/70 hover:text-white transition-colors"
onClick={closeLightbox} onClick={() => window.history.back()}
> >
<X size={32} /> <X size={32} />
</button> </button>
@ -537,7 +556,7 @@ function AlbumDetail() {
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4"
onClick={() => setShowDescriptionModal(false)} onClick={() => window.history.back()}
> >
<motion.div <motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }} initial={{ opacity: 0, scale: 0.95, y: 20 }}
@ -550,7 +569,7 @@ function AlbumDetail() {
<div className="flex items-center justify-between p-5 border-b border-gray-100"> <div className="flex items-center justify-between p-5 border-b border-gray-100">
<h3 className="text-lg font-bold">앨범 소개</h3> <h3 className="text-lg font-bold">앨범 소개</h3>
<button <button
onClick={() => setShowDescriptionModal(false)} onClick={() => window.history.back()}
className="p-1.5 hover:bg-gray-100 rounded-full transition-colors" className="p-1.5 hover:bg-gray-100 rounded-full transition-colors"
> >
<X size={20} className="text-gray-500" /> <X size={20} className="text-gray-500" />

View file

@ -70,17 +70,30 @@ function AlbumGallery() {
return allPhotos; return allPhotos;
}, [album]); }, [album]);
// // -
const openLightbox = (index) => { const openLightbox = useCallback((index) => {
setImageLoaded(false); setImageLoaded(false);
setLightbox({ open: true, index }); setLightbox({ open: true, index });
}; window.history.pushState({ lightbox: true }, '');
}, []);
// //
const closeLightbox = useCallback(() => { const closeLightbox = useCallback(() => {
setLightbox(prev => ({ ...prev, open: false })); setLightbox(prev => ({ ...prev, open: false }));
}, []); }, []);
//
useEffect(() => {
const handlePopState = () => {
if (lightbox.open) {
setLightbox(prev => ({ ...prev, open: false }));
}
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, [lightbox.open]);
// body // body
useEffect(() => { useEffect(() => {
if (lightbox.open) { if (lightbox.open) {
@ -146,7 +159,7 @@ function AlbumGallery() {
switch (e.key) { switch (e.key) {
case 'ArrowLeft': goToPrev(); break; case 'ArrowLeft': goToPrev(); break;
case 'ArrowRight': goToNext(); break; case 'ArrowRight': goToNext(); break;
case 'Escape': closeLightbox(); break; case 'Escape': window.history.back(); break;
default: break; default: break;
} }
}; };
@ -267,9 +280,9 @@ function AlbumGallery() {
> >
<Download size={28} /> <Download size={28} />
</button> </button>
<button <button
className="text-white/70 hover:text-white transition-colors" className="text-white/70 hover:text-white transition-colors"
onClick={closeLightbox} onClick={() => window.history.back()}
> >
<X size={32} /> <X size={32} />
</button> </button>

View file

@ -1,4 +1,4 @@
import { useState } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import Linkify from 'react-linkify'; import Linkify from 'react-linkify';
import { decodeHtmlEntities } from './utils'; import { decodeHtmlEntities } from './utils';
@ -33,10 +33,27 @@ function XSection({ schedule }) {
const [lightboxOpen, setLightboxOpen] = useState(false); const [lightboxOpen, setLightboxOpen] = useState(false);
const [lightboxIndex, setLightboxIndex] = useState(0); const [lightboxIndex, setLightboxIndex] = useState(0);
const openLightbox = (index) => { const openLightbox = useCallback((index) => {
setLightboxIndex(index); setLightboxIndex(index);
setLightboxOpen(true); setLightboxOpen(true);
}; window.history.pushState({ lightbox: true }, '');
}, []);
const closeLightbox = useCallback(() => {
setLightboxOpen(false);
}, []);
//
useEffect(() => {
const handlePopState = () => {
if (lightboxOpen) {
setLightboxOpen(false);
}
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, [lightboxOpen]);
// ( ) // ( )
const linkDecorator = (href, text, key) => ( const linkDecorator = (href, text, key) => (
@ -160,7 +177,7 @@ function XSection({ schedule }) {
images={schedule.imageUrls || []} images={schedule.imageUrls || []}
currentIndex={lightboxIndex} currentIndex={lightboxIndex}
isOpen={lightboxOpen} isOpen={lightboxOpen}
onClose={() => setLightboxOpen(false)} onClose={() => window.history.back()}
onIndexChange={setLightboxIndex} onIndexChange={setLightboxIndex}
/> />
</div> </div>