refactor: PC public 페이지 공통 컴포넌트 및 유틸 적용

- LightboxIndicator 공통 컴포넌트 생성 및 AlbumDetail, AlbumGallery에 적용
- formatDate를 utils/date에서 import하도록 변경 (Album, Members, AlbumDetail)
- 중복 코드 약 100줄 제거
This commit is contained in:
caadiq 2026-01-10 09:06:26 +09:00
parent 22db79e960
commit cdca23e317
5 changed files with 55 additions and 98 deletions

View 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;

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>