feat: 페이지 전환 로딩에 FoxSpinner 적용
전환 중 뜨는 표시가 Tailwind 클래스(animate-spin·border-t-transparent·size-8)로만 그려져 있었는데, Tailwind가 제거된 뒤로 그 클래스들이 정의되지 않아 아무것도 보이지 않는 상태였다. 감싸던 배치 클래스(flex·flex-1·items-center)도 마찬가지였다. - components/ui/spinner.tsx가 FoxSpinner를 그리도록 바꿨다. size·tone 축은 그대로 둬서 호출부 다섯 곳을 건드리지 않는다(loading.tsx 넷 + 전역 오버레이 하나). - 계열 색은 CSS 모듈로 옮겼다. FoxSpinner 기본이 테마색이라 inverse·muted만 덮는다. - 화면 넷의 loading.tsx가 같은 내용이라 PageLoading 공용 컴포넌트로 모으고, 죽어 있던 배치를 CSS 모듈로 되살렸다. 확인한 것: 두 CSS 모듈이 토큰 var()로 컴파일되고, FoxSpinner가 32px에서 시안대로 그려지며, tsc·eslint가 통과한다. 확인하지 못한 것: 전환 화면 자체는 로그인이 필요한 라우트라 눈으로 보지 못했다. Co-Authored-By: Claude Opus 5
@e67a86ee538cef14d9fceaa9a934cbb1be32434e
--- app/(protected)/(basic)/admins/loading.tsx
+++ app/(protected)/(basic)/admins/loading.tsx
... | ... | @@ -1,12 +1,6 @@ |
| 1 |
-import { Spinner } from '@/components/ui/spinner';
|
|
| 1 |
+import { PageLoading } from '@/components/ui/page-loading';
|
|
| 2 | 2 |
|
| 3 |
-// page.tsx가 스트리밍되는 동안의 폴백 — 같은 section 배치(flex-1 items-center justify-center)를 |
|
| 4 |
-// 써서 콘텐츠 교체 시 레이아웃이 흔들리지 않게 한다. Server Component로 충분해 'use client'를 |
|
| 5 |
-// 붙이지 않는다. |
|
| 3 |
+// page.tsx가 스트리밍되는 동안의 폴백. 네 화면이 같은 모양이라 공용 컴포넌트를 쓴다. |
|
| 6 | 4 |
export default function Loading() {
|
| 7 |
- return ( |
|
| 8 |
- <section className="flex flex-1 flex-col items-center justify-center gap-2 p-8"> |
|
| 9 |
- <Spinner size="lg" /> |
|
| 10 |
- </section> |
|
| 11 |
- ); |
|
| 5 |
+ return <PageLoading />; |
|
| 12 | 6 |
} |
--- app/(protected)/(basic)/boards/loading.tsx
+++ app/(protected)/(basic)/boards/loading.tsx
... | ... | @@ -1,11 +1,6 @@ |
| 1 |
-import { Spinner } from '@/components/ui/spinner';
|
|
| 1 |
+import { PageLoading } from '@/components/ui/page-loading';
|
|
| 2 | 2 |
|
| 3 |
-// 게시판 세 화면(공지사항·1:1문의·FAQ)이 공유하는 스트리밍 폴백 — 이 세그먼트에 두면 하위 |
|
| 4 |
-// 라우트 전체에 적용되므로 화면마다 따로 두지 않는다. Server Component로 충분하다. |
|
| 3 |
+// page.tsx가 스트리밍되는 동안의 폴백. 네 화면이 같은 모양이라 공용 컴포넌트를 쓴다. |
|
| 5 | 4 |
export default function Loading() {
|
| 6 |
- return ( |
|
| 7 |
- <section className="flex flex-1 flex-col items-center justify-center gap-2 p-8"> |
|
| 8 |
- <Spinner size="lg" /> |
|
| 9 |
- </section> |
|
| 10 |
- ); |
|
| 5 |
+ return <PageLoading />; |
|
| 11 | 6 |
} |
--- app/(protected)/(basic)/decoration-items/loading.tsx
+++ app/(protected)/(basic)/decoration-items/loading.tsx
... | ... | @@ -1,12 +1,6 @@ |
| 1 |
-import { Spinner } from '@/components/ui/spinner';
|
|
| 1 |
+import { PageLoading } from '@/components/ui/page-loading';
|
|
| 2 | 2 |
|
| 3 |
-// page.tsx가 스트리밍되는 동안의 폴백 — 같은 section 배치(flex-1 items-center justify-center)를 |
|
| 4 |
-// 써서 콘텐츠 교체 시 레이아웃이 흔들리지 않게 한다. Server Component로 충분해 'use client'를 |
|
| 5 |
-// 붙이지 않는다(admins/loading.tsx와 동일한 패턴). |
|
| 3 |
+// page.tsx가 스트리밍되는 동안의 폴백. 네 화면이 같은 모양이라 공용 컴포넌트를 쓴다. |
|
| 6 | 4 |
export default function Loading() {
|
| 7 |
- return ( |
|
| 8 |
- <section className="flex flex-1 flex-col items-center justify-center gap-2 p-8"> |
|
| 9 |
- <Spinner size="lg" /> |
|
| 10 |
- </section> |
|
| 11 |
- ); |
|
| 5 |
+ return <PageLoading />; |
|
| 12 | 6 |
} |
--- app/(protected)/(basic)/students/loading.tsx
+++ app/(protected)/(basic)/students/loading.tsx
... | ... | @@ -1,12 +1,6 @@ |
| 1 |
-import { Spinner } from '@/components/ui/spinner';
|
|
| 1 |
+import { PageLoading } from '@/components/ui/page-loading';
|
|
| 2 | 2 |
|
| 3 |
-// page.tsx가 스트리밍되는 동안의 폴백 — 같은 section 배치(flex-1 items-center justify-center)를 |
|
| 4 |
-// 써서 콘텐츠 교체 시 레이아웃이 흔들리지 않게 한다. Server Component로 충분해 'use client'를 |
|
| 5 |
-// 붙이지 않는다. |
|
| 3 |
+// page.tsx가 스트리밍되는 동안의 폴백. 네 화면이 같은 모양이라 공용 컴포넌트를 쓴다. |
|
| 6 | 4 |
export default function Loading() {
|
| 7 |
- return ( |
|
| 8 |
- <section className="flex flex-1 flex-col items-center justify-center gap-2 p-8"> |
|
| 9 |
- <Spinner size="lg" /> |
|
| 10 |
- </section> |
|
| 11 |
- ); |
|
| 5 |
+ return <PageLoading />; |
|
| 12 | 6 |
} |
+++ components/ui/page-loading.module.scss
... | ... | @@ -0,0 +1,13 @@ |
| 1 | +@use "@fox/styles/abstracts" as fox; | |
| 2 | + | |
| 3 | +// 콘텐츠가 들어올 자리를 그대로 차지하고 가운데에 표시를 놓는다 — 교체될 때 레이아웃이 | |
| 4 | +// 흔들리지 않는다. | |
| 5 | +.root { | |
| 6 | + display: flex; | |
| 7 | + flex: 1 0 0; | |
| 8 | + flex-direction: column; | |
| 9 | + align-items: center; | |
| 10 | + justify-content: center; | |
| 11 | + min-block-size: 24rem; | |
| 12 | + padding: fox.padding(9); | |
| 13 | +} |
+++ components/ui/page-loading.tsx
... | ... | @@ -0,0 +1,17 @@ |
| 1 | +import { Spinner } from './spinner'; | |
| 2 | +import styles from './page-loading.module.scss'; | |
| 3 | + | |
| 4 | +/** | |
| 5 | + * 라우트 세그먼트가 스트리밍되는 동안의 폴백. 화면 네 곳의 `loading.tsx`가 같은 모양이라 | |
| 6 | + * 여기 한 번만 둔다 — Next.js는 세그먼트마다 `loading.tsx` 파일을 요구하지만 내용은 공유할 | |
| 7 | + * 수 있다. | |
| 8 | + * | |
| 9 | + * 상태를 갖지 않아 Server Component로 충분하다. | |
| 10 | + */ | |
| 11 | +export function PageLoading() { | |
| 12 | + return ( | |
| 13 | + <section className={styles.root}> | |
| 14 | + <Spinner size="lg" /> | |
| 15 | + </section> | |
| 16 | + ); | |
| 17 | +} |
+++ components/ui/spinner.module.scss
... | ... | @@ -0,0 +1,15 @@ |
| 1 | +@use "@fox/styles/abstracts" as fox; | |
| 2 | + | |
| 3 | +// 계열별 색. FoxSpinner는 기본이 테마 기본색이라, 다른 색이 필요한 자리만 여기서 덮는다. | |
| 4 | +.primary { | |
| 5 | + color: fox.color(element-theme-primary); | |
| 6 | +} | |
| 7 | + | |
| 8 | +// 어두운 막 위에 얹힐 때. | |
| 9 | +.inverse { | |
| 10 | + color: fox.color(element-neutral-inverse); | |
| 11 | +} | |
| 12 | + | |
| 13 | +.muted { | |
| 14 | + color: fox.color(element-neutral-default); | |
| 15 | +} |
--- components/ui/spinner.tsx
+++ components/ui/spinner.tsx
... | ... | @@ -1,16 +1,20 @@ |
| 1 |
-type SpinnerSize = "sm" | "md" | "lg"; |
|
| 2 |
-type SpinnerTone = "primary" | "inverse" | "muted"; |
|
| 1 |
+import { FoxSpinner } from '@/@fox/core/components/fox-spinner';
|
|
| 2 |
+import styles from './spinner.module.scss'; |
|
| 3 | 3 |
|
| 4 |
-const SIZE_CLASSES: Record<SpinnerSize, string> = {
|
|
| 5 |
- sm: "size-4 border-2", |
|
| 6 |
- md: "size-6 border-2", |
|
| 7 |
- lg: "size-8 border-4", |
|
| 4 |
+type SpinnerSize = 'sm' | 'md' | 'lg'; |
|
| 5 |
+type SpinnerTone = 'primary' | 'inverse' | 'muted'; |
|
| 6 |
+ |
|
| 7 |
+/** 시안에 크기 축이 없어 이 앱이 쓰던 세 단계를 그대로 유지한다. */ |
|
| 8 |
+const SIZE_PX: Record<SpinnerSize, number> = {
|
|
| 9 |
+ sm: 16, |
|
| 10 |
+ md: 24, |
|
| 11 |
+ lg: 32, |
|
| 8 | 12 |
}; |
| 9 | 13 |
|
| 10 |
-const TONE_CLASSES: Record<SpinnerTone, string> = {
|
|
| 11 |
- primary: "text-primary", |
|
| 12 |
- inverse: "text-on-primary", |
|
| 13 |
- muted: "text-foreground-subtle", |
|
| 14 |
+const TONE_CLASS: Record<SpinnerTone, string> = {
|
|
| 15 |
+ primary: styles.primary, |
|
| 16 |
+ inverse: styles.inverse, |
|
| 17 |
+ muted: styles.muted, |
|
| 14 | 18 |
}; |
| 15 | 19 |
|
| 16 | 20 |
interface SpinnerProps {
|
... | ... | @@ -20,19 +24,14 @@ |
| 20 | 24 |
label?: string; |
| 21 | 25 |
} |
| 22 | 26 |
|
| 23 |
-/** 로딩 인디케이터. 상태 없는 프레젠테이션 컴포넌트 — 표시 여부는 호출부 책임. */ |
|
| 24 |
-export function Spinner({
|
|
| 25 |
- size = "md", |
|
| 26 |
- tone = "primary", |
|
| 27 |
- label = "불러오는 중", |
|
| 28 |
-}: SpinnerProps) {
|
|
| 27 |
+/** |
|
| 28 |
+ * 로딩 인디케이터. 상태 없는 프레젠테이션 컴포넌트 — 표시 여부는 호출부 책임. |
|
| 29 |
+ * |
|
| 30 |
+ * 모양은 디자인 시스템의 `FoxSpinner`가 갖는다. 이 껍데기는 앱이 쓰던 size·tone 축을 |
|
| 31 |
+ * 그대로 유지해 호출부를 건드리지 않으려고 남겨 둔다. |
|
| 32 |
+ */ |
|
| 33 |
+export function Spinner({ size = 'md', tone = 'primary', label = '불러오는 중' }: SpinnerProps) {
|
|
| 29 | 34 |
return ( |
| 30 |
- <span role="status" className="inline-flex items-center"> |
|
| 31 |
- <span |
|
| 32 |
- aria-hidden="true" |
|
| 33 |
- className={`inline-block animate-spin rounded-full border-current border-t-transparent ${SIZE_CLASSES[size]} ${TONE_CLASSES[tone]}`}
|
|
| 34 |
- /> |
|
| 35 |
- <span className="sr-only">{label}</span>
|
|
| 36 |
- </span> |
|
| 35 |
+ <FoxSpinner className={TONE_CLASS[tone]} size={SIZE_PX[size]} label={label} />
|
|
| 37 | 36 |
); |
| 38 | 37 |
} |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?