From d1764dea9473cb635a6ce991a80accc0e62687c2 Mon Sep 17 00:00:00 2001 From: caadiq Date: Wed, 22 Apr 2026 00:13:15 +0900 Subject: [PATCH] =?UTF-8?q?feature=20=ED=8E=98=EC=9D=B4=EC=A7=80=20Suspens?= =?UTF-8?q?e=20=EC=8A=A4=ED=94=BC=EB=84=88=20=EC=A0=9C=EA=B1=B0=20+=20?= =?UTF-8?q?=EC=84=B9=EC=85=98=20=EC=88=9C=EC=B0=A8=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EB=93=9C=EC=9D=B8=20=EC=95=A0=EB=8B=88=EB=A9=94=EC=9D=B4?= =?UTF-8?q?=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .../src/components/common/StaggerGroup.jsx | 39 +++++++++++++++++++ frontend/src/features/FeaturePage.jsx | 6 +-- .../features/admin/pc/AdminFeaturePage.jsx | 9 +---- .../features/boss-crystal/pc/BossCrystal.jsx | 10 ++++- .../src/features/liberation/pc/Destiny.jsx | 5 ++- .../src/features/liberation/pc/Genesis.jsx | 5 ++- frontend/src/features/symbol/pc/Symbol.jsx | 5 ++- 7 files changed, 58 insertions(+), 21 deletions(-) create mode 100644 frontend/src/components/common/StaggerGroup.jsx diff --git a/frontend/src/components/common/StaggerGroup.jsx b/frontend/src/components/common/StaggerGroup.jsx new file mode 100644 index 0000000..5a6ebb5 --- /dev/null +++ b/frontend/src/components/common/StaggerGroup.jsx @@ -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 ( + + {Children.map(children, (child, i) => ( + child == null || child === false + ? null + : {child} + ))} + + ) +} diff --git a/frontend/src/features/FeaturePage.jsx b/frontend/src/features/FeaturePage.jsx index f8c7a48..1a5aa60 100644 --- a/frontend/src/features/FeaturePage.jsx +++ b/frontend/src/features/FeaturePage.jsx @@ -11,11 +11,7 @@ export default function FeaturePage() { } return ( - -
-
- }> + ) diff --git a/frontend/src/features/admin/pc/AdminFeaturePage.jsx b/frontend/src/features/admin/pc/AdminFeaturePage.jsx index b0e891b..55e220f 100644 --- a/frontend/src/features/admin/pc/AdminFeaturePage.jsx +++ b/frontend/src/features/admin/pc/AdminFeaturePage.jsx @@ -49,14 +49,7 @@ export default function AdminFeaturePage() { } return ( - -
-
- }> + ) diff --git a/frontend/src/features/boss-crystal/pc/BossCrystal.jsx b/frontend/src/features/boss-crystal/pc/BossCrystal.jsx index f6e13e3..e47a8cf 100644 --- a/frontend/src/features/boss-crystal/pc/BossCrystal.jsx +++ b/frontend/src/features/boss-crystal/pc/BossCrystal.jsx @@ -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 ( -
+ {isLoading ? (
)} -
+ ) } diff --git a/frontend/src/features/liberation/pc/Destiny.jsx b/frontend/src/features/liberation/pc/Destiny.jsx index 6d92049..280fd9c 100644 --- a/frontend/src/features/liberation/pc/Destiny.jsx +++ b/frontend/src/features/liberation/pc/Destiny.jsx @@ -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 ( - <> + {/* 계산 모드 탭 */}
- + ) } diff --git a/frontend/src/features/liberation/pc/Genesis.jsx b/frontend/src/features/liberation/pc/Genesis.jsx index 6f3621e..9edeb9a 100644 --- a/frontend/src/features/liberation/pc/Genesis.jsx +++ b/frontend/src/features/liberation/pc/Genesis.jsx @@ -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 ( - <> + {/* 계산 모드 탭 */}
- + ) } diff --git a/frontend/src/features/symbol/pc/Symbol.jsx b/frontend/src/features/symbol/pc/Symbol.jsx index 735eb47..a0fce65 100644 --- a/frontend/src/features/symbol/pc/Symbol.jsx +++ b/frontend/src/features/symbol/pc/Symbol.jsx @@ -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 ( -
+ {/* 캐릭터 조회 */}
-
+
) }