feat: 사전 관리, 일정 추가 페이지에 애니메이션 추가

- framer-motion을 사용한 페이지 진입 애니메이션
- 섹션별 stagger 애니메이션으로 순차적 등장 효과
- 카테고리 전환 시 폼 fade 애니메이션

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-01-19 16:01:54 +09:00
parent 84ed48fa78
commit 2576a244c0
2 changed files with 116 additions and 29 deletions

View file

@ -9,6 +9,35 @@ import useAdminAuth from '../../../hooks/useAdminAuth';
import useToast from '../../../hooks/useToast';
import * as suggestionsApi from '../../../api/admin/suggestions';
// variants
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.08,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.4, ease: "easeOut" },
},
};
const cardVariants = {
hidden: { opacity: 0, scale: 0.95 },
visible: {
opacity: 1,
scale: 1,
transition: { duration: 0.3, ease: "easeOut" },
},
};
//
const POS_TAGS = [
{ value: 'NNP', label: '고유명사 (NNP)', description: '사람, 그룹, 프로그램 이름 등', examples: '프로미스나인, 송하영, 뮤직뱅크' },
@ -403,9 +432,14 @@ function AdminScheduleDict() {
/>
{/* 메인 콘텐츠 */}
<div className="max-w-5xl mx-auto px-6 py-8">
<motion.div
className="max-w-5xl mx-auto px-6 py-8"
variants={containerVariants}
initial="hidden"
animate="visible"
>
{/* 브레드크럼 */}
<div className="flex items-center gap-2 text-sm text-gray-400 mb-8">
<motion.div variants={itemVariants} className="flex items-center gap-2 text-sm text-gray-400 mb-8">
<Link to="/admin/dashboard" className="hover:text-primary transition-colors">
<Home size={16} />
</Link>
@ -415,36 +449,36 @@ function AdminScheduleDict() {
</Link>
<ChevronRight size={14} />
<span className="text-gray-700">사전 관리</span>
</div>
</motion.div>
{/* 타이틀 */}
<div className="mb-8">
<motion.div variants={itemVariants} className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">사전 관리</h1>
<p className="text-gray-500">형태소 분석기 사용자 사전을 관리합니다</p>
</div>
</motion.div>
{/* 통계 카드 */}
<div className="grid grid-cols-4 gap-4 mb-6">
<div className="bg-white rounded-xl p-4 border border-gray-100">
<motion.div variants={itemVariants} className="grid grid-cols-4 gap-4 mb-6">
<motion.div variants={cardVariants} className="bg-white rounded-xl p-4 border border-gray-100">
<div className="text-2xl font-bold text-gray-900">{posStats.total || 0}</div>
<div className="text-sm text-gray-500">전체 단어</div>
</div>
<div className="bg-white rounded-xl p-4 border border-gray-100">
</motion.div>
<motion.div variants={cardVariants} className="bg-white rounded-xl p-4 border border-gray-100">
<div className="text-2xl font-bold text-blue-500">{posStats.NNP || 0}</div>
<div className="text-sm text-gray-500">고유명사</div>
</div>
<div className="bg-white rounded-xl p-4 border border-gray-100">
</motion.div>
<motion.div variants={cardVariants} className="bg-white rounded-xl p-4 border border-gray-100">
<div className="text-2xl font-bold text-green-500">{posStats.NNG || 0}</div>
<div className="text-sm text-gray-500">일반명사</div>
</div>
<div className="bg-white rounded-xl p-4 border border-gray-100">
</motion.div>
<motion.div variants={cardVariants} className="bg-white rounded-xl p-4 border border-gray-100">
<div className="text-2xl font-bold text-purple-500">{posStats.SL || 0}</div>
<div className="text-sm text-gray-500">외국어</div>
</div>
</div>
</motion.div>
</motion.div>
{/* 단어 추가 영역 */}
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-6 mb-6">
<motion.div variants={itemVariants} className="bg-white rounded-2xl shadow-sm border border-gray-100 p-6 mb-6">
<h3 className="font-bold text-gray-900 mb-4">단어 추가</h3>
<div className="flex items-center gap-3">
<div className="flex-1">
@ -502,10 +536,10 @@ function AdminScheduleDict() {
추가
</button>
</div>
</div>
</motion.div>
{/* 단어 목록 */}
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<motion.div variants={itemVariants} className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
{/* 검색 및 필터 */}
<div className="p-4 border-b border-gray-100 flex items-center gap-4">
<div className="flex-1 relative">
@ -616,8 +650,8 @@ function AdminScheduleDict() {
)}
</div>
)}
</div>
</div>
</motion.div>
</motion.div>
</AdminLayout>
);
}

View file

@ -1,5 +1,6 @@
import { useState, useEffect } from "react";
import { useNavigate, Link } from "react-router-dom";
import { motion, AnimatePresence } from "framer-motion";
import { Home, ChevronRight } from "lucide-react";
import AdminLayout from "../../../../../components/admin/AdminLayout";
import useAdminAuth from "../../../../../hooks/useAdminAuth";
@ -8,6 +9,40 @@ import CategorySelector from "./components/CategorySelector";
import YouTubeForm from "./YouTubeForm";
import XForm from "./XForm";
// variants
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.4, ease: "easeOut" },
},
};
const formVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.3, ease: "easeOut" },
},
exit: {
opacity: 0,
y: -10,
transition: { duration: 0.2 },
},
};
// ID
const CATEGORY_IDS = {
YOUTUBE: 2,
@ -86,9 +121,17 @@ function ScheduleFormPage() {
return (
<AdminLayout user={user}>
<div className="max-w-4xl mx-auto px-6 py-8">
<motion.div
className="max-w-4xl mx-auto px-6 py-8"
variants={containerVariants}
initial="hidden"
animate="visible"
>
{/* 브레드크럼 */}
<div className="flex items-center gap-2 text-sm text-gray-400 mb-8">
<motion.div
variants={itemVariants}
className="flex items-center gap-2 text-sm text-gray-400 mb-8"
>
<Link
to="/admin/dashboard"
className="hover:text-primary transition-colors"
@ -104,26 +147,36 @@ function ScheduleFormPage() {
</Link>
<ChevronRight size={14} />
<span className="text-gray-700">일정 추가</span>
</div>
</motion.div>
{/* 타이틀 */}
<div className="mb-8">
<motion.div variants={itemVariants} className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">일정 추가</h1>
<p className="text-gray-500">카테고리를 선택하고 일정을 등록하세요</p>
</div>
</motion.div>
{/* 카테고리 선택 */}
<div className="mb-6">
<motion.div variants={itemVariants} className="mb-6">
<CategorySelector
categories={categories}
selectedId={selectedCategory}
onChange={setSelectedCategory}
/>
</div>
</motion.div>
{/* 카테고리별 폼 */}
{renderForm()}
</div>
<AnimatePresence mode="wait">
<motion.div
key={selectedCategory}
variants={formVariants}
initial="hidden"
animate="visible"
exit="exit"
>
{renderForm()}
</motion.div>
</AnimatePresence>
</motion.div>
</AdminLayout>
);
}