Admin 페이지에 AdminLayout 적용하여 헤더 고정 + 본문 스크롤 구조 구현

- AdminLayout.jsx 컴포넌트 생성 (헤더 고정 + overflow-y-auto)
- AdminDashboard, AdminMembers, AdminMemberEdit에 적용
- AdminAlbums, AdminAlbumForm, AdminAlbumPhotos에 적용
- AdminScheduleCategory, AdminScheduleBots, AdminScheduleForm에 적용
- AdminSchedule은 내부 스크롤 처리로 자동 감지하여 제외
This commit is contained in:
caadiq 2026-01-11 12:12:46 +09:00
parent 233b76355e
commit 1d86c6b841
13 changed files with 178 additions and 141 deletions

View file

@ -0,0 +1,25 @@
/**
* AdminLayout 컴포넌트
* 모든 Admin 페이지에서 공통으로 사용하는 레이아웃
* 헤더 고정 + 본문 스크롤 구조
*/
import { useLocation } from 'react-router-dom';
import AdminHeader from './AdminHeader';
function AdminLayout({ user, children }) {
const location = useLocation();
//
const isSchedulePage = location.pathname.includes('/admin/schedules');
return (
<div className="h-screen overflow-hidden flex flex-col bg-gray-50">
<AdminHeader user={user} />
<main className={`flex-1 min-h-0 ${isSchedulePage ? 'overflow-hidden' : 'overflow-y-auto'}`}>
{children}
</main>
</div>
);
}
export default AdminLayout;

View file

@ -5,14 +5,23 @@ import '../../pc.css';
function Layout({ children }) { function Layout({ children }) {
const location = useLocation(); const location = useLocation();
// Footer ( )
const hideFooter = location.pathname === '/schedule'; // Footer ( )
const hideFooterPages = ['/schedule', '/members', '/album'];
const hideFooter = hideFooterPages.some(path =>
location.pathname === path || location.pathname.startsWith(path + '/')
);
// ( )
const isSchedulePage = location.pathname === '/schedule';
return ( return (
<div className="min-h-screen flex flex-col"> <div className="h-screen overflow-hidden flex flex-col">
<Header /> <Header />
<main className="flex-1">{children}</main> <main className={`flex-1 min-h-0 ${isSchedulePage ? 'overflow-hidden' : 'overflow-y-auto'}`}>
{children}
{!hideFooter && <Footer />} {!hideFooter && <Footer />}
</main>
</div> </div>
); );
} }

View file

@ -7,7 +7,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import Toast from '../../../components/Toast'; import Toast from '../../../components/Toast';
import CustomDatePicker from '../../../components/admin/CustomDatePicker'; import CustomDatePicker from '../../../components/admin/CustomDatePicker';
import AdminHeader from '../../../components/admin/AdminHeader'; import AdminLayout from '../../../components/admin/AdminLayout';
import useToast from '../../../hooks/useToast'; import useToast from '../../../hooks/useToast';
// //
@ -260,15 +260,12 @@ function AdminAlbumForm() {
const albumTypes = ['정규', '미니', '싱글']; const albumTypes = ['정규', '미니', '싱글'];
return ( return (
<div className="min-h-screen bg-gray-50"> <AdminLayout user={user}>
{/* Toast */} {/* Toast */}
<Toast toast={toast} onClose={() => setToast(null)} /> <Toast toast={toast} onClose={() => setToast(null)} />
{/* 헤더 */}
<AdminHeader user={user} />
{/* 메인 콘텐츠 */} {/* 메인 콘텐츠 */}
<main className="max-w-4xl mx-auto px-6 py-8"> <div className="max-w-4xl mx-auto px-6 py-8">
{/* 브레드크럼 */} {/* 브레드크럼 */}
<motion.div <motion.div
className="flex items-center gap-2 text-sm text-gray-400 mb-8" className="flex items-center gap-2 text-sm text-gray-400 mb-8"
@ -659,8 +656,8 @@ function AdminAlbumForm() {
</motion.div> </motion.div>
</form> </form>
)} )}
</main>
</div> </div>
</AdminLayout>
); );
} }

View file

@ -8,7 +8,7 @@ import {
Tag, FolderOpen, Save Tag, FolderOpen, Save
} from 'lucide-react'; } from 'lucide-react';
import Toast from '../../../components/Toast'; import Toast from '../../../components/Toast';
import AdminHeader from '../../../components/admin/AdminHeader'; import AdminLayout from '../../../components/admin/AdminLayout';
import ConfirmDialog from '../../../components/admin/ConfirmDialog'; import ConfirmDialog from '../../../components/admin/ConfirmDialog';
import useToast from '../../../hooks/useToast'; import useToast from '../../../hooks/useToast';
import * as authApi from '../../../api/admin/auth'; import * as authApi from '../../../api/admin/auth';
@ -513,20 +513,17 @@ function AdminAlbumPhotos() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-gray-50"> <AdminLayout user={user}>
{/* 헤더 */}
<AdminHeader user={user} />
{/* 로딩 스피너 */} {/* 로딩 스피너 */}
<main className="max-w-7xl mx-auto px-6 py-8 flex items-center justify-center" style={{ minHeight: 'calc(100vh - 80px)' }}> <div className="max-w-7xl mx-auto px-6 py-8 flex items-center justify-center" style={{ minHeight: 'calc(100vh - 80px)' }}>
<div className="animate-spin rounded-full h-12 w-12 border-4 border-primary border-t-transparent"></div> <div className="animate-spin rounded-full h-12 w-12 border-4 border-primary border-t-transparent"></div>
</main>
</div> </div>
</AdminLayout>
); );
} }
return ( return (
<div className="min-h-screen bg-gray-50"> <AdminLayout user={user}>
{/* Toast */} {/* Toast */}
<Toast toast={toast} onClose={() => setToast(null)} /> <Toast toast={toast} onClose={() => setToast(null)} />
@ -588,11 +585,8 @@ function AdminAlbumPhotos() {
)} )}
</AnimatePresence> </AnimatePresence>
{/* 헤더 */}
<AdminHeader user={user} />
{/* 메인 콘텐츠 */} {/* 메인 콘텐츠 */}
<main className="max-w-7xl mx-auto px-6 py-8"> <div className="max-w-7xl mx-auto px-6 py-8">
{/* 브레드크럼 */} {/* 브레드크럼 */}
<motion.div <motion.div
className="flex items-center gap-2 text-sm text-gray-400 mb-8" className="flex items-center gap-2 text-sm text-gray-400 mb-8"
@ -1543,8 +1537,8 @@ function AdminAlbumPhotos() {
onChange={handleFileSelect} onChange={handleFileSelect}
className="hidden" className="hidden"
/> />
</main>
</div> </div>
</AdminLayout>
); );
} }

View file

@ -7,13 +7,14 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import Toast from '../../../components/Toast'; import Toast from '../../../components/Toast';
import Tooltip from '../../../components/Tooltip'; import Tooltip from '../../../components/Tooltip';
import AdminHeader from '../../../components/admin/AdminHeader'; import AdminLayout from '../../../components/admin/AdminLayout';
import ConfirmDialog from '../../../components/admin/ConfirmDialog'; import ConfirmDialog from '../../../components/admin/ConfirmDialog';
import useToast from '../../../hooks/useToast'; import useToast from '../../../hooks/useToast';
import * as authApi from '../../../api/admin/auth'; import * as authApi from '../../../api/admin/auth';
import { getAlbums } from '../../../api/public/albums'; import { getAlbums } from '../../../api/public/albums';
import * as albumsApi from '../../../api/admin/albums'; import * as albumsApi from '../../../api/admin/albums';
function AdminAlbums() { function AdminAlbums() {
const navigate = useNavigate(); const navigate = useNavigate();
const [albums, setAlbums] = useState([]); const [albums, setAlbums] = useState([]);
@ -76,7 +77,7 @@ function AdminAlbums() {
); );
return ( return (
<div className="min-h-screen bg-gray-50"> <AdminLayout user={user}>
{/* Toast */} {/* Toast */}
<Toast toast={toast} onClose={() => setToast(null)} /> <Toast toast={toast} onClose={() => setToast(null)} />
@ -96,11 +97,8 @@ function AdminAlbums() {
loading={deleting} loading={deleting}
/> />
{/* 헤더 */}
<AdminHeader user={user} />
{/* 메인 콘텐츠 */} {/* 메인 콘텐츠 */}
<main className="max-w-7xl mx-auto px-6 py-8"> <div className="max-w-7xl mx-auto px-6 py-8">
{/* 브레드크럼 */} {/* 브레드크럼 */}
<div className="flex items-center gap-2 text-sm text-gray-400 mb-8"> <div className="flex items-center gap-2 text-sm text-gray-400 mb-8">
<Link to="/admin/dashboard" className="hover:text-primary transition-colors"> <Link to="/admin/dashboard" className="hover:text-primary transition-colors">
@ -234,8 +232,8 @@ function AdminAlbums() {
)} )}
</div> </div>
)} )}
</main>
</div> </div>
</AdminLayout>
); );
} }

View file

@ -5,7 +5,7 @@ import {
Disc3, Calendar, Users, Disc3, Calendar, Users,
Home, ChevronRight Home, ChevronRight
} from 'lucide-react'; } from 'lucide-react';
import AdminHeader from '../../../components/admin/AdminHeader'; import AdminLayout from '../../../components/admin/AdminLayout';
import * as authApi from '../../../api/admin/auth'; import * as authApi from '../../../api/admin/auth';
import { getMembers } from '../../../api/public/members'; import { getMembers } from '../../../api/public/members';
import { getAlbums, getAlbum } from '../../../api/public/albums'; import { getAlbums, getAlbum } from '../../../api/public/albums';
@ -134,12 +134,9 @@ function AdminDashboard() {
]; ];
return ( return (
<div className="min-h-screen bg-gray-50"> <AdminLayout user={user}>
{/* 헤더 */}
<AdminHeader user={user} />
{/* 메인 콘텐츠 */} {/* 메인 콘텐츠 */}
<main className="max-w-7xl mx-auto px-6 py-8"> <div className="max-w-7xl mx-auto px-6 py-8">
{/* 브레드크럼 */} {/* 브레드크럼 */}
<div className="flex items-center gap-2 text-sm text-gray-400 mb-8"> <div className="flex items-center gap-2 text-sm text-gray-400 mb-8">
<Home size={16} /> <Home size={16} />
@ -198,8 +195,8 @@ function AdminDashboard() {
</div> </div>
</div> </div>
</div> </div>
</main>
</div> </div>
</AdminLayout>
); );
} }

View file

@ -7,7 +7,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import Toast from '../../../components/Toast'; import Toast from '../../../components/Toast';
import CustomDatePicker from '../../../components/admin/CustomDatePicker'; import CustomDatePicker from '../../../components/admin/CustomDatePicker';
import AdminHeader from '../../../components/admin/AdminHeader'; import AdminLayout from '../../../components/admin/AdminLayout';
import useToast from '../../../hooks/useToast'; import useToast from '../../../hooks/useToast';
import * as authApi from '../../../api/admin/auth'; import * as authApi from '../../../api/admin/auth';
import * as membersApi from '../../../api/admin/members'; import * as membersApi from '../../../api/admin/members';
@ -99,15 +99,12 @@ function AdminMemberEdit() {
}; };
return ( return (
<div className="min-h-screen bg-gray-50"> <AdminLayout user={user}>
{/* Toast */} {/* Toast */}
<Toast toast={toast} onClose={() => setToast(null)} /> <Toast toast={toast} onClose={() => setToast(null)} />
{/* 헤더 */}
<AdminHeader user={user} />
{/* 메인 콘텐츠 */} {/* 메인 콘텐츠 */}
<main className="max-w-4xl mx-auto px-6 py-8"> <div className="max-w-4xl mx-auto px-6 py-8">
{/* 브레드크럼 */} {/* 브레드크럼 */}
<div className="flex items-center gap-2 text-sm text-gray-400 mb-8"> <div className="flex items-center gap-2 text-sm text-gray-400 mb-8">
<Link to="/admin/dashboard" className="hover:text-primary transition-colors"> <Link to="/admin/dashboard" className="hover:text-primary transition-colors">
@ -300,8 +297,8 @@ function AdminMemberEdit() {
</div> </div>
</motion.form> </motion.form>
)} )}
</main>
</div> </div>
</AdminLayout>
); );
} }

View file

@ -6,7 +6,7 @@ import {
Home, ChevronRight, Users, User Home, ChevronRight, Users, User
} from 'lucide-react'; } from 'lucide-react';
import Toast from '../../../components/Toast'; import Toast from '../../../components/Toast';
import AdminHeader from '../../../components/admin/AdminHeader'; import AdminLayout from '../../../components/admin/AdminLayout';
import useToast from '../../../hooks/useToast'; import useToast from '../../../hooks/useToast';
function AdminMembers() { function AdminMembers() {
@ -93,15 +93,12 @@ function AdminMembers() {
); );
return ( return (
<div className="min-h-screen bg-gray-50"> <AdminLayout user={user}>
{/* Toast */} {/* Toast */}
<Toast toast={toast} onClose={() => setToast(null)} /> <Toast toast={toast} onClose={() => setToast(null)} />
{/* 헤더 */}
<AdminHeader user={user} />
{/* 메인 콘텐츠 */} {/* 메인 콘텐츠 */}
<main className="max-w-7xl mx-auto px-6 py-8"> <div className="max-w-7xl mx-auto px-6 py-8">
{/* 브레드크럼 */} {/* 브레드크럼 */}
<div className="flex items-center gap-2 text-sm text-gray-400 mb-8"> <div className="flex items-center gap-2 text-sm text-gray-400 mb-8">
<Link to="/admin/dashboard" className="hover:text-primary transition-colors"> <Link to="/admin/dashboard" className="hover:text-primary transition-colors">
@ -169,8 +166,8 @@ function AdminMembers() {
)} )}
</div> </div>
)} )}
</main>
</div> </div>
</AdminLayout>
); );
} }

View file

@ -7,7 +7,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import Toast from '../../../components/Toast'; import Toast from '../../../components/Toast';
import Tooltip from '../../../components/Tooltip'; import Tooltip from '../../../components/Tooltip';
import AdminHeader from '../../../components/admin/AdminHeader'; import AdminLayout from '../../../components/admin/AdminLayout';
import useToast from '../../../hooks/useToast'; import useToast from '../../../hooks/useToast';
import * as botsApi from '../../../api/admin/bots'; import * as botsApi from '../../../api/admin/bots';
@ -214,14 +214,11 @@ function AdminScheduleBots() {
}; };
return ( return (
<div className="min-h-screen bg-gray-50"> <AdminLayout user={user}>
<Toast toast={toast} onClose={() => setToast(null)} /> <Toast toast={toast} onClose={() => setToast(null)} />
{/* 헤더 */}
<AdminHeader user={user} />
{/* 메인 콘텐츠 */} {/* 메인 콘텐츠 */}
<main className="max-w-7xl mx-auto px-6 py-8"> <div className="max-w-7xl mx-auto px-6 py-8">
{/* 브레드크럼 */} {/* 브레드크럼 */}
<div className="flex items-center gap-2 text-sm text-gray-400 mb-8"> <div className="flex items-center gap-2 text-sm text-gray-400 mb-8">
<Link to="/admin/dashboard" className="hover:text-primary transition-colors"> <Link to="/admin/dashboard" className="hover:text-primary transition-colors">
@ -451,8 +448,8 @@ function AdminScheduleBots() {
)} )}
</div> </div>
</main>
</div> </div>
</AdminLayout>
); );
} }

View file

@ -4,7 +4,7 @@ import { motion, AnimatePresence, Reorder } from 'framer-motion';
import { Home, ChevronRight, Plus, Edit3, Trash2, GripVertical, X } from 'lucide-react'; import { Home, ChevronRight, Plus, Edit3, Trash2, GripVertical, X } from 'lucide-react';
import { HexColorPicker } from 'react-colorful'; import { HexColorPicker } from 'react-colorful';
import Toast from '../../../components/Toast'; import Toast from '../../../components/Toast';
import AdminHeader from '../../../components/admin/AdminHeader'; import AdminLayout from '../../../components/admin/AdminLayout';
import ConfirmDialog from '../../../components/admin/ConfirmDialog'; import ConfirmDialog from '../../../components/admin/ConfirmDialog';
import useToast from '../../../hooks/useToast'; import useToast from '../../../hooks/useToast';
import * as authApi from '../../../api/admin/auth'; import * as authApi from '../../../api/admin/auth';
@ -174,21 +174,20 @@ function AdminScheduleCategory() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center"> <AdminLayout user={user}>
<div className="flex items-center justify-center" style={{ minHeight: 'calc(100vh - 80px)' }}>
<div className="animate-spin rounded-full h-12 w-12 border-4 border-primary border-t-transparent"></div> <div className="animate-spin rounded-full h-12 w-12 border-4 border-primary border-t-transparent"></div>
</div> </div>
</AdminLayout>
); );
} }
return ( return (
<div className="min-h-screen bg-gray-50"> <AdminLayout user={user}>
<Toast toast={toast} onClose={() => setToast(null)} /> <Toast toast={toast} onClose={() => setToast(null)} />
{/* 헤더 */}
<AdminHeader user={user} />
{/* 메인 콘텐츠 */} {/* 메인 콘텐츠 */}
<main className="max-w-4xl mx-auto px-6 py-8"> <div className="max-w-4xl mx-auto px-6 py-8">
{/* 브레드크럼 */} {/* 브레드크럼 */}
<div className="flex items-center gap-2 text-sm text-gray-500 mb-6"> <div className="flex items-center gap-2 text-sm text-gray-500 mb-6">
<Link to="/admin/dashboard" className="hover:text-primary flex items-center gap-1"> <Link to="/admin/dashboard" className="hover:text-primary flex items-center gap-1">
@ -279,7 +278,7 @@ function AdminScheduleCategory() {
</Reorder.Group> </Reorder.Group>
)} )}
</div> </div>
</main> </div>
{/* 추가/수정 모달 */} {/* 추가/수정 모달 */}
<AnimatePresence> <AnimatePresence>
@ -460,7 +459,7 @@ function AdminScheduleCategory() {
</> </>
} }
/> />
</div> </AdminLayout>
); );
} }

View file

@ -26,7 +26,7 @@ import Toast from "../../../components/Toast";
import Lightbox from "../../../components/common/Lightbox"; import Lightbox from "../../../components/common/Lightbox";
import CustomDatePicker from "../../../components/admin/CustomDatePicker"; import CustomDatePicker from "../../../components/admin/CustomDatePicker";
import CustomTimePicker from "../../../components/admin/CustomTimePicker"; import CustomTimePicker from "../../../components/admin/CustomTimePicker";
import AdminHeader from "../../../components/admin/AdminHeader"; import AdminLayout from "../../../components/admin/AdminLayout";
import ConfirmDialog from "../../../components/admin/ConfirmDialog"; import ConfirmDialog from "../../../components/admin/ConfirmDialog";
import useToast from "../../../hooks/useToast"; import useToast from "../../../hooks/useToast";
import * as authApi from "../../../api/admin/auth"; import * as authApi from "../../../api/admin/auth";
@ -508,7 +508,7 @@ function AdminScheduleForm() {
}; };
return ( return (
<div className="min-h-screen bg-gray-50"> <AdminLayout user={user}>
<Toast toast={toast} onClose={() => setToast(null)} /> <Toast toast={toast} onClose={() => setToast(null)} />
{/* 삭제 확인 다이얼로그 */} {/* 삭제 확인 다이얼로그 */}
@ -664,11 +664,8 @@ function AdminScheduleForm() {
onIndexChange={setLightboxIndex} onIndexChange={setLightboxIndex}
/> />
{/* 헤더 */}
<AdminHeader user={user} />
{/* 메인 콘텐츠 */} {/* 메인 콘텐츠 */}
<main className="max-w-4xl mx-auto px-6 py-8"> <div className="max-w-4xl mx-auto px-6 py-8">
{/* 브레드크럼 */} {/* 브레드크럼 */}
<div className="flex items-center gap-2 text-sm text-gray-400 mb-8"> <div className="flex items-center gap-2 text-sm text-gray-400 mb-8">
<Link <Link
@ -1179,8 +1176,8 @@ function AdminScheduleForm() {
</button> </button>
</div> </div>
</form> </form>
</main>
</div> </div>
</AdminLayout>
); );
} }

View file

@ -61,44 +61,35 @@ function Home() {
{/* 그룹 통계 섹션 */} {/* 그룹 통계 섹션 */}
<section className="py-16 bg-gray-50"> <section className="py-16 bg-gray-50">
<div className="max-w-7xl mx-auto px-6"> <div className="max-w-7xl mx-auto px-6">
<div className="grid grid-cols-4 gap-6">
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} className="grid grid-cols-4 gap-6"
animate={{ opacity: 1, y: 0 }} initial="hidden"
transition={{ delay: 0.1 }} whileInView="visible"
viewport={{ once: true, amount: 0.1 }}
variants={{
hidden: { opacity: 1 },
visible: { opacity: 1, transition: { staggerChildren: 0.1 } }
}}
>
{[
{ value: "2018.01.24", label: "데뷔일" },
{ value: `D+${(Math.floor((new Date() - new Date('2018-01-24')) / (1000 * 60 * 60 * 24)) + 1).toLocaleString()}`, label: "D+Day" },
{ value: "5", label: "멤버 수" },
{ value: "flover", label: "팬덤명" }
].map((stat, index) => (
<motion.div
key={index}
variants={{
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.4, ease: "easeOut" } }
}}
className="bg-gradient-to-br from-primary to-primary-dark rounded-2xl p-6 text-white text-center" className="bg-gradient-to-br from-primary to-primary-dark rounded-2xl p-6 text-white text-center"
> >
<p className="text-3xl font-bold mb-1">2018.01.24</p> <p className="text-3xl font-bold mb-1">{stat.value}</p>
<p className="text-white/70 text-sm">데뷔일</p> <p className="text-white/70 text-sm">{stat.label}</p>
</motion.div> </motion.div>
<motion.div ))}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="bg-gradient-to-br from-primary to-primary-dark rounded-2xl p-6 text-white text-center"
>
<p className="text-3xl font-bold mb-1">D+{(Math.floor((new Date() - new Date('2018-01-24')) / (1000 * 60 * 60 * 24)) + 1).toLocaleString()}</p>
<p className="text-white/70 text-sm">D+Day</p>
</motion.div> </motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="bg-gradient-to-br from-primary to-primary-dark rounded-2xl p-6 text-white text-center"
>
<p className="text-3xl font-bold mb-1">5</p>
<p className="text-white/70 text-sm">멤버 </p>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="bg-gradient-to-br from-primary to-primary-dark rounded-2xl p-6 text-white text-center"
>
<p className="text-3xl font-bold mb-1">flover</p>
<p className="text-white/70 text-sm">팬덤명</p>
</motion.div>
</div>
</div> </div>
</section> </section>
@ -115,21 +106,26 @@ function Home() {
{members.filter(m => !m.is_former).map((member, index) => ( {members.filter(m => !m.is_former).map((member, index) => (
<motion.div <motion.div
key={member.id} key={member.id}
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }} transition={{ delay: 0.3 + index * 0.1, duration: 0.5, ease: "easeOut" }}
className="bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-lg transition-shadow" className="group relative rounded-2xl overflow-hidden shadow-sm hover:shadow-xl transition-all duration-300"
> >
<div className="aspect-square bg-gray-100"> {/* 이미지 컨테이너 */}
<div className="aspect-[3/4] overflow-hidden">
<img <img
src={member.image_url} src={member.image_url}
alt={member.name} alt={member.name}
className="w-full h-full object-cover" className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
/> />
</div> </div>
<div className="p-4 text-center">
<h3 className="font-bold text-lg">{member.name}</h3> {/* 그라데이션 오버레이 */}
<p className="text-sm text-gray-500">{member.position?.split(',')[0]}</p> <div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent opacity-60 group-hover:opacity-90 transition-opacity duration-300" />
{/* 멤버 정보 */}
<div className="absolute bottom-0 left-0 right-0 p-5 text-white">
<h3 className="font-bold text-xl drop-shadow-lg">{member.name}</h3>
</div> </div>
</motion.div> </motion.div>
))} ))}
@ -152,9 +148,27 @@ function Home() {
<p>예정된 일정이 없습니다</p> <p>예정된 일정이 없습니다</p>
</div> </div>
) : ( ) : (
<div className="space-y-4"> <motion.div
{upcomingSchedules.map((schedule, index) => { className="space-y-4"
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.1 }}
variants={{
hidden: { opacity: 1 },
visible: { opacity: 1, transition: { staggerChildren: 0.1 } }
}}
>
{upcomingSchedules.map((schedule) => {
const scheduleDate = new Date(schedule.date); 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 day = scheduleDate.getDate(); const day = scheduleDate.getDate();
const weekdays = ['일', '월', '화', '수', '목', '금', '토']; const weekdays = ['일', '월', '화', '수', '목', '금', '토'];
const weekday = weekdays[scheduleDate.getDay()]; const weekday = weekdays[scheduleDate.getDay()];
@ -166,13 +180,26 @@ function Home() {
return ( return (
<motion.div <motion.div
key={schedule.id} key={schedule.id}
initial={{ opacity: 0 }} variants={{
animate={{ opacity: 1 }} hidden: { opacity: 0, x: -30 },
transition={{ delay: index * 0.05 }} visible: { opacity: 1, x: 0, transition: { duration: 0.4, ease: "easeOut" } }
}}
className="flex items-stretch bg-white rounded-2xl shadow-sm hover:shadow-md transition-shadow overflow-hidden" className="flex items-stretch bg-white rounded-2xl shadow-sm hover:shadow-md transition-shadow overflow-hidden"
> >
{/* 날짜 영역 - primary 색상 고정 */} {/* 날짜 영역 - primary 색상 고정 */}
<div className="w-20 flex flex-col items-center justify-center text-white py-5 bg-primary"> <div className="w-20 flex flex-col items-center justify-center text-white py-5 bg-primary">
{/* 현재 년도가 아니면 년.월 표시 */}
{!isCurrentYear && (
<span className="text-xs font-medium opacity-70">
{scheduleYear}.{scheduleMonth + 1}
</span>
)}
{/* 현재 달이 아니면 월 표시 (현재 년도일 때) */}
{isCurrentYear && !isCurrentMonth && (
<span className="text-xs font-medium opacity-70">
{scheduleMonth + 1}
</span>
)}
<span className="text-3xl font-bold">{day}</span> <span className="text-3xl font-bold">{day}</span>
<span className="text-sm font-medium opacity-80">{weekday}</span> <span className="text-sm font-medium opacity-80">{weekday}</span>
</div> </div>
@ -219,7 +246,7 @@ function Home() {
</motion.div> </motion.div>
); );
})} })}
</div> </motion.div>
)} )}

View file

@ -1,11 +1,14 @@
/* PC 전용 스타일 - body.is-pc 클래스가 있을 때만 적용 */ /* PC 전용 스타일 - body.is-pc 클래스가 있을 때만 적용 */
/* PC 항상 스크롤바 공간 확보 - 화면 밀림 방지 */ /* PC에서는 body 스크롤 숨기고 내부 영역에서만 스크롤 */
html.is-pc,
body.is-pc { body.is-pc {
overflow-y: scroll; height: 100%;
overflow: hidden;
} }
/* PC 최소 너비 설정 */ /* PC 최소 너비 설정 */
body.is-pc #root { body.is-pc #root {
min-width: 1440px; min-width: 1440px;
height: 100%;
} }