feat(design): 디자인 시스템 — Pretendard 실적용·토큰·Lucide 아이콘 132개·/design
All checks were successful
CI / backend-test (push) Successful in 3m25s
CI / frontend-build (push) Successful in 48s
CI / backend-dep-scan (push) Successful in 31s

이모지 아이콘을 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행)
This commit is contained in:
AWESOMEDEV 2026-07-22 20:33:49 +09:00
parent 1b559f5da2
commit 5c9959ce08
34 changed files with 1110 additions and 149 deletions

1
.gitignore vendored
View File

@ -15,5 +15,6 @@ deploy/.env
# IDE·OS # IDE·OS
.idea/ .idea/
.vscode/ .vscode/
.claude/
*.iml *.iml
.DS_Store .DS_Store

113
DESIGN.md Normal file
View File

@ -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 금지)
<Rocket size={18} strokeWidth={1.5} aria-hidden="true" />
```
- 굵기는 **항상 `strokeWidth={1.5}`** — 섞이면 통일감이 깨집니다.
- 아이콘은 장식이므로 `aria-hidden="true"`. 글자 없이 아이콘만 쓸 땐 부모에 `aria-label`.
- ⚠️ `lucide-react`는 아이콘 하나를 이름 3개로 내보냅니다(`Rocket`, `RocketIcon`, `LucideRocket`). 전부 세면 5,981개지만 **실제 아이콘은 1,993개**입니다. `/design`은 별칭을 걸러서 보여 줍니다.
이름은 `/design`에서 검색해 클릭하면 import 구문째로 복사됩니다.
---
## 5. 비용은 얼마나 드나 — 재 봤습니다
| | 크기 | 언제 받나 |
|---|---|---|
| 폰트 | 285 KB | 첫 방문 1회, 이후 캐시 |
| 아이콘 132개 (앱에서 실제 사용) | gzip +14 KB | 첫 화면 번들에 포함 |
| `/design` 페이지 | gzip 164 KB | **그 페이지에 들어갈 때만** (lazy) |
`/design`은 아이콘 1,993개를 전부 훑어야 해서 무겁습니다. 그래서 `App.jsx`에서 `lazy()`로 분리했습니다. 덕분에 이 페이지를 추가하고도 **첫 화면 번들은 1.9KB만 늘었습니다.**
> 학습 포인트: "무거운 기능을 넣으면 앱이 느려진다"는 **꼭 그렇지 않습니다.** 그 기능을 필요한 순간에만 내려받게 하면 됩니다. 무엇이 첫 화면에 실려야 하고 무엇이 나중에 와도 되는지를 나누는 것이 프론트엔드 성능 설계의 핵심입니다.
---
AWESOMEDEV · 디자인 재료 v1

219
ICONS.md Normal file
View File

@ -0,0 +1,219 @@
# ICONS.md — 아이콘 규약과 대응표
> 이 앱의 아이콘은 **Lucide**(SVG) 하나로 통일합니다. 이모지는 UI에 쓰지 않습니다.
> 아이콘을 새로 넣거나 바꿀 때 이 문서를 먼저 읽으세요.
> 색·글자·여백 등 나머지 재료는 [DESIGN.md](./DESIGN.md)에, 실물은 앱의 **`/design`** 에 있습니다.
---
## 1. 왜 이모지를 걷어냈나
원래 강좌 카드마다 이모지(📜 💻 🌐 …)가 붙어 있었습니다. 편하지만 세 가지가 발목을 잡습니다.
**① 기기마다 다른 그림이 나온다.** 이모지는 글꼴입니다. 윈도우·맥·안드로이드가 각자 다른 그림을 그려서, 내 화면에서 예쁜 게 남의 화면에선 아닐 수 있습니다. **우리가 통제할 수 없는 디자인**이라는 게 핵심 문제입니다.
**② 굵기·크기·색을 못 맞춘다.** 어떤 이모지는 선이 두껍고 어떤 건 알록달록합니다. 12개를 한 화면에 모으면 통일감이 안 생깁니다. SVG 아이콘은 `size`·`strokeWidth`·`color`를 코드로 지시할 수 있어 132개가 한 세트처럼 보입니다.
**③ 스크린리더가 엉뚱하게 읽는다.** 👤 하나만 있는 링크를 리더는 "bust in silhouette 링크"라고 읽습니다. (이 문제는 `App.jsx` 헤더에서 실제로 겪었습니다.)
> 학습 포인트: "그냥 취향 문제 아닌가?"로 보이는 UI 결정에도 **통제 가능성·일관성·접근성**이라는 기술적 근거가 있습니다. 디자인 논쟁은 근거를 대는 쪽이 이깁니다.
---
## 2. 쓰는 법
카탈로그는 아이콘을 **문자가 아니라 컴포넌트**로 들고 있습니다.
```jsx
// courseCatalog.jsx — 데이터
import { ScrollText } from 'lucide-react';
{ slug: 'history', Icon: ScrollText, title: '컴퓨터의 역사', /* … */ }
```
```jsx
// LearnHubPage.jsx — 화면
<course.Icon size={26} strokeWidth={1.5} aria-hidden="true" />
```
지켜야 할 규칙 네 가지:
| 규칙 | 이유 |
|---|---|
| 필드 이름은 `Icon` (대문자 I) | JSX는 **대문자로 시작하는 이름만** 컴포넌트로 취급합니다. 소문자는 `<div>` 같은 HTML 태그로 봅니다. |
| `strokeWidth={1.5}` 고정 | 굵기가 섞이면 통일감이 깨집니다. 이 값이 우리 앱의 기본 굵기입니다. |
| `aria-hidden="true"` 필수 | 아이콘은 장식입니다. 의미는 옆의 글자가 전달합니다. |
| 글자 없이 아이콘만 쓸 땐 `aria-label` | 예: 헤더의 내 정보·비밀번호 버튼. 안 그러면 스크린리더가 읽을 게 없습니다. |
**개별 import만 씁니다.** `import * as Icons from 'lucide-react'`처럼 통째로 가져오면 안 쓰는 아이콘까지 전부 번들에 실립니다. 지금처럼 이름을 하나씩 적으면 번들러가 쓰는 것만 남깁니다(트리셰이킹).
새 아이콘은 <https://lucide.dev/icons> 에서 고르고, 파일 상단 import 목록에 **알파벳 순서**로 끼워 넣으세요.
---
## 3. 비용
아이콘 132종을 넣고 번들이 얼마나 커졌는지 실제로 재 봤습니다.
| | 변경 전 | 변경 후 | 차이 |
|---|---|---|---|
| index 청크 | 518.36 kB | 575.57 kB | +57.2 kB |
| gzip 전송 크기 | 171.96 kB | 186.01 kB | **+14.0 kB** |
> 학습 포인트: "라이브러리 넣으면 무거워진다"는 말은 **숫자로 확인하기 전까진 추측**입니다. 실제로 재 보면 아이콘 132개에 gzip 14KB — 사진 한 장보다 작습니다. 빌드 결과를 읽는 습관이 이런 판단을 가능하게 합니다.
Lucide를 고른 이유: 아이콘 약 1,600종으로 우리가 필요한 개념(CPU·네트워크·DB·보안·디자인·앱)을 전부 덮고, 굵기가 한 종류라 섞어 써도 통일감이 유지됩니다.
⚠️ 브랜드 로고(Figma 등)는 Lucide v1에서 제거됐습니다. 그래서 「피그마와 디자인 툴」은 로고 대신 도구의 성격을 나타내는 `PenTool`을 씁니다.
---
## 4. 전체 대응표 (132개)
<!-- 이 표는 카탈로그에서 자동 생성했습니다. 강좌를 추가하면 이 표에도 한 줄 추가하세요. -->
### 수습 트랙(83강좌 + 설치 가이드) — `frontend/src/courseCatalog.jsx`
| 강좌 | 이전(이모지) | 이후(Lucide) | 아이콘 |
|---|:---:|---|---|
| 컴퓨터의 역사 | 📜 | `ScrollText` | scroll-text |
| 컴퓨터의 구성 | 💻 | `Computer` | computer |
| 2진수와 데이터 표현 | 🔢 | `Binary` | binary |
| CPU 깊이 보기 | 🧠 | `Cpu` | cpu |
| 메모리와 저장장치 깊이 보기 | 💾 | `HardDrive` | hard-drive |
| 그래픽카드와 GPU의 세계 | 🎮 | `MemoryStick` | memory-stick |
| 메인보드와 파워 깊이 보기 | 🔋 | `CircuitBoard` | circuit-board |
| 데스크탑 조립하기 | 🔧 | `Wrench` | wrench |
| 운영체제의 이해 | 🪟 | `AppWindow` | app-window |
| 파일과 폴더의 원리 | 📁 | `Folder` | folder |
| 노트북과 주변기기 고르기 | 🖥️ | `Laptop` | laptop |
| 컴퓨터 문제 해결 | 🩺 | `Stethoscope` | stethoscope |
| 네트워크의 이해 | 🌐 | `Globe` | globe |
| 네트워크 속도와 대역폭 | 🚗 | `Gauge` | gauge |
| IP 주소 깊이 보기 | 🔍 | `MapPin` | map-pin |
| 공유기와 홈 네트워크 실전 | 📡 | `Router` | router |
| DNS 깊이 보기 | 📖 | `BookMarked` | book-marked |
| TCP와 UDP | 🤝 | `Handshake` | handshake |
| HTTP와 HTTPS | 📨 | `ArrowRightLeft` | arrow-right-left |
| 네트워크 명령어 실전 | 🧰 | `SquareTerminal` | square-terminal |
| 무선 네트워크와 보안 | 🔐 | `Wifi` | wifi |
| VPN과 프록시 | 🚇 | `Route` | route |
| 방화벽과 보안 기초 | 🛡️ | `BrickWall` | brick-wall |
| 클라우드 네트워크 입문 | ☁️ | `Cloud` | cloud |
| 서버란 무엇인가 | 🏗️ | `Server` | server |
| 리눅스 / 터미널 기초 | 🐧 | `Terminal` | terminal |
| 리눅스 심화 | ⚙️ | `Settings2` | settings-2 |
| 도커 입문 | 🐳 | `Container` | container |
| 클라우드와 AWS 입문 | 🌩️ | `CloudLightning` | cloud-lightning |
| SQL 입문 | 🗄️ | `Database` | database |
| SQL 중급 | 📊 | `ChartColumn` | chart-column |
| 데이터 모델링 기초 | 🗂️ | `Table` | table |
| 캐시와 NoSQL 맛보기 | ⚡ | `Zap` | zap |
| API의 이해 | 🔌 | `Plug` | plug |
| CI/CD와 배포 자동화 입문 | 🔁 | `RotateCw` | rotate-cw |
| 운영과 장애 대응 기초 | 🚨 | `Siren` | siren |
| 코딩 기초 | ⌨️ | `Keyboard` | keyboard |
| HTML / CSS 기초 | 🧱 | `FileCode` | file-code |
| 자바스크립트 기초 | ⚡ | `Braces` | braces |
| 자료구조 맛보기 | 🧺 | `Boxes` | boxes |
| React 입문 | ⚛️ | `Atom` | atom |
| Java 기초 | ☕ | `Coffee` | coffee |
| Spring Boot 입문 | 🍃 | `Leaf` | leaf |
| Git 깊이 보기 | 🌳 | `GitBranch` | git-branch |
| 디버깅과 에러 읽는 법 | 🐞 | `Bug` | bug |
| 테스트 코드 입문 | 🧪 | `FlaskConical` | flask-conical |
| 클린 코드와 코드리뷰 | ✨ | `Sparkles` | sparkles |
| AI 도구와 함께 코딩하기 | 🤖 | `Bot` | bot |
| UI/UX 기초 | 🎨 | `Palette` | palette |
| 색 이론 기초 | 🌈 | `SwatchBook` | swatch-book |
| 타이포그래피 | 🔤 | `Type` | type |
| 레이아웃과 그리드 | 📐 | `LayoutGrid` | layout-grid |
| 아이콘과 이미지 | 🖼️ | `Image` | image |
| 와이어프레임 기초 | ✏️ | `PencilRuler` | pencil-ruler |
| 피그마와 디자인 툴 | 🖌️ | `PenTool` | pen-tool |
| 디자인 시스템 입문 | 🧩 | `Component` | component |
| 모바일 디자인 | 📱 | `Smartphone` | smartphone |
| 사용자 리서치 기초 | 🔬 | `Microscope` | microscope |
| 레퍼런스와 무드보드 | 🗃️ | `Images` | images |
| 포트폴리오 만들기 | 💼 | `Briefcase` | briefcase |
| 개발 환경 설치 가이드 | 🛠️ | `Wrench` | wrench |
| 윈도우 단축키 마스터 | ⌨️ | `Command` | command |
| 윈도우 터미널과 PowerShell | 🖤 | `SquareChevronRight` | square-chevron-right |
| VS Code 200% 활용 | 📝 | `Code` | code |
| IntelliJ 꿀팁 모음 | 💡 | `Lightbulb` | lightbulb |
| 크롬 개발자도구 | 🔎 | `Inspect` | inspect |
| Gitea 200% 활용 | 🍵 | `GitFork` | git-fork |
| Docker Desktop 사용법 | 🐳 | `Ship` | ship |
| DBeaver로 DB 들여다보기 | 🦫 | `DatabaseZap` | database-zap |
| Postman으로 API 테스트 | 🚀 | `Send` | send |
| 캡처와 화면 녹화 | 📸 | `Camera` | camera |
| 노션으로 기록하기 | 🗒️ | `NotebookPen` | notebook-pen |
| 앱의 지형 | 📱 | `LayoutTemplate` | layout-template |
| 안드로이드 앱의 구조 | 🤖 | `TabletSmartphone` | tablet-smartphone |
| Kotlin 입문 | 🟣 | `Blocks` | blocks |
| iOS 앱의 구조 | 🍎 | `SquareDashedBottomCode` | square-dashed-bottom-code |
| Swift 입문 | 🐦 | `Bird` | bird |
| React Native | ⚛️ | `Orbit` | orbit |
| Flutter | 🦋 | `Feather` | feather |
| 앱 화면과 이동 | 🧭 | `Compass` | compass |
| 앱의 상태와 데이터 | 🗃️ | `Layers` | layers |
| PWA — 웹앱을 앱처럼 | 🌐 | `MonitorSmartphone` | monitor-smartphone |
| 앱 보안과 권한 | 🔐 | `ShieldCheck` | shield-check |
| 스토어 배포 | 🚀 | `Rocket` | rocket |
### 수준별 과정(초급~특급 48강좌) — `frontend/src/levelCatalog.jsx`
| 강좌 | 이전(이모지) | 이후(Lucide) | 아이콘 |
|---|:---:|---|---|
| 프로그램이란 무엇일까? | 💡 | `Lightbulb` | lightbulb |
| 컴퓨터처럼 생각하기: 컴퓨팅 사고 | 🧩 | `Puzzle` | puzzle |
| 나의 첫 웹페이지: HTML 첫걸음 | 🌐 | `Globe` | globe |
| 태그로 페이지에 살 붙이기 | 🏗️ | `Blocks` | blocks |
| 브라우저는 내 코드를 어떻게 읽을까 | 🔍 | `Search` | search |
| 나의 첫 JavaScript | ✨ | `Sparkles` | sparkles |
| 변수와 자료형: 이름 붙인 상자 | 📦 | `Package` | package |
| 조건문: 컴퓨터가 판단하게 하기 | 🚦 | `GitBranch` | git-branch |
| 반복문: 지루한 일은 컴퓨터에게 | 🔁 | `Repeat` | repeat |
| 함수: 나만의 명령어 만들기 | 🧰 | `SquareFunction` | square-function |
| 에러와 친해지기: 빨간 글씨 해독법 | 🩹 | `Bandage` | bandage |
| 첫 프로젝트: 숫자 맞히기 게임 만들기 | 🚀 | `Rocket` | rocket |
| 비동기 JavaScript: fetch와 Promise 정복 | ⏳ | `Hourglass` | hourglass |
| 배열 메서드로 데이터 요리하기 | 🔪 | `ChefHat` | chef-hat |
| REST API 설계: 주소와 동사의 문법 | 📐 | `Ruler` | ruler |
| Express로 CRUD API 만들기 | 🛠️ | `Hammer` | hammer |
| DB 연동과 트랜잭션: 데이터가 사는 집 | 🗄️ | `Database` | database |
| 로그인 구현: 세션과 비밀번호 해시 | 🔐 | `KeyRound` | key-round |
| React 상태와 이펙트 실전 | ⚛️ | `Atom` | atom |
| React Router로 멀티 페이지 SPA | 🧭 | `Compass` | compass |
| 폼 검증: 사용자를 돕고 서버를 지키기 | ✅ | `CircleCheck` | circle-check |
| 에러 처리 패턴: 무너지지 않는 서비스 | 🚨 | `TriangleAlert` | triangle-alert |
| 테스트 작성 실전: 자신 있게 고치는 힘 | 🧪 | `FlaskConical` | flask-conical |
| 미니 프로젝트: 설계부터 배포까지 | 🚀 | `Rocket` | rocket |
| 레이어드 아키텍처와 관심사 분리 | 🏛️ | `Layers` | layers |
| 디자인 패턴 실전 | 🧩 | `Puzzle` | puzzle |
| DB 인덱스와 실행계획 읽기 | 🔍 | `ListTree` | list-tree |
| 트랜잭션과 격리 수준 | 🔒 | `Lock` | lock |
| N+1과 쿼리 최적화 | 🐢 | `Turtle` | turtle |
| 캐싱 전략 — 로컬과 Redis | ⚡ | `Zap` | zap |
| 성능 측정과 프로파일링 | 📈 | `TrendingUp` | trending-up |
| 웹 보안 심화 — OWASP Top 10 | 🛡️ | `Shield` | shield |
| 인증 아키텍처 — 세션 vs JWT vs OAuth | 🔑 | `Fingerprint` | fingerprint |
| 로깅·모니터링·알림 설계 | 🔭 | `Telescope` | telescope |
| 무중단 배포와 롤백 | 🚦 | `Milestone` | milestone |
| 코드 리뷰와 리팩터링 전략 | 🧭 | `GitPullRequest` | git-pull-request |
| 분산 시스템 첫걸음 — 왜 서버 한 대로는 안 될까 | 🌐 | `Network` | network |
| 메시지 큐와 이벤트 드리븐 — 시스템을 느슨하게 잇는 법 | 📨 | `Inbox` | inbox |
| 모놀리스 vs 마이크로서비스 — 쪼개는 것의 진짜 비용 | 🧩 | `Boxes` | boxes |
| DB 레플리케이션과 샤딩 — 데이터베이스를 늘리는 두 가지 축 | 🗄️ | `DatabaseBackup` | database-backup |
| 대용량 트래픽 설계 — 밀려오는 요청 앞에서 무너지지 않기 | 🚦 | `Gauge` | gauge |
| 쿠버네티스와 클라우드 네이티브 — 선언하면 알아서 굴러가는 인프라 | ☸️ | `Hexagon` | hexagon |
| 관측성 — 보이지 않으면 고칠 수 없다 | 🔭 | `Activity` | activity |
| SRE와 장애 대응 — 무너져도 빨리 일어나는 팀 | 🚨 | `Siren` | siren |
| 시스템 설계 훈련 1 — URL 단축기를 처음부터 끝까지 | 🔗 | `Link` | link |
| 시스템 설계 훈련 2 — 실시간 채팅 시스템 | 💬 | `MessageSquare` | message-square |
| 시스템 설계 훈련 3 — 뉴스피드와 타임라인 | 📰 | `Newspaper` | newspaper |
| 기술 리더십과 ADR — 결정을 설계하고 기록하는 사람 | 🧭 | `Signpost` | signpost |
---
AWESOMEDEV · 아이콘 규약 v1 (Lucide 기준)

View File

@ -4,6 +4,8 @@
> 미림마이스터고 수습생 4명이 직접 보고, 배우고, 고치는 학습용 웹 앱입니다. > 미림마이스터고 수습생 4명이 직접 보고, 배우고, 고치는 학습용 웹 앱입니다.
> 처음에는 문서를 읽는 곳이지만, 5주차부터는 여러분이 직접 기능을 붙이고 버그를 고치는 실전 연습장이 됩니다. > 처음에는 문서를 읽는 곳이지만, 5주차부터는 여러분이 직접 기능을 붙이고 버그를 고치는 실전 연습장이 됩니다.
> 코드를 읽는 법이 궁금하면 [STUDY.md](./STUDY.md)부터 열어 보세요. > 코드를 읽는 법이 궁금하면 [STUDY.md](./STUDY.md)부터 열어 보세요.
> 화면을 만들 일이 생기면 [DESIGN.md](./DESIGN.md)(색·글자·여백·폰트)와 [ICONS.md](./ICONS.md)(아이콘)를 먼저 보세요.
> 앱에서 `/design`을 열면 재료를 눈으로 보고 클릭해서 복사할 수 있습니다.
## 무엇을 하는 앱인가? ## 무엇을 하는 앱인가?

View File

@ -9,6 +9,7 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"axios": "^1.7.9", "axios": "^1.7.9",
"lucide-react": "^1.25.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-router-dom": "^7.1.0", "react-router-dom": "^7.1.0",
@ -3405,6 +3406,15 @@
"yallist": "^3.0.2" "yallist": "^3.0.2"
} }
}, },
"node_modules/lucide-react": {
"version": "1.25.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.25.0.tgz",
"integrity": "sha512-/mdJTRbiwcLOQ1NZZK1amZF9rIZyvO18D6r9TngE6TG1NmqHgFuT4eE7Xrkm9UsXMbBJD1NlfwHVltCDWHrOTw==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/lz-string": { "node_modules/lz-string": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",

View File

@ -14,6 +14,7 @@
}, },
"dependencies": { "dependencies": {
"axios": "^1.7.9", "axios": "^1.7.9",
"lucide-react": "^1.25.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-router-dom": "^7.1.0", "react-router-dom": "^7.1.0",

View File

@ -5,6 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>캡스톤 & 데모데이 · AWESOMEDEV</title> <title>캡스톤 & 데모데이 · AWESOMEDEV</title>
<style> <style>
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
@font-face {
font-family: "Pretendard";
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
font-weight: 45 920;
font-display: swap;
}
:root { :root {
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A; --paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7; --ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;

View File

@ -5,6 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>코드리뷰 실습 가이드 · AWESOMEDEV</title> <title>코드리뷰 실습 가이드 · AWESOMEDEV</title>
<style> <style>
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
@font-face {
font-family: "Pretendard";
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
font-weight: 45 920;
font-display: swap;
}
:root { :root {
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A; --paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7; --ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;

View File

@ -5,6 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>CS 기본기 4주 커리큘럼</title> <title>CS 기본기 4주 커리큘럼</title>
<style> <style>
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
@font-face {
font-family: "Pretendard";
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
font-weight: 45 920;
font-display: swap;
}
:root { :root {
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A; --paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7; --ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;

View File

@ -5,6 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>어썸데브 수습 평가 루브릭</title> <title>어썸데브 수습 평가 루브릭</title>
<style> <style>
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
@font-face {
font-family: "Pretendard";
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
font-weight: 45 920;
font-display: swap;
}
:root { :root {
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A; --paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7; --ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;

View File

@ -5,6 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>AWESOMEDEV 수습 가이드</title> <title>AWESOMEDEV 수습 가이드</title>
<style> <style>
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
@font-face {
font-family: "Pretendard";
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
font-weight: 45 920;
font-display: swap;
}
:root { :root {
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A; --paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7; --ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;

View File

@ -5,6 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>수습용 실전 티켓 풀 34 · AWESOMEDEV</title> <title>수습용 실전 티켓 풀 34 · AWESOMEDEV</title>
<style> <style>
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
@font-face {
font-family: "Pretendard";
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
font-weight: 45 920;
font-display: swap;
}
:root { :root {
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A; --paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7; --ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;

View File

@ -5,6 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>AWESOMEDEV 1주차 일일 과제집 — 온보딩 & 예절 적용</title> <title>AWESOMEDEV 1주차 일일 과제집 — 온보딩 & 예절 적용</title>
<style> <style>
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
@font-face {
font-family: "Pretendard";
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
font-weight: 45 920;
font-display: swap;
}
:root { :root {
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A; --paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7; --ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;

View File

@ -5,6 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>1주차 상세 강의안 · 온보딩 & 예절</title> <title>1주차 상세 강의안 · 온보딩 & 예절</title>
<style> <style>
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
@font-face {
font-family: "Pretendard";
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
font-weight: 45 920;
font-display: swap;
}
:root { :root {
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A; --paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7; --ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;

View File

@ -5,6 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>어썸데브 수습 교육 · 2주차 일일 과제집</title> <title>어썸데브 수습 교육 · 2주차 일일 과제집</title>
<style> <style>
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
@font-face {
font-family: "Pretendard";
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
font-weight: 45 920;
font-display: swap;
}
:root { :root {
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A; --paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7; --ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;

View File

@ -5,6 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>2주차 상세 강의안 — 기초 다지기 · AWESOMEDEV 수습 교육</title> <title>2주차 상세 강의안 — 기초 다지기 · AWESOMEDEV 수습 교육</title>
<style> <style>
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
@font-face {
font-family: "Pretendard";
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
font-weight: 45 920;
font-display: swap;
}
:root { :root {
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A; --paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7; --ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;

View File

@ -5,6 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>AWESOMEDEV 3주차 일일 과제집 — 첫 소과제 실전</title> <title>AWESOMEDEV 3주차 일일 과제집 — 첫 소과제 실전</title>
<style> <style>
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
@font-face {
font-family: "Pretendard";
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
font-weight: 45 920;
font-display: swap;
}
:root { :root {
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A; --paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7; --ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;

View File

@ -5,6 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>AWESOMEDEV 수습 상세 강의안 · 3주차</title> <title>AWESOMEDEV 수습 상세 강의안 · 3주차</title>
<style> <style>
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
@font-face {
font-family: "Pretendard";
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
font-weight: 45 920;
font-display: swap;
}
:root { :root {
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A; --paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7; --ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;

View File

@ -5,6 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>AWESOMEDEV 4주차 일일 과제집 — 통합·발표 준비</title> <title>AWESOMEDEV 4주차 일일 과제집 — 통합·발표 준비</title>
<style> <style>
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
@font-face {
font-family: "Pretendard";
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
font-weight: 45 920;
font-display: swap;
}
:root { :root {
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A; --paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7; --ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;

View File

@ -5,6 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>4주차 상세 강의안 · 기본기 마무리 · 중간평가 — AWESOMEDEV 수습 교육</title> <title>4주차 상세 강의안 · 기본기 마무리 · 중간평가 — AWESOMEDEV 수습 교육</title>
<style> <style>
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
@font-face {
font-family: "Pretendard";
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
font-weight: 45 920;
font-display: swap;
}
:root { :root {
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A; --paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7; --ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;

View File

@ -5,6 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>5주차 상세 강의안 · 실전 티켓 ① · AWESOMEDEV 수습</title> <title>5주차 상세 강의안 · 실전 티켓 ① · AWESOMEDEV 수습</title>
<style> <style>
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
@font-face {
font-family: "Pretendard";
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
font-weight: 45 920;
font-display: swap;
}
:root { :root {
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A; --paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7; --ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;

View File

@ -5,6 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>6주차 상세 강의안 — 실전 티켓 ② · AWESOMEDEV 수습</title> <title>6주차 상세 강의안 — 실전 티켓 ② · AWESOMEDEV 수습</title>
<style> <style>
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
@font-face {
font-family: "Pretendard";
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
font-weight: 45 920;
font-display: swap;
}
:root { :root {
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A; --paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7; --ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;

View File

@ -5,6 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>7주차 상세 강의안 — 종합 프로젝트 기획·설계 (멘토용)</title> <title>7주차 상세 강의안 — 종합 프로젝트 기획·설계 (멘토용)</title>
<style> <style>
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
@font-face {
font-family: "Pretendard";
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
font-weight: 45 920;
font-display: swap;
}
:root { :root {
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A; --paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7; --ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;

View File

@ -5,6 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>8주차 상세 강의안 · 구현 · 발표 · 최종평가 — AWESOMEDEV 수습 교육</title> <title>8주차 상세 강의안 · 구현 · 발표 · 최종평가 — AWESOMEDEV 수습 교육</title>
<style> <style>
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
@font-face {
font-family: "Pretendard";
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
font-weight: 45 920;
font-display: swap;
}
:root { :root {
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A; --paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7; --ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;

View File

@ -5,6 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>어썸데브 수습 8주 상세 교육안</title> <title>어썸데브 수습 8주 상세 교육안</title>
<style> <style>
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
@font-face {
font-family: "Pretendard";
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
font-weight: 45 920;
font-display: swap;
}
:root { :root {
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A; --paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7; --ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;

Binary file not shown.

View File

@ -3,8 +3,9 @@
// //
// : courseCatalog.jsx .map // : courseCatalog.jsx .map
// . . // . .
import { Suspense } from 'react'; import { Suspense, lazy } from 'react';
import { Routes, Route, NavLink, Navigate, Outlet, useNavigate, useLocation } from 'react-router-dom'; import { Routes, Route, NavLink, Navigate, Outlet, useNavigate, useLocation } from 'react-router-dom';
import { CircleUser, KeyRound } from 'lucide-react';
import { useAuth } from './AuthContext'; import { useAuth } from './AuthContext';
import { ALL_COURSES } from './courseCatalog'; import { ALL_COURSES } from './courseCatalog';
import { LEVEL_COURSES } from './levelCatalog'; import { LEVEL_COURSES } from './levelCatalog';
@ -29,6 +30,11 @@ import CodingPage from './pages/CodingPage';
import CodingProblemPage from './pages/CodingProblemPage'; import CodingProblemPage from './pages/CodingProblemPage';
import MentorPage from './pages/MentorPage'; import MentorPage from './pages/MentorPage';
// (/design) Lucide .
// : lazy " " .
// .
const DesignPage = lazy(() => import('./pages/DesignPage'));
// : " " . // : " " .
// Outlet , /login . // Outlet , /login .
function RequireAuth() { function RequireAuth() {
@ -133,16 +139,18 @@ function Layout() {
<span className="topnav-name">{user?.name}</span> <span className="topnav-name">{user?.name}</span>
{user?.role === 'MENTOR' && <span className="badge badge-primary">멘토</span>} {user?.role === 'MENTOR' && <span className="badge badge-primary">멘토</span>}
{/* . ( ) {/* . ( )
a11y: 내용이 이모지뿐이면 스크린리더가 "bust in silhouette 링크" 읽는다. a11y: 내용이 그림뿐이면 스크린리더가 읽을 없다. 예전엔 이모지 👤 썼는데
title은 접근 이름으로 쓰이므로 aria-label로 목적을 알리고, 이모지는 숨긴다. */} 리더가 "bust in silhouette 링크"라고 읽어 버렸다. 지금은 Lucide 아이콘(SVG)이고,
SVG도 사정은 같다 그림은 aria-hidden으로 숨기고 목적은 aria-label로 말한다.
title 속성은 접근 이름으로 신뢰할 없으니 aria-label을 쓴다. */}
{user?.role === 'STUDENT' && ( {user?.role === 'STUDENT' && (
<NavLink to="/profile" className="btn btn-ghost" aria-label="내 정보"> <NavLink to="/profile" className="btn btn-ghost" aria-label="내 정보">
<span aria-hidden="true">👤</span> <CircleUser size={18} strokeWidth={1.5} aria-hidden="true" />
</NavLink> </NavLink>
)} )}
{/* 비밀번호 변경 — 처음 받은 비밀번호를 바꿀 수 있는 통로. 눈에 띄는 곳에 둔다. */} {/* 비밀번호 변경 — 처음 받은 비밀번호를 바꿀 수 있는 통로. 눈에 띄는 곳에 둔다. */}
<NavLink to="/change-password" className="btn btn-ghost" aria-label="비밀번호 변경"> <NavLink to="/change-password" className="btn btn-ghost" aria-label="비밀번호 변경">
<span aria-hidden="true">🔑</span> <KeyRound size={18} strokeWidth={1.5} aria-hidden="true" />
</NavLink> </NavLink>
<button className="btn btn-ghost" onClick={logout} title="로그아웃"> <button className="btn btn-ghost" onClick={logout} title="로그아웃">
로그아웃 로그아웃
@ -205,6 +213,8 @@ export default function App() {
<Route path="/profile" element={<MyProfilePage />} /> <Route path="/profile" element={<MyProfilePage />} />
<Route path="/certificate" element={<CertificatePage />} /> <Route path="/certificate" element={<CertificatePage />} />
<Route path="/glossary" element={<GlossaryPage />} /> <Route path="/glossary" element={<GlossaryPage />} />
{/* 디자인 재료 모음 — 색·글자·여백 토큰과 아이콘을 고르는 곳. */}
<Route path="/design" element={<DesignPage />} />
<Route path="/docs" element={<DocsPage />} /> <Route path="/docs" element={<DocsPage />} />
<Route path="/docs/:slug" element={<DocViewerPage />} /> <Route path="/docs/:slug" element={<DocViewerPage />} />
<Route path="/assignments" element={<AssignmentsPage />} /> <Route path="/assignments" element={<AssignmentsPage />} />

View File

@ -6,6 +6,7 @@
// " " React . // " " React .
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { ClipboardCheck } from 'lucide-react';
import client from '../api/client'; import client from '../api/client';
export default function CourseShell({ course }) { export default function CourseShell({ course }) {
@ -54,11 +55,11 @@ export default function CourseShell({ course }) {
학습 센터 학습 센터
</Link> </Link>
<span className="course-complete-label"> <span className="course-complete-label">
{course.icon} {course.title} <course.Icon size={18} strokeWidth={1.5} aria-hidden="true" /> {course.title}
{completed && <span className="badge badge-teal" style={{ marginLeft: 8 }}>완료됨</span>} {completed && <span className="badge badge-teal" style={{ marginLeft: 8 }}>완료됨</span>}
</span> </span>
<Link to={`/learn/${course.slug}/quiz`} className="btn"> <Link to={`/learn/${course.slug}/quiz`} className="btn">
📝 퀴즈 풀기 <ClipboardCheck size={16} strokeWidth={1.5} aria-hidden="true" /> 퀴즈 풀기
</Link> </Link>
<button <button
className={completed ? 'btn' : 'btn btn-primary'} className={completed ? 'btn' : 'btn btn-primary'}

View File

@ -8,6 +8,93 @@
// (dist/assets) (chunk) . // (dist/assets) (chunk) .
import { lazy } from 'react'; import { lazy } from 'react';
// : Lucide( SVG). import ().
import {
AppWindow,
ArrowRightLeft,
Atom,
Binary,
Bird,
Blocks,
BookMarked,
Bot,
Boxes,
Braces,
BrickWall,
Briefcase,
Bug,
Camera,
ChartColumn,
CircuitBoard,
Cloud,
CloudLightning,
Code,
Coffee,
Command,
Compass,
Component,
Computer,
Container,
Cpu,
Database,
DatabaseZap,
Feather,
FileCode,
FlaskConical,
Folder,
Gauge,
GitBranch,
GitFork,
Globe,
Handshake,
HardDrive,
Image,
Images,
Inspect,
Keyboard,
Laptop,
Layers,
LayoutGrid,
LayoutTemplate,
Leaf,
Lightbulb,
MapPin,
MemoryStick,
Microscope,
MonitorSmartphone,
NotebookPen,
Orbit,
Palette,
PenTool,
PencilRuler,
Plug,
Rocket,
RotateCw,
Route,
Router,
ScrollText,
Send,
Server,
Settings2,
ShieldCheck,
Ship,
Siren,
Smartphone,
Sparkles,
SquareChevronRight,
SquareDashedBottomCode,
SquareTerminal,
Stethoscope,
SwatchBook,
Table,
TabletSmartphone,
Terminal,
Type,
Wifi,
Wrench,
Zap,
} from 'lucide-react';
// (pages/) // (pages/)
const HardwarePage = lazy(() => import('./pages/HardwarePage')); const HardwarePage = lazy(() => import('./pages/HardwarePage'));
const AssemblyPage = lazy(() => import('./pages/AssemblyPage')); const AssemblyPage = lazy(() => import('./pages/AssemblyPage'));
@ -102,126 +189,126 @@ export const COURSE_CATEGORIES = [
category: '컴퓨터 기초', category: '컴퓨터 기초',
desc: '소프트웨어 이전에, 기계 그 자체를 이해해요.', desc: '소프트웨어 이전에, 기계 그 자체를 이해해요.',
items: [ items: [
{ slug: 'history', icon: '📜', title: '컴퓨터의 역사', desc: '에니악에서 AI까지 — 우리가 어디쯤 서 있는지부터.', level: '교양', Component: HistoryPage }, { slug: 'history', Icon: ScrollText, title: '컴퓨터의 역사', desc: '에니악에서 AI까지 — 우리가 어디쯤 서 있는지부터.', level: '교양', Component: HistoryPage },
{ slug: 'hardware', icon: '💻', title: '컴퓨터의 구성', desc: 'CPU·RAM·스토리지·메인보드 — 부품의 역할과 스펙 읽기.', level: '입문', Component: HardwarePage }, { slug: 'hardware', Icon: Computer, title: '컴퓨터의 구성', desc: 'CPU·RAM·스토리지·메인보드 — 부품의 역할과 스펙 읽기.', level: '입문', Component: HardwarePage },
{ slug: 'binary', icon: '🔢', title: '2진수와 데이터 표현', desc: '문자·색·이미지가 전부 숫자인 이유.', level: '입문', Component: BinaryPage }, { slug: 'binary', Icon: Binary, title: '2진수와 데이터 표현', desc: '문자·색·이미지가 전부 숫자인 이유.', level: '입문', Component: BinaryPage },
{ slug: 'cpu-deep', icon: '🧠', title: 'CPU 깊이 보기', desc: '명령어 사이클·코어·캐시·발열 — 두뇌의 작동 원리.', level: '심화', Component: CpuDeepPage }, { slug: 'cpu-deep', Icon: Cpu, title: 'CPU 깊이 보기', desc: '명령어 사이클·코어·캐시·발열 — 두뇌의 작동 원리.', level: '심화', Component: CpuDeepPage },
{ slug: 'memory-storage', icon: '💾', title: '메모리와 저장장치 깊이 보기', desc: 'RAM·SSD의 내부, 스왑과 느려짐의 연쇄.', level: '심화', Component: MemoryStoragePage }, { slug: 'memory-storage', Icon: HardDrive, title: '메모리와 저장장치 깊이 보기', desc: 'RAM·SSD의 내부, 스왑과 느려짐의 연쇄.', level: '심화', Component: MemoryStoragePage },
{ slug: 'gpu', icon: '🎮', title: '그래픽카드와 GPU의 세계', desc: '화면 그리기 전담 — AI 시대에 GPU가 금값인 이유.', level: '심화', Component: GpuPage }, { slug: 'gpu', Icon: MemoryStick, title: '그래픽카드와 GPU의 세계', desc: '화면 그리기 전담 — AI 시대에 GPU가 금값인 이유.', level: '심화', Component: GpuPage },
{ slug: 'mainboard-power', icon: '🔋', title: '메인보드와 파워 깊이 보기', desc: '모든 부품이 만나는 도로망, 그리고 전원의 심장.', level: '심화', Component: MainboardPowerPage }, { slug: 'mainboard-power', Icon: CircuitBoard, title: '메인보드와 파워 깊이 보기', desc: '모든 부품이 만나는 도로망, 그리고 전원의 심장.', level: '심화', Component: MainboardPowerPage },
{ slug: 'assembly', icon: '🔧', title: '데스크탑 조립하기', desc: '배운 부품들을 내 손으로 — 9단계 조립 순서.', level: '실습', Component: AssemblyPage }, { slug: 'assembly', Icon: Wrench, title: '데스크탑 조립하기', desc: '배운 부품들을 내 손으로 — 9단계 조립 순서.', level: '실습', Component: AssemblyPage },
{ slug: 'os', icon: '🪟', title: '운영체제의 이해', desc: '윈도우·맥·리눅스가 하는 일 — 자원 관리부터 멀티태스킹까지.', level: '입문', Component: OsPage }, { slug: 'os', Icon: AppWindow, title: '운영체제의 이해', desc: '윈도우·맥·리눅스가 하는 일 — 자원 관리부터 멀티태스킹까지.', level: '입문', Component: OsPage },
{ slug: 'files', icon: '📁', title: '파일과 폴더의 원리', desc: '확장자의 진실, 경로, 휴지통과 완전삭제.', level: '입문', Component: FilesPage }, { slug: 'files', Icon: Folder, title: '파일과 폴더의 원리', desc: '확장자의 진실, 경로, 휴지통과 완전삭제.', level: '입문', Component: FilesPage },
{ slug: 'peripherals', icon: '🖥️', title: '노트북과 주변기기 고르기', desc: '노트북·모니터·키보드 스펙 읽기 — 개발자 관점.', level: '입문', Component: PeripheralsPage }, { slug: 'peripherals', Icon: Laptop, title: '노트북과 주변기기 고르기', desc: '노트북·모니터·키보드 스펙 읽기 — 개발자 관점.', level: '입문', Component: PeripheralsPage },
{ slug: 'troubleshooting', icon: '🩺', title: '컴퓨터 문제 해결', desc: '느려질 때 점검 순서, 백업 3-2-1, 검색의 기술.', level: '실습', Component: TroubleshootingPage }, { slug: 'troubleshooting', Icon: Stethoscope, title: '컴퓨터 문제 해결', desc: '느려질 때 점검 순서, 백업 3-2-1, 검색의 기술.', level: '실습', Component: TroubleshootingPage },
], ],
}, },
{ {
category: '네트워크', category: '네트워크',
desc: '랜선 한 가닥에서 클라우드까지.', desc: '랜선 한 가닥에서 클라우드까지.',
items: [ items: [
{ slug: 'network', icon: '🌐', title: '네트워크의 이해', desc: '랜선(UTP) 구조 → 인터넷의 구조 → DNS·TCP/IP 한 흐름.', level: '입문', Component: NetworkPage }, { slug: 'network', Icon: Globe, title: '네트워크의 이해', desc: '랜선(UTP) 구조 → 인터넷의 구조 → DNS·TCP/IP 한 흐름.', level: '입문', Component: NetworkPage },
{ slug: 'bandwidth', icon: '🚗', title: '네트워크 속도와 대역폭', desc: 'Mbps의 진실 — 다운로드가 생각보다 느린 이유.', level: '입문', Component: BandwidthPage }, { slug: 'bandwidth', Icon: Gauge, title: '네트워크 속도와 대역폭', desc: 'Mbps의 진실 — 다운로드가 생각보다 느린 이유.', level: '입문', Component: BandwidthPage },
{ slug: 'ip-deep', icon: '🔍', title: 'IP 주소 깊이 보기', desc: '사설 대역·서브넷·DHCP·IPv6 — 주소 체계의 모든 것.', level: '심화', Component: IpDeepPage }, { slug: 'ip-deep', Icon: MapPin, title: 'IP 주소 깊이 보기', desc: '사설 대역·서브넷·DHCP·IPv6 — 주소 체계의 모든 것.', level: '심화', Component: IpDeepPage },
{ slug: 'home-network', icon: '📡', title: '공유기와 홈 네트워크 실전', desc: '관리자 페이지, WiFi 채널, 포트포워딩까지.', level: '실습', Component: HomeNetworkPage }, { slug: 'home-network', Icon: Router, title: '공유기와 홈 네트워크 실전', desc: '관리자 페이지, WiFi 채널, 포트포워딩까지.', level: '실습', Component: HomeNetworkPage },
{ slug: 'dns-deep', icon: '📖', title: 'DNS 깊이 보기', desc: '재귀 조회의 여행, 레코드 타입, TTL과 전파.', level: '심화', Component: DnsDeepPage }, { slug: 'dns-deep', Icon: BookMarked, title: 'DNS 깊이 보기', desc: '재귀 조회의 여행, 레코드 타입, TTL과 전파.', level: '심화', Component: DnsDeepPage },
{ slug: 'tcp-udp', icon: '🤝', title: 'TCP와 UDP', desc: '3-way 핸드셰이크, 패킷, 왜 게임은 UDP인가.', level: '심화', Component: TcpUdpPage }, { slug: 'tcp-udp', Icon: Handshake, title: 'TCP와 UDP', desc: '3-way 핸드셰이크, 패킷, 왜 게임은 UDP인가.', level: '심화', Component: TcpUdpPage },
{ slug: 'http', icon: '📨', title: 'HTTP와 HTTPS', desc: '메서드·상태코드·쿠키(우리 로그인!)·TLS 인증서.', level: '입문', Component: HttpPage }, { slug: 'http', Icon: ArrowRightLeft, title: 'HTTP와 HTTPS', desc: '메서드·상태코드·쿠키(우리 로그인!)·TLS 인증서.', level: '입문', Component: HttpPage },
{ slug: 'net-tools', icon: '🧰', title: '네트워크 명령어 실전', desc: 'ping·tracert·nslookup·curl — 진단의 무기들.', level: '실습', Component: NetToolsPage }, { slug: 'net-tools', Icon: SquareTerminal, title: '네트워크 명령어 실전', desc: 'ping·tracert·nslookup·curl — 진단의 무기들.', level: '실습', Component: NetToolsPage },
{ slug: 'wifi-security', icon: '🔐', title: '무선 네트워크와 보안', desc: '공용 와이파이의 위험, 피싱 구별, 2단계 인증.', level: '입문', Component: WifiSecurityPage }, { slug: 'wifi-security', Icon: Wifi, title: '무선 네트워크와 보안', desc: '공용 와이파이의 위험, 피싱 구별, 2단계 인증.', level: '입문', Component: WifiSecurityPage },
{ slug: 'vpn-proxy', icon: '🚇', title: 'VPN과 프록시', desc: '암호화 터널과 대리인 — 우리 Caddy가 실물 교재.', level: '심화', Component: VpnProxyPage }, { slug: 'vpn-proxy', Icon: Route, title: 'VPN과 프록시', desc: '암호화 터널과 대리인 — 우리 Caddy가 실물 교재.', level: '심화', Component: VpnProxyPage },
{ slug: 'firewall', icon: '🛡️', title: '방화벽과 보안 기초', desc: '포트의 여닫음, 우리 AWS 보안그룹 실례, 최소 권한.', level: '입문', Component: FirewallPage }, { slug: 'firewall', Icon: BrickWall, title: '방화벽과 보안 기초', desc: '포트의 여닫음, 우리 AWS 보안그룹 실례, 최소 권한.', level: '입문', Component: FirewallPage },
{ slug: 'cloud-network', icon: '☁️', title: '클라우드 네트워크 입문', desc: '리전·VPC·탄력적 IP — 우리 플랫폼의 실제 구성도.', level: '심화', Component: CloudNetworkPage }, { slug: 'cloud-network', Icon: Cloud, title: '클라우드 네트워크 입문', desc: '리전·VPC·탄력적 IP — 우리 플랫폼의 실제 구성도.', level: '심화', Component: CloudNetworkPage },
], ],
}, },
{ {
category: '서버와 데이터', category: '서버와 데이터',
desc: '백엔드 개발자가 매일 쓰는 도구들.', desc: '백엔드 개발자가 매일 쓰는 도구들.',
items: [ items: [
{ slug: 'server-anatomy', icon: '🏗️', title: '서버란 무엇인가', desc: '웹서버·WAS·DB 3계층 — 우리 구성 그대로 해부.', level: '입문', Component: ServerAnatomyPage }, { slug: 'server-anatomy', Icon: Server, title: '서버란 무엇인가', desc: '웹서버·WAS·DB 3계층 — 우리 구성 그대로 해부.', level: '입문', Component: ServerAnatomyPage },
{ slug: 'linux', icon: '🐧', title: '리눅스 / 터미널 기초', desc: '터미널 명령어부터 SSH 서버 접속까지.', level: '입문', Component: LinuxPage }, { slug: 'linux', Icon: Terminal, title: '리눅스 / 터미널 기초', desc: '터미널 명령어부터 SSH 서버 접속까지.', level: '입문', Component: LinuxPage },
{ slug: 'linux-advanced', icon: '⚙️', title: '리눅스 심화', desc: '셸 스크립트, 크론, 파이프 — 자동화의 시작.', level: '심화', Component: LinuxAdvancedPage }, { slug: 'linux-advanced', Icon: Settings2, title: '리눅스 심화', desc: '셸 스크립트, 크론, 파이프 — 자동화의 시작.', level: '심화', Component: LinuxAdvancedPage },
{ slug: 'docker-intro', icon: '🐳', title: '도커 입문', desc: '컨테이너 개념과 우리 docker-compose.yml 한 줄씩 해부.', level: '입문', Component: DockerIntroPage }, { slug: 'docker-intro', Icon: Container, title: '도커 입문', desc: '컨테이너 개념과 우리 docker-compose.yml 한 줄씩 해부.', level: '입문', Component: DockerIntroPage },
{ slug: 'aws-intro', icon: '🌩️', title: '클라우드와 AWS 입문', desc: 'EC2·요금·리전 — 우리 인프라가 교재.', level: '입문', Component: AwsIntroPage }, { slug: 'aws-intro', Icon: CloudLightning, title: '클라우드와 AWS 입문', desc: 'EC2·요금·리전 — 우리 인프라가 교재.', level: '입문', Component: AwsIntroPage },
{ slug: 'sql', icon: '🗄️', title: 'SQL 입문', desc: '우리 플랫폼의 진짜 테이블로 SELECT부터 JOIN까지.', level: '입문', Component: SqlPage }, { slug: 'sql', Icon: Database, title: 'SQL 입문', desc: '우리 플랫폼의 진짜 테이블로 SELECT부터 JOIN까지.', level: '입문', Component: SqlPage },
{ slug: 'sql-intermediate', icon: '📊', title: 'SQL 중급', desc: 'GROUP BY·집계·인덱스 — 데이터에서 답을 꺼내기.', level: '심화', Component: SqlIntermediatePage }, { slug: 'sql-intermediate', Icon: ChartColumn, title: 'SQL 중급', desc: 'GROUP BY·집계·인덱스 — 데이터에서 답을 꺼내기.', level: '심화', Component: SqlIntermediatePage },
{ slug: 'data-modeling', icon: '🗂️', title: '데이터 모델링 기초', desc: 'ERD·키·정규화 — 테이블 설계는 정리정돈.', level: '심화', Component: DataModelingPage }, { slug: 'data-modeling', Icon: Table, title: '데이터 모델링 기초', desc: 'ERD·키·정규화 — 테이블 설계는 정리정돈.', level: '심화', Component: DataModelingPage },
{ slug: 'cache-nosql', icon: '⚡', title: '캐시와 NoSQL 맛보기', desc: '자주 쓰는 건 책상 위에 — Redis가 빠른 이유.', level: '심화', Component: CacheNosqlPage }, { slug: 'cache-nosql', Icon: Zap, title: '캐시와 NoSQL 맛보기', desc: '자주 쓰는 건 책상 위에 — Redis가 빠른 이유.', level: '심화', Component: CacheNosqlPage },
{ slug: 'api', icon: '🔌', title: 'API의 이해', desc: 'REST·JSON — 이미 다 써본 우리 API로 배우기.', level: '입문', Component: ApiPage }, { slug: 'api', Icon: Plug, title: 'API의 이해', desc: 'REST·JSON — 이미 다 써본 우리 API로 배우기.', level: '입문', Component: ApiPage },
{ slug: 'cicd', icon: '🔁', title: 'CI/CD와 배포 자동화 입문', desc: '푸시하면 알아서 검사·배포 — 파이프라인의 세계.', level: '심화', Component: CicdPage }, { slug: 'cicd', Icon: RotateCw, title: 'CI/CD와 배포 자동화 입문', desc: '푸시하면 알아서 검사·배포 — 파이프라인의 세계.', level: '심화', Component: CicdPage },
{ slug: 'ops-basics', icon: '🚨', title: '운영과 장애 대응 기초', desc: '장애의 신호, 로그 읽기, 재시작의 정석.', level: '실습', Component: OpsBasicsPage }, { slug: 'ops-basics', Icon: Siren, title: '운영과 장애 대응 기초', desc: '장애의 신호, 로그 읽기, 재시작의 정석.', level: '실습', Component: OpsBasicsPage },
], ],
}, },
{ {
category: '소프트웨어', category: '소프트웨어',
desc: '코드를 읽고, 쓰고, 함께 다듬는 법.', desc: '코드를 읽고, 쓰고, 함께 다듬는 법.',
items: [ items: [
{ slug: 'coding', icon: '⌨️', title: '코딩 기초', desc: '변수·함수·조건·반복을 이 플랫폼의 실제 코드로.', level: '입문', Component: CodingBasicsPage }, { slug: 'coding', Icon: Keyboard, title: '코딩 기초', desc: '변수·함수·조건·반복을 이 플랫폼의 실제 코드로.', level: '입문', Component: CodingBasicsPage },
{ slug: 'htmlcss', icon: '🧱', title: 'HTML / CSS 기초', desc: '웹의 뼈대와 옷 — 박스 모델·플렉스를 우리 스타일로.', level: '입문', Component: HtmlCssPage }, { slug: 'htmlcss', Icon: FileCode, title: 'HTML / CSS 기초', desc: '웹의 뼈대와 옷 — 박스 모델·플렉스를 우리 스타일로.', level: '입문', Component: HtmlCssPage },
{ slug: 'javascript', icon: '⚡', title: '자바스크립트 기초', desc: 'F12 콘솔이 실습장 — 변수에서 fetch까지.', level: '입문', Component: JavascriptPage }, { slug: 'javascript', Icon: Braces, title: '자바스크립트 기초', desc: 'F12 콘솔이 실습장 — 변수에서 fetch까지.', level: '입문', Component: JavascriptPage },
{ slug: 'data-structures', icon: '🧺', title: '자료구조 맛보기', desc: '배열·스택·큐·해시 — 그릇마다 쓰임이 다르다.', level: '입문', Component: DataStructuresPage }, { slug: 'data-structures', Icon: Boxes, title: '자료구조 맛보기', desc: '배열·스택·큐·해시 — 그릇마다 쓰임이 다르다.', level: '입문', Component: DataStructuresPage },
{ slug: 'react-intro', icon: '⚛️', title: 'React 입문', desc: '컴포넌트·props·state — 우리 프론트가 교재.', level: '입문', Component: ReactIntroPage }, { slug: 'react-intro', Icon: Atom, title: 'React 입문', desc: '컴포넌트·props·state — 우리 프론트가 교재.', level: '입문', Component: ReactIntroPage },
{ slug: 'java-basics', icon: '☕', title: 'Java 기초', desc: '타입·클래스·객체 — Spring으로 가는 첫걸음.', level: '입문', Component: JavaBasicsPage }, { slug: 'java-basics', Icon: Coffee, title: 'Java 기초', desc: '타입·클래스·객체 — Spring으로 가는 첫걸음.', level: '입문', Component: JavaBasicsPage },
{ slug: 'spring-intro', icon: '🍃', title: 'Spring Boot 입문', desc: 'DI·어노테이션·계층 구조 — 우리 백엔드 여행.', level: '심화', Component: SpringIntroPage }, { slug: 'spring-intro', Icon: Leaf, title: 'Spring Boot 입문', desc: 'DI·어노테이션·계층 구조 — 우리 백엔드 여행.', level: '심화', Component: SpringIntroPage },
{ slug: 'git-deep', icon: '🌳', title: 'Git 깊이 보기', desc: '브랜치·충돌 해결·되돌리기 3형제·PR 매너.', level: '심화', Component: GitDeepPage }, { slug: 'git-deep', Icon: GitBranch, title: 'Git 깊이 보기', desc: '브랜치·충돌 해결·되돌리기 3형제·PR 매너.', level: '심화', Component: GitDeepPage },
{ slug: 'debugging', icon: '🐞', title: '디버깅과 에러 읽는 법', desc: '스택트레이스·브레이크포인트 — 에러는 단서다.', level: '실습', Component: DebuggingPage }, { slug: 'debugging', Icon: Bug, title: '디버깅과 에러 읽는 법', desc: '스택트레이스·브레이크포인트 — 에러는 단서다.', level: '실습', Component: DebuggingPage },
{ slug: 'testing', icon: '🧪', title: '테스트 코드 입문', desc: '미래의 나를 지키는 보험 — 우리 테스트가 교재.', level: '심화', Component: TestingPage }, { slug: 'testing', Icon: FlaskConical, title: '테스트 코드 입문', desc: '미래의 나를 지키는 보험 — 우리 테스트가 교재.', level: '심화', Component: TestingPage },
{ slug: 'clean-code', icon: '✨', title: '클린 코드와 코드리뷰', desc: '이름 짓기·작은 함수·리뷰 매너.', level: '심화', Component: CleanCodePage }, { slug: 'clean-code', Icon: Sparkles, title: '클린 코드와 코드리뷰', desc: '이름 짓기·작은 함수·리뷰 매너.', level: '심화', Component: CleanCodePage },
{ slug: 'ai-tools', icon: '🤖', title: 'AI 도구와 함께 코딩하기', desc: '잘 시키고, 검증하고, 배움에 쓰는 법 — 보안 수칙까지.', level: '실습', Component: AiToolsPage }, { slug: 'ai-tools', Icon: Bot, title: 'AI 도구와 함께 코딩하기', desc: '잘 시키고, 검증하고, 배움에 쓰는 법 — 보안 수칙까지.', level: '실습', Component: AiToolsPage },
], ],
}, },
{ {
category: '디자인', category: '디자인',
desc: '보기 좋은 것을 넘어, 쓰기 좋은 것을.', desc: '보기 좋은 것을 넘어, 쓰기 좋은 것을.',
items: [ items: [
{ slug: 'uiux', icon: '🎨', title: 'UI/UX 기초', desc: '좋은 화면의 원칙들 — 우리 플랫폼을 뜯어보며.', level: '입문', Component: UiUxPage }, { slug: 'uiux', Icon: Palette, title: 'UI/UX 기초', desc: '좋은 화면의 원칙들 — 우리 플랫폼을 뜯어보며.', level: '입문', Component: UiUxPage },
{ slug: 'color', icon: '🌈', title: '색 이론 기초', desc: '색상환·배색·우리 브랜드 블루의 비밀.', level: '입문', Component: ColorPage }, { slug: 'color', Icon: SwatchBook, title: '색 이론 기초', desc: '색상환·배색·우리 브랜드 블루의 비밀.', level: '입문', Component: ColorPage },
{ slug: 'typography', icon: '🔤', title: '타이포그래피', desc: '폰트·크기 위계·행간 — 가독성의 과학.', level: '입문', Component: TypographyPage }, { slug: 'typography', Icon: Type, title: '타이포그래피', desc: '폰트·크기 위계·행간 — 가독성의 과학.', level: '입문', Component: TypographyPage },
{ slug: 'layout', icon: '📐', title: '레이아웃과 그리드', desc: '8pt 그리드·정렬·여백 — 보이지 않는 뼈대.', level: '입문', Component: LayoutPage }, { slug: 'layout', Icon: LayoutGrid, title: '레이아웃과 그리드', desc: '8pt 그리드·정렬·여백 — 보이지 않는 뼈대.', level: '입문', Component: LayoutPage },
{ slug: 'icons-images', icon: '🖼️', title: '아이콘과 이미지', desc: 'SVG vs PNG, 해상도, 그리고 저작권!', level: '입문', Component: IconsImagesPage }, { slug: 'icons-images', Icon: Image, title: '아이콘과 이미지', desc: 'SVG vs PNG, 해상도, 그리고 저작권!', level: '입문', Component: IconsImagesPage },
{ slug: 'wireframe', icon: '✏️', title: '와이어프레임 기초', desc: '색 없이 구조만 — 빨리 그리고 빨리 버리는 설계.', level: '실습', Component: WireframePage }, { slug: 'wireframe', Icon: PencilRuler, title: '와이어프레임 기초', desc: '색 없이 구조만 — 빨리 그리고 빨리 버리는 설계.', level: '실습', Component: WireframePage },
{ slug: 'figma', icon: '🖌️', title: '피그마와 디자인 툴', desc: '프레임·오토레이아웃·컴포넌트·핸드오프.', level: '실습', Component: FigmaPage }, { slug: 'figma', Icon: PenTool, title: '피그마와 디자인 툴', desc: '프레임·오토레이아웃·컴포넌트·핸드오프.', level: '실습', Component: FigmaPage },
{ slug: 'design-system', icon: '🧩', title: '디자인 시스템 입문', desc: '토큰과 컴포넌트 — 우리 global.css가 실물.', level: '심화', Component: DesignSystemPage }, { slug: 'design-system', Icon: Component, title: '디자인 시스템 입문', desc: '토큰과 컴포넌트 — 우리 global.css가 실물.', level: '심화', Component: DesignSystemPage },
{ slug: 'mobile-design', icon: '📱', title: '모바일 디자인', desc: '엄지의 법칙, 터치 타깃, 웹 vs 앱.', level: '입문', Component: MobileDesignPage }, { slug: 'mobile-design', Icon: Smartphone, title: '모바일 디자인', desc: '엄지의 법칙, 터치 타깃, 웹 vs 앱.', level: '입문', Component: MobileDesignPage },
{ slug: 'user-research', icon: '🔬', title: '사용자 리서치 기초', desc: '인터뷰·관찰·사용성 테스트 — 감이 아니라 근거로.', level: '심화', Component: UserResearchPage }, { slug: 'user-research', Icon: Microscope, title: '사용자 리서치 기초', desc: '인터뷰·관찰·사용성 테스트 — 감이 아니라 근거로.', level: '심화', Component: UserResearchPage },
{ slug: 'design-refs', icon: '🗃️', title: '레퍼런스와 무드보드', desc: '많이 본 사람이 잘 만든다 — 수집과 분석의 루틴.', level: '실습', Component: DesignRefsPage }, { slug: 'design-refs', Icon: Images, title: '레퍼런스와 무드보드', desc: '많이 본 사람이 잘 만든다 — 수집과 분석의 루틴.', level: '실습', Component: DesignRefsPage },
{ slug: 'portfolio', icon: '💼', title: '포트폴리오 만들기', desc: '수습 8주가 곧 재료 — 과정을 이야기로.', level: '실습', Component: PortfolioPage }, { slug: 'portfolio', Icon: Briefcase, title: '포트폴리오 만들기', desc: '수습 8주가 곧 재료 — 과정을 이야기로.', level: '실습', Component: PortfolioPage },
], ],
}, },
{ {
category: '부록 · 도구', category: '부록 · 도구',
desc: '손에 익으면 두 배 빨라지는 도구들.', desc: '손에 익으면 두 배 빨라지는 도구들.',
items: [ items: [
{ slug: null, external: '/setup', icon: '🛠️', title: '개발 환경 설치 가이드', desc: 'JDK·PATH·Node·Git·IntelliJ — 환경 구축 9단계.', level: '실습' }, { slug: null, external: '/setup', Icon: Wrench, title: '개발 환경 설치 가이드', desc: 'JDK·PATH·Node·Git·IntelliJ — 환경 구축 9단계.', level: '실습' },
{ slug: 'tool-shortcuts', icon: '⌨️', title: '윈도우 단축키 마스터', desc: '마우스를 버릴수록 빨라진다 — 필수 30개.', level: '실습', Component: ToolShortcutsPage }, { slug: 'tool-shortcuts', Icon: Command, title: '윈도우 단축키 마스터', desc: '마우스를 버릴수록 빨라진다 — 필수 30개.', level: '실습', Component: ToolShortcutsPage },
{ slug: 'tool-terminal', icon: '🖤', title: '윈도우 터미널과 PowerShell', desc: 'cmd·PowerShell·Git Bash 삼형제 정리.', level: '실습', Component: ToolTerminalPage }, { slug: 'tool-terminal', Icon: SquareChevronRight, title: '윈도우 터미널과 PowerShell', desc: 'cmd·PowerShell·Git Bash 삼형제 정리.', level: '실습', Component: ToolTerminalPage },
{ slug: 'tool-vscode', icon: '📝', title: 'VS Code 200% 활용', desc: '단축키 15개, 확장 추천, 검색의 기술.', level: '실습', Component: ToolVscodePage }, { slug: 'tool-vscode', Icon: Code, title: 'VS Code 200% 활용', desc: '단축키 15개, 확장 추천, 검색의 기술.', level: '실습', Component: ToolVscodePage },
{ slug: 'tool-intellij', icon: '💡', title: 'IntelliJ 꿀팁 모음', desc: 'Shift 두 번, Alt+Enter, 로컬 히스토리의 구원.', level: '실습', Component: ToolIntellijPage }, { slug: 'tool-intellij', Icon: Lightbulb, title: 'IntelliJ 꿀팁 모음', desc: 'Shift 두 번, Alt+Enter, 로컬 히스토리의 구원.', level: '실습', Component: ToolIntellijPage },
{ slug: 'tool-devtools', icon: '🔎', title: '크롬 개발자도구', desc: 'F12의 세계 — Elements·Console·Network 투어.', level: '실습', Component: ToolDevtoolsPage }, { slug: 'tool-devtools', Icon: Inspect, title: '크롬 개발자도구', desc: 'F12의 세계 — Elements·Console·Network 투어.', level: '실습', Component: ToolDevtoolsPage },
{ slug: 'tool-gitea', icon: '🍵', title: 'Gitea 200% 활용', desc: '우리 Git 서버 — 이슈·PR·리뷰 화면 사용법.', level: '실습', Component: ToolGiteaPage }, { slug: 'tool-gitea', Icon: GitFork, title: 'Gitea 200% 활용', desc: '우리 Git 서버 — 이슈·PR·리뷰 화면 사용법.', level: '실습', Component: ToolGiteaPage },
{ slug: 'tool-docker', icon: '🐳', title: 'Docker Desktop 사용법', desc: '대시보드 읽기, 컨테이너 GUI 관리, 용량 정리.', level: '실습', Component: ToolDockerPage }, { slug: 'tool-docker', Icon: Ship, title: 'Docker Desktop 사용법', desc: '대시보드 읽기, 컨테이너 GUI 관리, 용량 정리.', level: '실습', Component: ToolDockerPage },
{ slug: 'tool-dbeaver', icon: '🦫', title: 'DBeaver로 DB 들여다보기', desc: '우리 DB 연결, ERD 자동 생성, GUI로 쿼리.', level: '실습', Component: ToolDbeaverPage }, { slug: 'tool-dbeaver', Icon: DatabaseZap, title: 'DBeaver로 DB 들여다보기', desc: '우리 DB 연결, ERD 자동 생성, GUI로 쿼리.', level: '실습', Component: ToolDbeaverPage },
{ slug: 'tool-postman', icon: '🚀', title: 'Postman으로 API 테스트', desc: '화면 없이 API를 직접 두드리는 법.', level: '실습', Component: ToolPostmanPage }, { slug: 'tool-postman', Icon: Send, title: 'Postman으로 API 테스트', desc: '화면 없이 API를 직접 두드리는 법.', level: '실습', Component: ToolPostmanPage },
{ slug: 'tool-capture', icon: '📸', title: '캡처와 화면 녹화', desc: 'PR·버그 리포트의 언어 — Win+Shift+S부터.', level: '실습', Component: ToolCapturePage }, { slug: 'tool-capture', Icon: Camera, title: '캡처와 화면 녹화', desc: 'PR·버그 리포트의 언어 — Win+Shift+S부터.', level: '실습', Component: ToolCapturePage },
{ slug: 'tool-notion', icon: '🗒️', title: '노션으로 기록하기', desc: 'TIL·트러블슈팅 — 검색되는 기록이 자산.', level: '실습', Component: ToolNotionPage }, { slug: 'tool-notion', Icon: NotebookPen, title: '노션으로 기록하기', desc: 'TIL·트러블슈팅 — 검색되는 기록이 자산.', level: '실습', Component: ToolNotionPage },
], ],
}, },
{ {
category: '앱 개발', category: '앱 개발',
desc: '웹 개발자가 보는 모바일 — 안드로이드·iOS부터 우리 React로 앱까지.', desc: '웹 개발자가 보는 모바일 — 안드로이드·iOS부터 우리 React로 앱까지.',
items: [ items: [
{ slug: 'app-landscape', icon: '📱', title: '앱의 지형', desc: '네이티브·크로스플랫폼·웹 — 뭘 언제 고르나.', level: '교양', Component: AppLandscapePage }, { slug: 'app-landscape', Icon: LayoutTemplate, title: '앱의 지형', desc: '네이티브·크로스플랫폼·웹 — 뭘 언제 고르나.', level: '교양', Component: AppLandscapePage },
{ slug: 'android-basics', icon: '🤖', title: '안드로이드 앱의 구조', desc: '액티비티·생명주기·스튜디오 — 앱이 도는 원리.', level: '입문', Component: AndroidBasicsPage }, { slug: 'android-basics', Icon: TabletSmartphone, title: '안드로이드 앱의 구조', desc: '액티비티·생명주기·스튜디오 — 앱이 도는 원리.', level: '입문', Component: AndroidBasicsPage },
{ slug: 'kotlin-intro', icon: '🟣', title: 'Kotlin 입문', desc: '안드로이드의 언어 — JS/Java 관점으로 빠르게.', level: '입문', Component: KotlinIntroPage }, { slug: 'kotlin-intro', Icon: Blocks, title: 'Kotlin 입문', desc: '안드로이드의 언어 — JS/Java 관점으로 빠르게.', level: '입문', Component: KotlinIntroPage },
{ slug: 'ios-basics', icon: '🍎', title: 'iOS 앱의 구조', desc: '뷰·생명주기·Xcode — 애플 생태계의 규칙.', level: '입문', Component: IosBasicsPage }, { slug: 'ios-basics', Icon: SquareDashedBottomCode, title: 'iOS 앱의 구조', desc: '뷰·생명주기·Xcode — 애플 생태계의 규칙.', level: '입문', Component: IosBasicsPage },
{ slug: 'swift-intro', icon: '🐦', title: 'Swift 입문', desc: 'iOS의 언어 — 옵셔널·구조체 핵심만.', level: '입문', Component: SwiftIntroPage }, { slug: 'swift-intro', Icon: Bird, title: 'Swift 입문', desc: 'iOS의 언어 — 옵셔널·구조체 핵심만.', level: '입문', Component: SwiftIntroPage },
{ slug: 'react-native', icon: '⚛️', title: 'React Native', desc: '우리 React 지식이 그대로 앱이 되는 다리.', level: '심화', Component: ReactNativePage }, { slug: 'react-native', Icon: Orbit, title: 'React Native', desc: '우리 React 지식이 그대로 앱이 되는 다리.', level: '심화', Component: ReactNativePage },
{ slug: 'flutter', icon: '🦋', title: 'Flutter', desc: 'Dart·위젯 — 또 하나의 크로스플랫폼.', level: '심화', Component: FlutterPage }, { slug: 'flutter', Icon: Feather, title: 'Flutter', desc: 'Dart·위젯 — 또 하나의 크로스플랫폼.', level: '심화', Component: FlutterPage },
{ slug: 'app-navigation', icon: '🧭', title: '앱 화면과 이동', desc: '내비게이션·화면 전환 — 웹 라우팅과 비교.', level: '입문', Component: AppNavigationPage }, { slug: 'app-navigation', Icon: Compass, title: '앱 화면과 이동', desc: '내비게이션·화면 전환 — 웹 라우팅과 비교.', level: '입문', Component: AppNavigationPage },
{ slug: 'app-state-data', icon: '🗃️', title: '앱의 상태와 데이터', desc: '상태관리·로컬 저장·우리 Spring API 연동.', level: '심화', Component: AppStateDataPage }, { slug: 'app-state-data', Icon: Layers, title: '앱의 상태와 데이터', desc: '상태관리·로컬 저장·우리 Spring API 연동.', level: '심화', Component: AppStateDataPage },
{ slug: 'pwa', icon: '🌐', title: 'PWA — 웹앱을 앱처럼', desc: '설치·오프라인·푸시 — 우리 웹앱에 가장 가까운 길.', level: '심화', Component: PwaPage }, { slug: 'pwa', Icon: MonitorSmartphone, title: 'PWA — 웹앱을 앱처럼', desc: '설치·오프라인·푸시 — 우리 웹앱에 가장 가까운 길.', level: '심화', Component: PwaPage },
{ slug: 'app-security', icon: '🔐', title: '앱 보안과 권한', desc: '권한 모델·저장소 보안·심사 기준.', level: '입문', Component: AppSecurityPage }, { slug: 'app-security', Icon: ShieldCheck, title: '앱 보안과 권한', desc: '권한 모델·저장소 보안·심사 기준.', level: '입문', Component: AppSecurityPage },
{ slug: 'app-store-deploy', icon: '🚀', title: '스토어 배포', desc: 'Play·App Store 등록과 심사 흐름.', level: '실습', Component: AppStoreDeployPage }, { slug: 'app-store-deploy', Icon: Rocket, title: '스토어 배포', desc: 'Play·App Store 등록과 심사 흐름.', level: '실습', Component: AppStoreDeployPage },
], ],
}, },
]; ];

View File

@ -5,6 +5,56 @@
// LEVEL_COURSES map . // LEVEL_COURSES map .
import { lazy } from 'react'; import { lazy } from 'react';
// : Lucide( SVG). import ().
import {
Activity,
Atom,
Bandage,
Blocks,
Boxes,
ChefHat,
CircleCheck,
Compass,
Database,
DatabaseBackup,
Fingerprint,
FlaskConical,
Gauge,
GitBranch,
GitPullRequest,
Globe,
Hammer,
Hexagon,
Hourglass,
Inbox,
KeyRound,
Layers,
Lightbulb,
Link,
ListTree,
Lock,
MessageSquare,
Milestone,
Network,
Newspaper,
Package,
Puzzle,
Repeat,
Rocket,
Ruler,
Search,
Shield,
Signpost,
Siren,
Sparkles,
SquareFunction,
Telescope,
TrendingUp,
TriangleAlert,
Turtle,
Zap,
} from 'lucide-react';
// (lv1) (lv4) , 12 // (lv1) (lv4) , 12
const Lv1WhatIsAProgramPage = lazy(() => import('./pages/levels/Lv1WhatIsAProgramPage')); const Lv1WhatIsAProgramPage = lazy(() => import('./pages/levels/Lv1WhatIsAProgramPage'));
const Lv1ComputationalThinkingPage = lazy(() => import('./pages/levels/Lv1ComputationalThinkingPage')); const Lv1ComputationalThinkingPage = lazy(() => import('./pages/levels/Lv1ComputationalThinkingPage'));
@ -61,72 +111,72 @@ export const LEVEL_TRACKS = {
category: '초급 과정 — 코딩 첫걸음', category: '초급 과정 — 코딩 첫걸음',
desc: '코딩이 처음인 사람을 위한 12강좌 — 변수 하나에서 첫 프로그램까지.', desc: '코딩이 처음인 사람을 위한 12강좌 — 변수 하나에서 첫 프로그램까지.',
items: [ items: [
{ slug: 'lv1-what-is-a-program', icon: '💡', title: '프로그램이란 무엇일까?', desc: '코드를 한 줄도 쓰기 전에, 컴퓨터에게 \'일을 시킨다\'는 게 무슨 뜻인지부터 몸으로 이해해요.', level: '입문', Component: Lv1WhatIsAProgramPage }, { slug: 'lv1-what-is-a-program', Icon: Lightbulb, title: '프로그램이란 무엇일까?', desc: '코드를 한 줄도 쓰기 전에, 컴퓨터에게 \'일을 시킨다\'는 게 무슨 뜻인지부터 몸으로 이해해요.', level: '입문', Component: Lv1WhatIsAProgramPage },
{ slug: 'lv1-computational-thinking', icon: '🧩', title: '컴퓨터처럼 생각하기: 컴퓨팅 사고', desc: '코드 없이도 기를 수 있는 문제 해결 근육 — 쪼개고, 패턴을 찾고, 순서를 세우는 법을 배워요.', level: '입문', Component: Lv1ComputationalThinkingPage }, { slug: 'lv1-computational-thinking', Icon: Puzzle, title: '컴퓨터처럼 생각하기: 컴퓨팅 사고', desc: '코드 없이도 기를 수 있는 문제 해결 근육 — 쪼개고, 패턴을 찾고, 순서를 세우는 법을 배워요.', level: '입문', Component: Lv1ComputationalThinkingPage },
{ slug: 'lv1-first-html', icon: '🌐', title: '나의 첫 웹페이지: HTML 첫걸음', desc: '메모장 수준의 도구만으로 브라우저에 뜨는 \'진짜 내 페이지\'를 오늘 바로 만들어요.', level: '입문', Component: Lv1FirstHtmlPage }, { slug: 'lv1-first-html', Icon: Globe, title: '나의 첫 웹페이지: HTML 첫걸음', desc: '메모장 수준의 도구만으로 브라우저에 뜨는 \'진짜 내 페이지\'를 오늘 바로 만들어요.', level: '입문', Component: Lv1FirstHtmlPage },
{ slug: 'lv1-html-links-images-lists', icon: '🏗️', title: '태그로 페이지에 살 붙이기', desc: '링크·이미지·목록·표를 넣어서 밋밋한 문서를 \'진짜 웹페이지\'처럼 만들어요.', level: '입문', Component: Lv1HtmlLinksImagesListsPage }, { slug: 'lv1-html-links-images-lists', Icon: Blocks, title: '태그로 페이지에 살 붙이기', desc: '링크·이미지·목록·표를 넣어서 밋밋한 문서를 \'진짜 웹페이지\'처럼 만들어요.', level: '입문', Component: Lv1HtmlLinksImagesListsPage },
{ slug: 'lv1-how-browsers-work', icon: '🔍', title: '브라우저는 내 코드를 어떻게 읽을까', desc: '주소창에 엔터를 치면 무슨 일이 벌어질까? 개발자 도구를 열고 웹의 뒷면을 구경해요.', level: '입문', Component: Lv1HowBrowsersWorkPage }, { slug: 'lv1-how-browsers-work', Icon: Search, title: '브라우저는 내 코드를 어떻게 읽을까', desc: '주소창에 엔터를 치면 무슨 일이 벌어질까? 개발자 도구를 열고 웹의 뒷면을 구경해요.', level: '입문', Component: Lv1HowBrowsersWorkPage },
{ slug: 'lv1-first-javascript', icon: '✨', title: '나의 첫 JavaScript', desc: '멈춰 있던 페이지가 말을 걸고 반응하기 시작해요 — 첫 코드 한 줄의 짜릿함.', level: '입문', Component: Lv1FirstJavascriptPage }, { slug: 'lv1-first-javascript', Icon: Sparkles, title: '나의 첫 JavaScript', desc: '멈춰 있던 페이지가 말을 걸고 반응하기 시작해요 — 첫 코드 한 줄의 짜릿함.', level: '입문', Component: Lv1FirstJavascriptPage },
{ slug: 'lv1-variables-and-types', icon: '📦', title: '변수와 자료형: 이름 붙인 상자', desc: '값을 상자에 담아 두고 꺼내 쓰는 법 — 모든 프로그램의 가장 작은 부품을 배워요.', level: '실전', Component: Lv1VariablesAndTypesPage }, { slug: 'lv1-variables-and-types', Icon: Package, title: '변수와 자료형: 이름 붙인 상자', desc: '값을 상자에 담아 두고 꺼내 쓰는 법 — 모든 프로그램의 가장 작은 부품을 배워요.', level: '실전', Component: Lv1VariablesAndTypesPage },
{ slug: 'lv1-conditionals', icon: '🚦', title: '조건문: 컴퓨터가 판단하게 하기', desc: '"만약 ~라면" 한마디로 프로그램이 갈림길에서 스스로 길을 고르게 만들어요.', level: '실전', Component: Lv1ConditionalsPage }, { slug: 'lv1-conditionals', Icon: GitBranch, title: '조건문: 컴퓨터가 판단하게 하기', desc: '"만약 ~라면" 한마디로 프로그램이 갈림길에서 스스로 길을 고르게 만들어요.', level: '실전', Component: Lv1ConditionalsPage },
{ slug: 'lv1-loops', icon: '🔁', title: '반복문: 지루한 일은 컴퓨터에게', desc: '100번 반복도 한 줄로 — 컴퓨터가 가장 잘하는 일을 시키는 법을 배워요.', level: '실전', Component: Lv1LoopsPage }, { slug: 'lv1-loops', Icon: Repeat, title: '반복문: 지루한 일은 컴퓨터에게', desc: '100번 반복도 한 줄로 — 컴퓨터가 가장 잘하는 일을 시키는 법을 배워요.', level: '실전', Component: Lv1LoopsPage },
{ slug: 'lv1-functions', icon: '🧰', title: '함수: 나만의 명령어 만들기', desc: '자주 쓰는 코드에 이름을 붙여 언제든 다시 부르는 법 — 진짜 개발자처럼 코드를 조립해요.', level: '실전', Component: Lv1FunctionsPage }, { slug: 'lv1-functions', Icon: SquareFunction, title: '함수: 나만의 명령어 만들기', desc: '자주 쓰는 코드에 이름을 붙여 언제든 다시 부르는 법 — 진짜 개발자처럼 코드를 조립해요.', level: '실전', Component: Lv1FunctionsPage },
{ slug: 'lv1-reading-errors', icon: '🩹', title: '에러와 친해지기: 빨간 글씨 해독법', desc: '에러는 벌점이 아니라 힌트예요. 메시지를 읽고, 원인을 좁히고, 스스로 고치는 힘을 길러요.', level: '심화', Component: Lv1ReadingErrorsPage }, { slug: 'lv1-reading-errors', Icon: Bandage, title: '에러와 친해지기: 빨간 글씨 해독법', desc: '에러는 벌점이 아니라 힌트예요. 메시지를 읽고, 원인을 좁히고, 스스로 고치는 힘을 길러요.', level: '심화', Component: Lv1ReadingErrorsPage },
{ slug: 'lv1-first-mini-project', icon: '🚀', title: '첫 프로젝트: 숫자 맞히기 게임 만들기', desc: '지금까지 배운 전부를 총동원해 처음부터 끝까지 \'내가 만든 웹 게임\'을 완성해요.', level: '프로젝트', Component: Lv1FirstMiniProjectPage }, { slug: 'lv1-first-mini-project', Icon: Rocket, title: '첫 프로젝트: 숫자 맞히기 게임 만들기', desc: '지금까지 배운 전부를 총동원해 처음부터 끝까지 \'내가 만든 웹 게임\'을 완성해요.', level: '프로젝트', Component: Lv1FirstMiniProjectPage },
], ],
}], }],
lv2: [{ lv2: [{
category: '중급 과정 — 실전 웹 개발', category: '중급 과정 — 실전 웹 개발',
desc: '돌아가는 웹 서비스를 직접 만드는 12강좌.', desc: '돌아가는 웹 서비스를 직접 만드는 12강좌.',
items: [ items: [
{ slug: 'lv2-async-javascript', icon: '⏳', title: '비동기 JavaScript: fetch와 Promise 정복', desc: '콜백 지옥에서 async/await까지 — 서버와 대화하는 코드를 제대로 쓰는 법', level: '실전', Component: Lv2AsyncJavascriptPage }, { slug: 'lv2-async-javascript', Icon: Hourglass, title: '비동기 JavaScript: fetch와 Promise 정복', desc: '콜백 지옥에서 async/await까지 — 서버와 대화하는 코드를 제대로 쓰는 법', level: '실전', Component: Lv2AsyncJavascriptPage },
{ slug: 'lv2-array-data-wrangling', icon: '🔪', title: '배열 메서드로 데이터 요리하기', desc: 'map·filter·reduce로 API 응답을 화면에 딱 맞는 데이터로 썰고 볶는 실전 기술', level: '실전', Component: Lv2ArrayDataWranglingPage }, { slug: 'lv2-array-data-wrangling', Icon: ChefHat, title: '배열 메서드로 데이터 요리하기', desc: 'map·filter·reduce로 API 응답을 화면에 딱 맞는 데이터로 썰고 볶는 실전 기술', level: '실전', Component: Lv2ArrayDataWranglingPage },
{ slug: 'lv2-rest-api-design', icon: '📐', title: 'REST API 설계: 주소와 동사의 문법', desc: '코드를 짜기 전에 API 명세부터 — 프론트와 백이 싸우지 않는 계약서 쓰는 법', level: '실전', Component: Lv2RestApiDesignPage }, { slug: 'lv2-rest-api-design', Icon: Ruler, title: 'REST API 설계: 주소와 동사의 문법', desc: '코드를 짜기 전에 API 명세부터 — 프론트와 백이 싸우지 않는 계약서 쓰는 법', level: '실전', Component: Lv2RestApiDesignPage },
{ slug: 'lv2-express-crud', icon: '🛠️', title: 'Express로 CRUD API 만들기', desc: '설계한 명세를 진짜 돌아가는 서버로 — 라우터·미들웨어·계층 분리까지 한 번에', level: '실전', Component: Lv2ExpressCrudPage }, { slug: 'lv2-express-crud', Icon: Hammer, title: 'Express로 CRUD API 만들기', desc: '설계한 명세를 진짜 돌아가는 서버로 — 라우터·미들웨어·계층 분리까지 한 번에', level: '실전', Component: Lv2ExpressCrudPage },
{ slug: 'lv2-db-transactions', icon: '🗄️', title: 'DB 연동과 트랜잭션: 데이터가 사는 집', desc: '서버를 껐다 켜도 살아남는 데이터 — SQL 연동부터 all-or-nothing 트랜잭션까지', level: '심화', Component: Lv2DbTransactionsPage }, { slug: 'lv2-db-transactions', Icon: Database, title: 'DB 연동과 트랜잭션: 데이터가 사는 집', desc: '서버를 껐다 켜도 살아남는 데이터 — SQL 연동부터 all-or-nothing 트랜잭션까지', level: '심화', Component: Lv2DbTransactionsPage },
{ slug: 'lv2-auth-session-hash', icon: '🔐', title: '로그인 구현: 세션과 비밀번호 해시', desc: '비밀번호를 지키고 사용자를 기억하는 법 — 회원가입부터 보호 라우트까지 직접 구현', level: '심화', Component: Lv2AuthSessionHashPage }, { slug: 'lv2-auth-session-hash', Icon: KeyRound, title: '로그인 구현: 세션과 비밀번호 해시', desc: '비밀번호를 지키고 사용자를 기억하는 법 — 회원가입부터 보호 라우트까지 직접 구현', level: '심화', Component: Lv2AuthSessionHashPage },
{ slug: 'lv2-react-state-effects', icon: '⚛️', title: 'React 상태와 이펙트 실전', desc: '상태를 \'설계\'하는 단계로 — 불변 업데이트, useEffect 데이터 페칭, 커스텀 훅까지', level: '실전', Component: Lv2ReactStateEffectsPage }, { slug: 'lv2-react-state-effects', Icon: Atom, title: 'React 상태와 이펙트 실전', desc: '상태를 \'설계\'하는 단계로 — 불변 업데이트, useEffect 데이터 페칭, 커스텀 훅까지', level: '실전', Component: Lv2ReactStateEffectsPage },
{ slug: 'lv2-react-routing', icon: '🧭', title: 'React Router로 멀티 페이지 SPA', desc: '주소창이 곧 상태다 — URL 파라미터, 중첩 레이아웃, 보호 라우트로 진짜 앱 골격 만들기', level: '실전', Component: Lv2ReactRoutingPage }, { slug: 'lv2-react-routing', Icon: Compass, title: 'React Router로 멀티 페이지 SPA', desc: '주소창이 곧 상태다 — URL 파라미터, 중첩 레이아웃, 보호 라우트로 진짜 앱 골격 만들기', level: '실전', Component: Lv2ReactRoutingPage },
{ slug: 'lv2-form-validation', icon: '✅', title: '폼 검증: 사용자를 돕고 서버를 지키기', desc: '검증은 두 번 한다 — 프론트에서 친절하게, 서버에서 단호하게. 이중 검증 완전 정복', level: '실전', Component: Lv2FormValidationPage }, { slug: 'lv2-form-validation', Icon: CircleCheck, title: '폼 검증: 사용자를 돕고 서버를 지키기', desc: '검증은 두 번 한다 — 프론트에서 친절하게, 서버에서 단호하게. 이중 검증 완전 정복', level: '실전', Component: Lv2FormValidationPage },
{ slug: 'lv2-error-handling-patterns', icon: '🚨', title: '에러 처리 패턴: 무너지지 않는 서비스', desc: '에러는 없앨 수 없다, 다스릴 뿐 — 백엔드 에러 미들웨어부터 프론트 폴백 UI까지', level: '심화', Component: Lv2ErrorHandlingPatternsPage }, { slug: 'lv2-error-handling-patterns', Icon: TriangleAlert, title: '에러 처리 패턴: 무너지지 않는 서비스', desc: '에러는 없앨 수 없다, 다스릴 뿐 — 백엔드 에러 미들웨어부터 프론트 폴백 UI까지', level: '심화', Component: Lv2ErrorHandlingPatternsPage },
{ slug: 'lv2-practical-testing', icon: '🧪', title: '테스트 작성 실전: 자신 있게 고치는 힘', desc: '고칠 때마다 손이 떨리지 않으려면 — 단위·API·컴포넌트 테스트를 실제 코드에 붙이기', level: '심화', Component: Lv2PracticalTestingPage }, { slug: 'lv2-practical-testing', Icon: FlaskConical, title: '테스트 작성 실전: 자신 있게 고치는 힘', desc: '고칠 때마다 손이 떨리지 않으려면 — 단위·API·컴포넌트 테스트를 실제 코드에 붙이기', level: '심화', Component: Lv2PracticalTestingPage },
{ slug: 'lv2-mini-project-deploy', icon: '🚀', title: '미니 프로젝트: 설계부터 배포까지', desc: '11주간 만든 조각을 하나로 — 기획·조립·배포·회고까지, 포트폴리오가 되는 첫 서비스', level: '프로젝트', Component: Lv2MiniProjectDeployPage }, { slug: 'lv2-mini-project-deploy', Icon: Rocket, title: '미니 프로젝트: 설계부터 배포까지', desc: '11주간 만든 조각을 하나로 — 기획·조립·배포·회고까지, 포트폴리오가 되는 첫 서비스', level: '프로젝트', Component: Lv2MiniProjectDeployPage },
], ],
}], }],
lv3: [{ lv3: [{
category: '고급 과정 — 아키텍처·성능·보안', category: '고급 과정 — 아키텍처·성능·보안',
desc: '"잘 만드는 법"을 파고드는 12강좌.', desc: '"잘 만드는 법"을 파고드는 12강좌.',
items: [ items: [
{ slug: 'lv3-layered-architecture', icon: '🏛️', title: '레이어드 아키텍처와 관심사 분리', desc: '돌아가는 코드에서 오래 사는 코드로 — 계층으로 변경을 가두는 법.', level: '심화', Component: Lv3LayeredArchitecturePage }, { slug: 'lv3-layered-architecture', Icon: Layers, title: '레이어드 아키텍처와 관심사 분리', desc: '돌아가는 코드에서 오래 사는 코드로 — 계층으로 변경을 가두는 법.', level: '심화', Component: Lv3LayeredArchitecturePage },
{ slug: 'lv3-design-patterns', icon: '🧩', title: '디자인 패턴 실전', desc: '외우는 패턴 말고 쓰는 패턴 — \'이 문제엔 이 해법\'을 우리 코드로.', level: '실전', Component: Lv3DesignPatternsPage }, { slug: 'lv3-design-patterns', Icon: Puzzle, title: '디자인 패턴 실전', desc: '외우는 패턴 말고 쓰는 패턴 — \'이 문제엔 이 해법\'을 우리 코드로.', level: '실전', Component: Lv3DesignPatternsPage },
{ slug: 'lv3-db-indexing', icon: '🔍', title: 'DB 인덱스와 실행계획 읽기', desc: '쿼리가 왜 느린지 감으로 찍지 말고 EXPLAIN으로 증명하는 법.', level: '심화', Component: Lv3DbIndexingPage }, { slug: 'lv3-db-indexing', Icon: ListTree, title: 'DB 인덱스와 실행계획 읽기', desc: '쿼리가 왜 느린지 감으로 찍지 말고 EXPLAIN으로 증명하는 법.', level: '심화', Component: Lv3DbIndexingPage },
{ slug: 'lv3-transaction-isolation', icon: '🔒', title: '트랜잭션과 격리 수준', desc: '동시에 밀려드는 요청 속에서 돈과 데이터를 지키는 규칙.', level: '심화', Component: Lv3TransactionIsolationPage }, { slug: 'lv3-transaction-isolation', Icon: Lock, title: '트랜잭션과 격리 수준', desc: '동시에 밀려드는 요청 속에서 돈과 데이터를 지키는 규칙.', level: '심화', Component: Lv3TransactionIsolationPage },
{ slug: 'lv3-query-optimization', icon: '🐢', title: 'N+1과 쿼리 최적화', desc: '목록 하나 여는데 쿼리 수백 개 — ORM이 숨긴 재앙을 걷어내기.', level: '실전', Component: Lv3QueryOptimizationPage }, { slug: 'lv3-query-optimization', Icon: Turtle, title: 'N+1과 쿼리 최적화', desc: '목록 하나 여는데 쿼리 수백 개 — ORM이 숨긴 재앙을 걷어내기.', level: '실전', Component: Lv3QueryOptimizationPage },
{ slug: 'lv3-caching-strategy', icon: '⚡', title: '캐싱 전략 — 로컬과 Redis', desc: '빠르게 만드는 마법이 아니라, 무효화와 일관성을 감당하는 설계.', level: '심화', Component: Lv3CachingStrategyPage }, { slug: 'lv3-caching-strategy', Icon: Zap, title: '캐싱 전략 — 로컬과 Redis', desc: '빠르게 만드는 마법이 아니라, 무효화와 일관성을 감당하는 설계.', level: '심화', Component: Lv3CachingStrategyPage },
{ slug: 'lv3-performance-profiling', icon: '📈', title: '성능 측정과 프로파일링', desc: '추측하지 말고 측정하라 — 병목은 늘 예상 밖에 숨어 있다.', level: '실전', Component: Lv3PerformanceProfilingPage }, { slug: 'lv3-performance-profiling', Icon: TrendingUp, title: '성능 측정과 프로파일링', desc: '추측하지 말고 측정하라 — 병목은 늘 예상 밖에 숨어 있다.', level: '실전', Component: Lv3PerformanceProfilingPage },
{ slug: 'lv3-web-security', icon: '🛡️', title: '웹 보안 심화 — OWASP Top 10', desc: '공격자의 눈으로 내 서비스를 보고, 계층으로 방어하는 법.', level: '심화', Component: Lv3WebSecurityPage }, { slug: 'lv3-web-security', Icon: Shield, title: '웹 보안 심화 — OWASP Top 10', desc: '공격자의 눈으로 내 서비스를 보고, 계층으로 방어하는 법.', level: '심화', Component: Lv3WebSecurityPage },
{ slug: 'lv3-auth-architecture', icon: '🔑', title: '인증 아키텍처 — 세션 vs JWT vs OAuth', desc: '쿠키냐 토큰이냐, 무상태의 자유와 대가를 저울질하는 설계.', level: '심화', Component: Lv3AuthArchitecturePage }, { slug: 'lv3-auth-architecture', Icon: Fingerprint, title: '인증 아키텍처 — 세션 vs JWT vs OAuth', desc: '쿠키냐 토큰이냐, 무상태의 자유와 대가를 저울질하는 설계.', level: '심화', Component: Lv3AuthArchitecturePage },
{ slug: 'lv3-observability', icon: '🔭', title: '로깅·모니터링·알림 설계', desc: '무엇이 느린가와 왜 느린가를 나눠 답하는 관측 가능성.', level: '실전', Component: Lv3ObservabilityPage }, { slug: 'lv3-observability', Icon: Telescope, title: '로깅·모니터링·알림 설계', desc: '무엇이 느린가와 왜 느린가를 나눠 답하는 관측 가능성.', level: '실전', Component: Lv3ObservabilityPage },
{ slug: 'lv3-zero-downtime-deploy', icon: '🚦', title: '무중단 배포와 롤백', desc: '배포가 무섭지 않아야 자주 배포한다 — 되돌릴 수 있게 나가는 법.', level: '실전', Component: Lv3ZeroDowntimeDeployPage }, { slug: 'lv3-zero-downtime-deploy', Icon: Milestone, title: '무중단 배포와 롤백', desc: '배포가 무섭지 않아야 자주 배포한다 — 되돌릴 수 있게 나가는 법.', level: '실전', Component: Lv3ZeroDowntimeDeployPage },
{ slug: 'lv3-code-review-refactoring', icon: '🧭', title: '코드 리뷰와 리팩터링 전략', desc: '이 과정의 종착점 — 설계·성능·보안·운영을 리뷰 한 판에 녹여내기.', level: '프로젝트', Component: Lv3CodeReviewRefactoringPage }, { slug: 'lv3-code-review-refactoring', Icon: GitPullRequest, title: '코드 리뷰와 리팩터링 전략', desc: '이 과정의 종착점 — 설계·성능·보안·운영을 리뷰 한 판에 녹여내기.', level: '프로젝트', Component: Lv3CodeReviewRefactoringPage },
], ],
}], }],
lv4: [{ lv4: [{
category: '특급 과정 — 대규모 시스템', category: '특급 과정 — 대규모 시스템',
desc: '대용량·분산 환경을 설계하는 전문가 12강좌.', desc: '대용량·분산 환경을 설계하는 전문가 12강좌.',
items: [ items: [
{ slug: 'lv4-distributed-systems-basics', icon: '🌐', title: '분산 시스템 첫걸음 — 왜 서버 한 대로는 안 될까', desc: '서버가 여러 대가 되는 순간 생기는 근본 문제들 — CAP, 일관성 모델, 부분 실패를 몸으로 이해합니다.', level: '심화', Component: Lv4DistributedSystemsBasicsPage }, { slug: 'lv4-distributed-systems-basics', Icon: Network, title: '분산 시스템 첫걸음 — 왜 서버 한 대로는 안 될까', desc: '서버가 여러 대가 되는 순간 생기는 근본 문제들 — CAP, 일관성 모델, 부분 실패를 몸으로 이해합니다.', level: '심화', Component: Lv4DistributedSystemsBasicsPage },
{ slug: 'lv4-message-queue-event-driven', icon: '📨', title: '메시지 큐와 이벤트 드리븐 — 시스템을 느슨하게 잇는 법', desc: '동기 호출의 한계를 넘어 Kafka 같은 메시지 브로커로 시스템을 분리하고, 이벤트 중심으로 사고하는 법을 배웁니다.', level: '심화', Component: Lv4MessageQueueEventDrivenPage }, { slug: 'lv4-message-queue-event-driven', Icon: Inbox, title: '메시지 큐와 이벤트 드리븐 — 시스템을 느슨하게 잇는 법', desc: '동기 호출의 한계를 넘어 Kafka 같은 메시지 브로커로 시스템을 분리하고, 이벤트 중심으로 사고하는 법을 배웁니다.', level: '심화', Component: Lv4MessageQueueEventDrivenPage },
{ slug: 'lv4-monolith-vs-microservices', icon: '🧩', title: '모놀리스 vs 마이크로서비스 — 쪼개는 것의 진짜 비용', desc: '유행이 아니라 트레이드오프로 아키텍처를 고릅니다. 서비스 경계 긋기, 분산 트랜잭션, 그리고 \'쪼개지 않을 용기\'까지.', level: '심화', Component: Lv4MonolithVsMicroservicesPage }, { slug: 'lv4-monolith-vs-microservices', Icon: Boxes, title: '모놀리스 vs 마이크로서비스 — 쪼개는 것의 진짜 비용', desc: '유행이 아니라 트레이드오프로 아키텍처를 고릅니다. 서비스 경계 긋기, 분산 트랜잭션, 그리고 \'쪼개지 않을 용기\'까지.', level: '심화', Component: Lv4MonolithVsMicroservicesPage },
{ slug: 'lv4-db-replication-sharding', icon: '🗄️', title: 'DB 레플리케이션과 샤딩 — 데이터베이스를 늘리는 두 가지 축', desc: '읽기를 늘리는 복제, 쓰기를 늘리는 샤딩. 복제 지연과 핫 샤드 같은 부작용까지 다루는 데이터 확장 설계.', level: '심화', Component: Lv4DbReplicationShardingPage }, { slug: 'lv4-db-replication-sharding', Icon: DatabaseBackup, title: 'DB 레플리케이션과 샤딩 — 데이터베이스를 늘리는 두 가지 축', desc: '읽기를 늘리는 복제, 쓰기를 늘리는 샤딩. 복제 지연과 핫 샤드 같은 부작용까지 다루는 데이터 확장 설계.', level: '심화', Component: Lv4DbReplicationShardingPage },
{ slug: 'lv4-high-traffic-architecture', icon: '🚦', title: '대용량 트래픽 설계 — 밀려오는 요청 앞에서 무너지지 않기', desc: '로드밸런싱, CDN, 캐시 계층, 레이트리밋, 백프레셔까지 — 트래픽이 10배가 되어도 버티는 구조를 설계합니다.', level: '심화', Component: Lv4HighTrafficArchitecturePage }, { slug: 'lv4-high-traffic-architecture', Icon: Gauge, title: '대용량 트래픽 설계 — 밀려오는 요청 앞에서 무너지지 않기', desc: '로드밸런싱, CDN, 캐시 계층, 레이트리밋, 백프레셔까지 — 트래픽이 10배가 되어도 버티는 구조를 설계합니다.', level: '심화', Component: Lv4HighTrafficArchitecturePage },
{ slug: 'lv4-kubernetes-cloud-native', icon: '☸️', title: '쿠버네티스와 클라우드 네이티브 — 선언하면 알아서 굴러가는 인프라', desc: 'docker compose 다음 단계. 파드·서비스·디플로이먼트부터 오토스케일링까지, 선언형 운영의 사고방식을 익힙니다.', level: '심화', Component: Lv4KubernetesCloudNativePage }, { slug: 'lv4-kubernetes-cloud-native', Icon: Hexagon, title: '쿠버네티스와 클라우드 네이티브 — 선언하면 알아서 굴러가는 인프라', desc: 'docker compose 다음 단계. 파드·서비스·디플로이먼트부터 오토스케일링까지, 선언형 운영의 사고방식을 익힙니다.', level: '심화', Component: Lv4KubernetesCloudNativePage },
{ slug: 'lv4-observability-metrics-tracing', icon: '🔭', title: '관측성 — 보이지 않으면 고칠 수 없다', desc: '메트릭·로그·트레이스 세 기둥으로 분산 시스템 내부를 들여다봅니다. \'요청이 어디서 느려졌나\'에 답하는 기술.', level: '심화', Component: Lv4ObservabilityMetricsTracingPage }, { slug: 'lv4-observability-metrics-tracing', Icon: Activity, title: '관측성 — 보이지 않으면 고칠 수 없다', desc: '메트릭·로그·트레이스 세 기둥으로 분산 시스템 내부를 들여다봅니다. \'요청이 어디서 느려졌나\'에 답하는 기술.', level: '심화', Component: Lv4ObservabilityMetricsTracingPage },
{ slug: 'lv4-sre-incident-response', icon: '🚨', title: 'SRE와 장애 대응 — 무너져도 빨리 일어나는 팀', desc: 'SLO와 에러 버짓으로 신뢰성을 숫자로 관리하고, 장애를 침착하게 지휘하고, 비난 없는 포스트모템으로 배우는 법.', level: '심화', Component: Lv4SreIncidentResponsePage }, { slug: 'lv4-sre-incident-response', Icon: Siren, title: 'SRE와 장애 대응 — 무너져도 빨리 일어나는 팀', desc: 'SLO와 에러 버짓으로 신뢰성을 숫자로 관리하고, 장애를 침착하게 지휘하고, 비난 없는 포스트모템으로 배우는 법.', level: '심화', Component: Lv4SreIncidentResponsePage },
{ slug: 'lv4-system-design-url-shortener', icon: '🔗', title: '시스템 설계 훈련 1 — URL 단축기를 처음부터 끝까지', desc: '가장 고전적인 설계 문제로 \'요구사항 → 용량 산정 → API → 데이터 → 확장\'의 설계 루틴을 몸에 새깁니다.', level: '프로젝트', Component: Lv4SystemDesignUrlShortenerPage }, { slug: 'lv4-system-design-url-shortener', Icon: Link, title: '시스템 설계 훈련 1 — URL 단축기를 처음부터 끝까지', desc: '가장 고전적인 설계 문제로 \'요구사항 → 용량 산정 → API → 데이터 → 확장\'의 설계 루틴을 몸에 새깁니다.', level: '프로젝트', Component: Lv4SystemDesignUrlShortenerPage },
{ slug: 'lv4-system-design-chat', icon: '💬', title: '시스템 설계 훈련 2 — 실시간 채팅 시스템', desc: '연결이 오래 유지되는 실시간 시스템의 설계. WebSocket, 메시지 순서, 읽음 표시, 오프라인 동기화까지 파고듭니다.', level: '프로젝트', Component: Lv4SystemDesignChatPage }, { slug: 'lv4-system-design-chat', Icon: MessageSquare, title: '시스템 설계 훈련 2 — 실시간 채팅 시스템', desc: '연결이 오래 유지되는 실시간 시스템의 설계. WebSocket, 메시지 순서, 읽음 표시, 오프라인 동기화까지 파고듭니다.', level: '프로젝트', Component: Lv4SystemDesignChatPage },
{ slug: 'lv4-system-design-newsfeed', icon: '📰', title: '시스템 설계 훈련 3 — 뉴스피드와 타임라인', desc: '푸시 vs 풀, 팬아웃, 랭킹, 셀럽 문제 — 읽기 폭발형 시스템의 끝판왕 설계로 3부작을 완성합니다.', level: '프로젝트', Component: Lv4SystemDesignNewsfeedPage }, { slug: 'lv4-system-design-newsfeed', Icon: Newspaper, title: '시스템 설계 훈련 3 — 뉴스피드와 타임라인', desc: '푸시 vs 풀, 팬아웃, 랭킹, 셀럽 문제 — 읽기 폭발형 시스템의 끝판왕 설계로 3부작을 완성합니다.', level: '프로젝트', Component: Lv4SystemDesignNewsfeedPage },
{ slug: 'lv4-tech-leadership-adr', icon: '🧭', title: '기술 리더십과 ADR — 결정을 설계하고 기록하는 사람', desc: '특급 과정의 마무리. 아키텍처 결정 기록(ADR), 설계 리뷰 문화, 트레이드오프 커뮤니케이션으로 팀을 움직이는 개발자가 됩니다.', level: '심화', Component: Lv4TechLeadershipAdrPage }, { slug: 'lv4-tech-leadership-adr', Icon: Signpost, title: '기술 리더십과 ADR — 결정을 설계하고 기록하는 사람', desc: '특급 과정의 마무리. 아키텍처 결정 기록(ADR), 설계 리뷰 문화, 트레이드오프 커뮤니케이션으로 팀을 움직이는 개발자가 됩니다.', level: '심화', Component: Lv4TechLeadershipAdrPage },
], ],
}], }],
}; };

View File

@ -3,6 +3,7 @@
// . // .
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Award, BookA, BookOpen, Compass, Palette, PartyPopper } from 'lucide-react';
import client from '../api/client'; import client from '../api/client';
import { useAuth } from '../AuthContext'; import { useAuth } from '../AuthContext';
import Card from '../components/Card'; import Card from '../components/Card';
@ -45,7 +46,9 @@ function CourseProgressWidget() {
return ( return (
<Card style={{ marginBottom: 16 }}> <Card style={{ marginBottom: 16 }}>
<div className="assign-title-row" style={{ marginBottom: 10 }}> <div className="assign-title-row" style={{ marginBottom: 10 }}>
<span className="assign-title">📚 학습 센터 진도</span> <span className="assign-title" style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
<BookOpen size={18} strokeWidth={1.5} aria-hidden="true" /> 학습 센터 진도
</span>
<Link to="/learn" className="btn btn-ghost" style={{ marginLeft: 'auto', fontSize: 13 }}> <Link to="/learn" className="btn btn-ghost" style={{ marginLeft: 'auto', fontSize: 13 }}>
학습 센터로 학습 센터로
</Link> </Link>
@ -88,17 +91,19 @@ function CourseProgressWidget() {
background: 'var(--primary-soft)', display: 'flex', alignItems: 'center', gap: 10, background: 'var(--primary-soft)', display: 'flex', alignItems: 'center', gap: 10,
}} }}
> >
<span style={{ fontSize: 22 }}>{nextCourse.icon}</span> <nextCourse.Icon size={22} strokeWidth={1.5} aria-hidden="true" />
<div style={{ minWidth: 0 }}> <div style={{ minWidth: 0 }}>
<div style={{ fontSize: 12, fontWeight: 700, color: 'var(--primary)' }}>다음에 들을 강좌 🧭</div> <div style={{ fontSize: 12, fontWeight: 700, color: 'var(--primary)', display: 'flex', alignItems: 'center', gap: 4 }}>
다음에 들을 강좌 <Compass size={13} strokeWidth={1.5} aria-hidden="true" />
</div>
<div style={{ fontWeight: 700 }}>{nextCourse.title}</div> <div style={{ fontWeight: 700 }}>{nextCourse.title}</div>
</div> </div>
<span style={{ marginLeft: 'auto', color: 'var(--primary)', fontWeight: 700 }}></span> <span style={{ marginLeft: 'auto', color: 'var(--primary)', fontWeight: 700 }}></span>
</div> </div>
</Link> </Link>
) : ( ) : (
<p style={{ marginTop: 14, marginBottom: 0, color: 'var(--teal)', fontWeight: 700 }}> <p style={{ marginTop: 14, marginBottom: 0, color: 'var(--teal)', fontWeight: 700, display: 'flex', alignItems: 'center', gap: 6 }}>
🎉 모든 강좌를 완료했어요! 대단합니다. <PartyPopper size={18} strokeWidth={1.5} aria-hidden="true" /> 모든 강좌를 완료했어요! 대단합니다.
</p> </p>
)} )}
</Card> </Card>
@ -211,22 +216,29 @@ export default function DashboardPage() {
{/* 학습 센터 코스 진도 요약 */} {/* 학습 센터 코스 진도 요약 */}
<CourseProgressWidget /> <CourseProgressWidget />
{/* 빠른 이동 — 용어 사전과 내 학습 증명서. 자주 찾는 곳을 카드로. */} {/* 빠른 이동 — 자주 찾는 곳을 카드로. */}
<div className="grid-cards" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', marginBottom: 16 }}> <div className="grid-cards" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', marginBottom: 16 }}>
<Link to="/glossary" style={{ color: 'inherit' }}> <Link to="/glossary" style={{ color: 'inherit' }}>
<div className="card card-clickable" style={{ height: '100%' }}> <div className="card card-clickable" style={{ height: '100%' }}>
<div style={{ fontSize: 24, marginBottom: 6 }}>📖</div> <BookA size={24} strokeWidth={1.5} aria-hidden="true" style={{ marginBottom: 6 }} />
<div style={{ fontWeight: 700 }}>개발 용어 사전</div> <div style={{ fontWeight: 700 }}>개발 용어 사전</div>
<p className="muted" style={{ fontSize: 13, marginTop: 4 }}>모르는 용어가 나오면 여기서 검색하세요.</p> <p className="muted" style={{ fontSize: 13, marginTop: 4 }}>모르는 용어가 나오면 여기서 검색하세요.</p>
</div> </div>
</Link> </Link>
<Link to="/certificate" style={{ color: 'inherit' }}> <Link to="/certificate" style={{ color: 'inherit' }}>
<div className="card card-clickable" style={{ height: '100%' }}> <div className="card card-clickable" style={{ height: '100%' }}>
<div style={{ fontSize: 24, marginBottom: 6 }}>🏅</div> <Award size={24} strokeWidth={1.5} aria-hidden="true" style={{ marginBottom: 6 }} />
<div style={{ fontWeight: 700 }}> 학습 증명서</div> <div style={{ fontWeight: 700 }}> 학습 증명서</div>
<p className="muted" style={{ fontSize: 13, marginTop: 4 }}>지금까지의 성취를 증명서로 인쇄·PDF 저장.</p> <p className="muted" style={{ fontSize: 13, marginTop: 4 }}>지금까지의 성취를 증명서로 인쇄·PDF 저장.</p>
</div> </div>
</Link> </Link>
<Link to="/design" style={{ color: 'inherit' }}>
<div className="card card-clickable" style={{ height: '100%' }}>
<Palette size={24} strokeWidth={1.5} aria-hidden="true" style={{ marginBottom: 6 }} />
<div style={{ fontWeight: 700 }}>디자인 재료 모음</div>
<p className="muted" style={{ fontSize: 13, marginTop: 4 }}>·글자·여백 토큰과 아이콘을 골라 쓰는 .</p>
</div>
</Link>
</div> </div>
<div className="tabs"> <div className="tabs">

View File

@ -0,0 +1,242 @@
// : " " .
// · ·· Lucide 1,600 .
//
// 1: " (living style guide)" .
// () . CSS
// , .
// " " " " .
// 2: .
// .
import { useMemo, useState } from 'react';
import * as Lucide from 'lucide-react';
// global.css :root . CSS .
// : CSS (getComputedStyle)
// . .
const COLOR_TOKENS = [
['--bg', '페이지 배경'],
['--card', '카드 배경'],
['--ink', '본문 글자'],
['--muted', '보조 글자'],
['--ink-faint', '더 옅은 글자'],
['--line', '테두리·구분선'],
['--primary', '브랜드 파랑 (링크·강조)'],
['--primary-soft', '파랑 옅은 배경'],
['--teal', '성공·완료'],
['--amber', '주의'],
['--rose', '오류'],
['--code-bg', '코드 블록 배경'],
];
const SPACE_TOKENS = ['--space-1', '--space-2', '--space-3', '--space-4', '--space-5', '--space-6', '--space-7'];
const TEXT_TOKENS = ['--text-xs', '--text-sm', '--text-md', '--text-lg', '--text-xl', '--text-2xl'];
const RADIUS_TOKENS = ['--radius-sm', '--radius', '--radius-lg', '--radius-full'];
// CSS " " . .
function tokenValue(name) {
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
}
function Section({ title, desc, children }) {
return (
<section style={{ marginBottom: 'var(--space-7)' }}>
<h2 style={{ fontSize: 'var(--text-xl)', marginBottom: 'var(--space-2)' }}>{title}</h2>
<p className="muted" style={{ fontSize: 'var(--text-sm)', marginTop: 0, marginBottom: 'var(--space-4)' }}>
{desc}
</p>
{children}
</section>
);
}
export default function DesignPage() {
const [query, setQuery] = useState('');
const [copied, setCopied] = useState('');
// : lucide-react .
// useMemo 5,000 .
// import . App.jsx
// lazy( ) .
// lucide-react 3 : Rocket, RocketIcon, LucideRocket.
// 5,981 1,993.
// .
// : " 믿 "
// .
const allIcons = useMemo(
() => Object.keys(Lucide).filter((k) => /^[A-Z]/.test(k) && !/Icon$/.test(k) && !/^Lucide/.test(k)),
[]
);
const icons = useMemo(() => {
const q = query.trim().toLowerCase();
const list = q ? allIcons.filter((n) => n.toLowerCase().includes(q)) : allIcons;
return list.slice(0, 240); // 240.
}, [allIcons, query]);
function copy(text) {
navigator.clipboard?.writeText(text);
setCopied(text);
setTimeout(() => setCopied(''), 1200);
}
return (
<div>
<div className="hero">
<div className="eyebrow">Design System</div>
<h1>디자인 재료 모음</h1>
<p>
·글자·여백은 여기 있는 <strong>토큰 이름</strong> 쓰세요. 값을 직접 적으면 다크 모드에서 깨지고,
화면마다 미묘하게 어긋납니다. 아이콘은 아래에서 검색해 이름을 복사해 쓰면 됩니다.
</p>
<div className="chip-row">
<span className="chip">클릭하면 복사돼요</span>
<span className="chip">규약은 ICONS.md</span>
<span className="chip">아이콘 {allIcons.length.toLocaleString()}</span>
</div>
</div>
<Section title="색" desc="global.css의 :root에 정의돼 있습니다. 다크 모드에서는 같은 이름이 다른 값으로 바뀝니다.">
<div className="grid-cards" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(190px, 1fr))' }}>
{COLOR_TOKENS.map(([name, desc]) => (
<button
key={name}
type="button"
className="card card-clickable"
style={{ textAlign: 'left', cursor: 'pointer' }}
onClick={() => copy(`var(${name})`)}
>
<div
style={{
height: 44,
borderRadius: 'var(--radius-sm)',
background: `var(${name})`,
border: '1px solid var(--line)',
marginBottom: 'var(--space-2)',
}}
/>
<code style={{ fontSize: 'var(--text-sm)', fontWeight: 'var(--weight-bold)' }}>{name}</code>
<p className="muted" style={{ fontSize: 'var(--text-xs)', margin: '4px 0 0' }}>{desc}</p>
</button>
))}
</div>
</Section>
<Section title="글자 크기" desc="본문은 --text-md(15px)입니다. 13px과 13.5px이 한 화면에 섞이지 않도록 이 계단만 밟으세요.">
{TEXT_TOKENS.map((t) => (
<div key={t} style={{ display: 'flex', alignItems: 'baseline', gap: 'var(--space-4)', marginBottom: 'var(--space-2)' }}>
<code style={{ width: 120, flexShrink: 0, fontSize: 'var(--text-sm)', color: 'var(--primary)' }}>{t}</code>
<span className="muted" style={{ width: 56, flexShrink: 0, fontSize: 'var(--text-xs)' }}>{tokenValue(t)}</span>
<span style={{ fontSize: `var(${t})` }}>다람쥐 쳇바퀴에 타고파</span>
</div>
))}
</Section>
<Section title="글자 굵기" desc="variable font라 아래 굵기가 전부 파일 하나(285KB)에서 나옵니다.">
{[['--weight-regular', '본문'], ['--weight-medium', '살짝 강조'], ['--weight-bold', '제목'], ['--weight-black', '숫자·수치']].map(([t, d]) => (
<div key={t} style={{ display: 'flex', alignItems: 'baseline', gap: 'var(--space-4)', marginBottom: 'var(--space-2)' }}>
<code style={{ width: 150, flexShrink: 0, fontSize: 'var(--text-sm)', color: 'var(--primary)' }}>{t}</code>
<span className="muted" style={{ width: 40, flexShrink: 0, fontSize: 'var(--text-xs)' }}>{tokenValue(t)}</span>
<span style={{ fontSize: 'var(--text-lg)', fontWeight: `var(${t})` }}>어썸데브 학습 플랫폼 {d}</span>
</div>
))}
</Section>
<Section title="여백" desc="4의 배수 사다리입니다. 이 계단만 밟으면 화면 요소들이 저절로 정렬됩니다.">
{SPACE_TOKENS.map((t) => (
<div key={t} style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-4)', marginBottom: 6 }}>
<code style={{ width: 120, flexShrink: 0, fontSize: 'var(--text-sm)', color: 'var(--primary)' }}>{t}</code>
<span className="muted" style={{ width: 56, flexShrink: 0, fontSize: 'var(--text-xs)' }}>{tokenValue(t)}</span>
<div style={{ width: `var(${t})`, height: 20, background: 'var(--primary)', borderRadius: 3 }} />
</div>
))}
</Section>
<Section title="모서리" desc="카드는 --radius, 작은 요소는 --radius-sm, 배지·알약 모양은 --radius-full.">
<div style={{ display: 'flex', gap: 'var(--space-4)', flexWrap: 'wrap' }}>
{RADIUS_TOKENS.map((t) => (
<div key={t} style={{ textAlign: 'center' }}>
<div
style={{
width: 84, height: 56, background: 'var(--primary-soft)',
border: '1px solid var(--line)', borderRadius: `var(${t})`, marginBottom: 6,
}}
/>
<code style={{ fontSize: 'var(--text-xs)' }}>{t}</code>
</div>
))}
</div>
</Section>
<Section
title="아이콘"
desc="Lucide 아이콘입니다. 이름을 클릭하면 import 구문이 복사됩니다. 굵기는 항상 strokeWidth={1.5}로 맞추세요."
>
<input
className="input"
type="search"
placeholder="아이콘 검색 (영문) — database, shield, rocket …"
value={query}
onChange={(e) => setQuery(e.target.value)}
style={{ marginBottom: 'var(--space-4)' }}
aria-label="아이콘 검색"
/>
<p className="muted" style={{ fontSize: 'var(--text-sm)', marginTop: 0 }}>
{query ? `"${query}" 검색 결과` : '전체'} {icons.length} 표시
{icons.length === 240 && ' (앞의 240개만 — 검색으로 좁혀 보세요)'}
</p>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(112px, 1fr))',
gap: 'var(--space-2)',
}}
>
{icons.map((name) => {
const Icon = Lucide[name];
return (
<button
key={name}
type="button"
onClick={() => copy(`import { ${name} } from 'lucide-react';`)}
title={`클릭하면 import 구문 복사: ${name}`}
style={{
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6,
padding: 'var(--space-3) var(--space-2)', background: 'var(--card)',
border: '1px solid var(--line)', borderRadius: 'var(--radius-sm)',
cursor: 'pointer', color: 'var(--ink)',
}}
>
<Icon size={22} strokeWidth={1.5} aria-hidden="true" />
<span
style={{
fontSize: 11, color: 'var(--muted)', wordBreak: 'break-all',
lineHeight: 1.3, textAlign: 'center',
}}
>
{name}
</span>
</button>
);
})}
</div>
{icons.length === 0 && <p className="empty">그런 이름의 아이콘이 없어요. 영문으로 검색해 보세요.</p>}
</Section>
{/* 복사 완료 알림 — 화면 아래에 잠깐 떴다 사라진다 */}
{copied && (
<div
role="status"
style={{
position: 'fixed', bottom: 24, left: '50%', transform: 'translateX(-50%)',
background: 'var(--ink)', color: 'var(--bg)', padding: '10px 18px',
borderRadius: 'var(--radius-full)', fontSize: 'var(--text-sm)',
boxShadow: 'var(--shadow)', zIndex: 50, maxWidth: '90vw', overflow: 'hidden',
textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}
>
복사됨: {copied}
</div>
)}
</div>
);
}

View File

@ -141,7 +141,11 @@ export default function LearnHubPage() {
marginBottom: 8, marginBottom: 8,
}} }}
> >
<span style={{ fontSize: 26 }}>{course.icon}</span> {/* : Icon "" "".
그래서 {course.Icon}으로 찍는 아니라 <course.Icon />으로 부른다.
JSX는 대문자로 시작하는 이름만 컴포넌트로 취급한다(소문자는 <div> 같은 HTML 태그).
크기·굵기를 prop으로 지시할 있는 이모지와의 결정적 차이다. */}
<course.Icon size={26} strokeWidth={1.5} aria-hidden="true" />
<span <span
style={{ style={{
fontSize: 13, fontSize: 13,

View File

@ -1,7 +1,30 @@
/* 파일이 하는 : AWESOMEDEV 브랜드 ·폰트를 CSS 변수(토큰) 정의하고, /* 파일이 하는 : AWESOMEDEV 브랜드 ·폰트를 CSS 변수(토큰) 정의하고,
버튼/카드/입력창 같은 공통 클래스를 제공한다. 버튼/카드/입력창 같은 공통 클래스를 제공한다.
학습 포인트: 색을 컴포넌트마다 하드코딩하지 않고 :root 변수 하나로 모아두면 학습 포인트: 색을 컴포넌트마다 하드코딩하지 않고 :root 변수 하나로 모아두면
다크 모드 전환이 @media 블록 하나로 끝난다. */ 다크 모드 전환이 @media 블록 하나로 끝난다.
토큰 목록을 눈으로 보려면 앱에서 /design 페이지를 열어 보자. */
/* 웹폰트: Pretendard
실제로 있었던 문제: 예전엔 body에 font-family로 "Pretendard" 적어만 두고
폰트 파일은 어디서도 받지 않았다. 그래서 글씨체가 깔린 사람 외에는 전원
맑은 고딕으로 보고 있었다 "이름을 부른다고 폰트가 오지 않는다".
웹폰트는 반드시 @font-face로 파일 위치를 알려 줘야 한다.
학습 포인트 self-host: CDN 링크 대신 파일을 우리 서버(public/fonts) 둔다.
외부 서비스가 죽어도, 인터넷이 없어도 글씨가 깨지지 않는다.
학습 포인트 variable font: 굵기별로 파일을 9개 두는 대신 파일 하나가
45~920 사이 모든 굵기를 담는다. 요청 수와 용량이 함께 준다.
학습 포인트 font-display: swap: 폰트를 받는 동안 글자를 숨기지 않고
기본 글씨체로 먼저 보여 바꿔 끼운다. 느린 네트워크에서 "글자 없는
하얀 화면"(FOIT) 피하는 표준 처방이다.
Std (상용 한글 2,350자)이라 285KB다. 전체 한글 11,172자 판은 2MB다. */
@font-face {
font-family: "Pretendard";
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
font-weight: 45 920;
font-style: normal;
font-display: swap;
}
:root { :root {
--bg: #FAFBFD; --bg: #FAFBFD;
@ -22,6 +45,40 @@
--shadow: 0 1px 3px rgba(16, 26, 40, 0.06), 0 4px 14px rgba(16, 26, 40, 0.05); --shadow: 0 1px 3px rgba(16, 26, 40, 0.06), 0 4px 14px rgba(16, 26, 40, 0.05);
--code-bg: #F1F4F9; --code-bg: #F1F4F9;
--ink-faint: #7B8798; --ink-faint: #7B8798;
/* 여백(spacing)
학습 포인트: 여백을 그때그때 10px, 14px, 6px로 찍으면 화면이 미묘하게 어긋난다.
4의 배수로 사다리를 정해 두고 계단만 밟으면 저절로 정렬이 맞는다.
이름은 크기가 아니라 "단계" 짓는다 나중에 값을 조정해도 이름은 그대로다. */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 24px;
--space-6: 32px;
--space-7: 48px;
/* 글자 크기(type scale)
학습 포인트: 크기를 자유롭게 쓰면 13px과 13.5px이 화면에 섞인다.
사람 눈은 차이를 "다름" 아니라 "실수" 읽는다. 계단을 정해 두자. */
--text-xs: 12px;
--text-sm: 13px;
--text-md: 15px; /* 본문 기본 */
--text-lg: 18px;
--text-xl: 22px;
--text-2xl: 28px;
/* 글자 굵기
variable font라 아래 값들이 전부 파일 하나에서 나온다. */
--weight-regular: 400;
--weight-medium: 500;
--weight-bold: 700;
--weight-black: 800;
/* ── 모서리 ── (--radius는 위에 이미 있다. 더 작은/큰 짝) */
--radius-sm: 8px;
--radius-lg: 20px;
--radius-full: 999px;
} }
/* 학습 포인트: 사용자의 OS가 다크 모드면 블록이 변수 값만 바꿔치기한다. /* 학습 포인트: 사용자의 OS가 다크 모드면 블록이 변수 값만 바꿔치기한다.