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}
+
+ ))}
+
+
+
+ {/* 앨범 섹션 */}
+
+
+
앨범
+
+
+
+ {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.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}
+
+
+
+ ))}
+
+
+
+
+ {/* 앨범 미리보기 */}
+
+
+
+
+ {albums.map((album, index) => (
+
+ (window.location.href = `/album/${encodeURIComponent(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';