feat(frontend): Phase 8 - 앨범 페이지 마이그레이션
앨범 페이지: - Album.jsx: PC/Mobile 통합 (useIsMobile 분기) - PC: 통계 + 4열 그리드 + 호버 오버레이 - Mobile: 2열 그리드 간소화 훅 추가: - useAlbums: 앨범 목록 조회 - useAlbumDetail: 앨범 상세 조회 - useAlbumGallery: 앨범 갤러리 조회 라우팅: - /album 라우트 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
81b78be010
commit
5e03e48be5
6 changed files with 239 additions and 6 deletions
|
|
@ -3,7 +3,7 @@ import { cn, getTodayKST, formatFullDate } from "@/utils";
|
||||||
import { useUIStore } from "@/stores";
|
import { useUIStore } from "@/stores";
|
||||||
import { useIsMobile, useCategories, useScheduleData } from "@/hooks";
|
import { useIsMobile, useCategories, useScheduleData } from "@/hooks";
|
||||||
import { ErrorBoundary, Loading, ToastContainer, ScheduleCard, Layout } from "@/components";
|
import { ErrorBoundary, Loading, ToastContainer, ScheduleCard, Layout } from "@/components";
|
||||||
import { Schedule } from "@/pages";
|
import { Schedule, Album } from "@/pages";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 홈 페이지 (임시)
|
* 홈 페이지 (임시)
|
||||||
|
|
@ -137,10 +137,9 @@ function Home() {
|
||||||
/**
|
/**
|
||||||
* 프로미스나인 팬사이트 메인 앱
|
* 프로미스나인 팬사이트 메인 앱
|
||||||
*
|
*
|
||||||
* Phase 7: 스케줄 페이지 마이그레이션
|
* Phase 8: 앨범 페이지 마이그레이션
|
||||||
* - Layout 컴포넌트 (PC/Mobile 통합)
|
* - Album 페이지 (PC/Mobile 통합)
|
||||||
* - Header, Footer, MobileNav 컴포넌트
|
* - useAlbums 훅 추가
|
||||||
* - Schedule 페이지 (기본 구조)
|
|
||||||
*/
|
*/
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -178,11 +177,13 @@ function App() {
|
||||||
</Layout>
|
</Layout>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{/* 앨범 */}
|
||||||
<Route
|
<Route
|
||||||
path="/album"
|
path="/album"
|
||||||
element={
|
element={
|
||||||
<Layout pageTitle="앨범">
|
<Layout pageTitle="앨범">
|
||||||
<div className="p-4 text-center text-gray-500">앨범 페이지 (준비 중)</div>
|
<Album />
|
||||||
|
<ToastContainer />
|
||||||
</Layout>
|
</Layout>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -24,3 +24,6 @@ export { useCalendar } from './useCalendar';
|
||||||
|
|
||||||
// 인증
|
// 인증
|
||||||
export { useAdminAuth, useRedirectIfAuthenticated } from './useAdminAuth';
|
export { useAdminAuth, useRedirectIfAuthenticated } from './useAdminAuth';
|
||||||
|
|
||||||
|
// 앨범 데이터
|
||||||
|
export { useAlbums, useAlbumDetail, useAlbumGallery } from './useAlbumData';
|
||||||
|
|
|
||||||
36
frontend-temp/src/hooks/useAlbumData.js
Normal file
36
frontend-temp/src/hooks/useAlbumData.js
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { albumsApi } from '@/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 앨범 목록 조회 훅
|
||||||
|
*/
|
||||||
|
export function useAlbums() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['albums'],
|
||||||
|
queryFn: albumsApi.getAlbums,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 앨범 상세 조회 훅
|
||||||
|
* @param {string} title - 앨범 타이틀 또는 폴더명
|
||||||
|
*/
|
||||||
|
export function useAlbumDetail(title) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['album', title],
|
||||||
|
queryFn: () => albumsApi.getAlbumByTitle(title),
|
||||||
|
enabled: !!title,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 앨범 갤러리 조회 훅
|
||||||
|
* @param {string} title - 앨범 타이틀 또는 폴더명
|
||||||
|
*/
|
||||||
|
export function useAlbumGallery(title) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['album-gallery', title],
|
||||||
|
queryFn: () => albumsApi.getAlbumGallery(title),
|
||||||
|
enabled: !!title,
|
||||||
|
});
|
||||||
|
}
|
||||||
188
frontend-temp/src/pages/album/Album.jsx
Normal file
188
frontend-temp/src/pages/album/Album.jsx
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { Calendar, Music } from 'lucide-react';
|
||||||
|
import { useIsMobile } from '@/hooks';
|
||||||
|
import { useAlbums } from '@/hooks';
|
||||||
|
import { Loading } from '@/components';
|
||||||
|
import { formatDate } from '@/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 앨범 목록 페이지 (PC/Mobile 통합)
|
||||||
|
*/
|
||||||
|
function Album() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
// useQuery로 앨범 데이터 로드
|
||||||
|
const { data: albums = [], isLoading } = useAlbums();
|
||||||
|
|
||||||
|
// 타이틀곡 찾기
|
||||||
|
const getTitleTrack = (tracks) => {
|
||||||
|
if (!tracks || tracks.length === 0) return '';
|
||||||
|
const titleTrack = tracks.find((t) => t.is_title_track);
|
||||||
|
return titleTrack ? titleTrack.title : tracks[0].title;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 앨범 타입 (short 우선)
|
||||||
|
const getAlbumType = (album) => album.album_type_short || album.album_type;
|
||||||
|
|
||||||
|
// 앨범 통계
|
||||||
|
const albumStats = {
|
||||||
|
정규: albums.filter((a) => getAlbumType(a) === '정규').length,
|
||||||
|
미니: albums.filter((a) => getAlbumType(a) === '미니').length,
|
||||||
|
싱글: albums.filter((a) => getAlbumType(a) === '싱글').length,
|
||||||
|
총: albums.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 앨범 클릭 핸들러
|
||||||
|
const handleAlbumClick = (album) => {
|
||||||
|
const path = isMobile ? album.folder_name : encodeURIComponent(album.title);
|
||||||
|
navigate(`/album/${path}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center min-h-[60vh]">
|
||||||
|
<Loading text="앨범 로딩 중..." />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 모바일 레이아웃
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<div className="px-4 py-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
{albums.map((album, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={album.id}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: index * 0.05 }}
|
||||||
|
onClick={() => handleAlbumClick(album)}
|
||||||
|
className="bg-white rounded-2xl overflow-hidden shadow-md cursor-pointer"
|
||||||
|
>
|
||||||
|
<div className="aspect-square bg-gray-200">
|
||||||
|
{album.cover_thumb_url && (
|
||||||
|
<img
|
||||||
|
src={album.cover_thumb_url}
|
||||||
|
alt={album.title}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="p-3">
|
||||||
|
<p className="font-semibold text-sm truncate">{album.title}</p>
|
||||||
|
<p className="text-xs text-gray-400 mt-0.5">
|
||||||
|
{getAlbumType(album)} · {album.release_date?.slice(0, 4)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PC 레이아웃
|
||||||
|
return (
|
||||||
|
<div className="py-16">
|
||||||
|
<div className="max-w-7xl mx-auto px-6">
|
||||||
|
{/* 헤더 */}
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<motion.h1
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="text-4xl font-bold mb-4"
|
||||||
|
>
|
||||||
|
앨범
|
||||||
|
</motion.h1>
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
className="text-gray-500"
|
||||||
|
>
|
||||||
|
프로미스나인의 음악을 만나보세요
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 통계 */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.3 }}
|
||||||
|
className="mb-12 grid grid-cols-4 gap-4"
|
||||||
|
>
|
||||||
|
<div className="bg-gray-50 rounded-xl p-4 text-center">
|
||||||
|
<p className="text-2xl font-bold text-primary mb-1">{albumStats.정규}</p>
|
||||||
|
<p className="text-gray-500 text-sm">정규 앨범</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-50 rounded-xl p-4 text-center">
|
||||||
|
<p className="text-2xl font-bold text-primary mb-1">{albumStats.미니}</p>
|
||||||
|
<p className="text-gray-500 text-sm">미니 앨범</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-50 rounded-xl p-4 text-center">
|
||||||
|
<p className="text-2xl font-bold text-primary mb-1">{albumStats.싱글}</p>
|
||||||
|
<p className="text-gray-500 text-sm">싱글 앨범</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-50 rounded-xl p-4 text-center">
|
||||||
|
<p className="text-2xl font-bold text-primary mb-1">{albumStats.총}</p>
|
||||||
|
<p className="text-gray-500 text-sm">총 앨범</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* 앨범 그리드 */}
|
||||||
|
<div className="grid grid-cols-4 gap-8">
|
||||||
|
{albums.map((album, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={album.id}
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: index * 0.1, duration: 0.3 }}
|
||||||
|
className="group bg-white rounded-2xl overflow-hidden shadow-lg hover:shadow-2xl transition-shadow duration-300 cursor-pointer"
|
||||||
|
onClick={() => handleAlbumClick(album)}
|
||||||
|
>
|
||||||
|
{/* 앨범 커버 */}
|
||||||
|
<div className="relative aspect-square bg-gray-100 overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={album.cover_medium_url || album.cover_original_url}
|
||||||
|
alt={album.title}
|
||||||
|
loading="lazy"
|
||||||
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500 will-change-transform"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 호버 오버레이 */}
|
||||||
|
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center">
|
||||||
|
<div className="text-center text-white">
|
||||||
|
<Music size={40} className="mx-auto mb-2" />
|
||||||
|
<p className="text-sm">{album.tracks?.length || 0}곡 수록</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 앨범 정보 */}
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<h3 className="font-bold text-lg truncate flex-1">{album.title}</h3>
|
||||||
|
<span className="px-2 py-0.5 bg-primary/10 text-primary text-xs font-medium rounded-full flex-shrink-0">
|
||||||
|
{getAlbumType(album)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-primary text-sm font-medium mb-3">
|
||||||
|
{getTitleTrack(album.tracks)}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||||
|
<Calendar size={14} />
|
||||||
|
<span>{formatDate(album.release_date, 'YYYY.MM.DD')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Album;
|
||||||
4
frontend-temp/src/pages/album/index.js
Normal file
4
frontend-temp/src/pages/album/index.js
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
/**
|
||||||
|
* 앨범 페이지 export
|
||||||
|
*/
|
||||||
|
export { default as Album } from './Album';
|
||||||
|
|
@ -2,3 +2,4 @@
|
||||||
* 페이지 export
|
* 페이지 export
|
||||||
*/
|
*/
|
||||||
export * from './schedule';
|
export * from './schedule';
|
||||||
|
export * from './album';
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue