Admin 페이지에 AdminLayout 적용하여 헤더 고정 + 본문 스크롤 구조 구현
- AdminLayout.jsx 컴포넌트 생성 (헤더 고정 + overflow-y-auto) - AdminDashboard, AdminMembers, AdminMemberEdit에 적용 - AdminAlbums, AdminAlbumForm, AdminAlbumPhotos에 적용 - AdminScheduleCategory, AdminScheduleBots, AdminScheduleForm에 적용 - AdminSchedule은 내부 스크롤 처리로 자동 감지하여 제외
This commit is contained in:
parent
233b76355e
commit
1d86c6b841
13 changed files with 178 additions and 141 deletions
25
frontend/src/components/admin/AdminLayout.jsx
Normal file
25
frontend/src/components/admin/AdminLayout.jsx
Normal 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;
|
||||
|
|
@ -5,14 +5,23 @@ import '../../pc.css';
|
|||
|
||||
function Layout({ children }) {
|
||||
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 (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<div className="h-screen overflow-hidden flex flex-col">
|
||||
<Header />
|
||||
<main className="flex-1">{children}</main>
|
||||
<main className={`flex-1 min-h-0 ${isSchedulePage ? 'overflow-hidden' : 'overflow-y-auto'}`}>
|
||||
{children}
|
||||
{!hideFooter && <Footer />}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
} from 'lucide-react';
|
||||
import Toast from '../../../components/Toast';
|
||||
import CustomDatePicker from '../../../components/admin/CustomDatePicker';
|
||||
import AdminHeader from '../../../components/admin/AdminHeader';
|
||||
import AdminLayout from '../../../components/admin/AdminLayout';
|
||||
import useToast from '../../../hooks/useToast';
|
||||
|
||||
// 커스텀 드롭다운 컴포넌트
|
||||
|
|
@ -260,15 +260,12 @@ function AdminAlbumForm() {
|
|||
const albumTypes = ['정규', '미니', '싱글'];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<AdminLayout user={user}>
|
||||
{/* Toast */}
|
||||
<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
|
||||
className="flex items-center gap-2 text-sm text-gray-400 mb-8"
|
||||
|
|
@ -659,8 +656,8 @@ function AdminAlbumForm() {
|
|||
</motion.div>
|
||||
</form>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
Tag, FolderOpen, Save
|
||||
} from 'lucide-react';
|
||||
import Toast from '../../../components/Toast';
|
||||
import AdminHeader from '../../../components/admin/AdminHeader';
|
||||
import AdminLayout from '../../../components/admin/AdminLayout';
|
||||
import ConfirmDialog from '../../../components/admin/ConfirmDialog';
|
||||
import useToast from '../../../hooks/useToast';
|
||||
import * as authApi from '../../../api/admin/auth';
|
||||
|
|
@ -513,20 +513,17 @@ function AdminAlbumPhotos() {
|
|||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* 헤더 */}
|
||||
<AdminHeader user={user} />
|
||||
|
||||
<AdminLayout 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>
|
||||
</main>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<AdminLayout user={user}>
|
||||
{/* Toast */}
|
||||
<Toast toast={toast} onClose={() => setToast(null)} />
|
||||
|
||||
|
|
@ -588,11 +585,8 @@ function AdminAlbumPhotos() {
|
|||
)}
|
||||
</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
|
||||
className="flex items-center gap-2 text-sm text-gray-400 mb-8"
|
||||
|
|
@ -1543,8 +1537,8 @@ function AdminAlbumPhotos() {
|
|||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,13 +7,14 @@ import {
|
|||
} from 'lucide-react';
|
||||
import Toast from '../../../components/Toast';
|
||||
import Tooltip from '../../../components/Tooltip';
|
||||
import AdminHeader from '../../../components/admin/AdminHeader';
|
||||
import AdminLayout from '../../../components/admin/AdminLayout';
|
||||
import ConfirmDialog from '../../../components/admin/ConfirmDialog';
|
||||
import useToast from '../../../hooks/useToast';
|
||||
import * as authApi from '../../../api/admin/auth';
|
||||
import { getAlbums } from '../../../api/public/albums';
|
||||
import * as albumsApi from '../../../api/admin/albums';
|
||||
|
||||
|
||||
function AdminAlbums() {
|
||||
const navigate = useNavigate();
|
||||
const [albums, setAlbums] = useState([]);
|
||||
|
|
@ -76,7 +77,7 @@ function AdminAlbums() {
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<AdminLayout user={user}>
|
||||
{/* Toast */}
|
||||
<Toast toast={toast} onClose={() => setToast(null)} />
|
||||
|
||||
|
|
@ -96,11 +97,8 @@ function AdminAlbums() {
|
|||
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">
|
||||
<Link to="/admin/dashboard" className="hover:text-primary transition-colors">
|
||||
|
|
@ -234,8 +232,8 @@ function AdminAlbums() {
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
Disc3, Calendar, Users,
|
||||
Home, ChevronRight
|
||||
} from 'lucide-react';
|
||||
import AdminHeader from '../../../components/admin/AdminHeader';
|
||||
import AdminLayout from '../../../components/admin/AdminLayout';
|
||||
import * as authApi from '../../../api/admin/auth';
|
||||
import { getMembers } from '../../../api/public/members';
|
||||
import { getAlbums, getAlbum } from '../../../api/public/albums';
|
||||
|
|
@ -134,12 +134,9 @@ function AdminDashboard() {
|
|||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* 헤더 */}
|
||||
<AdminHeader user={user} />
|
||||
|
||||
<AdminLayout 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">
|
||||
<Home size={16} />
|
||||
|
|
@ -198,8 +195,8 @@ function AdminDashboard() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
} from 'lucide-react';
|
||||
import Toast from '../../../components/Toast';
|
||||
import CustomDatePicker from '../../../components/admin/CustomDatePicker';
|
||||
import AdminHeader from '../../../components/admin/AdminHeader';
|
||||
import AdminLayout from '../../../components/admin/AdminLayout';
|
||||
import useToast from '../../../hooks/useToast';
|
||||
import * as authApi from '../../../api/admin/auth';
|
||||
import * as membersApi from '../../../api/admin/members';
|
||||
|
|
@ -99,15 +99,12 @@ function AdminMemberEdit() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<AdminLayout user={user}>
|
||||
{/* Toast */}
|
||||
<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">
|
||||
<Link to="/admin/dashboard" className="hover:text-primary transition-colors">
|
||||
|
|
@ -300,8 +297,8 @@ function AdminMemberEdit() {
|
|||
</div>
|
||||
</motion.form>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
Home, ChevronRight, Users, User
|
||||
} from 'lucide-react';
|
||||
import Toast from '../../../components/Toast';
|
||||
import AdminHeader from '../../../components/admin/AdminHeader';
|
||||
import AdminLayout from '../../../components/admin/AdminLayout';
|
||||
import useToast from '../../../hooks/useToast';
|
||||
|
||||
function AdminMembers() {
|
||||
|
|
@ -93,15 +93,12 @@ function AdminMembers() {
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<AdminLayout user={user}>
|
||||
{/* Toast */}
|
||||
<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">
|
||||
<Link to="/admin/dashboard" className="hover:text-primary transition-colors">
|
||||
|
|
@ -169,8 +166,8 @@ function AdminMembers() {
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
} from 'lucide-react';
|
||||
import Toast from '../../../components/Toast';
|
||||
import Tooltip from '../../../components/Tooltip';
|
||||
import AdminHeader from '../../../components/admin/AdminHeader';
|
||||
import AdminLayout from '../../../components/admin/AdminLayout';
|
||||
import useToast from '../../../hooks/useToast';
|
||||
import * as botsApi from '../../../api/admin/bots';
|
||||
|
||||
|
|
@ -214,14 +214,11 @@ function AdminScheduleBots() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<AdminLayout user={user}>
|
||||
<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">
|
||||
<Link to="/admin/dashboard" className="hover:text-primary transition-colors">
|
||||
|
|
@ -451,8 +448,8 @@ function AdminScheduleBots() {
|
|||
)}
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { motion, AnimatePresence, Reorder } from 'framer-motion';
|
|||
import { Home, ChevronRight, Plus, Edit3, Trash2, GripVertical, X } from 'lucide-react';
|
||||
import { HexColorPicker } from 'react-colorful';
|
||||
import Toast from '../../../components/Toast';
|
||||
import AdminHeader from '../../../components/admin/AdminHeader';
|
||||
import AdminLayout from '../../../components/admin/AdminLayout';
|
||||
import ConfirmDialog from '../../../components/admin/ConfirmDialog';
|
||||
import useToast from '../../../hooks/useToast';
|
||||
import * as authApi from '../../../api/admin/auth';
|
||||
|
|
@ -174,21 +174,20 @@ function AdminScheduleCategory() {
|
|||
|
||||
if (loading) {
|
||||
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>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<AdminLayout user={user}>
|
||||
<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">
|
||||
<Link to="/admin/dashboard" className="hover:text-primary flex items-center gap-1">
|
||||
|
|
@ -279,7 +278,7 @@ function AdminScheduleCategory() {
|
|||
</Reorder.Group>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* 추가/수정 모달 */}
|
||||
<AnimatePresence>
|
||||
|
|
@ -460,7 +459,7 @@ function AdminScheduleCategory() {
|
|||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import Toast from "../../../components/Toast";
|
|||
import Lightbox from "../../../components/common/Lightbox";
|
||||
import CustomDatePicker from "../../../components/admin/CustomDatePicker";
|
||||
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 useToast from "../../../hooks/useToast";
|
||||
import * as authApi from "../../../api/admin/auth";
|
||||
|
|
@ -508,7 +508,7 @@ function AdminScheduleForm() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<AdminLayout user={user}>
|
||||
<Toast toast={toast} onClose={() => setToast(null)} />
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
|
|
@ -664,11 +664,8 @@ function AdminScheduleForm() {
|
|||
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">
|
||||
<Link
|
||||
|
|
@ -1179,8 +1176,8 @@ function AdminScheduleForm() {
|
|||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,44 +61,35 @@ function Home() {
|
|||
{/* 그룹 통계 섹션 */}
|
||||
<section className="py-16 bg-gray-50">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="grid grid-cols-4 gap-6">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="grid grid-cols-4 gap-6"
|
||||
initial="hidden"
|
||||
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"
|
||||
>
|
||||
<p className="text-3xl font-bold mb-1">2018.01.24</p>
|
||||
<p className="text-white/70 text-sm">데뷔일</p>
|
||||
<p className="text-3xl font-bold mb-1">{stat.value}</p>
|
||||
<p className="text-white/70 text-sm">{stat.label}</p>
|
||||
</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
|
||||
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>
|
||||
</section>
|
||||
|
||||
|
|
@ -115,21 +106,26 @@ function Home() {
|
|||
{members.filter(m => !m.is_former).map((member, index) => (
|
||||
<motion.div
|
||||
key={member.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
className="bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-lg transition-shadow"
|
||||
transition={{ delay: 0.3 + index * 0.1, duration: 0.5, ease: "easeOut" }}
|
||||
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
|
||||
src={member.image_url}
|
||||
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 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>
|
||||
</motion.div>
|
||||
))}
|
||||
|
|
@ -152,9 +148,27 @@ function Home() {
|
|||
<p>예정된 일정이 없습니다</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{upcomingSchedules.map((schedule, index) => {
|
||||
<motion.div
|
||||
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 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 weekdays = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
const weekday = weekdays[scheduleDate.getDay()];
|
||||
|
|
@ -166,13 +180,26 @@ function Home() {
|
|||
return (
|
||||
<motion.div
|
||||
key={schedule.id}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
variants={{
|
||||
hidden: { opacity: 0, x: -30 },
|
||||
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"
|
||||
>
|
||||
{/* 날짜 영역 - 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-sm font-medium opacity-80">{weekday}</span>
|
||||
</div>
|
||||
|
|
@ -219,7 +246,7 @@ function Home() {
|
|||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
/* PC 전용 스타일 - body.is-pc 클래스가 있을 때만 적용 */
|
||||
|
||||
/* PC 항상 스크롤바 공간 확보 - 화면 밀림 방지 */
|
||||
/* PC에서는 body 스크롤 숨기고 내부 영역에서만 스크롤 */
|
||||
html.is-pc,
|
||||
body.is-pc {
|
||||
overflow-y: scroll;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* PC 최소 너비 설정 */
|
||||
body.is-pc #root {
|
||||
min-width: 1440px;
|
||||
height: 100%;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue