133 lines
5.3 KiB
React
133 lines
5.3 KiB
React
|
|
import { useState, useEffect } from 'react';
|
||
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
||
|
|
import { ArrowLeft, X, ChevronLeft, ChevronRight } from 'lucide-react';
|
||
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
||
|
|
|
||
|
|
// 모바일 앨범 갤러리 페이지
|
||
|
|
function MobileAlbumGallery() {
|
||
|
|
const { name } = useParams();
|
||
|
|
const navigate = useNavigate();
|
||
|
|
const [album, setAlbum] = useState(null);
|
||
|
|
const [photos, setPhotos] = useState([]);
|
||
|
|
const [loading, setLoading] = useState(true);
|
||
|
|
const [selectedIndex, setSelectedIndex] = useState(null);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
fetch('/api/albums')
|
||
|
|
.then(res => res.json())
|
||
|
|
.then(data => {
|
||
|
|
const found = data.find(a => a.folder_name === name);
|
||
|
|
if (found) {
|
||
|
|
setAlbum(found);
|
||
|
|
fetch(`/api/albums/${found.id}/photos`)
|
||
|
|
.then(res => res.json())
|
||
|
|
.then(setPhotos)
|
||
|
|
.catch(console.error);
|
||
|
|
}
|
||
|
|
setLoading(false);
|
||
|
|
})
|
||
|
|
.catch(console.error);
|
||
|
|
}, [name]);
|
||
|
|
|
||
|
|
// 이미지 네비게이션
|
||
|
|
const goToImage = (delta) => {
|
||
|
|
const newIndex = selectedIndex + delta;
|
||
|
|
if (newIndex >= 0 && newIndex < photos.length) {
|
||
|
|
setSelectedIndex(newIndex);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
if (loading) {
|
||
|
|
return (
|
||
|
|
<div className="flex items-center justify-center h-64">
|
||
|
|
<div className="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="pb-4">
|
||
|
|
{/* 헤더 */}
|
||
|
|
<div className="sticky top-14 z-40 bg-white/80 backdrop-blur-sm px-4 py-3 flex items-center gap-3 border-b">
|
||
|
|
<button onClick={() => navigate(-1)} className="p-1">
|
||
|
|
<ArrowLeft size={24} />
|
||
|
|
</button>
|
||
|
|
<span className="font-semibold truncate">{album?.title} 갤러리</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 갤러리 그리드 */}
|
||
|
|
<div className="grid grid-cols-3 gap-0.5 p-0.5">
|
||
|
|
{photos.map((photo, index) => (
|
||
|
|
<motion.div
|
||
|
|
key={photo.id}
|
||
|
|
initial={{ opacity: 0 }}
|
||
|
|
animate={{ opacity: 1 }}
|
||
|
|
transition={{ delay: index * 0.02 }}
|
||
|
|
onClick={() => setSelectedIndex(index)}
|
||
|
|
className="aspect-square bg-gray-200 cursor-pointer"
|
||
|
|
>
|
||
|
|
<img
|
||
|
|
src={photo.thumb_url}
|
||
|
|
alt=""
|
||
|
|
className="w-full h-full object-cover"
|
||
|
|
loading="lazy"
|
||
|
|
/>
|
||
|
|
</motion.div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 풀스크린 뷰어 */}
|
||
|
|
<AnimatePresence>
|
||
|
|
{selectedIndex !== null && (
|
||
|
|
<motion.div
|
||
|
|
initial={{ opacity: 0 }}
|
||
|
|
animate={{ opacity: 1 }}
|
||
|
|
exit={{ opacity: 0 }}
|
||
|
|
className="fixed inset-0 bg-black z-50 flex flex-col"
|
||
|
|
>
|
||
|
|
{/* 뷰어 헤더 */}
|
||
|
|
<div className="flex items-center justify-between p-4 text-white">
|
||
|
|
<button onClick={() => setSelectedIndex(null)}>
|
||
|
|
<X size={24} />
|
||
|
|
</button>
|
||
|
|
<span className="text-sm">
|
||
|
|
{selectedIndex + 1} / {photos.length}
|
||
|
|
</span>
|
||
|
|
<div className="w-6" />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 이미지 */}
|
||
|
|
<div className="flex-1 flex items-center justify-center relative">
|
||
|
|
<img
|
||
|
|
src={photos[selectedIndex]?.medium_url || photos[selectedIndex]?.original_url}
|
||
|
|
alt=""
|
||
|
|
className="max-w-full max-h-full object-contain"
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* 좌우 네비게이션 */}
|
||
|
|
{selectedIndex > 0 && (
|
||
|
|
<button
|
||
|
|
onClick={() => goToImage(-1)}
|
||
|
|
className="absolute left-2 p-2 text-white/80"
|
||
|
|
>
|
||
|
|
<ChevronLeft size={32} />
|
||
|
|
</button>
|
||
|
|
)}
|
||
|
|
{selectedIndex < photos.length - 1 && (
|
||
|
|
<button
|
||
|
|
onClick={() => goToImage(1)}
|
||
|
|
className="absolute right-2 p-2 text-white/80"
|
||
|
|
>
|
||
|
|
<ChevronRight size={32} />
|
||
|
|
</button>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
)}
|
||
|
|
</AnimatePresence>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export default MobileAlbumGallery;
|