diff --git a/src/components/layout/RouteTransition.module.css b/src/components/layout/RouteTransition.module.css index 0000021..7ac3e74 100644 --- a/src/components/layout/RouteTransition.module.css +++ b/src/components/layout/RouteTransition.module.css @@ -2,6 +2,24 @@ animation: fadeIn var(--fowoco-motion-base) var(--fowoco-easing-decelerate); } +.loading { + display: flex; + align-items: center; + justify-content: center; + gap: var(--fowoco-spacing-8); + min-height: 240px; + color: var(--text-secondary); + font-size: 13px; +} + +.loadingDot { + width: 8px; + height: 8px; + border-radius: var(--fowoco-radius-999); + background: var(--brand-primary); + animation: loadingPulse 900ms ease-in-out infinite alternate; +} + @keyframes fadeIn { from { opacity: 0; @@ -13,3 +31,22 @@ transform: translateY(0); } } + +@keyframes loadingPulse { + from { + opacity: 0.35; + transform: scale(0.8); + } + + to { + opacity: 1; + transform: scale(1); + } +} + +@media (prefers-reduced-motion: reduce) { + .fade, + .loadingDot { + animation: none; + } +} diff --git a/src/components/layout/RouteTransition.test.tsx b/src/components/layout/RouteTransition.test.tsx index 24dfb45..4905ba7 100644 --- a/src/components/layout/RouteTransition.test.tsx +++ b/src/components/layout/RouteTransition.test.tsx @@ -1,4 +1,5 @@ -import { render, screen } from '@testing-library/react' +import { lazy, type ComponentType } from 'react' +import { act, render, screen } from '@testing-library/react' import { createMemoryRouter, RouterProvider } from 'react-router-dom' import { describe, expect, it } from 'vitest' import { RouteTransition } from './RouteTransition' @@ -13,4 +14,27 @@ describe('RouteTransition', () => { expect(screen.getByText('dashboard content')).toBeInTheDocument() }) + + it('announces route loading while a lazy page chunk is resolving', async () => { + let resolvePage!: (module: { default: ComponentType }) => void + const LazyPage = lazy( + () => + new Promise<{ default: ComponentType }>((resolve) => { + resolvePage = resolve + }), + ) + const router = createMemoryRouter( + [{ element: , children: [{ path: '/tasks', element: }] }], + { initialEntries: ['/tasks'] }, + ) + render() + + expect(screen.getByRole('status')).toHaveTextContent('화면을 불러오는 중입니다') + + await act(async () => { + resolvePage({ default: () =>

lazy task content

}) + }) + + expect(await screen.findByText('lazy task content')).toBeInTheDocument() + }) }) diff --git a/src/components/layout/RouteTransition.tsx b/src/components/layout/RouteTransition.tsx index e1c60d3..c041c5e 100644 --- a/src/components/layout/RouteTransition.tsx +++ b/src/components/layout/RouteTransition.tsx @@ -1,3 +1,4 @@ +import { Suspense } from 'react' import { Outlet, useLocation } from 'react-router-dom' import styles from './RouteTransition.module.css' @@ -6,7 +7,16 @@ export function RouteTransition() { return (
- + +
+ } + > + + ) } diff --git a/src/routes.tsx b/src/routes.tsx index bfe3436..6d5f3d0 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -1,12 +1,7 @@ +import { lazy } from 'react' import { createBrowserRouter } from 'react-router-dom' import { RequireAuth } from './components/auth/RequireAuth' import { AppLayout } from './components/layout/AppLayout' -import { AgentLogPage } from './pages/AgentLogPage/AgentLogPage' -import { CaseDetailPage } from './pages/CaseDetailPage/CaseDetailPage' -import { CreateWorkPage } from './pages/CreateWorkPage/CreateWorkPage' -import { DashboardPage } from './pages/DashboardPage/DashboardPage' -import { DocumentDetailPage } from './pages/DocumentDetailPage/DocumentDetailPage' -import { DocumentListPage } from './pages/DocumentListPage/DocumentListPage' import { EmailSentPage } from './pages/EmailSentPage/EmailSentPage' import { ForgotPasswordPage } from './pages/ForgotPasswordPage/ForgotPasswordPage' import { IntroPage } from './pages/IntroPage/IntroPage' @@ -16,16 +11,71 @@ import { LinkUploadPage } from './pages/LinkUploadPage/LinkUploadPage' import { LoginPage } from './pages/LoginPage/LoginPage' import { NotFoundPage } from './pages/NotFoundPage/NotFoundPage' import { OnboardingImportPage } from './pages/OnboardingImportPage/OnboardingImportPage' -import { ProfilePage } from './pages/ProfilePage/ProfilePage' import { ResetCompletePage } from './pages/ResetCompletePage/ResetCompletePage' import { ResetPasswordPage } from './pages/ResetPasswordPage/ResetPasswordPage' -import { ReviewWorkPage } from './pages/ReviewWorkPage/ReviewWorkPage' import { SignupPage } from './pages/SignupPage/SignupPage' -import { TicketDetailPage } from './pages/TicketDetailPage/TicketDetailPage' -import { TicketListPage } from './pages/TicketListPage/TicketListPage' -import { WorkerDetailPage } from './pages/WorkerDetailPage/WorkerDetailPage' -import { WorkerListPage } from './pages/WorkerListPage/WorkerListPage' -import { WorkListPage } from './pages/WorkListPage/WorkListPage' + +const AgentLogPage = lazy(() => + import('./pages/AgentLogPage/AgentLogPage').then((module) => ({ default: module.AgentLogPage })), +) +const CaseDetailPage = lazy(() => + import('./pages/CaseDetailPage/CaseDetailPage').then((module) => ({ + default: module.CaseDetailPage, + })), +) +const CreateWorkPage = lazy(() => + import('./pages/CreateWorkPage/CreateWorkPage').then((module) => ({ + default: module.CreateWorkPage, + })), +) +const DashboardPage = lazy(() => + import('./pages/DashboardPage/DashboardPage').then((module) => ({ + default: module.DashboardPage, + })), +) +const DocumentDetailPage = lazy(() => + import('./pages/DocumentDetailPage/DocumentDetailPage').then((module) => ({ + default: module.DocumentDetailPage, + })), +) +const DocumentListPage = lazy(() => + import('./pages/DocumentListPage/DocumentListPage').then((module) => ({ + default: module.DocumentListPage, + })), +) +const ProfilePage = lazy(() => + import('./pages/ProfilePage/ProfilePage').then((module) => ({ default: module.ProfilePage })), +) +const ReviewWorkPage = lazy(() => + import('./pages/ReviewWorkPage/ReviewWorkPage').then((module) => ({ + default: module.ReviewWorkPage, + })), +) +const TicketDetailPage = lazy(() => + import('./pages/TicketDetailPage/TicketDetailPage').then((module) => ({ + default: module.TicketDetailPage, + })), +) +const TicketListPage = lazy(() => + import('./pages/TicketListPage/TicketListPage').then((module) => ({ + default: module.TicketListPage, + })), +) +const WorkerDetailPage = lazy(() => + import('./pages/WorkerDetailPage/WorkerDetailPage').then((module) => ({ + default: module.WorkerDetailPage, + })), +) +const WorkerListPage = lazy(() => + import('./pages/WorkerListPage/WorkerListPage').then((module) => ({ + default: module.WorkerListPage, + })), +) +const WorkListPage = lazy(() => + import('./pages/WorkListPage/WorkListPage').then((module) => ({ + default: module.WorkListPage, + })), +) export const router = createBrowserRouter([ { path: '/', element: , errorElement: },