diff --git a/frontend-temp/src/App.jsx b/frontend-temp/src/App.jsx
index c256c2e..767b4a6 100644
--- a/frontend-temp/src/App.jsx
+++ b/frontend-temp/src/App.jsx
@@ -1,75 +1,144 @@
import { BrowserRouter, Routes, Route } from "react-router-dom";
-import { ErrorBoundary, ToastContainer, Layout } from "@/components";
-import { Schedule, Album, Home, Members, NotFound } from "@/pages";
+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";
/**
* 프로미스나인 팬사이트 메인 앱
*
- * Phase 9: 기타 공개 페이지 마이그레이션
- * - Home 페이지 (PC/Mobile 통합)
- * - Members 페이지 (PC/Mobile 통합)
- * - NotFound 페이지
+ * Phase 5: 커스텀 훅 완료
+ * - useMediaQuery, useIsMobile, useIsDesktop
+ * - useScheduleData, useScheduleDetail, useCategories
+ * - useScheduleSearch (무한 스크롤)
+ * - useScheduleFiltering, useCategoryCounts
+ * - useCalendar
+ * - useAdminAuth
*/
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 ? "✅" : "❌"}
- {/* 앨범 */}
-
-
-
-
- }
- />
+
+
useCategories 훅
+
+ {categoriesLoading ? "로딩 중..." : `${categories?.length || 0}개 카테고리`}
+
+ {categories && (
+
+ {categories.map((c) => (
+
+ {c.name}
+
+ ))}
+
+ )}
+
- {/* 스케줄 */}
-
-
-
-
- }
- />
+
+
useCalendar 훅
+
현재: {calendar.year}년 {calendar.monthName}
+
선택: {calendar.selectedDate}
+
+
+
+
+
+
- {/* 404 */}
-
-
-
- }
- />
-
-
+
+
멤버 데이터
+
{membersLoading ? "로딩 중..." : `${members?.length || 0}명`}
+ {members && (
+
+ {members.map((m) => (
+
+ {m.name}
+
+ ))}
+
+ )}
+
+
+
+
+ {/* 토스트 표시 */}
+
+ {toasts.map((toast) => (
+
+ {toast.message}
+
+ ))}
+
+
+ }
+ />
+
);
}
diff --git a/frontend-temp/src/components/common/ErrorBoundary.jsx b/frontend-temp/src/components/common/ErrorBoundary.jsx
deleted file mode 100644
index a4aff59..0000000
--- a/frontend-temp/src/components/common/ErrorBoundary.jsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import { Component } from 'react';
-import { AlertTriangle, RefreshCw } from 'lucide-react';
-
-/**
- * 에러 경계 컴포넌트
- * React 컴포넌트 트리에서 발생하는 에러를 캐치하여 폴백 UI를 표시
- */
-class ErrorBoundary extends Component {
- state = { hasError: false, error: null };
-
- static getDerivedStateFromError(error) {
- return { hasError: true, error };
- }
-
- componentDidCatch(error, errorInfo) {
- console.error('ErrorBoundary:', error, errorInfo);
- }
-
- handleReset = () => {
- this.setState({ hasError: false, error: null });
- };
-
- render() {
- if (this.state.hasError) {
- // 커스텀 폴백이 있으면 사용
- if (this.props.fallback) {
- return this.props.fallback;
- }
-
- return (
-
-
-
- 문제가 발생했습니다
-
-
- {this.state.error?.message || '알 수 없는 오류'}
-
-
-
-
-
-
- );
- }
-
- return this.props.children;
- }
-}
-
-export default ErrorBoundary;
diff --git a/frontend-temp/src/components/common/Lightbox.jsx b/frontend-temp/src/components/common/Lightbox.jsx
deleted file mode 100644
index 4fa2a20..0000000
--- a/frontend-temp/src/components/common/Lightbox.jsx
+++ /dev/null
@@ -1,225 +0,0 @@
-import { useState, useEffect, useCallback, memo } from 'react';
-import { motion, AnimatePresence } from 'framer-motion';
-import { X, ChevronLeft, ChevronRight } from 'lucide-react';
-import { useUIStore } from '@/stores';
-
-/**
- * 라이트박스 인디케이터
- */
-const LightboxIndicator = memo(function LightboxIndicator({
- count,
- currentIndex,
- goToIndex,
-}) {
- const translateX = -(currentIndex * 18) + 100 - 6;
-
- return (
-
- {/* 양옆 페이드 그라데이션 */}
-
- {/* 슬라이딩 컨테이너 */}
-
- {Array.from({ length: count }).map((_, i) => (
-
-
- );
-});
-
-/**
- * 라이트박스 컴포넌트
- * useUIStore와 연동하거나 props로 직접 제어 가능
- */
-function Lightbox({ images: propImages, currentIndex: propIndex, isOpen: propIsOpen, onClose: propOnClose, onIndexChange: propOnIndexChange }) {
- // useUIStore에서 상태 가져오기 (props가 없으면 스토어 사용)
- const store = useUIStore();
-
- const images = propImages ?? store.lightboxImages;
- const currentIndex = propIndex ?? store.lightboxIndex;
- const isOpen = propIsOpen ?? store.lightboxOpen;
- const onClose = propOnClose ?? store.closeLightbox;
- const onIndexChange = propOnIndexChange ?? store.setLightboxIndex;
-
- const [imageLoaded, setImageLoaded] = useState(false);
- const [slideDirection, setSlideDirection] = useState(0);
-
- // 이전/다음 네비게이션
- const goToPrev = useCallback(() => {
- if (images.length <= 1) return;
- setImageLoaded(false);
- setSlideDirection(-1);
- onIndexChange((currentIndex - 1 + images.length) % images.length);
- }, [images.length, currentIndex, onIndexChange]);
-
- const goToNext = useCallback(() => {
- if (images.length <= 1) return;
- setImageLoaded(false);
- setSlideDirection(1);
- onIndexChange((currentIndex + 1) % images.length);
- }, [images.length, currentIndex, onIndexChange]);
-
- const goToIndex = useCallback(
- (index) => {
- if (index === currentIndex) return;
- setImageLoaded(false);
- setSlideDirection(index > currentIndex ? 1 : -1);
- onIndexChange(index);
- },
- [currentIndex, onIndexChange]
- );
-
- // 라이트박스 열릴 때 body 스크롤 숨기기
- useEffect(() => {
- if (isOpen) {
- document.documentElement.style.overflow = 'hidden';
- document.body.style.overflow = 'hidden';
- } else {
- document.documentElement.style.overflow = '';
- document.body.style.overflow = '';
- }
- return () => {
- document.documentElement.style.overflow = '';
- document.body.style.overflow = '';
- };
- }, [isOpen]);
-
- // 키보드 이벤트 핸들러
- useEffect(() => {
- if (!isOpen) return;
-
- const handleKeyDown = (e) => {
- switch (e.key) {
- case 'ArrowLeft':
- goToPrev();
- break;
- case 'ArrowRight':
- goToNext();
- break;
- case 'Escape':
- onClose();
- break;
- default:
- break;
- }
- };
-
- window.addEventListener('keydown', handleKeyDown);
- return () => window.removeEventListener('keydown', handleKeyDown);
- }, [isOpen, goToPrev, goToNext, onClose]);
-
- // 이미지가 바뀔 때 로딩 상태 리셋
- useEffect(() => {
- setImageLoaded(false);
- }, [currentIndex]);
-
- return (
-
- {isOpen && images.length > 0 && (
-
-
- {/* 닫기 버튼 */}
-
-
- {/* 이전 버튼 */}
- {images.length > 1 && (
-
- )}
-
- {/* 로딩 스피너 */}
- {!imageLoaded && (
-
- )}
-
- {/* 이미지 */}
-
- e.stopPropagation()}
- onLoad={() => setImageLoaded(true)}
- initial={{ x: slideDirection * 100 }}
- animate={{ x: 0 }}
- transition={{ duration: 0.25, ease: 'easeOut' }}
- />
-
-
- {/* 다음 버튼 */}
- {images.length > 1 && (
-
- )}
-
- {/* 인디케이터 */}
- {images.length > 1 && (
-
- )}
-
-
- )}
-
- );
-}
-
-export default Lightbox;
diff --git a/frontend-temp/src/components/common/Loading.jsx b/frontend-temp/src/components/common/Loading.jsx
deleted file mode 100644
index ce23d97..0000000
--- a/frontend-temp/src/components/common/Loading.jsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import { cn } from '@/utils';
-
-/**
- * 로딩 스피너 컴포넌트
- * @param {object} props
- * @param {'sm'|'md'|'lg'} props.size - 크기
- * @param {string} props.className - 추가 클래스
- * @param {string} props.text - 로딩 텍스트
- */
-export default function Loading({ size = 'md', className = '', text = '' }) {
- const sizeClasses = {
- sm: 'h-6 w-6 border-2',
- md: 'h-10 w-10 border-3',
- lg: 'h-12 w-12 border-4',
- };
-
- return (
-
- );
-}
-
-/**
- * 전체 화면 로딩
- */
-export function FullPageLoading({ text = '로딩 중...' }) {
- return (
-
-
-
- );
-}
-
-/**
- * 인라인 로딩 (버튼 등에 사용)
- */
-export function InlineLoading({ className = '' }) {
- return (
-
- );
-}
diff --git a/frontend-temp/src/components/common/Toast.jsx b/frontend-temp/src/components/common/Toast.jsx
deleted file mode 100644
index 33556b2..0000000
--- a/frontend-temp/src/components/common/Toast.jsx
+++ /dev/null
@@ -1,103 +0,0 @@
-import { motion, AnimatePresence } from 'framer-motion';
-import { X, CheckCircle, AlertCircle, AlertTriangle, Info } from 'lucide-react';
-import { cn } from '@/utils';
-import { useUIStore } from '@/stores';
-
-/**
- * 토스트 아이콘 매핑
- */
-const icons = {
- success: CheckCircle,
- error: AlertCircle,
- warning: AlertTriangle,
- info: Info,
-};
-
-/**
- * 토스트 스타일 매핑
- */
-const styles = {
- success: 'bg-emerald-500/90',
- error: 'bg-red-500/90',
- warning: 'bg-amber-500/90',
- info: 'bg-blue-500/90',
-};
-
-/**
- * 단일 토스트 아이템
- */
-function ToastItem({ toast, onClose }) {
- const Icon = icons[toast.type] || icons.info;
-
- return (
-
-
- {toast.message}
-
-
- );
-}
-
-/**
- * 토스트 컨테이너
- * useUIStore와 연동하여 전역 토스트 관리
- */
-export default function ToastContainer() {
- const { toasts, removeToast } = useUIStore();
-
- return (
-
-
- {toasts.map((toast) => (
- removeToast(toast.id)}
- />
- ))}
-
-
- );
-}
-
-/**
- * 단일 토스트 (레거시 호환)
- */
-export function Toast({ toast, onClose }) {
- if (!toast) return null;
-
- return (
-
-
- {toast.message}
-
-
- );
-}
diff --git a/frontend-temp/src/components/common/index.js b/frontend-temp/src/components/common/index.js
deleted file mode 100644
index 1ec1edd..0000000
--- a/frontend-temp/src/components/common/index.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * 공통 컴포넌트 export
- */
-export { default as ErrorBoundary } from './ErrorBoundary';
-export { default as Loading, FullPageLoading, InlineLoading } from './Loading';
-export { default as ToastContainer, Toast } from './Toast';
-export { default as Lightbox } from './Lightbox';
diff --git a/frontend-temp/src/components/index.js b/frontend-temp/src/components/index.js
deleted file mode 100644
index c799ef5..0000000
--- a/frontend-temp/src/components/index.js
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * 컴포넌트 통합 export
- */
-
-// 공통 컴포넌트
-export * from './common';
-
-// 레이아웃 컴포넌트
-export * from './layout';
-
-// 스케줄 컴포넌트
-export * from './schedule';
diff --git a/frontend-temp/src/components/layout/Footer.jsx b/frontend-temp/src/components/layout/Footer.jsx
deleted file mode 100644
index afa25fc..0000000
--- a/frontend-temp/src/components/layout/Footer.jsx
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * PC용 푸터 컴포넌트
- */
-function Footer() {
- const currentYear = new Date().getFullYear();
-
- return (
-
- );
-}
-
-export default Footer;
diff --git a/frontend-temp/src/components/layout/Header.jsx b/frontend-temp/src/components/layout/Header.jsx
deleted file mode 100644
index 40dc75f..0000000
--- a/frontend-temp/src/components/layout/Header.jsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import { NavLink } from 'react-router-dom';
-import { Instagram, Youtube } from 'lucide-react';
-import { SOCIAL_LINKS, NAV_ITEMS } from '@/constants';
-
-/**
- * X (Twitter) 아이콘 컴포넌트
- */
-const XIcon = ({ size = 20 }) => (
-
-);
-
-/**
- * PC용 헤더 컴포넌트
- */
-function Header() {
- return (
-
-
-
- {/* 로고 */}
-
- fromis_9
-
-
- {/* 네비게이션 */}
-
-
- {/* SNS 링크 */}
-
-
-
-
- );
-}
-
-export default Header;
diff --git a/frontend-temp/src/components/layout/Layout.jsx b/frontend-temp/src/components/layout/Layout.jsx
deleted file mode 100644
index 6a4e683..0000000
--- a/frontend-temp/src/components/layout/Layout.jsx
+++ /dev/null
@@ -1,104 +0,0 @@
-import { useEffect } from 'react';
-import { useLocation, NavLink } from 'react-router-dom';
-import { useIsMobile } from '@/hooks';
-import Header from './Header';
-import Footer from './Footer';
-import MobileNav from './MobileNav';
-
-/**
- * 모바일 헤더 컴포넌트
- */
-function MobileHeader({ title, noShadow = false }) {
- return (
-
- );
-}
-
-/**
- * 통합 레이아웃 컴포넌트
- * PC/Mobile 자동 분기
- *
- * @param {object} props
- * @param {React.ReactNode} props.children - 페이지 컨텐츠
- * @param {string} props.pageTitle - 모바일 헤더 타이틀 (없으면 fromis_9)
- * @param {boolean} props.hideHeader - 헤더 숨김 여부 (자체 헤더가 있는 페이지)
- * @param {boolean} props.hideFooter - 푸터 숨김 여부
- * @param {boolean} props.useCustomLayout - 자체 레이아웃 사용 (모바일)
- * @param {boolean} props.noShadow - 모바일 헤더 그림자 숨김
- */
-function Layout({
- children,
- pageTitle,
- hideHeader = false,
- hideFooter = false,
- useCustomLayout = false,
- noShadow = false,
-}) {
- const isMobile = useIsMobile();
- const location = useLocation();
-
- // 모바일 레이아웃 활성화 (body 스크롤 방지)
- useEffect(() => {
- if (isMobile) {
- document.documentElement.classList.add('mobile-layout');
- return () => {
- document.documentElement.classList.remove('mobile-layout');
- };
- }
- }, [isMobile]);
-
- // PC 레이아웃
- if (!isMobile) {
- // Footer 숨김 페이지 (화면 고정 레이아웃)
- const hideFooterPages = ['/schedule', '/members', '/album'];
- const shouldHideFooter = hideFooter || hideFooterPages.some(
- (path) => location.pathname === path || location.pathname.startsWith(path + '/')
- );
-
- // 일정 페이지에서는 스크롤바도 숨김 (내부에서 자체 스크롤 처리)
- const isSchedulePage = location.pathname === '/schedule';
-
- return (
-
- {!hideHeader &&
}
-
-
- {children}
-
- {!shouldHideFooter && }
-
-
- );
- }
-
- // 모바일 레이아웃 - 자체 레이아웃 사용 시 (Schedule 페이지 등)
- if (useCustomLayout) {
- return (
-
- {children}
-
-
- );
- }
-
- // 모바일 레이아웃 - 기본
- return (
-
- {!hideHeader && }
- {children}
-
-
- );
-}
-
-export default Layout;
diff --git a/frontend-temp/src/components/layout/MobileNav.jsx b/frontend-temp/src/components/layout/MobileNav.jsx
deleted file mode 100644
index dab72e3..0000000
--- a/frontend-temp/src/components/layout/MobileNav.jsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import { NavLink, useLocation } from 'react-router-dom';
-import { Home, Users, Disc3, Calendar } from 'lucide-react';
-
-/**
- * 모바일 하단 네비게이션 컴포넌트
- */
-function MobileNav() {
- const location = useLocation();
-
- const navItems = [
- { path: '/', label: '홈', icon: Home },
- { path: '/members', label: '멤버', icon: Users },
- { path: '/album', label: '앨범', icon: Disc3 },
- { path: '/schedule', label: '일정', icon: Calendar },
- ];
-
- return (
-
- );
-}
-
-export default MobileNav;
diff --git a/frontend-temp/src/components/layout/index.js b/frontend-temp/src/components/layout/index.js
deleted file mode 100644
index f43f04a..0000000
--- a/frontend-temp/src/components/layout/index.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * 레이아웃 컴포넌트 export
- */
-export { default as Layout } from './Layout';
-export { default as Header } from './Header';
-export { default as Footer } from './Footer';
-export { default as MobileNav } from './MobileNav';
diff --git a/frontend-temp/src/components/schedule/ScheduleCard.jsx b/frontend-temp/src/components/schedule/ScheduleCard.jsx
deleted file mode 100644
index ccdfe72..0000000
--- a/frontend-temp/src/components/schedule/ScheduleCard.jsx
+++ /dev/null
@@ -1,233 +0,0 @@
-import { memo } from 'react';
-import { Clock, Tag, Link2, ExternalLink, Edit2, Trash2 } from 'lucide-react';
-import { cn, decodeHtmlEntities } from '@/utils';
-import {
- getCategoryInfo,
- getScheduleDate,
- getScheduleTime,
- getMemberList,
-} from '@/utils/schedule';
-import { WEEKDAYS } from '@/constants';
-
-/**
- * 멤버 뱃지 컴포넌트
- */
-const MemberBadges = memo(function MemberBadges({ members }) {
- if (!members || members.length === 0) return null;
-
- // 5명 이상이면 "프로미스나인"으로 표시
- if (members.length >= 5) {
- return (
-
- 프로미스나인
-
- );
- }
-
- return members.map((member, i) => (
-
- {typeof member === 'string' ? member : member.name}
-
- ));
-});
-
-/**
- * 스케줄 카드 컴포넌트
- * PC/Mobile 공용, variant로 스타일 변경
- *
- * @param {object} props
- * @param {object} props.schedule - 스케줄 데이터
- * @param {'public'|'admin'} props.variant - 카드 스타일 (public: 공개 페이지용 카드, admin: 관리자 페이지용 리스트)
- * @param {boolean} props.showDate - 날짜 표시 여부
- * @param {boolean} props.showYear - 연도 표시 여부
- * @param {function} props.onClick - 클릭 핸들러
- * @param {function} props.onEdit - 수정 버튼 클릭 핸들러 (admin variant)
- * @param {function} props.onDelete - 삭제 버튼 클릭 핸들러 (admin variant)
- * @param {string} props.className - 추가 클래스
- */
-const ScheduleCard = memo(function ScheduleCard({
- schedule,
- variant = 'public',
- showDate = true,
- showYear = false,
- onClick,
- onEdit,
- onDelete,
- className,
-}) {
- const dateStr = getScheduleDate(schedule);
- const date = dateStr ? new Date(dateStr) : null;
- const categoryInfo = getCategoryInfo(schedule);
- const timeStr = getScheduleTime(schedule);
- const memberList = getMemberList(schedule);
- const title = decodeHtmlEntities(schedule.title || '');
-
- // 관리자 페이지용 리스트 스타일
- if (variant === 'admin') {
- return (
-
-
- {/* 날짜 - 세로 중앙 정렬 */}
- {showDate && date && (
-
- {showYear && (
-
- {date.getFullYear()}.{date.getMonth() + 1}
-
- )}
-
- {date.getDate()}
-
-
- {WEEKDAYS[date.getDay()]}요일
-
-
- )}
-
- {/* 카테고리 바 */}
-
-
- {/* 내용 */}
-
-
{title}
-
- {timeStr && (
-
-
- {timeStr}
-
- )}
-
-
- {categoryInfo.name}
-
- {schedule.source?.name && (
-
-
- {schedule.source.name}
-
- )}
-
- {memberList.length > 0 && (
-
-
-
- )}
-
-
- {/* 액션 버튼 - 생일 일정이 아닐 때만 표시 */}
- {!schedule.is_birthday && !String(schedule.id).startsWith('birthday-') && (
-
- )}
-
-
- );
- }
-
- // 공개 페이지용 카드 스타일 (기본)
- return (
-
- {/* 좌측 날짜/카테고리 영역 */}
-
- {date && (
- <>
- {showYear && (
-
- {date.getFullYear()}.{date.getMonth() + 1}
-
- )}
- {date.getDate()}
- {WEEKDAYS[date.getDay()]}
- >
- )}
-
-
- {/* 우측 내용 영역 */}
-
-
{title}
-
- {timeStr && (
-
-
- {timeStr}
-
- )}
-
-
- {categoryInfo.name}
-
- {schedule.source?.name && (
-
-
- {schedule.source.name}
-
- )}
-
- {memberList.length > 0 && (
-
-
-
- )}
-
-
- );
-});
-
-export default ScheduleCard;
diff --git a/frontend-temp/src/components/schedule/index.js b/frontend-temp/src/components/schedule/index.js
deleted file mode 100644
index 02edebc..0000000
--- a/frontend-temp/src/components/schedule/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-/**
- * 스케줄 컴포넌트 export
- */
-export { default as ScheduleCard } from './ScheduleCard';
diff --git a/frontend-temp/src/constants/index.js b/frontend-temp/src/constants/index.js
index 53824e1..7b52724 100644
--- a/frontend-temp/src/constants/index.js
+++ b/frontend-temp/src/constants/index.js
@@ -57,11 +57,3 @@ export const MONTH_NAMES = [
'1월', '2월', '3월', '4월', '5월', '6월',
'7월', '8월', '9월', '10월', '11월', '12월',
];
-
-/** 네비게이션 메뉴 항목 */
-export const NAV_ITEMS = [
- { path: '/', label: '홈' },
- { path: '/members', label: '멤버' },
- { path: '/album', label: '앨범' },
- { path: '/schedule', label: '일정' },
-];
diff --git a/frontend-temp/src/hooks/index.js b/frontend-temp/src/hooks/index.js
index 3f93f14..c0447fe 100644
--- a/frontend-temp/src/hooks/index.js
+++ b/frontend-temp/src/hooks/index.js
@@ -24,9 +24,3 @@ export { useCalendar } from './useCalendar';
// 인증
export { useAdminAuth, useRedirectIfAuthenticated } from './useAdminAuth';
-
-// 앨범 데이터
-export { useAlbums, useAlbumDetail, useAlbumGallery } from './useAlbumData';
-
-// 멤버 데이터
-export { useMembers, useMemberDetail } from './useMemberData';
diff --git a/frontend-temp/src/hooks/useCalendar.js b/frontend-temp/src/hooks/useCalendar.js
index 59e434c..8a2e190 100644
--- a/frontend-temp/src/hooks/useCalendar.js
+++ b/frontend-temp/src/hooks/useCalendar.js
@@ -5,17 +5,10 @@ import { getTodayKST } from '@/utils';
/**
* 캘린더 훅
* 날짜 선택, 월 이동 등 캘린더 로직 제공
- * @param {Date|string} initialDate - 초기 날짜
+ * @param {Date} initialDate - 초기 날짜
*/
export function useCalendar(initialDate = new Date()) {
- // initialDate가 Date 객체가 아니면 변환
- const ensureDate = (date) => {
- if (date instanceof Date) return date;
- if (typeof date === 'string') return new Date(date);
- return new Date();
- };
-
- const [currentDate, setCurrentDate] = useState(() => ensureDate(initialDate));
+ const [currentDate, setCurrentDate] = useState(initialDate);
const [selectedDate, setSelectedDate] = useState(getTodayKST());
const year = currentDate.getFullYear();
@@ -27,37 +20,6 @@ export function useCalendar(initialDate = new Date()) {
const daysInMonth = new Date(year, month + 1, 0).getDate();
const prevMonthDays = new Date(year, month, 0).getDate();
- // 캘린더에 표시할 날짜 배열 생성
- const days = [];
-
- // 이전 달 날짜
- for (let i = firstDay - 1; i >= 0; i--) {
- days.push({
- day: prevMonthDays - i,
- isCurrentMonth: false,
- date: new Date(year, month - 1, prevMonthDays - i),
- });
- }
-
- // 현재 달 날짜
- for (let i = 1; i <= daysInMonth; i++) {
- days.push({
- day: i,
- isCurrentMonth: true,
- date: new Date(year, month, i),
- });
- }
-
- // 다음 달 날짜 (6주 채우기)
- const remaining = 42 - days.length; // 6주 * 7일 = 42
- for (let i = 1; i <= remaining; i++) {
- days.push({
- day: i,
- isCurrentMonth: false,
- date: new Date(year, month + 1, i),
- });
- }
-
return {
year,
month,
@@ -66,7 +28,6 @@ export function useCalendar(initialDate = new Date()) {
daysInMonth,
prevMonthDays,
weekdays: WEEKDAYS,
- days,
};
}, [year, month]);
@@ -132,7 +93,6 @@ export function useCalendar(initialDate = new Date()) {
...calendarData,
currentDate,
selectedDate,
- canGoPrev: canGoPrevMonth,
canGoPrevMonth,
goToPrevMonth,
goToNextMonth,
diff --git a/frontend-temp/src/pages/album/Album.jsx b/frontend-temp/src/pages/album/Album.jsx
deleted file mode 100644
index ee8d8fa..0000000
--- a/frontend-temp/src/pages/album/Album.jsx
+++ /dev/null
@@ -1,188 +0,0 @@
-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 (
-
-
-
- );
- }
-
- // 모바일 레이아웃
- if (isMobile) {
- return (
-
-
- {albums.map((album, index) => (
-
handleAlbumClick(album)}
- className="bg-white rounded-2xl overflow-hidden shadow-md cursor-pointer"
- >
-
- {album.cover_thumb_url && (
-

- )}
-
-
-
{album.title}
-
- {getAlbumType(album)} · {album.release_date?.slice(0, 4)}
-
-
-
- ))}
-
-
- );
- }
-
- // PC 레이아웃
- return (
-
-
- {/* 헤더 */}
-
-
- 앨범
-
-
- 프로미스나인의 음악을 만나보세요
-
-
-
- {/* 통계 */}
-
-
-
{albumStats.정규}
-
정규 앨범
-
-
-
{albumStats.미니}
-
미니 앨범
-
-
-
{albumStats.싱글}
-
싱글 앨범
-
-
-
{albumStats.총}
-
총 앨범
-
-
-
- {/* 앨범 그리드 */}
-
- {albums.map((album, index) => (
-
handleAlbumClick(album)}
- >
- {/* 앨범 커버 */}
-
-

-
- {/* 호버 오버레이 */}
-
-
-
-
{album.tracks?.length || 0}곡 수록
-
-
-
-
- {/* 앨범 정보 */}
-
-
-
{album.title}
-
- {getAlbumType(album)}
-
-
-
- {getTitleTrack(album.tracks)}
-
-
-
- {formatDate(album.release_date, 'YYYY.MM.DD')}
-
-
-
- ))}
-
-
-
- );
-}
-
-export default Album;
diff --git a/frontend-temp/src/pages/album/index.js b/frontend-temp/src/pages/album/index.js
deleted file mode 100644
index 64aae39..0000000
--- a/frontend-temp/src/pages/album/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-/**
- * 앨범 페이지 export
- */
-export { default as Album } from './Album';
diff --git a/frontend-temp/src/pages/common/NotFound.jsx b/frontend-temp/src/pages/common/NotFound.jsx
deleted file mode 100644
index 0fb2393..0000000
--- a/frontend-temp/src/pages/common/NotFound.jsx
+++ /dev/null
@@ -1,30 +0,0 @@
-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
deleted file mode 100644
index 4d20aa9..0000000
--- a/frontend-temp/src/pages/common/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default as NotFound } from './NotFound';
diff --git a/frontend-temp/src/pages/home/Home.jsx b/frontend-temp/src/pages/home/Home.jsx
deleted file mode 100644
index 5e24af7..0000000
--- a/frontend-temp/src/pages/home/Home.jsx
+++ /dev/null
@@ -1,333 +0,0 @@
-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 (
-
- {/* 히어로 */}
-
-
- {/* 통계 */}
-
- {[
- { 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}
-
- ))}
-
-
-
- {/* 앨범 미리보기 */}
-
-
-
- {albums.slice(0, 2).map((album) => (
-
-
-

-
-
{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}
-
-
- ))}
-
-
-
-
- {/* 앨범 미리보기 */}
-
-
-
-
- {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 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
deleted file mode 100644
index c2d92a5..0000000
--- a/frontend-temp/src/pages/home/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default as Home } from './Home';
diff --git a/frontend-temp/src/pages/index.js b/frontend-temp/src/pages/index.js
deleted file mode 100644
index 558eff9..0000000
--- a/frontend-temp/src/pages/index.js
+++ /dev/null
@@ -1,8 +0,0 @@
-/**
- * 페이지 export
- */
-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
deleted file mode 100644
index 8ad7a4a..0000000
--- a/frontend-temp/src/pages/members/Members.jsx
+++ /dev/null
@@ -1,181 +0,0 @@
-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}
-
-
-
-
- {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}
-
{formatDate(member.birth_date, 'YYYY.MM.DD')}
-
-
- ))}
-
-
- {/* 전 멤버 */}
- {formerMembers.length > 0 && (
- <>
-
전 멤버
-
- {formerMembers.map((member) => (
-
-
-

-
-
-
{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
deleted file mode 100644
index c1d2028..0000000
--- a/frontend-temp/src/pages/members/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default as Members } from './Members';
diff --git a/frontend-temp/src/pages/schedule/Schedule.jsx b/frontend-temp/src/pages/schedule/Schedule.jsx
deleted file mode 100644
index 4fb6827..0000000
--- a/frontend-temp/src/pages/schedule/Schedule.jsx
+++ /dev/null
@@ -1,330 +0,0 @@
-import { useState, useMemo, useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
-import { ChevronLeft, ChevronRight, Search } from 'lucide-react';
-import { useIsMobile, useScheduleData, useCategories, useCalendar } from '@/hooks';
-import { useScheduleStore } from '@/stores';
-import { Loading, ScheduleCard } from '@/components';
-import { cn, getTodayKST, decodeHtmlEntities } from '@/utils';
-import { WEEKDAYS, MIN_YEAR } from '@/constants';
-
-/**
- * PC 캘린더 컴포넌트
- */
-function PCCalendar({ selectedDate, schedules, categories, onSelectDate, onMonthChange }) {
- const { days, year, month, canGoPrev } = useCalendar(selectedDate);
-
- // 날짜별 일정 맵
- const scheduleDates = useMemo(() => {
- const dateMap = {};
- schedules?.forEach((schedule) => {
- const date = schedule.date?.split('T')[0];
- if (!dateMap[date]) dateMap[date] = [];
- const cat = categories?.find((c) => c.id === (schedule.category_id || schedule.category?.id));
- dateMap[date].push(cat?.color || '#6b7280');
- });
- return dateMap;
- }, [schedules, categories]);
-
- const isToday = (date) => {
- const today = new Date();
- return (
- date.getDate() === today.getDate() &&
- date.getMonth() === today.getMonth() &&
- date.getFullYear() === today.getFullYear()
- );
- };
-
- const isSelected = (date) => {
- const sel = new Date(selectedDate);
- return (
- date.getDate() === sel.getDate() &&
- date.getMonth() === sel.getMonth() &&
- date.getFullYear() === sel.getFullYear()
- );
- };
-
- const formatDateStr = (date) => {
- const y = date.getFullYear();
- const m = String(date.getMonth() + 1).padStart(2, '0');
- const d = String(date.getDate()).padStart(2, '0');
- return `${y}-${m}-${d}`;
- };
-
- return (
-
- {/* 헤더 */}
-
-
-
- {year}년 {month + 1}월
-
-
-
-
- {/* 요일 헤더 */}
-
- {WEEKDAYS.map((day, i) => (
-
- {day}
-
- ))}
-
-
- {/* 날짜 그리드 */}
-
- {days.map((item, index) => {
- const dayOfWeek = index % 7;
- const dateStr = formatDateStr(item.date);
- const colors = scheduleDates[dateStr] || [];
-
- return (
-
- );
- })}
-
-
- );
-}
-
-/**
- * 스케줄 페이지 (PC/Mobile 통합)
- */
-function Schedule() {
- const navigate = useNavigate();
- const isMobile = useIsMobile();
-
- // Zustand store
- const { currentDate: storedCurrentDate, setCurrentDate, selectedDate, setSelectedDate } = useScheduleStore();
-
- // 초기값 설정 - currentDate가 Date 객체가 아닐 수 있으므로 안전하게 변환
- const today = getTodayKST();
- const actualSelectedDate = selectedDate || today;
- const currentDate = storedCurrentDate instanceof Date ? storedCurrentDate : new Date(storedCurrentDate || today);
-
- // 데이터 로드
- const year = currentDate.getFullYear();
- const month = currentDate.getMonth() + 1;
- const { data: schedules, isLoading: schedulesLoading } = useScheduleData(year, month);
- const { data: categories } = useCategories();
-
- // 선택된 날짜의 일정
- const selectedDateSchedules = useMemo(() => {
- if (!schedules) return [];
- const sel = new Date(actualSelectedDate);
- const dateStr = `${sel.getFullYear()}-${String(sel.getMonth() + 1).padStart(2, '0')}-${String(sel.getDate()).padStart(2, '0')}`;
- return schedules
- .filter((s) => s.date?.split('T')[0] === dateStr)
- .sort((a, b) => {
- const aIsBirthday = a.is_birthday || String(a.id).startsWith('birthday-');
- const bIsBirthday = b.is_birthday || String(b.id).startsWith('birthday-');
- if (aIsBirthday && !bIsBirthday) return -1;
- if (!aIsBirthday && bIsBirthday) return 1;
- return 0;
- });
- }, [schedules, actualSelectedDate]);
-
- // 월 변경
- const changeMonth = (delta) => {
- const newDate = new Date(currentDate);
- newDate.setMonth(newDate.getMonth() + delta);
-
- // 2017년 1월 이전으로 이동 불가
- if (newDate.getFullYear() < MIN_YEAR || (newDate.getFullYear() === MIN_YEAR && newDate.getMonth() < 0)) {
- return;
- }
-
- setCurrentDate(newDate);
-
- // 이번 달이면 오늘 날짜, 다른 달이면 1일 선택
- const now = new Date();
- if (newDate.getFullYear() === now.getFullYear() && newDate.getMonth() === now.getMonth()) {
- setSelectedDate(getTodayKST());
- } else {
- const firstDay = `${newDate.getFullYear()}-${String(newDate.getMonth() + 1).padStart(2, '0')}-01`;
- setSelectedDate(firstDay);
- }
- };
-
- // 날짜 선택
- const handleSelectDate = (date) => {
- const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
- setSelectedDate(dateStr);
-
- // 월이 다르면 currentDate도 변경
- if (date.getMonth() !== currentDate.getMonth() || date.getFullYear() !== currentDate.getFullYear()) {
- setCurrentDate(date);
- }
- };
-
- // PC 레이아웃
- if (!isMobile) {
- return (
-
- {/* 좌측: 캘린더 */}
-
-
- {/* 우측: 일정 목록 */}
-
- {/* 헤더 */}
-
-
-
- {new Date(actualSelectedDate).getMonth() + 1}월{' '}
- {new Date(actualSelectedDate).getDate()}일{' '}
- {WEEKDAYS[new Date(actualSelectedDate).getDay()]}요일
-
-
- {selectedDateSchedules.length}개의 일정
-
-
-
-
-
- {/* 일정 목록 */}
-
- {schedulesLoading ? (
-
-
-
- ) : selectedDateSchedules.length === 0 ? (
-
- {new Date(actualSelectedDate).getMonth() + 1}월{' '}
- {new Date(actualSelectedDate).getDate()}일 일정이 없습니다
-
- ) : (
-
- {selectedDateSchedules.map((schedule) => (
- navigate(`/schedule/${schedule.id}`)}
- />
- ))}
-
- )}
-
-
-
- );
- }
-
- // 모바일 레이아웃 (간소화된 버전)
- return (
- <>
- {/* 모바일 툴바 */}
-
-
-
-
- {currentDate.getFullYear()}년 {currentDate.getMonth() + 1}월
-
-
-
-
-
-
-
-
- {/* 모바일 컨텐츠 */}
-
-
- {schedulesLoading ? (
-
-
-
- ) : selectedDateSchedules.length === 0 ? (
-
- {new Date(actualSelectedDate).getMonth() + 1}월{' '}
- {new Date(actualSelectedDate).getDate()}일 일정이 없습니다
-
- ) : (
-
- {selectedDateSchedules.map((schedule) => (
- navigate(`/schedule/${schedule.id}`)}
- />
- ))}
-
- )}
-
-
- >
- );
-}
-
-export default Schedule;
diff --git a/frontend-temp/src/pages/schedule/index.js b/frontend-temp/src/pages/schedule/index.js
deleted file mode 100644
index b11430b..0000000
--- a/frontend-temp/src/pages/schedule/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-/**
- * 스케줄 페이지 export
- */
-export { default as Schedule } from './Schedule';