임동욱 임동욱 08-18
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
+++ app/(protected)/(basic)/admins/loading.tsx
@@ -1,12 +1,6 @@
-import { Spinner } from '@/components/ui/spinner';
+import { PageLoading } from '@/components/ui/page-loading';
 
-// page.tsx가 스트리밍되는 동안의 폴백 — 같은 section 배치(flex-1 items-center justify-center)를
-// 써서 콘텐츠 교체 시 레이아웃이 흔들리지 않게 한다. Server Component로 충분해 'use client'를
-// 붙이지 않는다.
+// page.tsx가 스트리밍되는 동안의 폴백. 네 화면이 같은 모양이라 공용 컴포넌트를 쓴다.
 export default function Loading() {
-  return (
-    <section className="flex flex-1 flex-col items-center justify-center gap-2 p-8">
-      <Spinner size="lg" />
-    </section>
-  );
+  return <PageLoading />;
 }
app/(protected)/(basic)/boards/loading.tsx
--- app/(protected)/(basic)/boards/loading.tsx
+++ app/(protected)/(basic)/boards/loading.tsx
@@ -1,11 +1,6 @@
-import { Spinner } from '@/components/ui/spinner';
+import { PageLoading } from '@/components/ui/page-loading';
 
-// 게시판 세 화면(공지사항·1:1문의·FAQ)이 공유하는 스트리밍 폴백 — 이 세그먼트에 두면 하위
-// 라우트 전체에 적용되므로 화면마다 따로 두지 않는다. Server Component로 충분하다.
+// page.tsx가 스트리밍되는 동안의 폴백. 네 화면이 같은 모양이라 공용 컴포넌트를 쓴다.
 export default function Loading() {
-  return (
-    <section className="flex flex-1 flex-col items-center justify-center gap-2 p-8">
-      <Spinner size="lg" />
-    </section>
-  );
+  return <PageLoading />;
 }
app/(protected)/(basic)/decoration-items/loading.tsx
--- app/(protected)/(basic)/decoration-items/loading.tsx
+++ app/(protected)/(basic)/decoration-items/loading.tsx
@@ -1,12 +1,6 @@
-import { Spinner } from '@/components/ui/spinner';
+import { PageLoading } from '@/components/ui/page-loading';
 
-// page.tsx가 스트리밍되는 동안의 폴백 — 같은 section 배치(flex-1 items-center justify-center)를
-// 써서 콘텐츠 교체 시 레이아웃이 흔들리지 않게 한다. Server Component로 충분해 'use client'를
-// 붙이지 않는다(admins/loading.tsx와 동일한 패턴).
+// page.tsx가 스트리밍되는 동안의 폴백. 네 화면이 같은 모양이라 공용 컴포넌트를 쓴다.
 export default function Loading() {
-  return (
-    <section className="flex flex-1 flex-col items-center justify-center gap-2 p-8">
-      <Spinner size="lg" />
-    </section>
-  );
+  return <PageLoading />;
 }
app/(protected)/(basic)/students/loading.tsx
--- app/(protected)/(basic)/students/loading.tsx
+++ app/(protected)/(basic)/students/loading.tsx
@@ -1,12 +1,6 @@
-import { Spinner } from '@/components/ui/spinner';
+import { PageLoading } from '@/components/ui/page-loading';
 
-// page.tsx가 스트리밍되는 동안의 폴백 — 같은 section 배치(flex-1 items-center justify-center)를
-// 써서 콘텐츠 교체 시 레이아웃이 흔들리지 않게 한다. Server Component로 충분해 'use client'를
-// 붙이지 않는다.
+// page.tsx가 스트리밍되는 동안의 폴백. 네 화면이 같은 모양이라 공용 컴포넌트를 쓴다.
 export default function Loading() {
-  return (
-    <section className="flex flex-1 flex-col items-center justify-center gap-2 p-8">
-      <Spinner size="lg" />
-    </section>
-  );
+  return <PageLoading />;
 }
 
components/ui/page-loading.module.scss (added)
+++ components/ui/page-loading.module.scss
@@ -0,0 +1,13 @@
+@use "@fox/styles/abstracts" as fox;
+
+// 콘텐츠가 들어올 자리를 그대로 차지하고 가운데에 표시를 놓는다 — 교체될 때 레이아웃이
+// 흔들리지 않는다.
+.root {
+  display: flex;
+  flex: 1 0 0;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  min-block-size: 24rem;
+  padding: fox.padding(9);
+}
 
components/ui/page-loading.tsx (added)
+++ components/ui/page-loading.tsx
@@ -0,0 +1,17 @@
+import { Spinner } from './spinner';
+import styles from './page-loading.module.scss';
+
+/**
+ * 라우트 세그먼트가 스트리밍되는 동안의 폴백. 화면 네 곳의 `loading.tsx`가 같은 모양이라
+ * 여기 한 번만 둔다 — Next.js는 세그먼트마다 `loading.tsx` 파일을 요구하지만 내용은 공유할
+ * 수 있다.
+ *
+ * 상태를 갖지 않아 Server Component로 충분하다.
+ */
+export function PageLoading() {
+  return (
+    <section className={styles.root}>
+      <Spinner size="lg" />
+    </section>
+  );
+}
 
components/ui/spinner.module.scss (added)
+++ components/ui/spinner.module.scss
@@ -0,0 +1,15 @@
+@use "@fox/styles/abstracts" as fox;
+
+// 계열별 색. FoxSpinner는 기본이 테마 기본색이라, 다른 색이 필요한 자리만 여기서 덮는다.
+.primary {
+  color: fox.color(element-theme-primary);
+}
+
+// 어두운 막 위에 얹힐 때.
+.inverse {
+  color: fox.color(element-neutral-inverse);
+}
+
+.muted {
+  color: fox.color(element-neutral-default);
+}
components/ui/spinner.tsx
--- components/ui/spinner.tsx
+++ components/ui/spinner.tsx
@@ -1,16 +1,20 @@
-type SpinnerSize = "sm" | "md" | "lg";
-type SpinnerTone = "primary" | "inverse" | "muted";
+import { FoxSpinner } from '@/@fox/core/components/fox-spinner';
+import styles from './spinner.module.scss';
 
-const SIZE_CLASSES: Record<SpinnerSize, string> = {
-  sm: "size-4 border-2",
-  md: "size-6 border-2",
-  lg: "size-8 border-4",
+type SpinnerSize = 'sm' | 'md' | 'lg';
+type SpinnerTone = 'primary' | 'inverse' | 'muted';
+
+/** 시안에 크기 축이 없어 이 앱이 쓰던 세 단계를 그대로 유지한다. */
+const SIZE_PX: Record<SpinnerSize, number> = {
+  sm: 16,
+  md: 24,
+  lg: 32,
 };
 
-const TONE_CLASSES: Record<SpinnerTone, string> = {
-  primary: "text-primary",
-  inverse: "text-on-primary",
-  muted: "text-foreground-subtle",
+const TONE_CLASS: Record<SpinnerTone, string> = {
+  primary: styles.primary,
+  inverse: styles.inverse,
+  muted: styles.muted,
 };
 
 interface SpinnerProps {
@@ -20,19 +24,14 @@
   label?: string;
 }
 
-/** 로딩 인디케이터. 상태 없는 프레젠테이션 컴포넌트 — 표시 여부는 호출부 책임. */
-export function Spinner({
-  size = "md",
-  tone = "primary",
-  label = "불러오는 중",
-}: SpinnerProps) {
+/**
+ * 로딩 인디케이터. 상태 없는 프레젠테이션 컴포넌트 — 표시 여부는 호출부 책임.
+ *
+ * 모양은 디자인 시스템의 `FoxSpinner`가 갖는다. 이 껍데기는 앱이 쓰던 size·tone 축을
+ * 그대로 유지해 호출부를 건드리지 않으려고 남겨 둔다.
+ */
+export function Spinner({ size = 'md', tone = 'primary', label = '불러오는 중' }: SpinnerProps) {
   return (
-    <span role="status" className="inline-flex items-center">
-      <span
-        aria-hidden="true"
-        className={`inline-block animate-spin rounded-full border-current border-t-transparent ${SIZE_CLASSES[size]} ${TONE_CLASSES[tone]}`}
-      />
-      <span className="sr-only">{label}</span>
-    </span>
+    <FoxSpinner className={TONE_CLASS[tone]} size={SIZE_PX[size]} label={label} />
   );
 }
Add a comment
List