임동욱 임동욱 08-11
refactor: 앱 글로벌 스타일을 @fox 기반 SCSS로 전환
globals는 @fox 로드와 앱 셸 구조만 담당하고 디자인 값은 갖지 않는다.
폰트는 앱이 소유하고 @fox는 --app-font-sans/--app-font-mono를 참조만 하므로,
@fox 폴더를 다른 프로젝트로 복사해도 그쪽 서체를 그대로 따른다.

Co-Authored-By: Claude Opus 5 
@03f6ce0f02e4d76ad84836ca8995341819ffcac8
 
app/globals.css (deleted)
--- app/globals.css
@@ -1,273 +0,0 @@
-@import "tailwindcss";
-
-/* 색상 토큰 출처: edupay-app lib/ui/core/tokens/app_colors.dart
-   (Figma: FOX PAY 1차 제작시안, node 91:10205). 다크 값은 edupay-app에 대응
-   다크 시안이 없어 "임의 도출 — 정식 다크 시안 확정 시 교체".
-   메커니즘: `color-scheme` + `light-dark()`(Chrome 123+/Safari 17.5+/Firefox 120+
-   지원, 미지원 브라우저는 값이 무효화되므로 사내 어드민 한정 트레이드오프로 채택
-   — design 계획 §4.1/§5.3, 2026-07-28 게이트 승인). 각 시맨틱 토큰이
-   `light-dark(라이트값, 다크값)`으로 라이트/다크 값을 한 선언에 함께 보유해
-   드리프트를 구조적으로 차단한다. 수동 전환은 아래 `:root[data-theme]`가
-   `color-scheme`만 덮어써 처리하므로 값 자체는 이 블록 한 곳에만 존재한다
-   (Next.js 16 번들 문서 preventing-flash-before-hydration.md §Themes 패턴). */
-:root {
-  color-scheme: light dark;
-
-  /* Brand */
-  --primary: light-dark(#502ef5, #877aec);
-  --primary-container: light-dark(#6d5ce7, #502ef5);
-  /* on-primary 다크값 대비 보정: 원래 #ffffff였으나 다크 --primary(#877aec) 위에서
-     3.49:1로 WCAG AA(4.5:1) 텍스트 기준 미달 확인 → #14161b로 교체해 5.18:1 통과.
-     Material 3 다크 규약(밝은 primary 표면 + 어두운 onPrimary 텍스트)과 일치하며
-     text-primary on surface(4.61:1)에는 영향 없음 — design 계획 §5.1(i),
-     2026-07-28 게이트 승인. */
-  --on-primary: light-dark(#ffffff, #14161b);
-  --on-primary-container: light-dark(#e2e0fb, #e2e0fb);
-  --secondary: light-dark(#877aec, #9b92ff);
-
-  /* Surface / background (원 토큰: surface / gray0) */
-  --background: light-dark(#ffffff, #14161b);
-  --surface: light-dark(#ffffff, #1e2128);
-  --surface-muted: light-dark(#f5f7fa, #2c3038);
-
-  /* Foreground / text (원 토큰: onSurface 계열) */
-  --foreground: light-dark(#2c3038, #f5f7fa);
-  --foreground-muted: light-dark(#565d6b, #bbc4d8);
-  --foreground-subtle: light-dark(#6b7486, #8790a3);
-
-  /* Border (원 토큰: outline 계열) */
-  --border: light-dark(#dae0ed, #424752);
-  --border-subtle: light-dark(#e1e6f1, #343943);
-
-  /* Status */
-  --danger: light-dark(#ec2e2f, #ff5c5d);
-  --success: light-dark(#21b770, #3acb93);
-  --warning: light-dark(#ffb020, #ffc24d);
-
-  /* Overlay */
-  --scrim: light-dark(rgb(0 0 0 / 0.6), rgb(0 0 0 / 0.7));
-}
-
-/* 수동 테마 선택 — <html data-theme="light"|"dark">가 `color-scheme`만 덮어써
-   위 light-dark() 값이 해당 분기로 고정된다(값 자체는 여기서 재정의하지 않음).
-   data-theme 속성이 없거나 "system"이면 :root의 `color-scheme: light dark`가
-   유지되어 OS `prefers-color-scheme`를 그대로 따른다(feature 전달 사양). */
-:root[data-theme="light"] {
-  color-scheme: light;
-}
-
-:root[data-theme="dark"] {
-  color-scheme: dark;
-}
-
-@theme inline {
-  --color-primary: var(--primary);
-  --color-primary-container: var(--primary-container);
-  --color-on-primary: var(--on-primary);
-  --color-on-primary-container: var(--on-primary-container);
-  --color-secondary: var(--secondary);
-
-  --color-background: var(--background);
-  --color-surface: var(--surface);
-  --color-surface-muted: var(--surface-muted);
-
-  --color-foreground: var(--foreground);
-  --color-foreground-muted: var(--foreground-muted);
-  --color-foreground-subtle: var(--foreground-subtle);
-
-  --color-border: var(--border);
-  --color-border-subtle: var(--border-subtle);
-
-  --color-danger: var(--danger);
-  --color-success: var(--success);
-  --color-warning: var(--warning);
-
-  --color-scrim: var(--scrim);
-
-  /* WantedSans 연결 전에도 안전한 폴백(feature 레인이 app/layout.tsx에서
-     next/font/local로 --font-wanted-sans를 정의). */
-  --font-sans: var(--font-wanted-sans, system-ui), system-ui,
-    "Apple SD Gothic Neo", sans-serif;
-  --font-mono: var(--font-geist-mono);
-
-  /* 타이포 토큰 출처: edupay-app lib/ui/core/tokens/app_typography.dart
-     (Figma: FOX PAY 1차 제작시안, node 112:11439). voucher/numeric 등 앱 전용
-     스타일은 어드민 무관으로 미이식. */
-  --text-display-lg: 3rem;
-  --text-display-lg--line-height: 1.5;
-  --text-display-lg--font-weight: 700;
-
-  --text-display-md: 2.25rem;
-  --text-display-md--line-height: 1.5;
-  --text-display-md--letter-spacing: -0.72px;
-  --text-display-md--font-weight: 700;
-
-  --text-display-sm: 2rem;
-  --text-display-sm--line-height: 1.5;
-  --text-display-sm--letter-spacing: -0.64px;
-  --text-display-sm--font-weight: 700;
-
-  --text-headline-lg: 1.5rem;
-  --text-headline-lg--line-height: 1.5;
-  --text-headline-lg--letter-spacing: -0.72px;
-  --text-headline-lg--font-weight: 700;
-
-  --text-headline-md: 1.25rem;
-  --text-headline-md--line-height: 1.5;
-  --text-headline-md--letter-spacing: -0.6px;
-  --text-headline-md--font-weight: 700;
-
-  --text-headline-sm: 1.125rem;
-  --text-headline-sm--line-height: 1.3;
-  --text-headline-sm--letter-spacing: -0.54px;
-  --text-headline-sm--font-weight: 700;
-
-  --text-title-lg: 1.125rem;
-  --text-title-lg--line-height: 1.5;
-  --text-title-lg--font-weight: 700;
-
-  --text-title-md: 1rem;
-  --text-title-md--line-height: 1.5;
-  --text-title-md--font-weight: 700;
-
-  --text-title-sm: 0.875rem;
-  --text-title-sm--line-height: 1.5;
-  --text-title-sm--letter-spacing: -0.42px;
-  --text-title-sm--font-weight: 700;
-
-  --text-body-lg: 1rem;
-  --text-body-lg--line-height: 1.5;
-  --text-body-lg--letter-spacing: -0.48px;
-  --text-body-lg--font-weight: 400;
-
-  --text-body-md: 0.875rem;
-  --text-body-md--line-height: 1.5;
-  --text-body-md--font-weight: 400;
-
-  --text-body-sm: 0.75rem;
-  --text-body-sm--line-height: 1.5;
-  --text-body-sm--font-weight: 400;
-
-  --text-label-lg: 1rem;
-  --text-label-lg--line-height: 1.4;
-  --text-label-lg--letter-spacing: -0.48px;
-  --text-label-lg--font-weight: 700;
-
-  --text-label-md: 0.875rem;
-  --text-label-md--letter-spacing: -0.28px;
-  --text-label-md--font-weight: 400;
-
-  --text-label-sm: 0.75rem;
-  --text-label-sm--font-weight: 400;
-
-  /* radius 토큰 출처: edupay-app lib/ui/core/tokens/app_radius.dart.
-     Tailwind 기본 스케일과 다른 값으로 의도적 재정의(앱 일체화, §8-4 승인).
-     pill(999)은 기본 rounded-full로 충족, card(50)은 앱 전용 미이식. */
-  --radius-xs: 0.25rem;
-  --radius-sm: 0.5rem;
-  --radius-md: 0.75rem;
-  --radius-lg: 1rem;
-  --radius-xl: 1.25rem;
-  --radius-2xl: 1.5rem;
-
-  /* 그림자 토큰 출처: edupay-app lib/ui/core/tokens/app_shadows.dart —
-     어드민 유효분만 이식(voucher inner·stacked 등 미이식). */
-  --shadow-card: 0 5.16px 5.16px rgb(93 100 119 / 0.12);
-  --shadow-list-card: 0 8px 16px rgb(93 100 119 / 0.12);
-  --shadow-dialog: 0 8px 16px rgb(0 0 0 / 0.1);
-  --shadow-popup: 2.16px 6.32px 6px rgb(177 178 180 / 0.24);
-  --shadow-toast: 0 10px 12px rgb(0 0 0 / 0.2);
-  --shadow-subtle: 0 2px 4px rgb(0 0 0 / 0.08);
-
-  /* 애니메이션 토큰(신규 네임스페이스 — §4.2 승인). 오버레이·다이얼로그·토스트·
-     스플래시 전환에 사용. --ease-*는 Tailwind 기본값 사용, duration은 값에
-     내장(트랜지션 duration 표준은 bare 유틸 duration-200 사용). */
-  --animate-fade-in: fade-in 200ms var(--ease-out);
-  --animate-fade-out: fade-out 200ms var(--ease-in);
-  --animate-dialog-in: dialog-in 200ms var(--ease-out);
-  --animate-toast-in: toast-in 300ms var(--ease-out);
-}
-
-@keyframes fade-in {
-  from {
-    opacity: 0;
-  }
-  to {
-    opacity: 1;
-  }
-}
-
-/* 스플래시 등 풀스크린 오버레이 퇴장용 — fade-in의 역방향 키프레임. */
-@keyframes fade-out {
-  from {
-    opacity: 1;
-  }
-  to {
-    opacity: 0;
-  }
-}
-
-@keyframes dialog-in {
-  from {
-    opacity: 0;
-    transform: scale(0.95);
-  }
-  to {
-    opacity: 1;
-    transform: scale(1);
-  }
-}
-
-@keyframes toast-in {
-  from {
-    opacity: 0;
-    transform: translateY(0.5rem);
-  }
-  to {
-    opacity: 1;
-    transform: translateY(0);
-  }
-}
-
-/* 간격 토큰 매핑 (변경 없음) — edupay-app lib/ui/core/tokens/app_spacing.dart의
-   4/8 grid(4·8·12·16·20·24·32·40)는 Tailwind 기본 --spacing: 0.25rem 스케일과
-   정확히 일치한다: xs→p-1, sm→p-2, md→p-3, lg→p-4, xl→p-5, xxl→p-6,
-   xxxl→p-8, xxxxl→p-10. */
-
-/* 테마 수동 전환 — feature 소비처(예: 헤더 테마 토글)를 위한 CSS 구동 변형자.
-   design 계획 §9 게이트 교차 검증에서 feature 안(아이콘 3개를 모두 DOM에 렌더하고
-   CSS만으로 하나를 표시)을 채택했다 — 서버/클라 출력이 항상 동일해 hydration
-   불일치 표면적이 0이다. `theme-light`/`theme-dark`는 명시적 선택에만 매칭되고,
-   `theme-system`은 data-theme 부재 **및** 명시적 data-theme="system" 값 둘 다
-   커버해야 하므로(둘은 동일하게 동작 — feature 전달 사양) 부정 선택자 두 개로
-   "light도 dark도 아님"을 표현한다(속성 부재 시에도 :not()은 매칭된다). */
-@custom-variant theme-system (&:where(html:not([data-theme="light"]):not([data-theme="dark"]) *));
-@custom-variant theme-light (&:where(html[data-theme="light"] *));
-@custom-variant theme-dark (&:where(html[data-theme="dark"] *));
-
-/* 테마 전환 색상 트랜지션 — feature가 토글 직전 <html>에 [data-theme-transition]을
-   부여하고 ~200ms 후 제거하는 시간 한정 opt-in이다(상시 트랜지션이 아니므로 다른
-   hover/트랜지션 유틸과 경쟁하지 않는다). prefers-reduced-motion: no-preference로
-   게이팅해 감소 모션 환경에서는 아무 트랜지션도 걸리지 않는다(전환 없이 즉시 반영).
-   !important는 유틸리티의 transition 계열 클래스(transition-none 포함)가 이 200ms
-   동안만 존재하는 규칙을 덮어쓰지 않도록 한다. box-shadow는 제외 — --shadow 토큰은
-   테마별로 값이 바뀌지 않아 전환할 대상이 없다. */
-@media (prefers-reduced-motion: no-preference) {
-  html[data-theme-transition],
-  html[data-theme-transition] *,
-  html[data-theme-transition] *::before,
-  html[data-theme-transition] *::after {
-    transition:
-      background-color 200ms var(--ease-out),
-      border-color 200ms var(--ease-out),
-      color 200ms var(--ease-out),
-      fill 200ms var(--ease-out),
-      stroke 200ms var(--ease-out),
-      outline-color 200ms var(--ease-out) !important;
-  }
-}
-
-body {
-  background: var(--background);
-  color: var(--foreground);
-}
 
app/globals.scss (added)
+++ app/globals.scss
@@ -0,0 +1,20 @@
+// 앱 글로벌 스타일 — 디자인 시스템 로드 + **이 앱에만 해당하는** 최소 베이스.
+//
+// 색·타이포·간격 같은 디자인 값은 여기에 두지 않는다. 전부 `@fox/styles/tokens/`가
+// 소유하며, 이 파일은 "@fox를 켜고, 이 앱의 셸 구조를 잡는다"까지만 한다.
+// 화면·컴포넌트 스타일은 각자의 `.module.scss`로 간다.
+
+@use "@fox/styles";
+
+// 앱 셸 — 루트 레이아웃이 뷰포트 높이를 채우고 세로로 쌓이는 구조.
+// 푸터를 바닥에 붙이거나 본문 영역만 스크롤시키는 화면들이 이 전제에 기댄다.
+html,
+body {
+  block-size: 100%;
+}
+
+body {
+  display: flex;
+  flex-direction: column;
+  min-block-size: 100%;
+}
app/layout.tsx
--- app/layout.tsx
+++ app/layout.tsx
@@ -9,7 +9,7 @@
   THEME_ATTRIBUTE_NAME,
   THEME_STORAGE_KEY,
 } from "@/lib/constants/theme";
-import "./globals.css";
+import "./globals.scss";
 
 // 첫 페인트 전 테마 적용 — <head>는 <body>보다 먼저 파싱되므로 <html>만 조작 가능하다
 // (preventing-flash-before-hydration.md의 Themes 패턴). localStorage에 명시 선택("light"
@@ -41,9 +41,11 @@
   SPLASH_SHOWN_STORAGE_KEY
 )}))document.documentElement.setAttribute("data-splash-shown","")}catch(e){}})()`;
 
-// 어드민 전체 기본 서체(edupay-app 일체화, design 전달 사양) — Geist는 대체되어 제거했다.
-// globals.css `--font-sans`가 이 변수를 우선 참조하고, 로드 전에도 system-ui로 안전하게
-// 폴백하도록 이미 구성돼 있다.
+// 어드민 전체 기본 서체. 폰트 파일은 **앱이 소유**하고 디자인 시스템은 참조만 한다 —
+// `@fox`는 `--app-font-sans`를 읽을 뿐이라(@fox/README.md "호스트 앱과의 계약") 폴더를
+// 다른 프로젝트로 복사해도 그 프로젝트의 서체를 그대로 따른다. 변수명이 `--app-*`인 것도
+// 그 계약의 이름이기 때문이며, 미주입 시에도 `system-ui`로 폴백하도록 토큰 쪽에 폴백이
+// 들어 있다.
 const wantedSans = localFont({
   src: [
     {
@@ -67,14 +69,13 @@
       style: "normal",
     },
   ],
-  variable: "--font-wanted-sans",
+  variable: "--app-font-sans",
   display: "swap",
 });
 
-// 코드/숫자 표기용 고정폭 서체 — globals.css `--font-mono`가 여전히 이 변수를 직접 참조하므로
-// (폴백 체인 없음) 유지한다.
+// 코드/숫자 표기용 고정폭 서체 — sans와 동일하게 `--app-font-mono` 계약으로 주입한다.
 const geistMono = Geist_Mono({
-  variable: "--font-geist-mono",
+  variable: "--app-font-mono",
   subsets: ["latin"],
 });
 
@@ -107,14 +108,14 @@
     <html
       lang="ko"
       suppressHydrationWarning
-      className={`${wantedSans.variable} ${geistMono.variable} h-full antialiased`}
+      className={`${wantedSans.variable} ${geistMono.variable}`}
     >
       <head>
         <script dangerouslySetInnerHTML={{ __html: THEME_BOOTSTRAP_SCRIPT }} />
         <style dangerouslySetInnerHTML={{ __html: SPLASH_HIDE_STYLE }} />
         <script dangerouslySetInnerHTML={{ __html: SPLASH_GATE_SCRIPT }} />
       </head>
-      <body className="min-h-full flex flex-col">
+      <body>
         <SplashGate />
         <FeedbackProvider>
           {children}
Add a comment
List