2026-01-22 14:11:52 +09:00
|
|
|
import { useState, useCallback, useMemo } from 'react';
|
2026-01-24 10:36:27 +09:00
|
|
|
import { useParams } from 'react-router-dom';
|
2026-01-22 11:32:43 +09:00
|
|
|
import { useQuery } from '@tanstack/react-query';
|
2026-01-22 14:11:52 +09:00
|
|
|
import { motion } from 'framer-motion';
|
2026-01-22 18:37:30 +09:00
|
|
|
import { getAlbumByName } from '@/api';
|
2026-01-22 14:11:52 +09:00
|
|
|
import { MobileLightbox } from '@/components/common';
|
2026-01-22 11:32:43 +09:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Mobile 앨범 갤러리 페이지
|
|
|
|
|
*/
|
|
|
|
|
function MobileAlbumGallery() {
|
|
|
|
|
const { name } = useParams();
|
|
|
|
|
const [selectedIndex, setSelectedIndex] = useState(null);
|
|
|
|
|
|
|
|
|
|
// 앨범 데이터 로드
|
|
|
|
|
const { data: album, isLoading: loading } = useQuery({
|
|
|
|
|
queryKey: ['album', name],
|
|
|
|
|
queryFn: () => getAlbumByName(name),
|
|
|
|
|
enabled: !!name,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 앨범 데이터에서 사진 목록 추출
|
|
|
|
|
const photos = useMemo(() => {
|
|
|
|
|
if (!album?.conceptPhotos) return [];
|
|
|
|
|
const allPhotos = [];
|
|
|
|
|
Object.entries(album.conceptPhotos).forEach(([concept, conceptPhotos]) => {
|
|
|
|
|
conceptPhotos.forEach((p) =>
|
|
|
|
|
allPhotos.push({
|
|
|
|
|
...p,
|
|
|
|
|
concept: concept !== 'Default' ? concept : null,
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
return allPhotos;
|
|
|
|
|
}, [album]);
|
|
|
|
|
|
|
|
|
|
// 라이트박스 열기
|
|
|
|
|
const openLightbox = useCallback((index) => {
|
|
|
|
|
setSelectedIndex(index);
|
|
|
|
|
window.history.pushState({ lightbox: true }, '');
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// 사진을 2열로 균등 분배 (높이 기반)
|
|
|
|
|
const distributePhotos = () => {
|
|
|
|
|
const leftColumn = [];
|
|
|
|
|
const rightColumn = [];
|
|
|
|
|
let leftHeight = 0;
|
|
|
|
|
let rightHeight = 0;
|
|
|
|
|
|
|
|
|
|
photos.forEach((photo, index) => {
|
|
|
|
|
const aspectRatio = photo.height && photo.width ? photo.height / photo.width : 1;
|
|
|
|
|
|
|
|
|
|
if (leftHeight <= rightHeight) {
|
|
|
|
|
leftColumn.push({ ...photo, originalIndex: index });
|
|
|
|
|
leftHeight += aspectRatio;
|
|
|
|
|
} else {
|
|
|
|
|
rightColumn.push({ ...photo, originalIndex: index });
|
|
|
|
|
rightHeight += aspectRatio;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return { leftColumn, rightColumn };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const { leftColumn, rightColumn } = distributePhotos();
|
|
|
|
|
|
|
|
|
|
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">
|
|
|
|
|
{/* 앨범 헤더 카드 */}
|
2026-01-24 10:36:27 +09:00
|
|
|
<div className="mx-4 mt-4 mb-4 p-4 bg-gradient-to-r from-primary/5 to-primary/10 rounded-2xl flex items-center gap-4">
|
2026-01-22 11:32:43 +09:00
|
|
|
{album?.cover_thumb_url && (
|
|
|
|
|
<img
|
|
|
|
|
src={album.cover_thumb_url}
|
|
|
|
|
alt={album.title}
|
|
|
|
|
className="w-14 h-14 rounded-xl object-cover shadow-sm"
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
<p className="text-xs text-primary font-medium mb-0.5">컨셉 포토</p>
|
|
|
|
|
<p className="font-bold truncate">{album?.title}</p>
|
|
|
|
|
<p className="text-xs text-gray-500">{photos.length}장의 사진</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 2열 그리드 */}
|
|
|
|
|
<div className="px-3 flex gap-2">
|
|
|
|
|
<div className="flex-1 flex flex-col gap-2">
|
|
|
|
|
{leftColumn.map((photo) => (
|
|
|
|
|
<motion.div
|
|
|
|
|
key={photo.id || photo.originalIndex}
|
|
|
|
|
initial={{ opacity: 0, y: 10 }}
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
transition={{ delay: Math.min(photo.originalIndex * 0.02, 0.4) }}
|
|
|
|
|
onClick={() => openLightbox(photo.originalIndex)}
|
|
|
|
|
className="cursor-pointer overflow-hidden rounded-xl bg-gray-100"
|
|
|
|
|
>
|
|
|
|
|
<img
|
|
|
|
|
src={photo.thumb_url || photo.medium_url}
|
|
|
|
|
alt=""
|
|
|
|
|
className="w-full h-auto object-cover"
|
|
|
|
|
loading="lazy"
|
|
|
|
|
/>
|
|
|
|
|
</motion.div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex-1 flex flex-col gap-2">
|
|
|
|
|
{rightColumn.map((photo) => (
|
|
|
|
|
<motion.div
|
|
|
|
|
key={photo.id || photo.originalIndex}
|
|
|
|
|
initial={{ opacity: 0, y: 10 }}
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
transition={{ delay: Math.min(photo.originalIndex * 0.02, 0.4) }}
|
|
|
|
|
onClick={() => openLightbox(photo.originalIndex)}
|
|
|
|
|
className="cursor-pointer overflow-hidden rounded-xl bg-gray-100"
|
|
|
|
|
>
|
|
|
|
|
<img
|
|
|
|
|
src={photo.thumb_url || photo.medium_url}
|
|
|
|
|
alt=""
|
|
|
|
|
className="w-full h-auto object-cover"
|
|
|
|
|
loading="lazy"
|
|
|
|
|
/>
|
|
|
|
|
</motion.div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-22 14:11:52 +09:00
|
|
|
{/* 라이트박스 */}
|
|
|
|
|
<MobileLightbox
|
|
|
|
|
images={photos.map((p) => p.medium_url || p.original_url)}
|
|
|
|
|
photos={photos}
|
|
|
|
|
currentIndex={selectedIndex ?? 0}
|
|
|
|
|
isOpen={selectedIndex !== null}
|
|
|
|
|
onClose={() => setSelectedIndex(null)}
|
|
|
|
|
onIndexChange={setSelectedIndex}
|
|
|
|
|
showCounter={photos.length > 1}
|
|
|
|
|
downloadPrefix={`fromis9_${album?.title || 'photo'}`}
|
|
|
|
|
/>
|
2026-01-22 11:32:43 +09:00
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default MobileAlbumGallery;
|