diff --git a/frontend-temp/src/App.jsx b/frontend-temp/src/App.jsx index 767b4a6..289aff4 100644 --- a/frontend-temp/src/App.jsx +++ b/frontend-temp/src/App.jsx @@ -1,144 +1,83 @@ -import { BrowserRouter, Routes, Route } from "react-router-dom"; -import { cn, getTodayKST, formatFullDate } from "@/utils"; -import { useAuthStore, useUIStore } from "@/stores"; -import { useIsMobile, useCategories, useCalendar } from "@/hooks"; -import { memberApi } from "@/api"; -import { useQuery } from "@tanstack/react-query"; +import { useEffect } from 'react'; +import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { BrowserView, MobileView } from 'react-device-detect'; + +// 공통 컴포넌트 +import { ScrollToTop } from '@/components/common'; + +// PC 레이아웃 +import { Layout as PCLayout } from '@/components/pc'; + +// Mobile 레이아웃 +import { Layout as MobileLayout } from '@/components/mobile'; + +// 페이지 +import { PCHome, MobileHome } from '@/pages/home'; + +/** + * PC 환경에서 body에 클래스 추가하는 래퍼 + */ +function PCWrapper({ children }) { + useEffect(() => { + document.body.classList.add('is-pc'); + return () => document.body.classList.remove('is-pc'); + }, []); + return children; +} /** * 프로미스나인 팬사이트 메인 앱 - * - * Phase 5: 커스텀 훅 완료 - * - useMediaQuery, useIsMobile, useIsDesktop - * - useScheduleData, useScheduleDetail, useCategories - * - useScheduleSearch (무한 스크롤) - * - useScheduleFiltering, useCategoryCounts - * - useCalendar - * - useAdminAuth + * react-device-detect를 사용한 PC/Mobile 분리 */ function App() { - const today = getTodayKST(); - const isMobile = useIsMobile(); - const { isAuthenticated } = useAuthStore(); - const { showSuccess, toasts } = useUIStore(); - - // 커스텀 훅 사용 - const { data: categories, isLoading: categoriesLoading } = useCategories(); - const calendar = useCalendar(); - - // 멤버 데이터 (기존 방식 유지) - const { data: members, isLoading: membersLoading } = useQuery({ - queryKey: ["members"], - queryFn: memberApi.getMembers, - }); - return ( - - - -
-

- fromis_9 Frontend Refactoring -

-

Phase 5 완료 - 커스텀 훅

-

- 디바이스: {isMobile ? "모바일" : "PC"} (useIsMobile 훅) -

+ + -
-

오늘: {formatFullDate(today)}

-

인증: {isAuthenticated ? "✅" : "❌"}

+ {/* PC 뷰 */} + + + + {/* 관리자 페이지 (레이아웃 없음) - 추후 추가 */} + {/* } /> */} -
-

useCategories 훅

-

- {categoriesLoading ? "로딩 중..." : `${categories?.length || 0}개 카테고리`} -

- {categories && ( -
- {categories.map((c) => ( - - {c.name} - - ))} -
- )} -
+ {/* 일반 페이지 (레이아웃 포함) */} + + + } /> + {/* 추가 페이지는 Phase 8-11에서 구현 */} + {/* } /> */} + {/* } /> */} + {/* } /> */} + {/* } /> */} + + + } + /> +
+
+
-
-

useCalendar 훅

-

현재: {calendar.year}년 {calendar.monthName}

-

선택: {calendar.selectedDate}

-
- - - -
-
- -
-

멤버 데이터

-

{membersLoading ? "로딩 중..." : `${members?.length || 0}명`}

- {members && ( -
- {members.map((m) => ( - - {m.name} - - ))} -
- )} -
-
-
- - {/* 토스트 표시 */} -
- {toasts.map((toast) => ( -
- {toast.message} -
- ))} -
- - } - /> -
+ {/* Mobile 뷰 */} + + + + + + } + /> + {/* 추가 페이지는 Phase 8-11에서 구현 */} + {/* } /> */} + {/* } /> */} + {/* } /> */} + +
); } diff --git a/frontend-temp/src/pages/home/index.js b/frontend-temp/src/pages/home/index.js new file mode 100644 index 0000000..f08737e --- /dev/null +++ b/frontend-temp/src/pages/home/index.js @@ -0,0 +1,2 @@ +export { default as PCHome } from './pc/Home'; +export { default as MobileHome } from './mobile/Home'; diff --git a/frontend-temp/src/pages/home/mobile/Home.jsx b/frontend-temp/src/pages/home/mobile/Home.jsx new file mode 100644 index 0000000..9c1dd03 --- /dev/null +++ b/frontend-temp/src/pages/home/mobile/Home.jsx @@ -0,0 +1,266 @@ +import { motion } from 'framer-motion'; +import { ChevronRight, Clock, Tag } from 'lucide-react'; +import { useNavigate } from 'react-router-dom'; +import { useMembers, useAlbums, useUpcomingSchedules } from '@/hooks'; + +/** + * Mobile 홈 페이지 + */ +function MobileHome() { + const navigate = useNavigate(); + + // 멤버 데이터 (활동 중인 멤버만) + const { data: allMembers = [] } = useMembers(); + const members = allMembers.filter((m) => !m.is_former); + + // 앨범 데이터 (최신 2개) + const { data: allAlbums = [] } = useAlbums(); + const albums = allAlbums.slice(0, 2); + + // 다가오는 일정 (3개) + const { data: schedules = [] } = useUpcomingSchedules(3); + + return ( +
+ {/* 히어로 섹션 */} + +
+ +

fromis_9

+

프로미스나인

+

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

+
+ {/* 장식 */} +
+
+ + + {/* 멤버 섹션 */} + +
+

멤버

+ +
+
+ {members.map((member, index) => ( + +
+ {member.image_url && ( + {member.name} + )} +
+

{member.name}

+
+ ))} +
+
+ + {/* 앨범 섹션 */} + +
+

앨범

+ +
+
+ {albums.map((album, index) => ( + navigate(`/album/${album.folder_name}`)} + className="bg-white rounded-xl overflow-hidden shadow-md" + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: 0.6 + index * 0.1, duration: 0.3 }} + whileTap={{ scale: 0.98 }} + > +
+ {album.cover_thumb_url && ( + {album.title} + )} +
+
+

{album.title}

+

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

+
+
+ ))} +
+
+ + {/* 일정 섹션 */} + +
+

다가오는 일정

+ +
+ {schedules.length > 0 ? ( +
+ {schedules.map((schedule, index) => { + const scheduleDate = new Date(schedule.date); + const today = new Date(); + const currentYear = today.getFullYear(); + const currentMonth = today.getMonth(); + + const scheduleYear = scheduleDate.getFullYear(); + const scheduleMonth = scheduleDate.getMonth(); + const isCurrentYear = scheduleYear === currentYear; + const isCurrentMonth = + isCurrentYear && scheduleMonth === currentMonth; + + // 멤버 처리 + const memberList = schedule.member_names + ? schedule.member_names + .split(',') + .map((n) => n.trim()) + .filter(Boolean) + : schedule.members?.map((m) => m.name) || []; + + return ( + navigate('/schedule')} + > + {/* 날짜 영역 */} +
+ {!isCurrentYear && ( + + {scheduleYear}.{scheduleMonth + 1} + + )} + {isCurrentYear && !isCurrentMonth && ( + + {scheduleMonth + 1}월 + + )} + + {scheduleDate.getDate()} + + + { + ['일', '월', '화', '수', '목', '금', '토'][ + scheduleDate.getDay() + ] + } + +
+ + {/* 세로 구분선 */} +
+ + {/* 내용 영역 */} +
+

+ {schedule.title} +

+ {/* 시간 + 카테고리 */} +
+ {schedule.time && ( + + + {schedule.time.slice(0, 5)} + + )} + {schedule.category_name && ( + + + {schedule.category_name} + {schedule.source?.name && ` · ${schedule.source.name}`} + + )} +
+ {/* 멤버 */} + {memberList.length > 0 && ( +
+ {memberList.map((name, i) => ( + + {name.trim()} + + ))} +
+ )} +
+ + ); + })} +
+ ) : ( +
+

다가오는 일정이 없습니다

+
+ )} + +
+ ); +} + +export default MobileHome; diff --git a/frontend-temp/src/pages/home/pc/Home.jsx b/frontend-temp/src/pages/home/pc/Home.jsx new file mode 100644 index 0000000..d3624bc --- /dev/null +++ b/frontend-temp/src/pages/home/pc/Home.jsx @@ -0,0 +1,318 @@ +import { motion } from 'framer-motion'; +import { Link } from 'react-router-dom'; +import { Calendar, ArrowRight, Clock, Tag, Music } from 'lucide-react'; +import { useMembers, useAlbums, useUpcomingSchedules } from '@/hooks'; + +/** + * PC 홈 페이지 + */ +function Home() { + // 멤버 데이터 + 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; + + 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 todayDate = new Date(); + const currentYear = todayDate.getFullYear(); + const currentMonth = todayDate.getMonth(); + + const scheduleYear = scheduleDate.getFullYear(); + const scheduleMonth = scheduleDate.getMonth(); + const isCurrentYear = scheduleYear === currentYear; + const isCurrentMonth = + isCurrentYear && scheduleMonth === currentMonth; + + const day = scheduleDate.getDate(); + const weekdays = ['일', '월', '화', '수', '목', '금', '토']; + const weekday = weekdays[scheduleDate.getDay()]; + + // 멤버 처리 + const memberList = schedule.member_names + ? schedule.member_names.split(',') + : schedule.members?.map((m) => m.name) || []; + + const categoryColor = schedule.category_color || '#6366f1'; + + return ( + + {/* 날짜 영역 */} +
+ {!isCurrentYear && ( + + {scheduleYear}.{scheduleMonth + 1} + + )} + {isCurrentYear && !isCurrentMonth && ( + + {scheduleMonth + 1}월 + + )} + {day} + + {weekday} + +
+ + {/* 내용 영역 */} +
+

{schedule.title}

+
+ {schedule.time && ( + + + {schedule.time.slice(0, 5)} + + )} + + + {schedule.category_name} + {schedule.source?.name && ` · ${schedule.source.name}`} + +
+ {memberList.length > 0 && ( +
+ {memberList.map((name, i) => ( + + {name.trim()} + + ))} +
+ )} +
+
+ ); + })} +
+ )} +
+
+
+ ); +} + +export default Home; diff --git a/frontend-temp/src/pages/index.js b/frontend-temp/src/pages/index.js new file mode 100644 index 0000000..31dd15b --- /dev/null +++ b/frontend-temp/src/pages/index.js @@ -0,0 +1,2 @@ +// 홈 페이지 +export * from './home';