diff --git a/frontend-temp/src/App.jsx b/frontend-temp/src/App.jsx index 234310a..c256c2e 100644 --- a/frontend-temp/src/App.jsx +++ b/frontend-temp/src/App.jsx @@ -1,145 +1,14 @@ -import { BrowserRouter, Routes, Route, NavLink } from "react-router-dom"; -import { cn, getTodayKST, formatFullDate } from "@/utils"; -import { useUIStore } from "@/stores"; -import { useIsMobile, useCategories, useScheduleData } from "@/hooks"; -import { ErrorBoundary, Loading, ToastContainer, ScheduleCard, Layout } from "@/components"; -import { Schedule, Album } from "@/pages"; - -/** - * 홈 페이지 (임시) - */ -function Home() { - const today = getTodayKST(); - const isMobile = useIsMobile(); - const { showSuccess, showError } = useUIStore(); - const { data: categories, isLoading: categoriesLoading } = useCategories(); - const currentDate = new Date(); - const { data: schedules, isLoading: schedulesLoading } = useScheduleData( - currentDate.getFullYear(), - currentDate.getMonth() + 1 - ); - - return ( -
-
-
-

- fromis_9 Frontend Refactoring -

-

Phase 7 진행 중 - 스케줄 페이지

-

- 디바이스: {isMobile ? "모바일" : "PC"} -

-
- -
-

오늘: {formatFullDate(today)}

- -
-

카테고리 ({categories?.length || 0}개)

- {categoriesLoading ? ( - - ) : ( -
- {categories?.map((c) => ( - - {c.name} - - ))} -
- )} -
- -
-

토스트 테스트

-
- - -
-
- -
-

페이지 이동

-
- - 일정 페이지 - -
-
-
- - {/* ScheduleCard 컴포넌트 테스트 */} - {schedulesLoading ? ( -
- -
- ) : schedules?.length > 0 ? ( - <> - {/* Public Variant (공개 페이지용) */} -
-

variant="public" (공개 페이지용)

-
- {schedules.slice(0, 2).map((schedule) => ( - showSuccess(`${schedule.title} 클릭`)} - /> - ))} -
-
- - {/* Admin Variant (관리자 페이지용) */} -
-

variant="admin" (관리자 페이지용)

-
- {schedules.slice(0, 2).map((schedule) => ( - showSuccess(`${schedule.title} 클릭`)} - onEdit={(s) => showSuccess(`${s.title} 수정`)} - onDelete={(s) => showError(`${s.title} 삭제`)} - /> - ))} -
-
- - ) : ( -
-

이번 달 스케줄이 없습니다.

-
- )} -
-
- ); -} +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { ErrorBoundary, ToastContainer, Layout } from "@/components"; +import { Schedule, Album, Home, Members, NotFound } from "@/pages"; /** * 프로미스나인 팬사이트 메인 앱 * - * Phase 8: 앨범 페이지 마이그레이션 - * - Album 페이지 (PC/Mobile 통합) - * - useAlbums 훅 추가 + * Phase 9: 기타 공개 페이지 마이그레이션 + * - Home 페이지 (PC/Mobile 통합) + * - Members 페이지 (PC/Mobile 통합) + * - NotFound 페이지 */ function App() { return ( @@ -157,6 +26,28 @@ function App() { } /> + {/* 멤버 */} + + + + + } + /> + + {/* 앨범 */} + + + + + } + /> + {/* 스케줄 */} - {/* 임시 페이지들 */} + {/* 404 */} -
멤버 페이지 (준비 중)
- - } - /> - {/* 앨범 */} - - - + + } /> diff --git a/frontend-temp/src/hooks/index.js b/frontend-temp/src/hooks/index.js index 7bd4b1d..3f93f14 100644 --- a/frontend-temp/src/hooks/index.js +++ b/frontend-temp/src/hooks/index.js @@ -27,3 +27,6 @@ export { useAdminAuth, useRedirectIfAuthenticated } from './useAdminAuth'; // 앨범 데이터 export { useAlbums, useAlbumDetail, useAlbumGallery } from './useAlbumData'; + +// 멤버 데이터 +export { useMembers, useMemberDetail } from './useMemberData'; diff --git a/frontend-temp/src/hooks/useMemberData.js b/frontend-temp/src/hooks/useMemberData.js new file mode 100644 index 0000000..072e251 --- /dev/null +++ b/frontend-temp/src/hooks/useMemberData.js @@ -0,0 +1,24 @@ +import { useQuery } from '@tanstack/react-query'; +import { memberApi } from '@/api'; + +/** + * 멤버 목록 조회 훅 + */ +export function useMembers() { + return useQuery({ + queryKey: ['members'], + queryFn: memberApi.getMembers, + }); +} + +/** + * 멤버 상세 조회 훅 + * @param {number} id - 멤버 ID + */ +export function useMemberDetail(id) { + return useQuery({ + queryKey: ['member', id], + queryFn: () => memberApi.getMemberById(id), + enabled: !!id, + }); +} diff --git a/frontend-temp/src/pages/common/NotFound.jsx b/frontend-temp/src/pages/common/NotFound.jsx new file mode 100644 index 0000000..0fb2393 --- /dev/null +++ b/frontend-temp/src/pages/common/NotFound.jsx @@ -0,0 +1,30 @@ +import { Link } from 'react-router-dom'; +import { Home } from 'lucide-react'; +import { useIsMobile } from '@/hooks'; + +/** + * 404 페이지 (PC/Mobile 통합) + */ +function NotFound() { + const isMobile = useIsMobile(); + + return ( +
+

+ 404 +

+

+ 페이지를 찾을 수 없습니다 +

+ + + 홈으로 돌아가기 + +
+ ); +} + +export default NotFound; diff --git a/frontend-temp/src/pages/common/index.js b/frontend-temp/src/pages/common/index.js new file mode 100644 index 0000000..4d20aa9 --- /dev/null +++ b/frontend-temp/src/pages/common/index.js @@ -0,0 +1 @@ +export { default as NotFound } from './NotFound'; diff --git a/frontend-temp/src/pages/home/Home.jsx b/frontend-temp/src/pages/home/Home.jsx new file mode 100644 index 0000000..5e24af7 --- /dev/null +++ b/frontend-temp/src/pages/home/Home.jsx @@ -0,0 +1,333 @@ +import { Link } from 'react-router-dom'; +import { motion } from 'framer-motion'; +import { Calendar, ArrowRight, Clock, Tag, Music } from 'lucide-react'; +import { useIsMobile, useMembers, useAlbums, useUpcomingSchedules } from '@/hooks'; +import { Loading } from '@/components'; + +/** + * 홈 페이지 (PC/Mobile 통합) + */ +function Home() { + const isMobile = useIsMobile(); + + // 멤버 데이터 + const { data: members = [] } = useMembers(); + + // 앨범 데이터 (최신 4개) + const { data: allAlbums = [] } = useAlbums(); + const albums = allAlbums.slice(0, 4); + + // 다가오는 일정 (3개) + const { data: upcomingSchedules = [] } = useUpcomingSchedules(3); + + // D+Day 계산 + const debutDate = new Date('2018-01-24'); + const today = new Date(); + const dDay = Math.floor((today - debutDate) / (1000 * 60 * 60 * 24)) + 1; + + // 모바일 레이아웃 + if (isMobile) { + return ( +
+ {/* 히어로 */} +
+

fromis_9

+

프로미스나인

+
+ + {/* 통계 */} +
+ {[ + { value: '2018.01.24', label: '데뷔일' }, + { value: `D+${dDay.toLocaleString()}`, label: 'D+Day' }, + ].map((stat, i) => ( +
+

{stat.value}

+

{stat.label}

+
+ ))} +
+ + {/* 멤버 미리보기 */} +
+
+

멤버

+ + 전체보기 + +
+
+ {members.filter((m) => !m.is_former).map((member) => ( +
+
+ {member.name} +
+

{member.name}

+
+ ))} +
+
+ + {/* 앨범 미리보기 */} +
+
+

앨범

+ + 전체보기 + +
+
+ {albums.slice(0, 2).map((album) => ( + +
+ {album.title} +
+

{album.title}

+ + ))} +
+
+ + {/* 일정 미리보기 */} +
+
+

다가오는 일정

+ + 전체보기 + +
+ {upcomingSchedules.length === 0 ? ( +
+ +

예정된 일정이 없습니다

+
+ ) : ( +
+ {upcomingSchedules.map((schedule) => ( +
+

{schedule.title}

+

+ {new Date(schedule.date).getMonth() + 1}월 {new Date(schedule.date).getDate()}일 + {schedule.time && ` · ${schedule.time.slice(0, 5)}`} +

+
+ ))} +
+ )} +
+
+ ); + } + + // PC 레이아웃 + return ( +
+ {/* 히어로 섹션 */} +
+
+
+ +

fromis_9

+

프로미스나인

+

+ 인사드리겠습니다. 둘, 셋! +
+ 이제는 약속해 소중히 간직해, +
+ 당신의 아이돌로 성장하겠습니다! +

+
+
+
+
+
+
+
+ + {/* 그룹 통계 */} +
+
+
+ {[ + { value: '2018.01.24', label: '데뷔일' }, + { value: `D+${dDay.toLocaleString()}`, label: 'D+Day' }, + { value: '5', label: '멤버 수' }, + { value: 'flover', label: '팬덤명' }, + ].map((stat, index) => ( + +

{stat.value}

+

{stat.label}

+
+ ))} +
+
+
+ + {/* 멤버 미리보기 */} +
+
+
+

멤버

+ + 전체보기 + +
+
+ {members.filter((m) => !m.is_former).map((member, index) => ( + +
+ {member.name} +
+
+
+

{member.name}

+
+ + ))} +
+
+
+ + {/* 앨범 미리보기 */} +
+
+
+

앨범

+ + 전체보기 + +
+
+ {albums.map((album, index) => ( + (window.location.href = `/album/${encodeURIComponent(album.title)}`)} + > +
+ {album.title} +
+
+ +

{album.tracks?.length || 0}곡 수록

+
+
+
+
+

{album.title}

+

{album.release_date?.slice(0, 4)}

+
+
+ ))} +
+
+
+ + {/* 일정 미리보기 */} +
+
+
+

다가오는 일정

+ + 전체보기 + +
+ {upcomingSchedules.length === 0 ? ( +
+ +

예정된 일정이 없습니다

+
+ ) : ( +
+ {upcomingSchedules.map((schedule) => { + const scheduleDate = new Date(schedule.date); + const categoryColor = schedule.category_color || '#6366f1'; + const memberList = schedule.member_names + ? schedule.member_names.split(',') + : schedule.members?.map((m) => m.name) || []; + + return ( + +
+ {scheduleDate.getDate()} + + {['일', '월', '화', '수', '목', '금', '토'][scheduleDate.getDay()]} + +
+
+

{schedule.title}

+
+ {schedule.time && ( + + + {schedule.time.slice(0, 5)} + + )} + + + {schedule.category_name} + +
+ {memberList.length > 0 && ( +
+ {memberList.map((name, i) => ( + + {name.trim()} + + ))} +
+ )} +
+
+ ); + })} +
+ )} +
+
+
+ ); +} + +export default Home; diff --git a/frontend-temp/src/pages/home/index.js b/frontend-temp/src/pages/home/index.js new file mode 100644 index 0000000..c2d92a5 --- /dev/null +++ b/frontend-temp/src/pages/home/index.js @@ -0,0 +1 @@ +export { default as Home } from './Home'; diff --git a/frontend-temp/src/pages/index.js b/frontend-temp/src/pages/index.js index 2a3da66..558eff9 100644 --- a/frontend-temp/src/pages/index.js +++ b/frontend-temp/src/pages/index.js @@ -3,3 +3,6 @@ */ export * from './schedule'; export * from './album'; +export * from './home'; +export * from './members'; +export * from './common'; diff --git a/frontend-temp/src/pages/members/Members.jsx b/frontend-temp/src/pages/members/Members.jsx new file mode 100644 index 0000000..8ad7a4a --- /dev/null +++ b/frontend-temp/src/pages/members/Members.jsx @@ -0,0 +1,181 @@ +import { motion } from 'framer-motion'; +import { Instagram, Calendar } from 'lucide-react'; +import { useIsMobile, useMembers } from '@/hooks'; +import { Loading } from '@/components'; +import { formatDate } from '@/utils'; + +/** + * 멤버 카드 컴포넌트 (PC) + */ +function MemberCard({ member, isFormer = false, delay = 0 }) { + return ( + +
+ {/* 이미지 */} +
+ {member.name} +
+ + {/* 정보 */} +
+

+ {member.name} +

+ +
+ + {formatDate(member.birth_date, 'YYYY.MM.DD')} +
+ + {/* 인스타그램 링크 */} + {member.instagram && !isFormer && ( + + + Instagram + + )} +
+ + {/* 호버 효과 - 컬러 바 */} +
+
+ + ); +} + +/** + * 멤버 페이지 (PC/Mobile 통합) + */ +function Members() { + const isMobile = useIsMobile(); + const { data: members = [], isLoading } = useMembers(); + + const currentMembers = members.filter((m) => !m.is_former); + const formerMembers = members.filter((m) => m.is_former); + + if (isLoading) { + return ( +
+ +
+ ); + } + + // 모바일 레이아웃 + if (isMobile) { + return ( +
+ {/* 현재 멤버 */} +
+ {currentMembers.map((member) => ( +
+
+ {member.name} +
+
+

{member.name}

+

{formatDate(member.birth_date, 'YYYY.MM.DD')}

+
+
+ ))} +
+ + {/* 전 멤버 */} + {formerMembers.length > 0 && ( + <> +

전 멤버

+
+ {formerMembers.map((member) => ( +
+
+ {member.name} +
+
+

{member.name}

+

{formatDate(member.birth_date, 'YYYY.MM.DD')}

+
+
+ ))} +
+ + )} +
+ ); + } + + // PC 레이아웃 + return ( +
+
+ {/* 헤더 */} +
+ + 멤버 + + + 프로미스나인의 멤버를 소개합니다 + +
+ + {/* 현재 멤버 그리드 */} +
+ {currentMembers.map((member, index) => ( + + ))} +
+ + {/* 전 멤버 섹션 */} + {formerMembers.length > 0 && ( + +

전 멤버

+
+ {formerMembers.map((member, index) => ( + + ))} +
+
+ )} +
+
+ ); +} + +export default Members; diff --git a/frontend-temp/src/pages/members/index.js b/frontend-temp/src/pages/members/index.js new file mode 100644 index 0000000..c1d2028 --- /dev/null +++ b/frontend-temp/src/pages/members/index.js @@ -0,0 +1 @@ +export { default as Members } from './Members';