Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/components/layout/RouteTransition.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
}
26 changes: 25 additions & 1 deletion src/components/layout/RouteTransition.test.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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: <RouteTransition />, children: [{ path: '/tasks', element: <LazyPage /> }] }],
{ initialEntries: ['/tasks'] },
)
render(<RouterProvider router={router} />)

expect(screen.getByRole('status')).toHaveTextContent('화면을 불러오는 중입니다')

await act(async () => {
resolvePage({ default: () => <p>lazy task content</p> })
})

expect(await screen.findByText('lazy task content')).toBeInTheDocument()
})
})
12 changes: 11 additions & 1 deletion src/components/layout/RouteTransition.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Suspense } from 'react'
import { Outlet, useLocation } from 'react-router-dom'
import styles from './RouteTransition.module.css'

Expand All @@ -6,7 +7,16 @@ export function RouteTransition() {

return (
<div key={location.pathname} className={styles.fade}>
<Outlet />
<Suspense
fallback={
<div className={styles.loading} role="status" aria-live="polite">
<span className={styles.loadingDot} aria-hidden="true" />
화면을 불러오는 중입니다
</div>
}
>
<Outlet />
</Suspense>
</div>
)
}
76 changes: 63 additions & 13 deletions src/routes.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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: <IntroPage />, errorElement: <NotFoundPage /> },
Expand Down
Loading