diff --git a/frontend-temp/src/components/common/ErrorBoundary.jsx b/frontend-temp/src/components/common/ErrorBoundary.jsx
new file mode 100644
index 0000000..92ce861
--- /dev/null
+++ b/frontend-temp/src/components/common/ErrorBoundary.jsx
@@ -0,0 +1,45 @@
+import { Component } from 'react';
+
+/**
+ * 에러 바운더리 컴포넌트
+ * React 컴포넌트 트리에서 발생하는 에러를 캐치
+ */
+class ErrorBoundary extends Component {
+ constructor(props) {
+ super(props);
+ this.state = { hasError: false, error: null };
+ }
+
+ static getDerivedStateFromError(error) {
+ return { hasError: true, error };
+ }
+
+ componentDidCatch(error, errorInfo) {
+ console.error('ErrorBoundary caught an error:', error, errorInfo);
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return (
+
+
+ 문제가 발생했습니다
+
+
+ 페이지를 새로고침하거나 잠시 후 다시 시도해주세요.
+
+
+
+ );
+ }
+
+ 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
new file mode 100644
index 0000000..11b2dfc
--- /dev/null
+++ b/frontend-temp/src/components/common/Lightbox.jsx
@@ -0,0 +1,170 @@
+import { useState, useEffect, useCallback, memo } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import { X, ChevronLeft, ChevronRight } from 'lucide-react';
+import LightboxIndicator from './LightboxIndicator';
+
+/**
+ * 라이트박스 공통 컴포넌트
+ * 이미지 갤러리를 전체 화면으로 표시
+ */
+function Lightbox({ images, currentIndex, isOpen, onClose, onIndexChange }) {
+ 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/LightboxIndicator.jsx b/frontend-temp/src/components/common/LightboxIndicator.jsx
new file mode 100644
index 0000000..5766ba5
--- /dev/null
+++ b/frontend-temp/src/components/common/LightboxIndicator.jsx
@@ -0,0 +1,55 @@
+import { memo } from 'react';
+
+/**
+ * 라이트박스 인디케이터 컴포넌트
+ * 이미지 갤러리에서 현재 위치를 표시하는 슬라이딩 점 인디케이터
+ * CSS transition 사용으로 GPU 가속
+ */
+const LightboxIndicator = memo(function LightboxIndicator({
+ count,
+ currentIndex,
+ goToIndex,
+ width = 200,
+}) {
+ const halfWidth = width / 2;
+ const translateX = -(currentIndex * 18) + halfWidth - 6;
+
+ return (
+
+ {/* 양옆 페이드 그라데이션 */}
+
+ {/* 슬라이딩 컨테이너 - CSS transition으로 GPU 가속 */}
+
+ {Array.from({ length: count }).map((_, i) => (
+
+
+ );
+});
+
+export default LightboxIndicator;
diff --git a/frontend-temp/src/components/common/Loading.jsx b/frontend-temp/src/components/common/Loading.jsx
new file mode 100644
index 0000000..2ca5896
--- /dev/null
+++ b/frontend-temp/src/components/common/Loading.jsx
@@ -0,0 +1,12 @@
+/**
+ * 로딩 컴포넌트
+ */
+function Loading({ className = '' }) {
+ return (
+
+ );
+}
+
+export default Loading;
diff --git a/frontend-temp/src/components/common/ScrollToTop.jsx b/frontend-temp/src/components/common/ScrollToTop.jsx
new file mode 100644
index 0000000..99bcedc
--- /dev/null
+++ b/frontend-temp/src/components/common/ScrollToTop.jsx
@@ -0,0 +1,24 @@
+import { useEffect } from 'react';
+import { useLocation } from 'react-router-dom';
+
+/**
+ * 페이지 이동 시 스크롤을 맨 위로 이동시키는 컴포넌트
+ */
+function ScrollToTop() {
+ const { pathname } = useLocation();
+
+ useEffect(() => {
+ // window 스크롤 초기화
+ window.scrollTo(0, 0);
+
+ // 모바일 레이아웃 스크롤 컨테이너 초기화
+ const mobileContent = document.querySelector('.mobile-content');
+ if (mobileContent) {
+ mobileContent.scrollTop = 0;
+ }
+ }, [pathname]);
+
+ return null;
+}
+
+export default ScrollToTop;
diff --git a/frontend-temp/src/components/common/Toast.jsx b/frontend-temp/src/components/common/Toast.jsx
new file mode 100644
index 0000000..2cfc125
--- /dev/null
+++ b/frontend-temp/src/components/common/Toast.jsx
@@ -0,0 +1,32 @@
+import { motion, AnimatePresence } from 'framer-motion';
+
+/**
+ * Toast 컴포넌트
+ * - 하단 중앙에 표시
+ * - type: 'success' | 'error' | 'warning'
+ */
+function Toast({ toast, onClose }) {
+ return (
+
+ {toast && (
+
+ {toast.message}
+
+ )}
+
+ );
+}
+
+export default Toast;
diff --git a/frontend-temp/src/components/common/Tooltip.jsx b/frontend-temp/src/components/common/Tooltip.jsx
new file mode 100644
index 0000000..41ac0b1
--- /dev/null
+++ b/frontend-temp/src/components/common/Tooltip.jsx
@@ -0,0 +1,72 @@
+import { useState, useRef } from 'react';
+import ReactDOM from 'react-dom';
+import { motion, AnimatePresence } from 'framer-motion';
+
+/**
+ * 커스텀 툴팁 컴포넌트
+ * 마우스 커서를 따라다니는 방식
+ * @param {React.ReactNode} children - 툴팁을 표시할 요소
+ * @param {string|React.ReactNode} text - 툴팁에 표시할 내용 (content prop과 호환)
+ * @param {string|React.ReactNode} content - 툴팁에 표시할 내용 (text prop과 호환)
+ */
+function Tooltip({ children, text, content, className = '' }) {
+ const [isVisible, setIsVisible] = useState(false);
+ const [position, setPosition] = useState({ bottom: 0, left: 0 });
+ const triggerRef = useRef(null);
+
+ // text 또는 content prop 사용 (문자열 또는 React 노드)
+ const tooltipContent = text || content;
+
+ const handleMouseEnter = (e) => {
+ // 마우스 커서 위치를 기준으로 툴팁 위치 설정 (커서 위로)
+ setPosition({
+ bottom: window.innerHeight - e.clientY + 10,
+ left: e.clientX,
+ });
+ setIsVisible(true);
+ };
+
+ const handleMouseMove = (e) => {
+ // 마우스 이동 시 툴팁 위치 업데이트
+ setPosition({
+ bottom: window.innerHeight - e.clientY + 10,
+ left: e.clientX,
+ });
+ };
+
+ return (
+ <>
+ setIsVisible(false)}
+ >
+ {children}
+
+ {isVisible &&
+ tooltipContent &&
+ ReactDOM.createPortal(
+
+
+ {tooltipContent}
+
+ ,
+ document.body
+ )}
+ >
+ );
+}
+
+export default Tooltip;
diff --git a/frontend-temp/src/components/common/index.js b/frontend-temp/src/components/common/index.js
new file mode 100644
index 0000000..137ad55
--- /dev/null
+++ b/frontend-temp/src/components/common/index.js
@@ -0,0 +1,7 @@
+export { default as Loading } from './Loading';
+export { default as ErrorBoundary } from './ErrorBoundary';
+export { default as Toast } from './Toast';
+export { default as Tooltip } from './Tooltip';
+export { default as ScrollToTop } from './ScrollToTop';
+export { default as Lightbox } from './Lightbox';
+export { default as LightboxIndicator } from './LightboxIndicator';
diff --git a/frontend-temp/src/components/index.js b/frontend-temp/src/components/index.js
new file mode 100644
index 0000000..fb45a36
--- /dev/null
+++ b/frontend-temp/src/components/index.js
@@ -0,0 +1,8 @@
+// 공통 컴포넌트 (디바이스 무관)
+export * from './common';
+
+// PC 컴포넌트
+export * as PC from './pc';
+
+// Mobile 컴포넌트
+export * as Mobile from './mobile';
diff --git a/frontend-temp/src/components/mobile/Layout.jsx b/frontend-temp/src/components/mobile/Layout.jsx
new file mode 100644
index 0000000..9d44951
--- /dev/null
+++ b/frontend-temp/src/components/mobile/Layout.jsx
@@ -0,0 +1,110 @@
+import { NavLink, useLocation } from 'react-router-dom';
+import { Home, Users, Disc3, Calendar } from 'lucide-react';
+import { useEffect } from 'react';
+import '@/mobile.css';
+
+/**
+ * 모바일 헤더 컴포넌트
+ */
+function MobileHeader({ title, noShadow = false }) {
+ return (
+
+ );
+}
+
+/**
+ * 모바일 하단 네비게이션
+ */
+function MobileBottomNav() {
+ 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 (
+
+ );
+}
+
+/**
+ * 모바일 레이아웃 컴포넌트
+ * @param {React.ReactNode} children - 페이지 컨텐츠
+ * @param {string} pageTitle - 헤더에 표시할 제목 (없으면 fromis_9)
+ * @param {boolean} hideHeader - true면 헤더 숨김 (일정 페이지처럼 자체 헤더가 있는 경우)
+ * @param {boolean} useCustomLayout - true면 자체 레이아웃 사용
+ * @param {boolean} noShadow - 헤더 그림자 숨김
+ */
+function MobileLayout({
+ children,
+ pageTitle,
+ hideHeader = false,
+ useCustomLayout = false,
+ noShadow = false,
+}) {
+ // 모바일 레이아웃 활성화 (body 스크롤 방지)
+ useEffect(() => {
+ document.documentElement.classList.add('mobile-layout');
+ return () => {
+ document.documentElement.classList.remove('mobile-layout');
+ };
+ }, []);
+
+ // 자체 레이아웃 사용 시 (Schedule 페이지 등)
+ if (useCustomLayout) {
+ return (
+
+ {children}
+
+
+ );
+ }
+
+ return (
+
+ {!hideHeader && }
+ {children}
+
+
+ );
+}
+
+export default MobileLayout;
diff --git a/frontend-temp/src/components/mobile/index.js b/frontend-temp/src/components/mobile/index.js
new file mode 100644
index 0000000..6c48fae
--- /dev/null
+++ b/frontend-temp/src/components/mobile/index.js
@@ -0,0 +1 @@
+export { default as Layout } from './Layout';
diff --git a/frontend-temp/src/components/pc/Footer.jsx b/frontend-temp/src/components/pc/Footer.jsx
new file mode 100644
index 0000000..13e12ec
--- /dev/null
+++ b/frontend-temp/src/components/pc/Footer.jsx
@@ -0,0 +1,22 @@
+/**
+ * PC 푸터 컴포넌트
+ */
+function Footer() {
+ const currentYear = new Date().getFullYear();
+
+ return (
+
+ );
+}
+
+export default Footer;
diff --git a/frontend-temp/src/components/pc/Header.jsx b/frontend-temp/src/components/pc/Header.jsx
new file mode 100644
index 0000000..0090ae4
--- /dev/null
+++ b/frontend-temp/src/components/pc/Header.jsx
@@ -0,0 +1,84 @@
+import { NavLink } from 'react-router-dom';
+import { Instagram, Youtube } from 'lucide-react';
+import { SOCIAL_LINKS } from '@/constants';
+
+/**
+ * X (Twitter) 아이콘 컴포넌트
+ */
+const XIcon = ({ size = 20 }) => (
+
+);
+
+/**
+ * PC 헤더 컴포넌트
+ */
+function Header() {
+ const navItems = [
+ { path: '/', label: '홈' },
+ { path: '/members', label: '멤버' },
+ { path: '/album', label: '앨범' },
+ { path: '/schedule', label: '일정' },
+ ];
+
+ return (
+
+
+
+ {/* 로고 */}
+
+ fromis_9
+
+
+ {/* 네비게이션 */}
+
+
+ {/* SNS 링크 */}
+
+
+
+
+ );
+}
+
+export default Header;
diff --git a/frontend-temp/src/components/pc/Layout.jsx b/frontend-temp/src/components/pc/Layout.jsx
new file mode 100644
index 0000000..d962908
--- /dev/null
+++ b/frontend-temp/src/components/pc/Layout.jsx
@@ -0,0 +1,37 @@
+import { useLocation } from 'react-router-dom';
+import Header from './Header';
+import Footer from './Footer';
+import '@/pc.css';
+
+/**
+ * PC 레이아웃 컴포넌트
+ */
+function Layout({ children }) {
+ const location = useLocation();
+
+ // Footer 숨김 페이지 (화면 고정 레이아웃)
+ const hideFooterPages = ['/schedule', '/members', '/album'];
+ const hideFooter = hideFooterPages.some(
+ (path) =>
+ location.pathname === path || location.pathname.startsWith(path + '/')
+ );
+
+ // 일정 페이지에서는 스크롤바도 숨김 (내부에서 자체 스크롤 처리)
+ const isSchedulePage = location.pathname === '/schedule';
+
+ return (
+
+
+
+ {children}
+ {!hideFooter && }
+
+
+ );
+}
+
+export default Layout;
diff --git a/frontend-temp/src/components/pc/index.js b/frontend-temp/src/components/pc/index.js
new file mode 100644
index 0000000..85bb816
--- /dev/null
+++ b/frontend-temp/src/components/pc/index.js
@@ -0,0 +1,3 @@
+export { default as Layout } from './Layout';
+export { default as Header } from './Header';
+export { default as Footer } from './Footer';
diff --git a/frontend-temp/src/mobile.css b/frontend-temp/src/mobile.css
new file mode 100644
index 0000000..01ee413
--- /dev/null
+++ b/frontend-temp/src/mobile.css
@@ -0,0 +1,156 @@
+/* 모바일 전용 스타일 */
+
+/* 모바일 html,body 스크롤 방지 */
+html.mobile-layout,
+html.mobile-layout body {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ overflow: hidden;
+}
+
+/* 모바일 레이아웃 컨테이너 */
+.mobile-layout-container {
+ display: flex;
+ flex-direction: column;
+ height: 100dvh;
+ overflow: hidden;
+}
+
+/* 모바일 툴바 (기본 56px) */
+.mobile-toolbar {
+ flex-shrink: 0;
+ background-color: #ffffff;
+}
+
+/* 일정 페이지 툴바 (헤더 + 날짜 선택기) */
+.mobile-toolbar-schedule {
+ flex-shrink: 0;
+ background-color: #ffffff;
+}
+
+/* 하단 네비게이션 */
+.mobile-bottom-nav {
+ flex-shrink: 0;
+}
+
+/* 컨텐츠 영역 - 스크롤 가능, 스크롤바 숨김 */
+.mobile-content {
+ flex: 1;
+ overflow-y: auto;
+ -webkit-overflow-scrolling: touch;
+ overscroll-behavior: contain;
+ scrollbar-width: none; /* Firefox */
+ -ms-overflow-style: none; /* IE/Edge */
+}
+
+.mobile-content::-webkit-scrollbar {
+ display: none; /* Chrome, Safari, Opera */
+}
+
+/* 모바일 safe-area 지원 (노치, 홈 인디케이터) */
+.safe-area-bottom {
+ padding-bottom: env(safe-area-inset-bottom, 0);
+}
+
+.safe-area-top {
+ padding-top: env(safe-area-inset-top, 0);
+}
+
+/* 모바일 달력 스타일 */
+.mobile-calendar-wrapper .react-calendar__navigation button:hover {
+ background-color: #f3f4f6;
+ border-radius: 0.5rem;
+}
+
+.mobile-calendar-wrapper .react-calendar__navigation__label {
+ font-weight: 600;
+ font-size: 0.875rem;
+ color: #374151;
+}
+
+.mobile-calendar-wrapper .react-calendar__month-view__weekdays {
+ text-align: center;
+ font-size: 0.75rem;
+ font-weight: 500;
+ color: #6b7280;
+}
+
+.mobile-calendar-wrapper .react-calendar__month-view__weekdays__weekday {
+ padding: 0.5rem 0;
+}
+
+.mobile-calendar-wrapper .react-calendar__month-view__weekdays__weekday abbr {
+ text-decoration: none;
+}
+
+/* 일요일 (빨간색) */
+.mobile-calendar-wrapper
+ .react-calendar__month-view__weekdays__weekday:first-child {
+ color: #f87171;
+}
+
+/* 토요일 (파란색) */
+.mobile-calendar-wrapper
+ .react-calendar__month-view__weekdays__weekday:last-child {
+ color: #60a5fa;
+}
+
+.mobile-calendar-wrapper .react-calendar__tile {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 0.25rem;
+ background: none;
+ border: none;
+ font-size: 0.75rem;
+ color: #374151;
+}
+
+.mobile-calendar-wrapper .react-calendar__tile:hover {
+ background-color: #f3f4f6;
+ border-radius: 9999px;
+}
+
+.mobile-calendar-wrapper .react-calendar__tile abbr {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 1.75rem;
+ height: 1.75rem;
+ border-radius: 9999px;
+}
+
+/* 이웃 달 날짜 (흐리게) */
+.mobile-calendar-wrapper
+ .react-calendar__month-view__days__day--neighboringMonth {
+ color: #d1d5db;
+}
+
+/* 일요일 */
+.mobile-calendar-wrapper .react-calendar__tile.sunday abbr {
+ color: #ef4444;
+}
+
+/* 토요일 */
+.mobile-calendar-wrapper .react-calendar__tile.saturday abbr {
+ color: #3b82f6;
+}
+
+/* 오늘 */
+.mobile-calendar-wrapper .react-calendar__tile--now abbr {
+ background-color: #548360;
+ color: white;
+ font-weight: 700;
+}
+
+/* 선택된 날짜 */
+.mobile-calendar-wrapper .react-calendar__tile--active abbr {
+ background-color: #548360;
+ color: white;
+}
+
+.mobile-calendar-wrapper .react-calendar__tile--active:enabled:hover abbr,
+.mobile-calendar-wrapper .react-calendar__tile--active:enabled:focus abbr {
+ background-color: #456e50;
+}
diff --git a/frontend-temp/src/pc.css b/frontend-temp/src/pc.css
new file mode 100644
index 0000000..3d217c9
--- /dev/null
+++ b/frontend-temp/src/pc.css
@@ -0,0 +1,14 @@
+/* PC 전용 스타일 - body.is-pc 클래스가 있을 때만 적용 */
+
+/* PC에서는 body 스크롤 숨기고 내부 영역에서만 스크롤 */
+html.is-pc,
+body.is-pc {
+ height: 100%;
+ overflow: hidden;
+}
+
+/* PC 최소 너비 설정 */
+body.is-pc #root {
+ min-width: 1440px;
+ height: 100%;
+}