임동욱 임동욱 08-13
fix: Pretendard 미로드로 @fox 컴포넌트가 OS 기본 서체로 그려지던 문제 수정
@fox 토큰이 패밀리명을 리터럴 `Pretendard`로 지정하는데 프로젝트 어디에도 그 이름의 폰트
공급이 없었다. 그래서 모든 @fox 컴포넌트가 OS 기본 서체(맥 Apple SD Gothic Neo / 윈도
맑은 고딕)로 갈라져 그려졌고, 글자 폭이 시안과 어긋났다.

- public/fonts: Pretendard v1.3.9 woff2 400/600/800 반입(OFL 1.1). 토큰이 쓰는 weight가
  정확히 이 셋이다 — 특히 800은 wantedSans에 없어 가짜 굵기로 합성되고 있었다.
- globals.scss: @font-face로 직접 선언. next/font/local은 패밀리명을 해시로 바꿔 토큰의
  리터럴과 만나지 못해 이 용도로는 쓸 수 없다.
- globals.scss: body에 `--app-font-sans` 연결. 이 계약 변수를 읽는 곳이 없어 wantedSans가
  로드만 되고 어디에도 적용되지 않았다. 이제 @fox 밖 텍스트가 wantedSans를 쓴다.
- next.config.ts: `/fonts/*`를 no-store에서 제외. public/ 자산이라 그대로 두면 페이지를
  열 때마다 2.2MB를 다시 받는다.
- proxy.ts: 폰트 확장자를 matcher에서 제외. 세션 없는 방문자의 폰트 요청이 `/login`으로
  리다이렉트돼 로그인 화면만 대체 서체로 그려졌다.
- @fox/README.md: "호스트 앱과의 계약"이 @fox 컴포넌트도 `--app-font-sans`를 읽는 것처럼
  적혀 있었으나 사실이 아니다. 실제 요구사항(Pretendard @font-face·400/600/800)으로 교체.

FoxCarouselIndicator number형 56.92px → 59.69px. 시안 62px과의 잔여 2.31px은 Figma가 텍스트
상자 폭을 정수로 올리고(+1.57) 마지막 글자의 음수 자간을 빼지 않는(+0.75) 표기 차이다.

Co-Authored-By: Claude Opus 5 
@2d7a4a8e0be5958d9f4f91ad13f1a2c2baaeabec
@fox/README.md
--- @fox/README.md
+++ @fox/README.md
@@ -61,7 +61,8 @@
    { "compilerOptions": { "paths": { "@fox/*": ["./@fox/*"] } } }
    ```
 
-6. (선택) 폰트를 주입합니다 — 아래 "호스트 앱과의 계약" 참고.
+6. **폰트를 공급합니다** — 아래 "호스트 앱과의 계약" 참고. 빠뜨리면 컴포넌트가 OS 기본
+   서체로 그려져 글자 폭이 시안과 어긋납니다.
 
 ## React 없이 쓰기
 
@@ -220,15 +221,25 @@
 
 ## 호스트 앱과의 계약
 
-`@fox`가 소유하지 않고 앱에서 받는 값입니다.
+**폰트 파일은 앱이 소유합니다.** `@fox`는 폰트를 담고 있지 않으므로, 앱이 아래를 공급하지
+않으면 브라우저 기본 서체로 그려집니다.
 
-| CSS 변수 | 용도 | 미주입 시 |
+| 앱이 공급할 것 | 쓰이는 곳 | 미공급 시 |
 | --- | --- | --- |
-| `--app-font-sans` | 기본 서체 | `system-ui`로 폴백 |
-| `--app-font-mono` | 고정폭 서체 | `ui-monospace`로 폴백 |
+| `@font-face { font-family: "Pretendard" }` | **`@fox` 컴포넌트 전부** | OS 기본 서체(맥 Apple SD Gothic Neo / 윈도 맑은 고딕)로 갈라짐 |
+| `--app-font-sans` | `@fox` 밖 앱 텍스트 | 앱이 알아서 폴백 |
+| `--app-font-mono` | 고정폭이 필요한 앱 텍스트 | 앱이 알아서 폴백 |
 
-폰트 파일은 앱이 소유합니다. `@fox`는 변수를 참조만 하므로, 복사해 간 프로젝트는 그
-프로젝트의 폰트를 그대로 씁니다.
+> ⚠️ `@fox` 컴포넌트의 서체는 `--app-font-sans`가 **아닙니다.** 토큰
+> (`tokens/_primitive.scss`의 `font.family.*`)이 패밀리명을 리터럴 `Pretendard`로 못 박고
+> 있어, 그 이름의 `@font-face`를 앱이 선언해 줘야 합니다. 폰트를 바꾸려면 Figma 원본 토큰을
+> 고쳐 재생성하는 수밖에 없습니다 — 즉 이 지점만은 이식성이 없습니다.
+>
+> 필요한 굵기는 토큰이 쓰는 **400 / 600 / 800** 세 개입니다. 800을 빼면 `font-weight(bold)`가
+> 가짜 굵기로 합성됩니다.
+>
+> `next/font/local`은 패밀리명을 해시(`__Pretendard_xxxx`)로 바꾸므로 이 용도로는 쓸 수
+> 없습니다. 이 저장소는 `app/globals.scss`에 `@font-face`를 직접 선언합니다.
 
 ## 새 컴포넌트 추가
 
app/globals.scss
--- app/globals.scss
+++ app/globals.scss
@@ -10,6 +10,35 @@
 // 그래야 React 밖(다른 프레임워크·순수 SCSS)에서도 같은 방식으로 쓸 수 있다.
 @use "@fox/styles/components";
 
+// Pretendard — @fox 토큰이 패밀리명을 **리터럴 `Pretendard`로** 지정하므로(`font.family.*`),
+// 앱은 정확히 그 이름의 폰트를 공급해야 한다. `next/font/local`은 패밀리명을
+// 해시(`__Pretendard_xxxx`)로 바꿔 그 리터럴과 영영 만나지 못해 쓸 수 없다 — 토큰은 Figma
+// export 자동 생성이라 이름을 코드 쪽에 맞추는 반대 방향도 불가능하다.
+// weight는 토큰이 실제로 쓰는 400·600·800 셋뿐이다(`weight-regular`/`medium`/`bold`).
+@font-face {
+  font-family: "Pretendard";
+  font-style: normal;
+  font-weight: 400;
+  font-display: swap;
+  src: url("/fonts/pretendard-regular.woff2") format("woff2");
+}
+
+@font-face {
+  font-family: "Pretendard";
+  font-style: normal;
+  font-weight: 600;
+  font-display: swap;
+  src: url("/fonts/pretendard-semibold.woff2") format("woff2");
+}
+
+@font-face {
+  font-family: "Pretendard";
+  font-style: normal;
+  font-weight: 800;
+  font-display: swap;
+  src: url("/fonts/pretendard-extrabold.woff2") format("woff2");
+}
+
 // 앱 셸 — 루트 레이아웃이 뷰포트 높이를 채우고 세로로 쌓이는 구조.
 // 푸터를 바닥에 붙이거나 본문 영역만 스크롤시키는 화면들이 이 전제에 기댄다.
 html,
@@ -21,4 +50,10 @@
   display: flex;
   flex-direction: column;
   min-block-size: 100%;
+
+  // @fox 밖 텍스트의 서체. 디자인 값이 아니라 layout.tsx가 주입하는 계약 변수를 꽂는
+  // 지점이라 여기 둔다 — 이 한 줄이 없으면 `--app-font-sans`를 읽는 곳이 없어 wantedSans가
+  // 로드만 되고 아무 데도 적용되지 않는다. @fox 컴포넌트는 각자 토큰으로 Pretendard를
+  // 덮어쓰므로 영향을 받지 않는다.
+  font-family: var(--app-font-sans, system-ui), system-ui, sans-serif;
 }
app/layout.tsx
--- app/layout.tsx
+++ app/layout.tsx
@@ -25,11 +25,10 @@
   THEME_ATTRIBUTE_NAME
 )},t)}catch(e){}})()`;
 
-// 어드민 전체 기본 서체. 폰트 파일은 **앱이 소유**하고 디자인 시스템은 참조만 한다 —
-// `@fox`는 `--app-font-sans`를 읽을 뿐이라(@fox/README.md "호스트 앱과의 계약") 폴더를
-// 다른 프로젝트로 복사해도 그 프로젝트의 서체를 그대로 따른다. 변수명이 `--app-*`인 것도
-// 그 계약의 이름이기 때문이며, 미주입 시에도 `system-ui`로 폴백하도록 토큰 쪽에 폴백이
-// 들어 있다.
+// `@fox` **밖** 앱 텍스트의 기본 서체. 폰트 파일은 앱이 소유하고 `--app-font-sans` 계약으로
+// 넘긴다(@fox/README.md "호스트 앱과의 계약") — 그 변수를 실제로 꽂는 곳은 globals.scss의
+// `body`다. @fox 컴포넌트는 이 값을 쓰지 않는다: 토큰이 패밀리명을 `Pretendard`로 직접
+// 지정하고 그 @font-face도 globals.scss에 있다. 즉 한 화면에 두 서체가 공존한다.
 const wantedSans = localFont({
   src: [
     {
@@ -96,6 +95,17 @@
     >
       <head>
         <script dangerouslySetInnerHTML={{ __html: THEME_BOOTSTRAP_SCRIPT }} />
+        {/* @font-face는 CSS를 다 읽은 뒤에야 발견돼 본문이 한 번 폴백 서체로 그려진다.
+            거의 모든 화면이 쓰는 regular만 미리 받아 그 깜빡임을 없앤다 — 600/800은
+            빈도가 낮아 선점 비용이 이득보다 크다. next/font로 넣은 wantedSans는 Next.js가
+            알아서 preload하므로 여기 없다. */}
+        <link
+          rel="preload"
+          as="font"
+          type="font/woff2"
+          href="/fonts/pretendard-regular.woff2"
+          crossOrigin="anonymous"
+        />
       </head>
       <body>
         <FeedbackProvider>
next.config.ts
--- next.config.ts
+++ next.config.ts
@@ -31,9 +31,20 @@
   async headers() {
     return [
       {
-        source: "/((?!_next/static|_next/image).*)",
+        source: "/((?!_next/static|_next/image|fonts/).*)",
         headers: SECURITY_HEADERS,
       },
+      {
+        // `public/fonts`는 Next.js가 해시 경로를 붙여주지 않아 위 no-store 규칙에 그대로
+        // 걸린다 — 제외하지 않으면 페이지를 열 때마다 Pretendard 2.2MB를 다시 받는다.
+        // `immutable`은 일부러 뺐다: 파일명에 버전이 없어서 폰트를 같은 이름으로 교체했을 때
+        // 새로고침조차 먹지 않게 된다.
+        source: "/fonts/:path*",
+        headers: [
+          { key: "Cache-Control", value: "public, max-age=31536000" },
+          { key: "X-Content-Type-Options", value: "nosniff" },
+        ],
+      },
     ];
   },
 };
proxy.ts
--- proxy.ts
+++ proxy.ts
@@ -49,8 +49,14 @@
   return NextResponse.next();
 }
 
+/**
+ * 정적 자산은 인증 대상이 아니다. 폰트 확장자가 목록에 있어야 하는 이유: `public/fonts`의
+ * Pretendard는 `next/font`를 거치지 않아 `/_next/static`이 아닌 `/fonts/*`로 서빙된다 —
+ * 빼면 세션 없는 방문자의 폰트 요청이 `/login`으로 리다이렉트돼 로그인 화면만 대체 서체로
+ * 그려진다.
+ */
 export const config = {
   matcher: [
-    '/((?!_next/static|_next/image|favicon\\.ico|robots\\.txt|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico)$).*)',
+    '/((?!_next/static|_next/image|favicon\\.ico|robots\\.txt|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico|woff2|woff|ttf|otf)$).*)',
   ],
 };
 
public/fonts/pretendard-OFL.txt (added)
+++ public/fonts/pretendard-OFL.txt
@@ -0,0 +1,94 @@
+Copyright (c) 2021, Kil Hyung-jin (https://github.com/orioncactus/pretendard),
+with Reserved Font Name Pretendard.
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+https://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded, 
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
 
public/fonts/pretendard-extrabold.woff2 (Binary) (added)
+++ public/fonts/pretendard-extrabold.woff2
Binary file is not shown
 
public/fonts/pretendard-regular.woff2 (Binary) (added)
+++ public/fonts/pretendard-regular.woff2
Binary file is not shown
 
public/fonts/pretendard-semibold.woff2 (Binary) (added)
+++ public/fonts/pretendard-semibold.woff2
Binary file is not shown
Add a comment
List