From 5c9959ce087fef34b3b235709122cea834c95bda Mon Sep 17 00:00:00 2001 From: AWESOMEDEV Date: Wed, 22 Jul 2026 20:33:49 +0900 Subject: [PATCH] =?UTF-8?q?feat(design):=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=20=E2=80=94=20Pretendard=20?= =?UTF-8?q?=EC=8B=A4=EC=A0=81=EC=9A=A9=C2=B7=ED=86=A0=ED=81=B0=C2=B7Lucide?= =?UTF-8?q?=20=EC=95=84=EC=9D=B4=EC=BD=98=20132=EA=B0=9C=C2=B7/design?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이모지 아이콘을 SVG로 바꾸다가, 더 큰 원인을 발견해 함께 고쳤다. ■ 폰트: 이름만 부르고 파일은 안 받아오고 있었다 global.css에 font-family: "Pretendard"가 적혀 있었지만 @font-face도 CDN 링크도 없었다. 그래서 이 글꼴이 따로 깔린 사람 외에는 전원 맑은 고딕으로 보고 있었다 — 앱과 학습 문서 19개 전부. 에러가 안 나서 조용히 넘어간 케이스. - PretendardStdVariable.woff2(285KB)를 public/fonts에 self-host · CDN 의존 없음 → 외부 서비스가 죽어도, 오프라인에서도 안 깨진다 · variable font → 파일 하나가 굵기 45~920을 전부 담는다 · font-display: swap → 받는 동안 흰 화면(FOIT) 방지 - 전체판(2MB) 대신 Std판(상용 한글 2,350자)을 골랐다. 앱이 쓰는 한글 1,276자가 Std에 전부 있는지 브라우저에서 실측 확인했다(폴백 0자). - 학습 문서 19개에도 같은 @font-face 적용 ■ 아이콘: 이모지 95종 → Lucide 132개 이모지는 OS마다 다른 그림으로 렌더링되고, 굵기·색을 통제할 수 없고, 스크린리더가 "bust in silhouette"처럼 읽는다. - 카탈로그의 icon: '📜'(문자) → Icon: ScrollText(컴포넌트)로 전환 - strokeWidth={1.5}로 굵기 통일, aria-hidden으로 장식임을 명시 - 개별 import만 사용(트리셰이킹) → gzip +14KB - slug: null인 '개발 환경 설치 가이드' 항목이 일괄 변환에서 누락돼 Icon이 undefined가 될 뻔한 것을 정합성 검사(항목 수 vs Icon 수)로 잡았다 ■ 토큰: 여백·글자 크기·굵기를 :root로 승격 --space-1~7(4의 배수), --text-xs~2xl, --weight-*, --radius-sm/lg/full. 값을 직접 박으면 다크 모드에서 깨지고 화면마다 미묘하게 어긋난다. CSS 프레임워크는 도입하지 않았다 — 손으로 쓴 global.css와 충돌하고, 교재로서 "왜 이렇게 썼는지"가 사라지기 때문이다. ■ /design: 재료를 고르는 화면 색·글자·여백·모서리를 실제 CSS 변수로 렌더링(다크 모드 자동 반영) + Lucide 아이콘 1,993개 검색·복사. lucide-react는 아이콘 하나를 이름 3개로 내보내(Rocket/RocketIcon/LucideRocket) 5,981개처럼 보이므로 별칭을 걸렀다. 아이콘 전체를 훑어야 해 무거운 화면이라 lazy로 분리했다 → 첫 화면 번들은 1.9KB만 증가(575.57 → 577.44KB). 검증: ESLint 통과, Vitest 30개 통과, 프로덕션 빌드 성공, 콘솔 에러 0. 브라우저에서 수습 7카테고리 + 초급~특급 4과정 = 11개 화면 전수 확인 (각 12카드/12아이콘/이모지 0). 문서: DESIGN.md(폰트·토큰·비용), ICONS.md(규약·대응표 132행) --- .gitignore | 1 + DESIGN.md | 113 ++++++++ ICONS.md | 219 +++++++++++++++ README.md | 2 + frontend/package-lock.json | 10 + frontend/package.json | 1 + frontend/public/docs/capstone-demoday.html | 8 + frontend/public/docs/code-review.html | 8 + frontend/public/docs/cs-curriculum.html | 8 + frontend/public/docs/evaluation-rubric.html | 8 + frontend/public/docs/intern-guide.html | 8 + frontend/public/docs/ticket-pool.html | 8 + frontend/public/docs/week1-assignments.html | 8 + frontend/public/docs/week1-detailed.html | 8 + frontend/public/docs/week2-assignments.html | 8 + frontend/public/docs/week2-detailed.html | 8 + frontend/public/docs/week3-assignments.html | 8 + frontend/public/docs/week3-detailed.html | 8 + frontend/public/docs/week4-assignments.html | 8 + frontend/public/docs/week4-detailed.html | 8 + frontend/public/docs/week5-detailed.html | 8 + frontend/public/docs/week6-detailed.html | 8 + frontend/public/docs/week7-detailed.html | 8 + frontend/public/docs/week8-detailed.html | 8 + frontend/public/docs/weekly-plan.html | 8 + .../public/fonts/PretendardStdVariable.woff2 | Bin 0 -> 291680 bytes frontend/src/App.jsx | 20 +- frontend/src/components/CourseShell.jsx | 5 +- frontend/src/courseCatalog.jsx | 255 ++++++++++++------ frontend/src/levelCatalog.jsx | 146 ++++++---- frontend/src/pages/DashboardPage.jsx | 28 +- frontend/src/pages/DesignPage.jsx | 242 +++++++++++++++++ frontend/src/pages/LearnHubPage.jsx | 6 +- frontend/src/styles/global.css | 59 +++- 34 files changed, 1110 insertions(+), 149 deletions(-) create mode 100644 DESIGN.md create mode 100644 ICONS.md create mode 100644 frontend/public/fonts/PretendardStdVariable.woff2 create mode 100644 frontend/src/pages/DesignPage.jsx diff --git a/.gitignore b/.gitignore index d63d7aa..c993760 100644 --- a/.gitignore +++ b/.gitignore @@ -15,5 +15,6 @@ deploy/.env # IDE·OS .idea/ .vscode/ +.claude/ *.iml .DS_Store diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..0b10158 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,113 @@ +# DESIGN.md — 디자인 재료 사용법 + +> **매번 찾지 말고 여기서 고르세요.** 색·글자·여백·아이콘이 전부 저장소 안에 들어 있습니다. +> 앱을 켜고 **`/design`** 을 열면 이 문서의 내용을 눈으로 보고 클릭해서 복사할 수 있습니다. +> 아이콘 이름 대응표는 [ICONS.md](./ICONS.md)에 있습니다. + +--- + +## 1. 세 가지 재료가 어디 있나 + +| 재료 | 어디 | 매번 받아야 하나 | +|---|---|---| +| **아이콘** 1,993개 | `lucide-react` (node_modules) | ❌ `npm install` 한 번으로 끝. 오프라인 동작 | +| **폰트** Pretendard | `frontend/public/fonts/` | ❌ 저장소에 들어 있음. CDN 의존 없음 | +| **색·여백·글자 토큰** | `frontend/src/styles/global.css` 의 `:root` | ❌ 이미 정의돼 있음 | + +셋 다 **이미 받아 놨습니다.** 새로 검색하거나 다운로드할 일은 없습니다. 고르기만 하세요. + +--- + +## 2. 폰트 — 이름을 부른다고 폰트가 오지 않는다 + +⚠️ **실제로 있었던 문제입니다.** CSS에 이렇게 적혀 있었습니다. + +```css +font-family: "Pretendard", "Apple SD Gothic Neo", "Malgun Gothic", system-ui, sans-serif; +``` + +그런데 Pretendard 파일을 **어디서도 받아오지 않았습니다.** `@font-face`도, CDN 링크도 없었습니다. 그래서 그 폰트가 따로 깔린 사람 외에는 전원 **맑은 고딕**으로 보고 있었습니다. 앱과 학습 문서 19개 전부요. + +> 학습 포인트: `font-family`는 **"이 순서대로 시도해 봐"라는 희망 목록**일 뿐입니다. 브라우저는 목록의 이름을 (1) 내 컴퓨터에 깔린 폰트와 (2) `@font-face`로 알려 준 웹폰트에서만 찾습니다. 둘 다 없으면 조용히 다음 후보로 넘어갑니다. **에러가 안 나기 때문에** 몇 달 동안 아무도 모를 수 있습니다. "될 것 같은데 왜 안 되지?"의 절반은 이런 조용한 실패입니다. + +지금은 이렇게 고쳤습니다. + +```css +@font-face { + font-family: "Pretendard"; + src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations"); + font-weight: 45 920; /* 파일 하나가 모든 굵기를 담는다 (variable font) */ + font-display: swap; /* 받는 동안 기본 글씨로 먼저 보여 준다 */ +} +``` + +**왜 이 선택인가** + +- **self-host** (CDN 아님) — 외부 서비스가 죽어도, 인터넷이 없어도 글씨가 안 깨집니다. 사내 학습 앱엔 이쪽이 맞습니다. +- **variable font** — 굵기별 파일 9개 대신 파일 1개. 요청 수와 용량이 함께 줄어듭니다. +- **Std 판(285KB)** — 상용 한글 2,350자 판입니다. 전체 한글 11,172자 판은 2MB로 7배입니다. + 이 앱이 쓰는 한글 **1,276자가 Std에 100% 들어 있는지 실제로 측정해서 확인**했습니다(폴백된 글자 0개). +- **font-display: swap** — 폰트를 받는 동안 글자를 숨기면 흰 화면이 됩니다(FOIT). 기본 글씨로 먼저 보여 주고 나중에 바꿔 끼웁니다. + +--- + +## 3. 토큰 — 값 대신 이름을 쓴다 + +`global.css`의 `:root`에 정의돼 있습니다. **값을 직접 적지 마세요.** + +```css +/* ❌ 이렇게 쓰면 */ +padding: 10px; color: #46536A; font-size: 13.5px; + +/* ✅ 이렇게 쓰세요 */ +padding: var(--space-3); color: var(--muted); font-size: var(--text-sm); +``` + +| 종류 | 토큰 | 쓰는 곳 | +|---|---|---| +| 색 | `--bg` `--card` `--ink` `--muted` `--line` `--primary` `--teal` `--amber` `--rose` … | 전부 | +| 여백 | `--space-1`(4px) ~ `--space-7`(48px) | padding·margin·gap | +| 글자 크기 | `--text-xs`(12) `--text-sm`(13) `--text-md`(15, 본문) `--text-lg` `--text-xl` `--text-2xl` | font-size | +| 굵기 | `--weight-regular` `--weight-medium` `--weight-bold` `--weight-black` | font-weight | +| 모서리 | `--radius-sm` `--radius` `--radius-lg` `--radius-full` | border-radius | + +**왜 이름을 쓰나 — 이유 두 개** + +1. **다크 모드가 공짜로 따라옵니다.** `--ink`는 밝은 모드에서 거의 검정, 어두운 모드에서 거의 흰색입니다. `#101A28`이라고 직접 박으면 다크 모드에서 검은 배경에 검은 글씨가 됩니다. +2. **어긋남이 사라집니다.** 여백을 그때그때 10px, 14px, 6px로 찍으면 화면이 미묘하게 틀어집니다. 사람 눈은 그 차이를 "다름"이 아니라 "실수"로 읽습니다. 4의 배수 사다리만 밟으면 저절로 맞습니다. + +--- + +## 4. 아이콘 + +규약과 132개 대응표는 [ICONS.md](./ICONS.md)에 있습니다. 요약하면: + +```jsx +import { Rocket } from 'lucide-react'; // 개별 import (통째로 import 금지) + +