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

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

View file

@ -45,10 +45,30 @@ function AlbumDetail() {
}));
}, [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(() => {
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
useEffect(() => {
if (lightbox.open) {
@ -98,7 +118,7 @@ function AlbumDetail() {
goToNext();
break;
case 'Escape':
closeLightbox();
window.history.back();
break;
default:
break;
@ -202,11 +222,10 @@ function AlbumDetail() {
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.4 }}
className="w-80 h-80 flex-shrink-0 rounded-2xl overflow-hidden shadow-lg cursor-pointer group"
onClick={() => setLightbox({
open: true,
images: [album.cover_original_url || album.cover_medium_url],
index: 0
})}
onClick={() => openLightbox(
[album.cover_original_url || album.cover_medium_url],
0
)}
>
<img
src={album.cover_medium_url || album.cover_original_url}
@ -253,6 +272,7 @@ function AlbumDetail() {
<button
onClick={() => {
setShowDescriptionModal(true);
window.history.pushState({ description: true }, '');
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"
@ -291,16 +311,15 @@ function AlbumDetail() {
<p className="text-xs text-gray-400 mb-2">티저 이미지</p>
<div className="flex gap-2">
{album.teasers.map((teaser, index) => (
<div
<div
key={index}
onClick={() => setLightbox({
open: true,
images: album.teasers.map(t =>
onClick={() => openLightbox(
album.teasers.map(t =>
t.media_type === 'video' ? (t.video_url || t.original_url) : t.original_url
),
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"
>
<>
@ -397,9 +416,9 @@ function AlbumDetail() {
</div>
<div className="grid grid-cols-4 gap-4">
{previewPhotos.map((photo, idx) => (
<div
<div
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"
>
<img
@ -445,9 +464,9 @@ function AlbumDetail() {
<Download size={28} />
</button>
{/* 닫기 버튼 */}
<button
<button
className="text-white/70 hover:text-white transition-colors"
onClick={closeLightbox}
onClick={() => window.history.back()}
>
<X size={32} />
</button>
@ -537,7 +556,7 @@ function AlbumDetail() {
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
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
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">
<h3 className="text-lg font-bold">앨범 소개</h3>
<button
onClick={() => setShowDescriptionModal(false)}
onClick={() => window.history.back()}
className="p-1.5 hover:bg-gray-100 rounded-full transition-colors"
>
<X size={20} className="text-gray-500" />

View file

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

View file

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { motion } from 'framer-motion';
import Linkify from 'react-linkify';
import { decodeHtmlEntities } from './utils';
@ -33,10 +33,27 @@ function XSection({ schedule }) {
const [lightboxOpen, setLightboxOpen] = useState(false);
const [lightboxIndex, setLightboxIndex] = useState(0);
const openLightbox = (index) => {
const openLightbox = useCallback((index) => {
setLightboxIndex(index);
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) => (
@ -160,7 +177,7 @@ function XSection({ schedule }) {
images={schedule.imageUrls || []}
currentIndex={lightboxIndex}
isOpen={lightboxOpen}
onClose={() => setLightboxOpen(false)}
onClose={() => window.history.back()}
onIndexChange={setLightboxIndex}
/>
</div>