refactor: PC public 페이지 공통 컴포넌트 및 유틸 적용
- LightboxIndicator 공통 컴포넌트 생성 및 AlbumDetail, AlbumGallery에 적용 - formatDate를 utils/date에서 import하도록 변경 (Album, Members, AlbumDetail) - 중복 코드 약 100줄 제거
This commit is contained in:
parent
22db79e960
commit
cdca23e317
5 changed files with 55 additions and 98 deletions
42
frontend/src/components/common/LightboxIndicator.jsx
Normal file
42
frontend/src/components/common/LightboxIndicator.jsx
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 라이트박스 인디케이터 컴포넌트
|
||||||
|
* 이미지 갤러리에서 현재 위치를 표시하는 슬라이딩 점 인디케이터
|
||||||
|
* CSS transition 사용으로 GPU 가속
|
||||||
|
*/
|
||||||
|
const LightboxIndicator = memo(function LightboxIndicator({ count, currentIndex, goToIndex }) {
|
||||||
|
const translateX = -(currentIndex * 18) + 100 - 6;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 overflow-hidden" style={{ width: '200px' }}>
|
||||||
|
{/* 양옆 페이드 그라데이션 */}
|
||||||
|
<div className="absolute inset-0 pointer-events-none z-10" style={{
|
||||||
|
background: 'linear-gradient(to right, rgba(0,0,0,1) 0%, transparent 20%, transparent 80%, rgba(0,0,0,1) 100%)'
|
||||||
|
}} />
|
||||||
|
{/* 슬라이딩 컨테이너 - CSS transition으로 GPU 가속 */}
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 justify-center"
|
||||||
|
style={{
|
||||||
|
width: `${count * 18}px`,
|
||||||
|
transform: `translateX(${translateX}px)`,
|
||||||
|
transition: 'transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Array.from({ length: count }).map((_, i) => (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
className={`rounded-full flex-shrink-0 transition-all duration-300 ${
|
||||||
|
i === currentIndex
|
||||||
|
? 'w-3 h-3 bg-white'
|
||||||
|
: 'w-2.5 h-2.5 bg-white/40 hover:bg-white/60'
|
||||||
|
}`}
|
||||||
|
onClick={() => goToIndex(i)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default LightboxIndicator;
|
||||||
|
|
@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Calendar, Music } from 'lucide-react';
|
import { Calendar, Music } from 'lucide-react';
|
||||||
import { getAlbums } from '../../../api/public/albums';
|
import { getAlbums } from '../../../api/public/albums';
|
||||||
|
import { formatDate } from '../../../utils/date';
|
||||||
|
|
||||||
function Album() {
|
function Album() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -21,13 +22,6 @@ function Album() {
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 날짜 포맷팅
|
|
||||||
const formatDate = (dateStr) => {
|
|
||||||
if (!dateStr) return '';
|
|
||||||
const date = new Date(dateStr);
|
|
||||||
return `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(2, '0')}.${String(date.getDate()).padStart(2, '0')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 타이틀곡 찾기
|
// 타이틀곡 찾기
|
||||||
const getTitleTrack = (tracks) => {
|
const getTitleTrack = (tracks) => {
|
||||||
if (!tracks || tracks.length === 0) return '';
|
if (!tracks || tracks.length === 0) return '';
|
||||||
|
|
@ -149,7 +143,7 @@ function Album() {
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||||
<Calendar size={14} />
|
<Calendar size={14} />
|
||||||
<span>{formatDate(album.release_date)}</span>
|
<span>{formatDate(album.release_date, 'YYYY.MM.DD')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
||||||
|
|
@ -3,41 +3,9 @@ import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { Calendar, Music2, Clock, X, ChevronLeft, ChevronRight, Download, MoreVertical, FileText } from 'lucide-react';
|
import { Calendar, Music2, Clock, X, ChevronLeft, ChevronRight, Download, MoreVertical, FileText } from 'lucide-react';
|
||||||
import { getAlbumByName } from '../../../api/public/albums';
|
import { getAlbumByName } from '../../../api/public/albums';
|
||||||
|
import { formatDate } from '../../../utils/date';
|
||||||
|
import LightboxIndicator from '../../../components/common/LightboxIndicator';
|
||||||
|
|
||||||
// 인디케이터 컴포넌트 - CSS transition 사용으로 JS 블로킹에 영향받지 않음
|
|
||||||
const LightboxIndicator = memo(function LightboxIndicator({ count, currentIndex, setLightbox }) {
|
|
||||||
const translateX = -(currentIndex * 18) + 100 - 6;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 overflow-hidden" style={{ width: '200px' }}>
|
|
||||||
{/* 양옆 페이드 그라데이션 */}
|
|
||||||
<div className="absolute inset-0 pointer-events-none z-10" style={{
|
|
||||||
background: 'linear-gradient(to right, rgba(0,0,0,1) 0%, transparent 20%, transparent 80%, rgba(0,0,0,1) 100%)'
|
|
||||||
}} />
|
|
||||||
{/* 슬라이딩 컨테이너 - CSS transition으로 GPU 가속 */}
|
|
||||||
<div
|
|
||||||
className="flex items-center gap-2 justify-center"
|
|
||||||
style={{
|
|
||||||
width: `${count * 18}px`,
|
|
||||||
transform: `translateX(${translateX}px)`,
|
|
||||||
transition: 'transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Array.from({ length: count }).map((_, i) => (
|
|
||||||
<button
|
|
||||||
key={i}
|
|
||||||
className={`rounded-full flex-shrink-0 transition-all duration-300 ${
|
|
||||||
i === currentIndex
|
|
||||||
? 'w-3 h-3 bg-white'
|
|
||||||
: 'w-2.5 h-2.5 bg-white/40 hover:bg-white/60'
|
|
||||||
}`}
|
|
||||||
onClick={() => setLightbox(prev => ({ ...prev, index: i }))}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
function AlbumDetail() {
|
function AlbumDetail() {
|
||||||
const { name } = useParams();
|
const { name } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -171,13 +139,6 @@ function AlbumDetail() {
|
||||||
|
|
||||||
// URL 헬퍼 함수는 더 이상 필요 없음 - API에서 직접 제공
|
// URL 헬퍼 함수는 더 이상 필요 없음 - API에서 직접 제공
|
||||||
|
|
||||||
// 날짜 포맷팅
|
|
||||||
const formatDate = (dateStr) => {
|
|
||||||
if (!dateStr) return '';
|
|
||||||
const date = new Date(dateStr);
|
|
||||||
return `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(2, '0')}.${String(date.getDate()).padStart(2, '0')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 총 재생 시간 계산
|
// 총 재생 시간 계산
|
||||||
const getTotalDuration = () => {
|
const getTotalDuration = () => {
|
||||||
if (!album?.tracks) return '';
|
if (!album?.tracks) return '';
|
||||||
|
|
@ -317,7 +278,7 @@ function AlbumDetail() {
|
||||||
<div className="flex items-center gap-6 text-gray-500 mb-3">
|
<div className="flex items-center gap-6 text-gray-500 mb-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Calendar size={18} />
|
<Calendar size={18} />
|
||||||
<span>{formatDate(album.release_date)}</span>
|
<span>{formatDate(album.release_date, 'YYYY.MM.DD')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Music2 size={18} />
|
<Music2 size={18} />
|
||||||
|
|
@ -568,12 +529,12 @@ function AlbumDetail() {
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 인디케이터 - memo 컴포넌트로 분리 */}
|
{/* 인디케이터 - 공통 컴포넌트 사용 */}
|
||||||
{lightbox.images.length > 1 && (
|
{lightbox.images.length > 1 && (
|
||||||
<LightboxIndicator
|
<LightboxIndicator
|
||||||
count={lightbox.images.length}
|
count={lightbox.images.length}
|
||||||
currentIndex={lightbox.index}
|
currentIndex={lightbox.index}
|
||||||
setLightbox={setLightbox}
|
goToIndex={(i) => setLightbox(prev => ({ ...prev, index: i }))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,41 +5,7 @@ import { X, ChevronLeft, ChevronRight, Download } from 'lucide-react';
|
||||||
import { RowsPhotoAlbum } from 'react-photo-album';
|
import { RowsPhotoAlbum } from 'react-photo-album';
|
||||||
import 'react-photo-album/rows.css';
|
import 'react-photo-album/rows.css';
|
||||||
import { getAlbumByName } from '../../../api/public/albums';
|
import { getAlbumByName } from '../../../api/public/albums';
|
||||||
|
import LightboxIndicator from '../../../components/common/LightboxIndicator';
|
||||||
// 인디케이터 컴포넌트 - CSS transition 사용으로 JS 블로킹에 영향받지 않음
|
|
||||||
const LightboxIndicator = memo(function LightboxIndicator({ count, currentIndex, setLightbox }) {
|
|
||||||
const translateX = -(currentIndex * 18) + 100 - 6;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 overflow-hidden" style={{ width: '200px' }}>
|
|
||||||
{/* 양옆 페이드 그라데이션 */}
|
|
||||||
<div className="absolute inset-0 pointer-events-none z-10" style={{
|
|
||||||
background: 'linear-gradient(to right, rgba(0,0,0,1) 0%, transparent 20%, transparent 80%, rgba(0,0,0,1) 100%)'
|
|
||||||
}} />
|
|
||||||
{/* 슬라이딩 컨테이너 - CSS transition으로 GPU 가속 */}
|
|
||||||
<div
|
|
||||||
className="flex items-center gap-2 justify-center"
|
|
||||||
style={{
|
|
||||||
width: `${count * 18}px`,
|
|
||||||
transform: `translateX(${translateX}px)`,
|
|
||||||
transition: 'transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Array.from({ length: count }).map((_, i) => (
|
|
||||||
<button
|
|
||||||
key={i}
|
|
||||||
className={`rounded-full flex-shrink-0 transition-all duration-300 ${
|
|
||||||
i === currentIndex
|
|
||||||
? 'w-3 h-3 bg-white'
|
|
||||||
: 'w-2.5 h-2.5 bg-white/40 hover:bg-white/60'
|
|
||||||
}`}
|
|
||||||
onClick={() => setLightbox(prev => ({ ...prev, index: i }))}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// CSS로 호버 효과 추가 + overflow 문제 수정 + 로드 애니메이션
|
// CSS로 호버 효과 추가 + overflow 문제 수정 + 로드 애니메이션
|
||||||
const galleryStyles = `
|
const galleryStyles = `
|
||||||
|
|
@ -392,11 +358,11 @@ function AlbumGallery() {
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 하단 점 인디케이터 - memo 컴포넌트로 분리 */}
|
{/* 하단 점 인디케이터 - 공통 컴포넌트 사용 */}
|
||||||
<LightboxIndicator
|
<LightboxIndicator
|
||||||
count={photos.length}
|
count={photos.length}
|
||||||
currentIndex={lightbox.index}
|
currentIndex={lightbox.index}
|
||||||
setLightbox={setLightbox}
|
goToIndex={(i) => setLightbox(prev => ({ ...prev, index: i }))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Instagram, Calendar } from 'lucide-react';
|
import { Instagram, Calendar } from 'lucide-react';
|
||||||
import { getMembers } from '../../../api/public/members';
|
import { getMembers } from '../../../api/public/members';
|
||||||
|
import { formatDate } from '../../../utils/date';
|
||||||
|
|
||||||
function Members() {
|
function Members() {
|
||||||
const [members, setMembers] = useState([]);
|
const [members, setMembers] = useState([]);
|
||||||
|
|
@ -19,13 +20,6 @@ function Members() {
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 날짜 포맷팅 함수
|
|
||||||
const formatDate = (dateStr) => {
|
|
||||||
if (!dateStr) return '';
|
|
||||||
const date = new Date(dateStr);
|
|
||||||
return `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(2, '0')}.${String(date.getDate()).padStart(2, '0')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="py-16 flex justify-center items-center min-h-[60vh]">
|
<div className="py-16 flex justify-center items-center min-h-[60vh]">
|
||||||
|
|
@ -83,7 +77,7 @@ function Members() {
|
||||||
|
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-500 mb-4">
|
<div className="flex items-center gap-2 text-sm text-gray-500 mb-4">
|
||||||
<Calendar size={14} />
|
<Calendar size={14} />
|
||||||
<span>{formatDate(member.birth_date)}</span>
|
<span>{formatDate(member.birth_date, 'YYYY.MM.DD')}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 인스타그램 링크 */}
|
{/* 인스타그램 링크 */}
|
||||||
|
|
@ -142,7 +136,7 @@ function Members() {
|
||||||
|
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-400">
|
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||||
<Calendar size={14} />
|
<Calendar size={14} />
|
||||||
<span>{formatDate(member.birth_date)}</span>
|
<span>{formatDate(member.birth_date, 'YYYY.MM.DD')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue