feature 페이지 Suspense 스피너 제거 + 섹션 순차 페이드인 애니메이션
- FeaturePage / AdminFeaturePage의 Suspense fallback 스피너를 null로 변경 - components/common/StaggerGroup: 자식을 각 motion.div로 감싸 순차 페이드인 (staggerChildren 0.07s, duration 0.35s, ease 0.22,1,0.36,1) - Liberation(Genesis/Destiny), Symbol 페이지 root를 StaggerGroup으로 교체 - BossCrystal은 grid 레이아웃 특성상 root 전체를 motion.div로 감싸 fade-in - hover prefetch와 함께 chunk 로드 시 깜빡임 없이 자연스럽게 등장 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7ebfe4a449
commit
d1764dea94
7 changed files with 58 additions and 21 deletions
39
frontend/src/components/common/StaggerGroup.jsx
Normal file
39
frontend/src/components/common/StaggerGroup.jsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { Children } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
const containerVariants = {
|
||||
hidden: {},
|
||||
show: { transition: { staggerChildren: 0.07 } },
|
||||
}
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 10 },
|
||||
show: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.35, ease: [0.22, 1, 0.36, 1] },
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* 자식을 각 motion.div 로 감싸 순차 페이드인.
|
||||
* 레이아웃에 영향 주지 않도록 wrapper div 는 flex/grid 특성이 없어야 하는 자리에서만 사용.
|
||||
* space-y-* 같은 Tailwind 유틸은 그대로 className 에 넘겨 유지.
|
||||
*/
|
||||
export default function StaggerGroup({ children, className, style }) {
|
||||
return (
|
||||
<motion.div
|
||||
className={className}
|
||||
style={style}
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
>
|
||||
{Children.map(children, (child, i) => (
|
||||
child == null || child === false
|
||||
? null
|
||||
: <motion.div variants={itemVariants} key={i}>{child}</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
|
@ -11,11 +11,7 @@ export default function FeaturePage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="flex items-center justify-center pt-20">
|
||||
<div className="w-6 h-6 border-2 border-emerald-500 border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
}>
|
||||
<Suspense fallback={null}>
|
||||
<Component />
|
||||
</Suspense>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -49,14 +49,7 @@ export default function AdminFeaturePage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="flex items-center justify-center pt-20">
|
||||
<div
|
||||
className="w-6 h-6 border-2 border-t-transparent rounded-full animate-spin"
|
||||
style={{ borderColor: 'var(--accent)', borderTopColor: 'transparent' }}
|
||||
/>
|
||||
</div>
|
||||
}>
|
||||
<Suspense fallback={null}>
|
||||
<Component />
|
||||
</Suspense>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useEffect, useLayoutEffect } from 'react'
|
||||
import { useQuery, useQueries } from '@tanstack/react-query'
|
||||
import { motion } from 'framer-motion'
|
||||
import { api } from '../../../api/client'
|
||||
import { useLayout } from '../../../components/pc/Layout'
|
||||
import CharacterPanel from './user/CharacterPanel'
|
||||
|
|
@ -70,7 +71,12 @@ export default function BossCrystal() {
|
|||
const isMaxReached = currentSelectedCount >= MAX_PER_CHARACTER
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
<motion.div
|
||||
className="h-full"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.35, ease: [0.22, 1, 0.36, 1] }}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div
|
||||
className="rounded-2xl border p-16 text-center"
|
||||
|
|
@ -117,6 +123,6 @@ export default function BossCrystal() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import PointsInput from './components/PointsInput'
|
|||
import WeeklyDefault from './components/WeeklyDefault'
|
||||
import DatePicker from '../../../components/common/DatePicker'
|
||||
import ConfirmDialog from '../../../components/common/ConfirmDialog'
|
||||
import StaggerGroup from '../../../components/common/StaggerGroup'
|
||||
|
||||
export default function Destiny() {
|
||||
const calcMode = useLiberationStore((s) => s.destinyCalcMode)
|
||||
|
|
@ -69,7 +70,7 @@ export default function Destiny() {
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<StaggerGroup className="space-y-6">
|
||||
{/* 계산 모드 탭 */}
|
||||
<div
|
||||
className="max-w-3xl mx-auto flex gap-1 p-1 rounded-xl border"
|
||||
|
|
@ -212,6 +213,6 @@ export default function Destiny() {
|
|||
confirmText="초기화"
|
||||
destructive
|
||||
/>
|
||||
</>
|
||||
</StaggerGroup>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import ProgressBar from './components/ProgressBar'
|
|||
import WeeklyDefault from './components/WeeklyDefault'
|
||||
import DatePicker from '../../../components/common/DatePicker'
|
||||
import ConfirmDialog from '../../../components/common/ConfirmDialog'
|
||||
import StaggerGroup from '../../../components/common/StaggerGroup'
|
||||
|
||||
export default function Genesis() {
|
||||
const calcMode = useLiberationStore((s) => s.genesisCalcMode)
|
||||
|
|
@ -93,7 +94,7 @@ export default function Genesis() {
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<StaggerGroup className="space-y-6">
|
||||
{/* 계산 모드 탭 */}
|
||||
<div
|
||||
className="max-w-3xl mx-auto flex gap-1 p-1 rounded-xl border"
|
||||
|
|
@ -238,6 +239,6 @@ export default function Genesis() {
|
|||
confirmText="초기화"
|
||||
destructive
|
||||
/>
|
||||
</>
|
||||
</StaggerGroup>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { formatMesoKorean } from '../../../utils/formatting'
|
|||
import { formatKoreanDate, computeCompletion, TYPE_ORDER, eventBonusForType } from '../utils'
|
||||
import CharacterCard from './user/CharacterCard'
|
||||
import SymbolCard from './user/SymbolCard'
|
||||
import StaggerGroup from '../../../components/common/StaggerGroup'
|
||||
|
||||
export default function Symbol() {
|
||||
const { setFullscreen } = useLayout()
|
||||
|
|
@ -193,7 +194,7 @@ export default function Symbol() {
|
|||
}, [symbols, progress, selectedChar?.event_skill])
|
||||
|
||||
return (
|
||||
<div className="space-y-6 pb-10 max-w-5xl mx-auto">
|
||||
<StaggerGroup className="space-y-6 pb-10 max-w-5xl mx-auto">
|
||||
{/* 캐릭터 조회 */}
|
||||
<div
|
||||
className="rounded-2xl border p-5 space-y-4"
|
||||
|
|
@ -349,6 +350,6 @@ export default function Symbol() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StaggerGroup>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue