feat(design): 디자인 시스템 — Pretendard 실적용·토큰·Lucide 아이콘 132개·/design
이모지 아이콘을 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:
parent
1b559f5da2
commit
5c9959ce08
1
.gitignore
vendored
1
.gitignore
vendored
@ -15,5 +15,6 @@ deploy/.env
|
||||
# IDE·OS
|
||||
.idea/
|
||||
.vscode/
|
||||
.claude/
|
||||
*.iml
|
||||
.DS_Store
|
||||
|
||||
113
DESIGN.md
Normal file
113
DESIGN.md
Normal 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
219
ICONS.md
Normal 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 기준)
|
||||
@ -4,6 +4,8 @@
|
||||
> 미림마이스터고 수습생 4명이 직접 보고, 배우고, 고치는 학습용 웹 앱입니다.
|
||||
> 처음에는 문서를 읽는 곳이지만, 5주차부터는 여러분이 직접 기능을 붙이고 버그를 고치는 실전 연습장이 됩니다.
|
||||
> 코드를 읽는 법이 궁금하면 [STUDY.md](./STUDY.md)부터 열어 보세요.
|
||||
> 화면을 만들 일이 생기면 [DESIGN.md](./DESIGN.md)(색·글자·여백·폰트)와 [ICONS.md](./ICONS.md)(아이콘)를 먼저 보세요.
|
||||
> 앱에서 `/design`을 열면 재료를 눈으로 보고 클릭해서 복사할 수 있습니다.
|
||||
|
||||
## 무엇을 하는 앱인가?
|
||||
|
||||
|
||||
10
frontend/package-lock.json
generated
10
frontend/package-lock.json
generated
@ -9,6 +9,7 @@
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"axios": "^1.7.9",
|
||||
"lucide-react": "^1.25.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.1.0",
|
||||
@ -3405,6 +3406,15 @@
|
||||
"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": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.9",
|
||||
"lucide-react": "^1.25.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.1.0",
|
||||
|
||||
@ -5,6 +5,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>캡스톤 & 데모데이 · AWESOMEDEV</title>
|
||||
<style>
|
||||
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
|
||||
@font-face {
|
||||
font-family: "Pretendard";
|
||||
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
|
||||
font-weight: 45 920;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
|
||||
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;
|
||||
|
||||
@ -5,6 +5,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>코드리뷰 실습 가이드 · AWESOMEDEV</title>
|
||||
<style>
|
||||
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
|
||||
@font-face {
|
||||
font-family: "Pretendard";
|
||||
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
|
||||
font-weight: 45 920;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
|
||||
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;
|
||||
|
||||
@ -5,6 +5,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>CS 기본기 4주 커리큘럼</title>
|
||||
<style>
|
||||
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
|
||||
@font-face {
|
||||
font-family: "Pretendard";
|
||||
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
|
||||
font-weight: 45 920;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
|
||||
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;
|
||||
|
||||
@ -5,6 +5,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>어썸데브 수습 평가 루브릭</title>
|
||||
<style>
|
||||
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
|
||||
@font-face {
|
||||
font-family: "Pretendard";
|
||||
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
|
||||
font-weight: 45 920;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
|
||||
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;
|
||||
|
||||
@ -5,6 +5,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>AWESOMEDEV 수습 가이드</title>
|
||||
<style>
|
||||
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
|
||||
@font-face {
|
||||
font-family: "Pretendard";
|
||||
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
|
||||
font-weight: 45 920;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
|
||||
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;
|
||||
|
||||
@ -5,6 +5,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>수습용 실전 티켓 풀 34 · AWESOMEDEV</title>
|
||||
<style>
|
||||
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
|
||||
@font-face {
|
||||
font-family: "Pretendard";
|
||||
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
|
||||
font-weight: 45 920;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
|
||||
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;
|
||||
|
||||
@ -5,6 +5,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>AWESOMEDEV 1주차 일일 과제집 — 온보딩 & 예절 적용</title>
|
||||
<style>
|
||||
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
|
||||
@font-face {
|
||||
font-family: "Pretendard";
|
||||
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
|
||||
font-weight: 45 920;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
|
||||
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;
|
||||
|
||||
@ -5,6 +5,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>1주차 상세 강의안 · 온보딩 & 예절</title>
|
||||
<style>
|
||||
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
|
||||
@font-face {
|
||||
font-family: "Pretendard";
|
||||
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
|
||||
font-weight: 45 920;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
|
||||
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;
|
||||
|
||||
@ -5,6 +5,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>어썸데브 수습 교육 · 2주차 일일 과제집</title>
|
||||
<style>
|
||||
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
|
||||
@font-face {
|
||||
font-family: "Pretendard";
|
||||
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
|
||||
font-weight: 45 920;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
|
||||
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;
|
||||
|
||||
@ -5,6 +5,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>2주차 상세 강의안 — 기초 다지기 · AWESOMEDEV 수습 교육</title>
|
||||
<style>
|
||||
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
|
||||
@font-face {
|
||||
font-family: "Pretendard";
|
||||
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
|
||||
font-weight: 45 920;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
|
||||
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;
|
||||
|
||||
@ -5,6 +5,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>AWESOMEDEV 3주차 일일 과제집 — 첫 소과제 실전</title>
|
||||
<style>
|
||||
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
|
||||
@font-face {
|
||||
font-family: "Pretendard";
|
||||
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
|
||||
font-weight: 45 920;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
|
||||
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;
|
||||
|
||||
@ -5,6 +5,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>AWESOMEDEV 수습 상세 강의안 · 3주차</title>
|
||||
<style>
|
||||
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
|
||||
@font-face {
|
||||
font-family: "Pretendard";
|
||||
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
|
||||
font-weight: 45 920;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
|
||||
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;
|
||||
|
||||
@ -5,6 +5,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>AWESOMEDEV 4주차 일일 과제집 — 통합·발표 준비</title>
|
||||
<style>
|
||||
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
|
||||
@font-face {
|
||||
font-family: "Pretendard";
|
||||
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
|
||||
font-weight: 45 920;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
|
||||
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;
|
||||
|
||||
@ -5,6 +5,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>4주차 상세 강의안 · 기본기 마무리 · 중간평가 — AWESOMEDEV 수습 교육</title>
|
||||
<style>
|
||||
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
|
||||
@font-face {
|
||||
font-family: "Pretendard";
|
||||
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
|
||||
font-weight: 45 920;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
|
||||
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;
|
||||
|
||||
@ -5,6 +5,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>5주차 상세 강의안 · 실전 티켓 ① · AWESOMEDEV 수습</title>
|
||||
<style>
|
||||
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
|
||||
@font-face {
|
||||
font-family: "Pretendard";
|
||||
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
|
||||
font-weight: 45 920;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
|
||||
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;
|
||||
|
||||
@ -5,6 +5,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>6주차 상세 강의안 — 실전 티켓 ② · AWESOMEDEV 수습</title>
|
||||
<style>
|
||||
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
|
||||
@font-face {
|
||||
font-family: "Pretendard";
|
||||
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
|
||||
font-weight: 45 920;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
|
||||
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;
|
||||
|
||||
@ -5,6 +5,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>7주차 상세 강의안 — 종합 프로젝트 기획·설계 (멘토용)</title>
|
||||
<style>
|
||||
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
|
||||
@font-face {
|
||||
font-family: "Pretendard";
|
||||
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
|
||||
font-weight: 45 920;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
|
||||
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;
|
||||
|
||||
@ -5,6 +5,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>8주차 상세 강의안 · 구현 · 발표 · 최종평가 — AWESOMEDEV 수습 교육</title>
|
||||
<style>
|
||||
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
|
||||
@font-face {
|
||||
font-family: "Pretendard";
|
||||
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
|
||||
font-weight: 45 920;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
|
||||
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;
|
||||
|
||||
@ -5,6 +5,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>어썸데브 수습 8주 상세 교육안</title>
|
||||
<style>
|
||||
/* 웹폰트: 앱과 같은 Pretendard를 문서에도 적용한다(선언만 있고 파일이 없던 문제 수정). */
|
||||
@font-face {
|
||||
font-family: "Pretendard";
|
||||
src: url("/fonts/PretendardStdVariable.woff2") format("woff2-variations");
|
||||
font-weight: 45 920;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--paper: #FAFBFD; --card: #FFFFFF; --ink: #101A28; --ink-soft: #46536A;
|
||||
--ink-faint: #7B8798; --line: #E4E8F0; --line-soft: #EFF2F7;
|
||||
|
||||
BIN
frontend/public/fonts/PretendardStdVariable.woff2
Normal file
BIN
frontend/public/fonts/PretendardStdVariable.woff2
Normal file
Binary file not shown.
@ -3,8 +3,9 @@
|
||||
//
|
||||
// 학습 포인트: 코스 라우트는 여기 일일이 쓰지 않는다 — 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 { CircleUser, KeyRound } from 'lucide-react';
|
||||
import { useAuth } from './AuthContext';
|
||||
import { ALL_COURSES } from './courseCatalog';
|
||||
import { LEVEL_COURSES } from './levelCatalog';
|
||||
@ -29,6 +30,11 @@ import CodingPage from './pages/CodingPage';
|
||||
import CodingProblemPage from './pages/CodingProblemPage';
|
||||
import MentorPage from './pages/MentorPage';
|
||||
|
||||
// 디자인 재료 모음(/design)은 Lucide 아이콘 전체를 훑어야 해서 무겁다.
|
||||
// 학습 포인트: 이런 화면은 lazy로 분리해 "그 페이지에 들어갈 때만" 내려받게 한다.
|
||||
// 코스 페이지들과 똑같은 처방이다 — 첫 화면 속도는 모두가 치르는 비용이니까.
|
||||
const DesignPage = lazy(() => import('./pages/DesignPage'));
|
||||
|
||||
// 학습 포인트: "로그인 필요" 가드 컴포넌트.
|
||||
// Outlet 자리에 자식 라우트가 렌더링되며, 비로그인이면 /login으로 돌려보낸다.
|
||||
function RequireAuth() {
|
||||
@ -133,16 +139,18 @@ function Layout() {
|
||||
<span className="topnav-name">{user?.name}</span>
|
||||
{user?.role === 'MENTOR' && <span className="badge badge-primary">멘토</span>}
|
||||
{/* 내 정보 — 학생만. (멘토는 인적사항이 없어서 숨긴다)
|
||||
⚠️ a11y: 내용이 이모지뿐이면 스크린리더가 "bust in silhouette 링크"로 읽는다.
|
||||
title은 접근 이름으로 안 쓰이므로 aria-label로 목적을 알리고, 이모지는 숨긴다. */}
|
||||
⚠️ a11y: 내용이 그림뿐이면 스크린리더가 읽을 게 없다. 예전엔 이모지 👤를 썼는데
|
||||
리더가 "bust in silhouette 링크"라고 읽어 버렸다. 지금은 Lucide 아이콘(SVG)이고,
|
||||
SVG도 사정은 같다 — 그림은 aria-hidden으로 숨기고 목적은 aria-label로 말한다.
|
||||
title 속성은 접근 이름으로 신뢰할 수 없으니 aria-label을 쓴다. */}
|
||||
{user?.role === 'STUDENT' && (
|
||||
<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 to="/change-password" className="btn btn-ghost" aria-label="비밀번호 변경">
|
||||
<span aria-hidden="true">🔑</span>
|
||||
<KeyRound size={18} strokeWidth={1.5} aria-hidden="true" />
|
||||
</NavLink>
|
||||
<button className="btn btn-ghost" onClick={logout} title="로그아웃">
|
||||
로그아웃
|
||||
@ -205,6 +213,8 @@ export default function App() {
|
||||
<Route path="/profile" element={<MyProfilePage />} />
|
||||
<Route path="/certificate" element={<CertificatePage />} />
|
||||
<Route path="/glossary" element={<GlossaryPage />} />
|
||||
{/* 디자인 재료 모음 — 색·글자·여백 토큰과 아이콘을 고르는 곳. */}
|
||||
<Route path="/design" element={<DesignPage />} />
|
||||
<Route path="/docs" element={<DocsPage />} />
|
||||
<Route path="/docs/:slug" element={<DocViewerPage />} />
|
||||
<Route path="/assignments" element={<AssignmentsPage />} />
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
// 완료 바를 갖는다 — "공통 기능은 감싸는 컴포넌트로"가 React의 대표 패턴이다.
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ClipboardCheck } from 'lucide-react';
|
||||
import client from '../api/client';
|
||||
|
||||
export default function CourseShell({ course }) {
|
||||
@ -54,11 +55,11 @@ export default function CourseShell({ course }) {
|
||||
← 학습 센터
|
||||
</Link>
|
||||
<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>}
|
||||
</span>
|
||||
<Link to={`/learn/${course.slug}/quiz`} className="btn">
|
||||
📝 퀴즈 풀기
|
||||
<ClipboardCheck size={16} strokeWidth={1.5} aria-hidden="true" /> 퀴즈 풀기
|
||||
</Link>
|
||||
<button
|
||||
className={completed ? 'btn' : 'btn btn-primary'}
|
||||
|
||||
@ -8,6 +8,93 @@
|
||||
// 빌드 결과(dist/assets)를 보면 코스마다 파일이 쪼개져(chunk) 있는 걸 확인할 수 있다.
|
||||
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/) ──
|
||||
const HardwarePage = lazy(() => import('./pages/HardwarePage'));
|
||||
const AssemblyPage = lazy(() => import('./pages/AssemblyPage'));
|
||||
@ -102,126 +189,126 @@ export const COURSE_CATEGORIES = [
|
||||
category: '컴퓨터 기초',
|
||||
desc: '소프트웨어 이전에, 기계 그 자체를 이해해요.',
|
||||
items: [
|
||||
{ slug: 'history', icon: '📜', title: '컴퓨터의 역사', desc: '에니악에서 AI까지 — 우리가 어디쯤 서 있는지부터.', level: '교양', Component: HistoryPage },
|
||||
{ slug: 'hardware', icon: '💻', title: '컴퓨터의 구성', desc: 'CPU·RAM·스토리지·메인보드 — 부품의 역할과 스펙 읽기.', level: '입문', Component: HardwarePage },
|
||||
{ slug: 'binary', icon: '🔢', title: '2진수와 데이터 표현', desc: '문자·색·이미지가 전부 숫자인 이유.', level: '입문', Component: BinaryPage },
|
||||
{ slug: 'cpu-deep', icon: '🧠', title: 'CPU 깊이 보기', desc: '명령어 사이클·코어·캐시·발열 — 두뇌의 작동 원리.', level: '심화', Component: CpuDeepPage },
|
||||
{ slug: 'memory-storage', icon: '💾', title: '메모리와 저장장치 깊이 보기', desc: 'RAM·SSD의 내부, 스왑과 느려짐의 연쇄.', level: '심화', Component: MemoryStoragePage },
|
||||
{ slug: 'gpu', icon: '🎮', title: '그래픽카드와 GPU의 세계', desc: '화면 그리기 전담 — AI 시대에 GPU가 금값인 이유.', level: '심화', Component: GpuPage },
|
||||
{ slug: 'mainboard-power', icon: '🔋', title: '메인보드와 파워 깊이 보기', desc: '모든 부품이 만나는 도로망, 그리고 전원의 심장.', level: '심화', Component: MainboardPowerPage },
|
||||
{ slug: 'assembly', icon: '🔧', title: '데스크탑 조립하기', desc: '배운 부품들을 내 손으로 — 9단계 조립 순서.', level: '실습', Component: AssemblyPage },
|
||||
{ slug: 'os', icon: '🪟', title: '운영체제의 이해', desc: '윈도우·맥·리눅스가 하는 일 — 자원 관리부터 멀티태스킹까지.', level: '입문', Component: OsPage },
|
||||
{ slug: 'files', icon: '📁', title: '파일과 폴더의 원리', desc: '확장자의 진실, 경로, 휴지통과 완전삭제.', level: '입문', Component: FilesPage },
|
||||
{ slug: 'peripherals', icon: '🖥️', title: '노트북과 주변기기 고르기', desc: '노트북·모니터·키보드 스펙 읽기 — 개발자 관점.', level: '입문', Component: PeripheralsPage },
|
||||
{ slug: 'troubleshooting', icon: '🩺', title: '컴퓨터 문제 해결', desc: '느려질 때 점검 순서, 백업 3-2-1, 검색의 기술.', level: '실습', Component: TroubleshootingPage },
|
||||
{ slug: 'history', Icon: ScrollText, title: '컴퓨터의 역사', desc: '에니악에서 AI까지 — 우리가 어디쯤 서 있는지부터.', level: '교양', Component: HistoryPage },
|
||||
{ slug: 'hardware', Icon: Computer, title: '컴퓨터의 구성', desc: 'CPU·RAM·스토리지·메인보드 — 부품의 역할과 스펙 읽기.', level: '입문', Component: HardwarePage },
|
||||
{ slug: 'binary', Icon: Binary, title: '2진수와 데이터 표현', desc: '문자·색·이미지가 전부 숫자인 이유.', level: '입문', Component: BinaryPage },
|
||||
{ slug: 'cpu-deep', Icon: Cpu, title: 'CPU 깊이 보기', desc: '명령어 사이클·코어·캐시·발열 — 두뇌의 작동 원리.', level: '심화', Component: CpuDeepPage },
|
||||
{ slug: 'memory-storage', Icon: HardDrive, title: '메모리와 저장장치 깊이 보기', desc: 'RAM·SSD의 내부, 스왑과 느려짐의 연쇄.', level: '심화', Component: MemoryStoragePage },
|
||||
{ slug: 'gpu', Icon: MemoryStick, title: '그래픽카드와 GPU의 세계', desc: '화면 그리기 전담 — AI 시대에 GPU가 금값인 이유.', level: '심화', Component: GpuPage },
|
||||
{ slug: 'mainboard-power', Icon: CircuitBoard, title: '메인보드와 파워 깊이 보기', desc: '모든 부품이 만나는 도로망, 그리고 전원의 심장.', level: '심화', Component: MainboardPowerPage },
|
||||
{ slug: 'assembly', Icon: Wrench, title: '데스크탑 조립하기', desc: '배운 부품들을 내 손으로 — 9단계 조립 순서.', level: '실습', Component: AssemblyPage },
|
||||
{ slug: 'os', Icon: AppWindow, title: '운영체제의 이해', desc: '윈도우·맥·리눅스가 하는 일 — 자원 관리부터 멀티태스킹까지.', level: '입문', Component: OsPage },
|
||||
{ slug: 'files', Icon: Folder, title: '파일과 폴더의 원리', desc: '확장자의 진실, 경로, 휴지통과 완전삭제.', level: '입문', Component: FilesPage },
|
||||
{ slug: 'peripherals', Icon: Laptop, title: '노트북과 주변기기 고르기', desc: '노트북·모니터·키보드 스펙 읽기 — 개발자 관점.', level: '입문', Component: PeripheralsPage },
|
||||
{ slug: 'troubleshooting', Icon: Stethoscope, title: '컴퓨터 문제 해결', desc: '느려질 때 점검 순서, 백업 3-2-1, 검색의 기술.', level: '실습', Component: TroubleshootingPage },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: '네트워크',
|
||||
desc: '랜선 한 가닥에서 클라우드까지.',
|
||||
items: [
|
||||
{ slug: 'network', icon: '🌐', title: '네트워크의 이해', desc: '랜선(UTP) 구조 → 인터넷의 구조 → DNS·TCP/IP 한 흐름.', level: '입문', Component: NetworkPage },
|
||||
{ slug: 'bandwidth', icon: '🚗', title: '네트워크 속도와 대역폭', desc: 'Mbps의 진실 — 다운로드가 생각보다 느린 이유.', level: '입문', Component: BandwidthPage },
|
||||
{ slug: 'ip-deep', icon: '🔍', title: 'IP 주소 깊이 보기', desc: '사설 대역·서브넷·DHCP·IPv6 — 주소 체계의 모든 것.', level: '심화', Component: IpDeepPage },
|
||||
{ slug: 'home-network', icon: '📡', title: '공유기와 홈 네트워크 실전', desc: '관리자 페이지, WiFi 채널, 포트포워딩까지.', level: '실습', Component: HomeNetworkPage },
|
||||
{ slug: 'dns-deep', icon: '📖', title: 'DNS 깊이 보기', desc: '재귀 조회의 여행, 레코드 타입, TTL과 전파.', level: '심화', Component: DnsDeepPage },
|
||||
{ slug: 'tcp-udp', icon: '🤝', title: 'TCP와 UDP', desc: '3-way 핸드셰이크, 패킷, 왜 게임은 UDP인가.', level: '심화', Component: TcpUdpPage },
|
||||
{ slug: 'http', icon: '📨', title: 'HTTP와 HTTPS', desc: '메서드·상태코드·쿠키(우리 로그인!)·TLS 인증서.', level: '입문', Component: HttpPage },
|
||||
{ slug: 'net-tools', icon: '🧰', title: '네트워크 명령어 실전', desc: 'ping·tracert·nslookup·curl — 진단의 무기들.', level: '실습', Component: NetToolsPage },
|
||||
{ slug: 'wifi-security', icon: '🔐', title: '무선 네트워크와 보안', desc: '공용 와이파이의 위험, 피싱 구별, 2단계 인증.', level: '입문', Component: WifiSecurityPage },
|
||||
{ slug: 'vpn-proxy', icon: '🚇', title: 'VPN과 프록시', desc: '암호화 터널과 대리인 — 우리 Caddy가 실물 교재.', level: '심화', Component: VpnProxyPage },
|
||||
{ slug: 'firewall', icon: '🛡️', title: '방화벽과 보안 기초', desc: '포트의 여닫음, 우리 AWS 보안그룹 실례, 최소 권한.', level: '입문', Component: FirewallPage },
|
||||
{ slug: 'cloud-network', icon: '☁️', title: '클라우드 네트워크 입문', desc: '리전·VPC·탄력적 IP — 우리 플랫폼의 실제 구성도.', level: '심화', Component: CloudNetworkPage },
|
||||
{ slug: 'network', Icon: Globe, title: '네트워크의 이해', desc: '랜선(UTP) 구조 → 인터넷의 구조 → DNS·TCP/IP 한 흐름.', level: '입문', Component: NetworkPage },
|
||||
{ slug: 'bandwidth', Icon: Gauge, title: '네트워크 속도와 대역폭', desc: 'Mbps의 진실 — 다운로드가 생각보다 느린 이유.', level: '입문', Component: BandwidthPage },
|
||||
{ slug: 'ip-deep', Icon: MapPin, title: 'IP 주소 깊이 보기', desc: '사설 대역·서브넷·DHCP·IPv6 — 주소 체계의 모든 것.', level: '심화', Component: IpDeepPage },
|
||||
{ slug: 'home-network', Icon: Router, title: '공유기와 홈 네트워크 실전', desc: '관리자 페이지, WiFi 채널, 포트포워딩까지.', level: '실습', Component: HomeNetworkPage },
|
||||
{ slug: 'dns-deep', Icon: BookMarked, title: 'DNS 깊이 보기', desc: '재귀 조회의 여행, 레코드 타입, TTL과 전파.', level: '심화', Component: DnsDeepPage },
|
||||
{ slug: 'tcp-udp', Icon: Handshake, title: 'TCP와 UDP', desc: '3-way 핸드셰이크, 패킷, 왜 게임은 UDP인가.', level: '심화', Component: TcpUdpPage },
|
||||
{ slug: 'http', Icon: ArrowRightLeft, title: 'HTTP와 HTTPS', desc: '메서드·상태코드·쿠키(우리 로그인!)·TLS 인증서.', level: '입문', Component: HttpPage },
|
||||
{ slug: 'net-tools', Icon: SquareTerminal, title: '네트워크 명령어 실전', desc: 'ping·tracert·nslookup·curl — 진단의 무기들.', level: '실습', Component: NetToolsPage },
|
||||
{ slug: 'wifi-security', Icon: Wifi, title: '무선 네트워크와 보안', desc: '공용 와이파이의 위험, 피싱 구별, 2단계 인증.', level: '입문', Component: WifiSecurityPage },
|
||||
{ slug: 'vpn-proxy', Icon: Route, title: 'VPN과 프록시', desc: '암호화 터널과 대리인 — 우리 Caddy가 실물 교재.', level: '심화', Component: VpnProxyPage },
|
||||
{ slug: 'firewall', Icon: BrickWall, title: '방화벽과 보안 기초', desc: '포트의 여닫음, 우리 AWS 보안그룹 실례, 최소 권한.', level: '입문', Component: FirewallPage },
|
||||
{ slug: 'cloud-network', Icon: Cloud, title: '클라우드 네트워크 입문', desc: '리전·VPC·탄력적 IP — 우리 플랫폼의 실제 구성도.', level: '심화', Component: CloudNetworkPage },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: '서버와 데이터',
|
||||
desc: '백엔드 개발자가 매일 쓰는 도구들.',
|
||||
items: [
|
||||
{ slug: 'server-anatomy', icon: '🏗️', title: '서버란 무엇인가', desc: '웹서버·WAS·DB 3계층 — 우리 구성 그대로 해부.', level: '입문', Component: ServerAnatomyPage },
|
||||
{ slug: 'linux', icon: '🐧', title: '리눅스 / 터미널 기초', desc: '터미널 명령어부터 SSH 서버 접속까지.', level: '입문', Component: LinuxPage },
|
||||
{ slug: 'linux-advanced', icon: '⚙️', title: '리눅스 심화', desc: '셸 스크립트, 크론, 파이프 — 자동화의 시작.', level: '심화', Component: LinuxAdvancedPage },
|
||||
{ slug: 'docker-intro', icon: '🐳', title: '도커 입문', desc: '컨테이너 개념과 우리 docker-compose.yml 한 줄씩 해부.', level: '입문', Component: DockerIntroPage },
|
||||
{ slug: 'aws-intro', icon: '🌩️', title: '클라우드와 AWS 입문', desc: 'EC2·요금·리전 — 우리 인프라가 교재.', level: '입문', Component: AwsIntroPage },
|
||||
{ slug: 'sql', icon: '🗄️', title: 'SQL 입문', desc: '우리 플랫폼의 진짜 테이블로 SELECT부터 JOIN까지.', level: '입문', Component: SqlPage },
|
||||
{ slug: 'sql-intermediate', icon: '📊', title: 'SQL 중급', desc: 'GROUP BY·집계·인덱스 — 데이터에서 답을 꺼내기.', level: '심화', Component: SqlIntermediatePage },
|
||||
{ slug: 'data-modeling', icon: '🗂️', title: '데이터 모델링 기초', desc: 'ERD·키·정규화 — 테이블 설계는 정리정돈.', level: '심화', Component: DataModelingPage },
|
||||
{ slug: 'cache-nosql', icon: '⚡', title: '캐시와 NoSQL 맛보기', desc: '자주 쓰는 건 책상 위에 — Redis가 빠른 이유.', level: '심화', Component: CacheNosqlPage },
|
||||
{ slug: 'api', icon: '🔌', title: 'API의 이해', desc: 'REST·JSON — 이미 다 써본 우리 API로 배우기.', level: '입문', Component: ApiPage },
|
||||
{ slug: 'cicd', icon: '🔁', title: 'CI/CD와 배포 자동화 입문', desc: '푸시하면 알아서 검사·배포 — 파이프라인의 세계.', level: '심화', Component: CicdPage },
|
||||
{ slug: 'ops-basics', icon: '🚨', title: '운영과 장애 대응 기초', desc: '장애의 신호, 로그 읽기, 재시작의 정석.', level: '실습', Component: OpsBasicsPage },
|
||||
{ slug: 'server-anatomy', Icon: Server, title: '서버란 무엇인가', desc: '웹서버·WAS·DB 3계층 — 우리 구성 그대로 해부.', level: '입문', Component: ServerAnatomyPage },
|
||||
{ slug: 'linux', Icon: Terminal, title: '리눅스 / 터미널 기초', desc: '터미널 명령어부터 SSH 서버 접속까지.', level: '입문', Component: LinuxPage },
|
||||
{ slug: 'linux-advanced', Icon: Settings2, title: '리눅스 심화', desc: '셸 스크립트, 크론, 파이프 — 자동화의 시작.', level: '심화', Component: LinuxAdvancedPage },
|
||||
{ slug: 'docker-intro', Icon: Container, title: '도커 입문', desc: '컨테이너 개념과 우리 docker-compose.yml 한 줄씩 해부.', level: '입문', Component: DockerIntroPage },
|
||||
{ slug: 'aws-intro', Icon: CloudLightning, title: '클라우드와 AWS 입문', desc: 'EC2·요금·리전 — 우리 인프라가 교재.', level: '입문', Component: AwsIntroPage },
|
||||
{ slug: 'sql', Icon: Database, title: 'SQL 입문', desc: '우리 플랫폼의 진짜 테이블로 SELECT부터 JOIN까지.', level: '입문', Component: SqlPage },
|
||||
{ slug: 'sql-intermediate', Icon: ChartColumn, title: 'SQL 중급', desc: 'GROUP BY·집계·인덱스 — 데이터에서 답을 꺼내기.', level: '심화', Component: SqlIntermediatePage },
|
||||
{ slug: 'data-modeling', Icon: Table, title: '데이터 모델링 기초', desc: 'ERD·키·정규화 — 테이블 설계는 정리정돈.', level: '심화', Component: DataModelingPage },
|
||||
{ slug: 'cache-nosql', Icon: Zap, title: '캐시와 NoSQL 맛보기', desc: '자주 쓰는 건 책상 위에 — Redis가 빠른 이유.', level: '심화', Component: CacheNosqlPage },
|
||||
{ slug: 'api', Icon: Plug, title: 'API의 이해', desc: 'REST·JSON — 이미 다 써본 우리 API로 배우기.', level: '입문', Component: ApiPage },
|
||||
{ slug: 'cicd', Icon: RotateCw, title: 'CI/CD와 배포 자동화 입문', desc: '푸시하면 알아서 검사·배포 — 파이프라인의 세계.', level: '심화', Component: CicdPage },
|
||||
{ slug: 'ops-basics', Icon: Siren, title: '운영과 장애 대응 기초', desc: '장애의 신호, 로그 읽기, 재시작의 정석.', level: '실습', Component: OpsBasicsPage },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: '소프트웨어',
|
||||
desc: '코드를 읽고, 쓰고, 함께 다듬는 법.',
|
||||
items: [
|
||||
{ slug: 'coding', icon: '⌨️', title: '코딩 기초', desc: '변수·함수·조건·반복을 이 플랫폼의 실제 코드로.', level: '입문', Component: CodingBasicsPage },
|
||||
{ slug: 'htmlcss', icon: '🧱', title: 'HTML / CSS 기초', desc: '웹의 뼈대와 옷 — 박스 모델·플렉스를 우리 스타일로.', level: '입문', Component: HtmlCssPage },
|
||||
{ slug: 'javascript', icon: '⚡', title: '자바스크립트 기초', desc: 'F12 콘솔이 실습장 — 변수에서 fetch까지.', level: '입문', Component: JavascriptPage },
|
||||
{ slug: 'data-structures', icon: '🧺', title: '자료구조 맛보기', desc: '배열·스택·큐·해시 — 그릇마다 쓰임이 다르다.', level: '입문', Component: DataStructuresPage },
|
||||
{ slug: 'react-intro', icon: '⚛️', title: 'React 입문', desc: '컴포넌트·props·state — 우리 프론트가 교재.', level: '입문', Component: ReactIntroPage },
|
||||
{ slug: 'java-basics', icon: '☕', title: 'Java 기초', desc: '타입·클래스·객체 — Spring으로 가는 첫걸음.', level: '입문', Component: JavaBasicsPage },
|
||||
{ slug: 'spring-intro', icon: '🍃', title: 'Spring Boot 입문', desc: 'DI·어노테이션·계층 구조 — 우리 백엔드 여행.', level: '심화', Component: SpringIntroPage },
|
||||
{ slug: 'git-deep', icon: '🌳', title: 'Git 깊이 보기', desc: '브랜치·충돌 해결·되돌리기 3형제·PR 매너.', level: '심화', Component: GitDeepPage },
|
||||
{ slug: 'debugging', icon: '🐞', title: '디버깅과 에러 읽는 법', desc: '스택트레이스·브레이크포인트 — 에러는 단서다.', level: '실습', Component: DebuggingPage },
|
||||
{ slug: 'testing', icon: '🧪', title: '테스트 코드 입문', desc: '미래의 나를 지키는 보험 — 우리 테스트가 교재.', level: '심화', Component: TestingPage },
|
||||
{ slug: 'clean-code', icon: '✨', title: '클린 코드와 코드리뷰', desc: '이름 짓기·작은 함수·리뷰 매너.', level: '심화', Component: CleanCodePage },
|
||||
{ slug: 'ai-tools', icon: '🤖', title: 'AI 도구와 함께 코딩하기', desc: '잘 시키고, 검증하고, 배움에 쓰는 법 — 보안 수칙까지.', level: '실습', Component: AiToolsPage },
|
||||
{ slug: 'coding', Icon: Keyboard, title: '코딩 기초', desc: '변수·함수·조건·반복을 이 플랫폼의 실제 코드로.', level: '입문', Component: CodingBasicsPage },
|
||||
{ slug: 'htmlcss', Icon: FileCode, title: 'HTML / CSS 기초', desc: '웹의 뼈대와 옷 — 박스 모델·플렉스를 우리 스타일로.', level: '입문', Component: HtmlCssPage },
|
||||
{ slug: 'javascript', Icon: Braces, title: '자바스크립트 기초', desc: 'F12 콘솔이 실습장 — 변수에서 fetch까지.', level: '입문', Component: JavascriptPage },
|
||||
{ slug: 'data-structures', Icon: Boxes, title: '자료구조 맛보기', desc: '배열·스택·큐·해시 — 그릇마다 쓰임이 다르다.', level: '입문', Component: DataStructuresPage },
|
||||
{ slug: 'react-intro', Icon: Atom, title: 'React 입문', desc: '컴포넌트·props·state — 우리 프론트가 교재.', level: '입문', Component: ReactIntroPage },
|
||||
{ slug: 'java-basics', Icon: Coffee, title: 'Java 기초', desc: '타입·클래스·객체 — Spring으로 가는 첫걸음.', level: '입문', Component: JavaBasicsPage },
|
||||
{ slug: 'spring-intro', Icon: Leaf, title: 'Spring Boot 입문', desc: 'DI·어노테이션·계층 구조 — 우리 백엔드 여행.', level: '심화', Component: SpringIntroPage },
|
||||
{ slug: 'git-deep', Icon: GitBranch, title: 'Git 깊이 보기', desc: '브랜치·충돌 해결·되돌리기 3형제·PR 매너.', level: '심화', Component: GitDeepPage },
|
||||
{ slug: 'debugging', Icon: Bug, title: '디버깅과 에러 읽는 법', desc: '스택트레이스·브레이크포인트 — 에러는 단서다.', level: '실습', Component: DebuggingPage },
|
||||
{ slug: 'testing', Icon: FlaskConical, title: '테스트 코드 입문', desc: '미래의 나를 지키는 보험 — 우리 테스트가 교재.', level: '심화', Component: TestingPage },
|
||||
{ slug: 'clean-code', Icon: Sparkles, title: '클린 코드와 코드리뷰', desc: '이름 짓기·작은 함수·리뷰 매너.', level: '심화', Component: CleanCodePage },
|
||||
{ slug: 'ai-tools', Icon: Bot, title: 'AI 도구와 함께 코딩하기', desc: '잘 시키고, 검증하고, 배움에 쓰는 법 — 보안 수칙까지.', level: '실습', Component: AiToolsPage },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: '디자인',
|
||||
desc: '보기 좋은 것을 넘어, 쓰기 좋은 것을.',
|
||||
items: [
|
||||
{ slug: 'uiux', icon: '🎨', title: 'UI/UX 기초', desc: '좋은 화면의 원칙들 — 우리 플랫폼을 뜯어보며.', level: '입문', Component: UiUxPage },
|
||||
{ slug: 'color', icon: '🌈', title: '색 이론 기초', desc: '색상환·배색·우리 브랜드 블루의 비밀.', level: '입문', Component: ColorPage },
|
||||
{ slug: 'typography', icon: '🔤', title: '타이포그래피', desc: '폰트·크기 위계·행간 — 가독성의 과학.', level: '입문', Component: TypographyPage },
|
||||
{ slug: 'layout', icon: '📐', title: '레이아웃과 그리드', desc: '8pt 그리드·정렬·여백 — 보이지 않는 뼈대.', level: '입문', Component: LayoutPage },
|
||||
{ slug: 'icons-images', icon: '🖼️', title: '아이콘과 이미지', desc: 'SVG vs PNG, 해상도, 그리고 저작권!', level: '입문', Component: IconsImagesPage },
|
||||
{ slug: 'wireframe', icon: '✏️', title: '와이어프레임 기초', desc: '색 없이 구조만 — 빨리 그리고 빨리 버리는 설계.', level: '실습', Component: WireframePage },
|
||||
{ slug: 'figma', icon: '🖌️', title: '피그마와 디자인 툴', desc: '프레임·오토레이아웃·컴포넌트·핸드오프.', level: '실습', Component: FigmaPage },
|
||||
{ slug: 'design-system', icon: '🧩', title: '디자인 시스템 입문', desc: '토큰과 컴포넌트 — 우리 global.css가 실물.', level: '심화', Component: DesignSystemPage },
|
||||
{ slug: 'mobile-design', icon: '📱', title: '모바일 디자인', desc: '엄지의 법칙, 터치 타깃, 웹 vs 앱.', level: '입문', Component: MobileDesignPage },
|
||||
{ slug: 'user-research', icon: '🔬', title: '사용자 리서치 기초', desc: '인터뷰·관찰·사용성 테스트 — 감이 아니라 근거로.', level: '심화', Component: UserResearchPage },
|
||||
{ slug: 'design-refs', icon: '🗃️', title: '레퍼런스와 무드보드', desc: '많이 본 사람이 잘 만든다 — 수집과 분석의 루틴.', level: '실습', Component: DesignRefsPage },
|
||||
{ slug: 'portfolio', icon: '💼', title: '포트폴리오 만들기', desc: '수습 8주가 곧 재료 — 과정을 이야기로.', level: '실습', Component: PortfolioPage },
|
||||
{ slug: 'uiux', Icon: Palette, title: 'UI/UX 기초', desc: '좋은 화면의 원칙들 — 우리 플랫폼을 뜯어보며.', level: '입문', Component: UiUxPage },
|
||||
{ slug: 'color', Icon: SwatchBook, title: '색 이론 기초', desc: '색상환·배색·우리 브랜드 블루의 비밀.', level: '입문', Component: ColorPage },
|
||||
{ slug: 'typography', Icon: Type, title: '타이포그래피', desc: '폰트·크기 위계·행간 — 가독성의 과학.', level: '입문', Component: TypographyPage },
|
||||
{ slug: 'layout', Icon: LayoutGrid, title: '레이아웃과 그리드', desc: '8pt 그리드·정렬·여백 — 보이지 않는 뼈대.', level: '입문', Component: LayoutPage },
|
||||
{ slug: 'icons-images', Icon: Image, title: '아이콘과 이미지', desc: 'SVG vs PNG, 해상도, 그리고 저작권!', level: '입문', Component: IconsImagesPage },
|
||||
{ slug: 'wireframe', Icon: PencilRuler, title: '와이어프레임 기초', desc: '색 없이 구조만 — 빨리 그리고 빨리 버리는 설계.', level: '실습', Component: WireframePage },
|
||||
{ slug: 'figma', Icon: PenTool, title: '피그마와 디자인 툴', desc: '프레임·오토레이아웃·컴포넌트·핸드오프.', level: '실습', Component: FigmaPage },
|
||||
{ slug: 'design-system', Icon: Component, title: '디자인 시스템 입문', desc: '토큰과 컴포넌트 — 우리 global.css가 실물.', level: '심화', Component: DesignSystemPage },
|
||||
{ slug: 'mobile-design', Icon: Smartphone, title: '모바일 디자인', desc: '엄지의 법칙, 터치 타깃, 웹 vs 앱.', level: '입문', Component: MobileDesignPage },
|
||||
{ slug: 'user-research', Icon: Microscope, title: '사용자 리서치 기초', desc: '인터뷰·관찰·사용성 테스트 — 감이 아니라 근거로.', level: '심화', Component: UserResearchPage },
|
||||
{ slug: 'design-refs', Icon: Images, title: '레퍼런스와 무드보드', desc: '많이 본 사람이 잘 만든다 — 수집과 분석의 루틴.', level: '실습', Component: DesignRefsPage },
|
||||
{ slug: 'portfolio', Icon: Briefcase, title: '포트폴리오 만들기', desc: '수습 8주가 곧 재료 — 과정을 이야기로.', level: '실습', Component: PortfolioPage },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: '부록 · 도구',
|
||||
desc: '손에 익으면 두 배 빨라지는 도구들.',
|
||||
items: [
|
||||
{ slug: null, external: '/setup', icon: '🛠️', title: '개발 환경 설치 가이드', desc: 'JDK·PATH·Node·Git·IntelliJ — 환경 구축 9단계.', level: '실습' },
|
||||
{ slug: 'tool-shortcuts', icon: '⌨️', title: '윈도우 단축키 마스터', desc: '마우스를 버릴수록 빨라진다 — 필수 30개.', level: '실습', Component: ToolShortcutsPage },
|
||||
{ slug: 'tool-terminal', icon: '🖤', 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-intellij', icon: '💡', title: 'IntelliJ 꿀팁 모음', desc: 'Shift 두 번, Alt+Enter, 로컬 히스토리의 구원.', level: '실습', Component: ToolIntellijPage },
|
||||
{ slug: 'tool-devtools', icon: '🔎', title: '크롬 개발자도구', desc: 'F12의 세계 — Elements·Console·Network 투어.', level: '실습', Component: ToolDevtoolsPage },
|
||||
{ slug: 'tool-gitea', icon: '🍵', title: 'Gitea 200% 활용', desc: '우리 Git 서버 — 이슈·PR·리뷰 화면 사용법.', level: '실습', Component: ToolGiteaPage },
|
||||
{ slug: 'tool-docker', icon: '🐳', title: 'Docker Desktop 사용법', desc: '대시보드 읽기, 컨테이너 GUI 관리, 용량 정리.', level: '실습', Component: ToolDockerPage },
|
||||
{ slug: 'tool-dbeaver', icon: '🦫', title: 'DBeaver로 DB 들여다보기', desc: '우리 DB 연결, ERD 자동 생성, GUI로 쿼리.', level: '실습', Component: ToolDbeaverPage },
|
||||
{ slug: 'tool-postman', icon: '🚀', title: 'Postman으로 API 테스트', desc: '화면 없이 API를 직접 두드리는 법.', level: '실습', Component: ToolPostmanPage },
|
||||
{ slug: 'tool-capture', icon: '📸', title: '캡처와 화면 녹화', desc: 'PR·버그 리포트의 언어 — Win+Shift+S부터.', level: '실습', Component: ToolCapturePage },
|
||||
{ slug: 'tool-notion', icon: '🗒️', title: '노션으로 기록하기', desc: 'TIL·트러블슈팅 — 검색되는 기록이 자산.', level: '실습', Component: ToolNotionPage },
|
||||
{ slug: null, external: '/setup', Icon: Wrench, title: '개발 환경 설치 가이드', desc: 'JDK·PATH·Node·Git·IntelliJ — 환경 구축 9단계.', level: '실습' },
|
||||
{ slug: 'tool-shortcuts', Icon: Command, title: '윈도우 단축키 마스터', desc: '마우스를 버릴수록 빨라진다 — 필수 30개.', level: '실습', Component: ToolShortcutsPage },
|
||||
{ slug: 'tool-terminal', Icon: SquareChevronRight, title: '윈도우 터미널과 PowerShell', desc: 'cmd·PowerShell·Git Bash 삼형제 정리.', level: '실습', Component: ToolTerminalPage },
|
||||
{ slug: 'tool-vscode', Icon: Code, title: 'VS Code 200% 활용', desc: '단축키 15개, 확장 추천, 검색의 기술.', level: '실습', Component: ToolVscodePage },
|
||||
{ slug: 'tool-intellij', Icon: Lightbulb, title: 'IntelliJ 꿀팁 모음', desc: 'Shift 두 번, Alt+Enter, 로컬 히스토리의 구원.', level: '실습', Component: ToolIntellijPage },
|
||||
{ slug: 'tool-devtools', Icon: Inspect, title: '크롬 개발자도구', desc: 'F12의 세계 — Elements·Console·Network 투어.', level: '실습', Component: ToolDevtoolsPage },
|
||||
{ slug: 'tool-gitea', Icon: GitFork, title: 'Gitea 200% 활용', desc: '우리 Git 서버 — 이슈·PR·리뷰 화면 사용법.', level: '실습', Component: ToolGiteaPage },
|
||||
{ slug: 'tool-docker', Icon: Ship, title: 'Docker Desktop 사용법', desc: '대시보드 읽기, 컨테이너 GUI 관리, 용량 정리.', level: '실습', Component: ToolDockerPage },
|
||||
{ slug: 'tool-dbeaver', Icon: DatabaseZap, title: 'DBeaver로 DB 들여다보기', desc: '우리 DB 연결, ERD 자동 생성, GUI로 쿼리.', level: '실습', Component: ToolDbeaverPage },
|
||||
{ slug: 'tool-postman', Icon: Send, title: 'Postman으로 API 테스트', desc: '화면 없이 API를 직접 두드리는 법.', level: '실습', Component: ToolPostmanPage },
|
||||
{ slug: 'tool-capture', Icon: Camera, title: '캡처와 화면 녹화', desc: 'PR·버그 리포트의 언어 — Win+Shift+S부터.', level: '실습', Component: ToolCapturePage },
|
||||
{ slug: 'tool-notion', Icon: NotebookPen, title: '노션으로 기록하기', desc: 'TIL·트러블슈팅 — 검색되는 기록이 자산.', level: '실습', Component: ToolNotionPage },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: '앱 개발',
|
||||
desc: '웹 개발자가 보는 모바일 — 안드로이드·iOS부터 우리 React로 앱까지.',
|
||||
items: [
|
||||
{ slug: 'app-landscape', icon: '📱', title: '앱의 지형', desc: '네이티브·크로스플랫폼·웹 — 뭘 언제 고르나.', level: '교양', Component: AppLandscapePage },
|
||||
{ slug: 'android-basics', icon: '🤖', title: '안드로이드 앱의 구조', desc: '액티비티·생명주기·스튜디오 — 앱이 도는 원리.', level: '입문', Component: AndroidBasicsPage },
|
||||
{ slug: 'kotlin-intro', icon: '🟣', title: 'Kotlin 입문', desc: '안드로이드의 언어 — JS/Java 관점으로 빠르게.', level: '입문', Component: KotlinIntroPage },
|
||||
{ slug: 'ios-basics', icon: '🍎', title: 'iOS 앱의 구조', desc: '뷰·생명주기·Xcode — 애플 생태계의 규칙.', level: '입문', Component: IosBasicsPage },
|
||||
{ slug: 'swift-intro', icon: '🐦', title: 'Swift 입문', desc: 'iOS의 언어 — 옵셔널·구조체 핵심만.', level: '입문', Component: SwiftIntroPage },
|
||||
{ slug: 'react-native', icon: '⚛️', title: 'React Native', desc: '우리 React 지식이 그대로 앱이 되는 다리.', level: '심화', Component: ReactNativePage },
|
||||
{ slug: 'flutter', icon: '🦋', title: 'Flutter', desc: 'Dart·위젯 — 또 하나의 크로스플랫폼.', level: '심화', Component: FlutterPage },
|
||||
{ slug: 'app-navigation', icon: '🧭', title: '앱 화면과 이동', desc: '내비게이션·화면 전환 — 웹 라우팅과 비교.', level: '입문', Component: AppNavigationPage },
|
||||
{ slug: 'app-state-data', icon: '🗃️', title: '앱의 상태와 데이터', desc: '상태관리·로컬 저장·우리 Spring API 연동.', level: '심화', Component: AppStateDataPage },
|
||||
{ slug: 'pwa', icon: '🌐', title: 'PWA — 웹앱을 앱처럼', desc: '설치·오프라인·푸시 — 우리 웹앱에 가장 가까운 길.', level: '심화', Component: PwaPage },
|
||||
{ slug: 'app-security', icon: '🔐', title: '앱 보안과 권한', desc: '권한 모델·저장소 보안·심사 기준.', level: '입문', Component: AppSecurityPage },
|
||||
{ slug: 'app-store-deploy', icon: '🚀', title: '스토어 배포', desc: 'Play·App Store 등록과 심사 흐름.', level: '실습', Component: AppStoreDeployPage },
|
||||
{ slug: 'app-landscape', Icon: LayoutTemplate, title: '앱의 지형', desc: '네이티브·크로스플랫폼·웹 — 뭘 언제 고르나.', level: '교양', Component: AppLandscapePage },
|
||||
{ slug: 'android-basics', Icon: TabletSmartphone, title: '안드로이드 앱의 구조', desc: '액티비티·생명주기·스튜디오 — 앱이 도는 원리.', level: '입문', Component: AndroidBasicsPage },
|
||||
{ slug: 'kotlin-intro', Icon: Blocks, title: 'Kotlin 입문', desc: '안드로이드의 언어 — JS/Java 관점으로 빠르게.', level: '입문', Component: KotlinIntroPage },
|
||||
{ slug: 'ios-basics', Icon: SquareDashedBottomCode, title: 'iOS 앱의 구조', desc: '뷰·생명주기·Xcode — 애플 생태계의 규칙.', level: '입문', Component: IosBasicsPage },
|
||||
{ slug: 'swift-intro', Icon: Bird, title: 'Swift 입문', desc: 'iOS의 언어 — 옵셔널·구조체 핵심만.', level: '입문', Component: SwiftIntroPage },
|
||||
{ slug: 'react-native', Icon: Orbit, title: 'React Native', desc: '우리 React 지식이 그대로 앱이 되는 다리.', level: '심화', Component: ReactNativePage },
|
||||
{ slug: 'flutter', Icon: Feather, title: 'Flutter', desc: 'Dart·위젯 — 또 하나의 크로스플랫폼.', level: '심화', Component: FlutterPage },
|
||||
{ slug: 'app-navigation', Icon: Compass, title: '앱 화면과 이동', desc: '내비게이션·화면 전환 — 웹 라우팅과 비교.', level: '입문', Component: AppNavigationPage },
|
||||
{ slug: 'app-state-data', Icon: Layers, title: '앱의 상태와 데이터', desc: '상태관리·로컬 저장·우리 Spring API 연동.', level: '심화', Component: AppStateDataPage },
|
||||
{ slug: 'pwa', Icon: MonitorSmartphone, title: 'PWA — 웹앱을 앱처럼', desc: '설치·오프라인·푸시 — 우리 웹앱에 가장 가까운 길.', level: '심화', Component: PwaPage },
|
||||
{ slug: 'app-security', Icon: ShieldCheck, title: '앱 보안과 권한', desc: '권한 모델·저장소 보안·심사 기준.', level: '입문', Component: AppSecurityPage },
|
||||
{ slug: 'app-store-deploy', Icon: Rocket, title: '스토어 배포', desc: 'Play·App Store 등록과 심사 흐름.', level: '실습', Component: AppStoreDeployPage },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@ -5,6 +5,56 @@
|
||||
// LEVEL_COURSES를 map으로 돌려 자동 생성한다 — 수습용과 완전히 같은 패턴이다.
|
||||
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강좌 ──
|
||||
const Lv1WhatIsAProgramPage = lazy(() => import('./pages/levels/Lv1WhatIsAProgramPage'));
|
||||
const Lv1ComputationalThinkingPage = lazy(() => import('./pages/levels/Lv1ComputationalThinkingPage'));
|
||||
@ -61,72 +111,72 @@ export const LEVEL_TRACKS = {
|
||||
category: '초급 과정 — 코딩 첫걸음',
|
||||
desc: '코딩이 처음인 사람을 위한 12강좌 — 변수 하나에서 첫 프로그램까지.',
|
||||
items: [
|
||||
{ slug: 'lv1-what-is-a-program', icon: '💡', title: '프로그램이란 무엇일까?', desc: '코드를 한 줄도 쓰기 전에, 컴퓨터에게 \'일을 시킨다\'는 게 무슨 뜻인지부터 몸으로 이해해요.', level: '입문', Component: Lv1WhatIsAProgramPage },
|
||||
{ slug: 'lv1-computational-thinking', icon: '🧩', title: '컴퓨터처럼 생각하기: 컴퓨팅 사고', desc: '코드 없이도 기를 수 있는 문제 해결 근육 — 쪼개고, 패턴을 찾고, 순서를 세우는 법을 배워요.', level: '입문', Component: Lv1ComputationalThinkingPage },
|
||||
{ slug: 'lv1-first-html', icon: '🌐', title: '나의 첫 웹페이지: HTML 첫걸음', desc: '메모장 수준의 도구만으로 브라우저에 뜨는 \'진짜 내 페이지\'를 오늘 바로 만들어요.', level: '입문', Component: Lv1FirstHtmlPage },
|
||||
{ slug: 'lv1-html-links-images-lists', icon: '🏗️', title: '태그로 페이지에 살 붙이기', desc: '링크·이미지·목록·표를 넣어서 밋밋한 문서를 \'진짜 웹페이지\'처럼 만들어요.', level: '입문', Component: Lv1HtmlLinksImagesListsPage },
|
||||
{ slug: 'lv1-how-browsers-work', icon: '🔍', title: '브라우저는 내 코드를 어떻게 읽을까', desc: '주소창에 엔터를 치면 무슨 일이 벌어질까? 개발자 도구를 열고 웹의 뒷면을 구경해요.', level: '입문', Component: Lv1HowBrowsersWorkPage },
|
||||
{ slug: 'lv1-first-javascript', icon: '✨', title: '나의 첫 JavaScript', desc: '멈춰 있던 페이지가 말을 걸고 반응하기 시작해요 — 첫 코드 한 줄의 짜릿함.', level: '입문', Component: Lv1FirstJavascriptPage },
|
||||
{ slug: 'lv1-variables-and-types', icon: '📦', title: '변수와 자료형: 이름 붙인 상자', desc: '값을 상자에 담아 두고 꺼내 쓰는 법 — 모든 프로그램의 가장 작은 부품을 배워요.', level: '실전', Component: Lv1VariablesAndTypesPage },
|
||||
{ slug: 'lv1-conditionals', icon: '🚦', title: '조건문: 컴퓨터가 판단하게 하기', desc: '"만약 ~라면" 한마디로 프로그램이 갈림길에서 스스로 길을 고르게 만들어요.', level: '실전', Component: Lv1ConditionalsPage },
|
||||
{ slug: 'lv1-loops', icon: '🔁', title: '반복문: 지루한 일은 컴퓨터에게', desc: '100번 반복도 한 줄로 — 컴퓨터가 가장 잘하는 일을 시키는 법을 배워요.', level: '실전', Component: Lv1LoopsPage },
|
||||
{ slug: 'lv1-functions', icon: '🧰', title: '함수: 나만의 명령어 만들기', desc: '자주 쓰는 코드에 이름을 붙여 언제든 다시 부르는 법 — 진짜 개발자처럼 코드를 조립해요.', level: '실전', Component: Lv1FunctionsPage },
|
||||
{ slug: 'lv1-reading-errors', icon: '🩹', title: '에러와 친해지기: 빨간 글씨 해독법', desc: '에러는 벌점이 아니라 힌트예요. 메시지를 읽고, 원인을 좁히고, 스스로 고치는 힘을 길러요.', level: '심화', Component: Lv1ReadingErrorsPage },
|
||||
{ slug: 'lv1-first-mini-project', icon: '🚀', title: '첫 프로젝트: 숫자 맞히기 게임 만들기', desc: '지금까지 배운 전부를 총동원해 처음부터 끝까지 \'내가 만든 웹 게임\'을 완성해요.', level: '프로젝트', Component: Lv1FirstMiniProjectPage },
|
||||
{ slug: 'lv1-what-is-a-program', Icon: Lightbulb, title: '프로그램이란 무엇일까?', desc: '코드를 한 줄도 쓰기 전에, 컴퓨터에게 \'일을 시킨다\'는 게 무슨 뜻인지부터 몸으로 이해해요.', level: '입문', Component: Lv1WhatIsAProgramPage },
|
||||
{ slug: 'lv1-computational-thinking', Icon: Puzzle, title: '컴퓨터처럼 생각하기: 컴퓨팅 사고', desc: '코드 없이도 기를 수 있는 문제 해결 근육 — 쪼개고, 패턴을 찾고, 순서를 세우는 법을 배워요.', level: '입문', Component: Lv1ComputationalThinkingPage },
|
||||
{ slug: 'lv1-first-html', Icon: Globe, title: '나의 첫 웹페이지: HTML 첫걸음', desc: '메모장 수준의 도구만으로 브라우저에 뜨는 \'진짜 내 페이지\'를 오늘 바로 만들어요.', level: '입문', Component: Lv1FirstHtmlPage },
|
||||
{ slug: 'lv1-html-links-images-lists', Icon: Blocks, title: '태그로 페이지에 살 붙이기', desc: '링크·이미지·목록·표를 넣어서 밋밋한 문서를 \'진짜 웹페이지\'처럼 만들어요.', level: '입문', Component: Lv1HtmlLinksImagesListsPage },
|
||||
{ slug: 'lv1-how-browsers-work', Icon: Search, title: '브라우저는 내 코드를 어떻게 읽을까', desc: '주소창에 엔터를 치면 무슨 일이 벌어질까? 개발자 도구를 열고 웹의 뒷면을 구경해요.', level: '입문', Component: Lv1HowBrowsersWorkPage },
|
||||
{ slug: 'lv1-first-javascript', Icon: Sparkles, title: '나의 첫 JavaScript', desc: '멈춰 있던 페이지가 말을 걸고 반응하기 시작해요 — 첫 코드 한 줄의 짜릿함.', level: '입문', Component: Lv1FirstJavascriptPage },
|
||||
{ slug: 'lv1-variables-and-types', Icon: Package, title: '변수와 자료형: 이름 붙인 상자', desc: '값을 상자에 담아 두고 꺼내 쓰는 법 — 모든 프로그램의 가장 작은 부품을 배워요.', level: '실전', Component: Lv1VariablesAndTypesPage },
|
||||
{ slug: 'lv1-conditionals', Icon: GitBranch, title: '조건문: 컴퓨터가 판단하게 하기', desc: '"만약 ~라면" 한마디로 프로그램이 갈림길에서 스스로 길을 고르게 만들어요.', level: '실전', Component: Lv1ConditionalsPage },
|
||||
{ slug: 'lv1-loops', Icon: Repeat, title: '반복문: 지루한 일은 컴퓨터에게', desc: '100번 반복도 한 줄로 — 컴퓨터가 가장 잘하는 일을 시키는 법을 배워요.', level: '실전', Component: Lv1LoopsPage },
|
||||
{ slug: 'lv1-functions', Icon: SquareFunction, title: '함수: 나만의 명령어 만들기', desc: '자주 쓰는 코드에 이름을 붙여 언제든 다시 부르는 법 — 진짜 개발자처럼 코드를 조립해요.', level: '실전', Component: Lv1FunctionsPage },
|
||||
{ slug: 'lv1-reading-errors', Icon: Bandage, title: '에러와 친해지기: 빨간 글씨 해독법', desc: '에러는 벌점이 아니라 힌트예요. 메시지를 읽고, 원인을 좁히고, 스스로 고치는 힘을 길러요.', level: '심화', Component: Lv1ReadingErrorsPage },
|
||||
{ slug: 'lv1-first-mini-project', Icon: Rocket, title: '첫 프로젝트: 숫자 맞히기 게임 만들기', desc: '지금까지 배운 전부를 총동원해 처음부터 끝까지 \'내가 만든 웹 게임\'을 완성해요.', level: '프로젝트', Component: Lv1FirstMiniProjectPage },
|
||||
],
|
||||
}],
|
||||
lv2: [{
|
||||
category: '중급 과정 — 실전 웹 개발',
|
||||
desc: '돌아가는 웹 서비스를 직접 만드는 12강좌.',
|
||||
items: [
|
||||
{ slug: 'lv2-async-javascript', icon: '⏳', 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-rest-api-design', icon: '📐', title: 'REST API 설계: 주소와 동사의 문법', desc: '코드를 짜기 전에 API 명세부터 — 프론트와 백이 싸우지 않는 계약서 쓰는 법', level: '실전', Component: Lv2RestApiDesignPage },
|
||||
{ slug: 'lv2-express-crud', icon: '🛠️', 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-auth-session-hash', icon: '🔐', title: '로그인 구현: 세션과 비밀번호 해시', desc: '비밀번호를 지키고 사용자를 기억하는 법 — 회원가입부터 보호 라우트까지 직접 구현', level: '심화', Component: Lv2AuthSessionHashPage },
|
||||
{ slug: 'lv2-react-state-effects', icon: '⚛️', title: 'React 상태와 이펙트 실전', desc: '상태를 \'설계\'하는 단계로 — 불변 업데이트, useEffect 데이터 페칭, 커스텀 훅까지', level: '실전', Component: Lv2ReactStateEffectsPage },
|
||||
{ slug: 'lv2-react-routing', icon: '🧭', title: 'React Router로 멀티 페이지 SPA', desc: '주소창이 곧 상태다 — URL 파라미터, 중첩 레이아웃, 보호 라우트로 진짜 앱 골격 만들기', level: '실전', Component: Lv2ReactRoutingPage },
|
||||
{ slug: 'lv2-form-validation', icon: '✅', title: '폼 검증: 사용자를 돕고 서버를 지키기', desc: '검증은 두 번 한다 — 프론트에서 친절하게, 서버에서 단호하게. 이중 검증 완전 정복', level: '실전', Component: Lv2FormValidationPage },
|
||||
{ slug: 'lv2-error-handling-patterns', icon: '🚨', title: '에러 처리 패턴: 무너지지 않는 서비스', desc: '에러는 없앨 수 없다, 다스릴 뿐 — 백엔드 에러 미들웨어부터 프론트 폴백 UI까지', level: '심화', Component: Lv2ErrorHandlingPatternsPage },
|
||||
{ slug: 'lv2-practical-testing', icon: '🧪', title: '테스트 작성 실전: 자신 있게 고치는 힘', desc: '고칠 때마다 손이 떨리지 않으려면 — 단위·API·컴포넌트 테스트를 실제 코드에 붙이기', level: '심화', Component: Lv2PracticalTestingPage },
|
||||
{ slug: 'lv2-mini-project-deploy', icon: '🚀', title: '미니 프로젝트: 설계부터 배포까지', desc: '11주간 만든 조각을 하나로 — 기획·조립·배포·회고까지, 포트폴리오가 되는 첫 서비스', level: '프로젝트', Component: Lv2MiniProjectDeployPage },
|
||||
{ slug: 'lv2-async-javascript', Icon: Hourglass, title: '비동기 JavaScript: fetch와 Promise 정복', desc: '콜백 지옥에서 async/await까지 — 서버와 대화하는 코드를 제대로 쓰는 법', level: '실전', Component: Lv2AsyncJavascriptPage },
|
||||
{ slug: 'lv2-array-data-wrangling', Icon: ChefHat, title: '배열 메서드로 데이터 요리하기', desc: 'map·filter·reduce로 API 응답을 화면에 딱 맞는 데이터로 썰고 볶는 실전 기술', level: '실전', Component: Lv2ArrayDataWranglingPage },
|
||||
{ slug: 'lv2-rest-api-design', Icon: Ruler, title: 'REST API 설계: 주소와 동사의 문법', desc: '코드를 짜기 전에 API 명세부터 — 프론트와 백이 싸우지 않는 계약서 쓰는 법', level: '실전', Component: Lv2RestApiDesignPage },
|
||||
{ slug: 'lv2-express-crud', Icon: Hammer, title: 'Express로 CRUD API 만들기', desc: '설계한 명세를 진짜 돌아가는 서버로 — 라우터·미들웨어·계층 분리까지 한 번에', level: '실전', Component: Lv2ExpressCrudPage },
|
||||
{ slug: 'lv2-db-transactions', Icon: Database, title: 'DB 연동과 트랜잭션: 데이터가 사는 집', desc: '서버를 껐다 켜도 살아남는 데이터 — SQL 연동부터 all-or-nothing 트랜잭션까지', level: '심화', Component: Lv2DbTransactionsPage },
|
||||
{ slug: 'lv2-auth-session-hash', Icon: KeyRound, title: '로그인 구현: 세션과 비밀번호 해시', desc: '비밀번호를 지키고 사용자를 기억하는 법 — 회원가입부터 보호 라우트까지 직접 구현', level: '심화', Component: Lv2AuthSessionHashPage },
|
||||
{ slug: 'lv2-react-state-effects', Icon: Atom, title: 'React 상태와 이펙트 실전', desc: '상태를 \'설계\'하는 단계로 — 불변 업데이트, useEffect 데이터 페칭, 커스텀 훅까지', level: '실전', Component: Lv2ReactStateEffectsPage },
|
||||
{ slug: 'lv2-react-routing', Icon: Compass, title: 'React Router로 멀티 페이지 SPA', desc: '주소창이 곧 상태다 — URL 파라미터, 중첩 레이아웃, 보호 라우트로 진짜 앱 골격 만들기', level: '실전', Component: Lv2ReactRoutingPage },
|
||||
{ slug: 'lv2-form-validation', Icon: CircleCheck, title: '폼 검증: 사용자를 돕고 서버를 지키기', desc: '검증은 두 번 한다 — 프론트에서 친절하게, 서버에서 단호하게. 이중 검증 완전 정복', level: '실전', Component: Lv2FormValidationPage },
|
||||
{ slug: 'lv2-error-handling-patterns', Icon: TriangleAlert, title: '에러 처리 패턴: 무너지지 않는 서비스', desc: '에러는 없앨 수 없다, 다스릴 뿐 — 백엔드 에러 미들웨어부터 프론트 폴백 UI까지', level: '심화', Component: Lv2ErrorHandlingPatternsPage },
|
||||
{ slug: 'lv2-practical-testing', Icon: FlaskConical, title: '테스트 작성 실전: 자신 있게 고치는 힘', desc: '고칠 때마다 손이 떨리지 않으려면 — 단위·API·컴포넌트 테스트를 실제 코드에 붙이기', level: '심화', Component: Lv2PracticalTestingPage },
|
||||
{ slug: 'lv2-mini-project-deploy', Icon: Rocket, title: '미니 프로젝트: 설계부터 배포까지', desc: '11주간 만든 조각을 하나로 — 기획·조립·배포·회고까지, 포트폴리오가 되는 첫 서비스', level: '프로젝트', Component: Lv2MiniProjectDeployPage },
|
||||
],
|
||||
}],
|
||||
lv3: [{
|
||||
category: '고급 과정 — 아키텍처·성능·보안',
|
||||
desc: '"잘 만드는 법"을 파고드는 12강좌.',
|
||||
items: [
|
||||
{ slug: 'lv3-layered-architecture', icon: '🏛️', title: '레이어드 아키텍처와 관심사 분리', desc: '돌아가는 코드에서 오래 사는 코드로 — 계층으로 변경을 가두는 법.', level: '심화', Component: Lv3LayeredArchitecturePage },
|
||||
{ slug: 'lv3-design-patterns', icon: '🧩', title: '디자인 패턴 실전', desc: '외우는 패턴 말고 쓰는 패턴 — \'이 문제엔 이 해법\'을 우리 코드로.', level: '실전', Component: Lv3DesignPatternsPage },
|
||||
{ slug: 'lv3-db-indexing', icon: '🔍', title: 'DB 인덱스와 실행계획 읽기', desc: '쿼리가 왜 느린지 감으로 찍지 말고 EXPLAIN으로 증명하는 법.', level: '심화', Component: Lv3DbIndexingPage },
|
||||
{ slug: 'lv3-transaction-isolation', icon: '🔒', title: '트랜잭션과 격리 수준', desc: '동시에 밀려드는 요청 속에서 돈과 데이터를 지키는 규칙.', level: '심화', Component: Lv3TransactionIsolationPage },
|
||||
{ slug: 'lv3-query-optimization', icon: '🐢', title: 'N+1과 쿼리 최적화', desc: '목록 하나 여는데 쿼리 수백 개 — ORM이 숨긴 재앙을 걷어내기.', level: '실전', Component: Lv3QueryOptimizationPage },
|
||||
{ slug: 'lv3-caching-strategy', icon: '⚡', title: '캐싱 전략 — 로컬과 Redis', desc: '빠르게 만드는 마법이 아니라, 무효화와 일관성을 감당하는 설계.', level: '심화', Component: Lv3CachingStrategyPage },
|
||||
{ slug: 'lv3-performance-profiling', icon: '📈', title: '성능 측정과 프로파일링', desc: '추측하지 말고 측정하라 — 병목은 늘 예상 밖에 숨어 있다.', level: '실전', Component: Lv3PerformanceProfilingPage },
|
||||
{ slug: 'lv3-web-security', icon: '🛡️', title: '웹 보안 심화 — OWASP Top 10', desc: '공격자의 눈으로 내 서비스를 보고, 계층으로 방어하는 법.', level: '심화', Component: Lv3WebSecurityPage },
|
||||
{ slug: 'lv3-auth-architecture', icon: '🔑', title: '인증 아키텍처 — 세션 vs JWT vs OAuth', desc: '쿠키냐 토큰이냐, 무상태의 자유와 대가를 저울질하는 설계.', level: '심화', Component: Lv3AuthArchitecturePage },
|
||||
{ slug: 'lv3-observability', icon: '🔭', title: '로깅·모니터링·알림 설계', desc: '무엇이 느린가와 왜 느린가를 나눠 답하는 관측 가능성.', level: '실전', Component: Lv3ObservabilityPage },
|
||||
{ slug: 'lv3-zero-downtime-deploy', icon: '🚦', title: '무중단 배포와 롤백', desc: '배포가 무섭지 않아야 자주 배포한다 — 되돌릴 수 있게 나가는 법.', level: '실전', Component: Lv3ZeroDowntimeDeployPage },
|
||||
{ slug: 'lv3-code-review-refactoring', icon: '🧭', title: '코드 리뷰와 리팩터링 전략', desc: '이 과정의 종착점 — 설계·성능·보안·운영을 리뷰 한 판에 녹여내기.', level: '프로젝트', Component: Lv3CodeReviewRefactoringPage },
|
||||
{ slug: 'lv3-layered-architecture', Icon: Layers, title: '레이어드 아키텍처와 관심사 분리', desc: '돌아가는 코드에서 오래 사는 코드로 — 계층으로 변경을 가두는 법.', level: '심화', Component: Lv3LayeredArchitecturePage },
|
||||
{ slug: 'lv3-design-patterns', Icon: Puzzle, title: '디자인 패턴 실전', desc: '외우는 패턴 말고 쓰는 패턴 — \'이 문제엔 이 해법\'을 우리 코드로.', level: '실전', Component: Lv3DesignPatternsPage },
|
||||
{ slug: 'lv3-db-indexing', Icon: ListTree, title: 'DB 인덱스와 실행계획 읽기', desc: '쿼리가 왜 느린지 감으로 찍지 말고 EXPLAIN으로 증명하는 법.', level: '심화', Component: Lv3DbIndexingPage },
|
||||
{ slug: 'lv3-transaction-isolation', Icon: Lock, title: '트랜잭션과 격리 수준', desc: '동시에 밀려드는 요청 속에서 돈과 데이터를 지키는 규칙.', level: '심화', Component: Lv3TransactionIsolationPage },
|
||||
{ slug: 'lv3-query-optimization', Icon: Turtle, title: 'N+1과 쿼리 최적화', desc: '목록 하나 여는데 쿼리 수백 개 — ORM이 숨긴 재앙을 걷어내기.', level: '실전', Component: Lv3QueryOptimizationPage },
|
||||
{ slug: 'lv3-caching-strategy', Icon: Zap, title: '캐싱 전략 — 로컬과 Redis', desc: '빠르게 만드는 마법이 아니라, 무효화와 일관성을 감당하는 설계.', level: '심화', Component: Lv3CachingStrategyPage },
|
||||
{ slug: 'lv3-performance-profiling', Icon: TrendingUp, title: '성능 측정과 프로파일링', desc: '추측하지 말고 측정하라 — 병목은 늘 예상 밖에 숨어 있다.', level: '실전', Component: Lv3PerformanceProfilingPage },
|
||||
{ slug: 'lv3-web-security', Icon: Shield, title: '웹 보안 심화 — OWASP Top 10', desc: '공격자의 눈으로 내 서비스를 보고, 계층으로 방어하는 법.', level: '심화', Component: Lv3WebSecurityPage },
|
||||
{ slug: 'lv3-auth-architecture', Icon: Fingerprint, title: '인증 아키텍처 — 세션 vs JWT vs OAuth', desc: '쿠키냐 토큰이냐, 무상태의 자유와 대가를 저울질하는 설계.', level: '심화', Component: Lv3AuthArchitecturePage },
|
||||
{ slug: 'lv3-observability', Icon: Telescope, title: '로깅·모니터링·알림 설계', desc: '무엇이 느린가와 왜 느린가를 나눠 답하는 관측 가능성.', level: '실전', Component: Lv3ObservabilityPage },
|
||||
{ slug: 'lv3-zero-downtime-deploy', Icon: Milestone, title: '무중단 배포와 롤백', desc: '배포가 무섭지 않아야 자주 배포한다 — 되돌릴 수 있게 나가는 법.', level: '실전', Component: Lv3ZeroDowntimeDeployPage },
|
||||
{ slug: 'lv3-code-review-refactoring', Icon: GitPullRequest, title: '코드 리뷰와 리팩터링 전략', desc: '이 과정의 종착점 — 설계·성능·보안·운영을 리뷰 한 판에 녹여내기.', level: '프로젝트', Component: Lv3CodeReviewRefactoringPage },
|
||||
],
|
||||
}],
|
||||
lv4: [{
|
||||
category: '특급 과정 — 대규모 시스템',
|
||||
desc: '대용량·분산 환경을 설계하는 전문가 12강좌.',
|
||||
items: [
|
||||
{ slug: 'lv4-distributed-systems-basics', icon: '🌐', title: '분산 시스템 첫걸음 — 왜 서버 한 대로는 안 될까', desc: '서버가 여러 대가 되는 순간 생기는 근본 문제들 — CAP, 일관성 모델, 부분 실패를 몸으로 이해합니다.', level: '심화', Component: Lv4DistributedSystemsBasicsPage },
|
||||
{ slug: 'lv4-message-queue-event-driven', icon: '📨', title: '메시지 큐와 이벤트 드리븐 — 시스템을 느슨하게 잇는 법', desc: '동기 호출의 한계를 넘어 Kafka 같은 메시지 브로커로 시스템을 분리하고, 이벤트 중심으로 사고하는 법을 배웁니다.', level: '심화', Component: Lv4MessageQueueEventDrivenPage },
|
||||
{ slug: 'lv4-monolith-vs-microservices', icon: '🧩', title: '모놀리스 vs 마이크로서비스 — 쪼개는 것의 진짜 비용', desc: '유행이 아니라 트레이드오프로 아키텍처를 고릅니다. 서비스 경계 긋기, 분산 트랜잭션, 그리고 \'쪼개지 않을 용기\'까지.', level: '심화', Component: Lv4MonolithVsMicroservicesPage },
|
||||
{ slug: 'lv4-db-replication-sharding', icon: '🗄️', title: 'DB 레플리케이션과 샤딩 — 데이터베이스를 늘리는 두 가지 축', desc: '읽기를 늘리는 복제, 쓰기를 늘리는 샤딩. 복제 지연과 핫 샤드 같은 부작용까지 다루는 데이터 확장 설계.', level: '심화', Component: Lv4DbReplicationShardingPage },
|
||||
{ slug: 'lv4-high-traffic-architecture', icon: '🚦', title: '대용량 트래픽 설계 — 밀려오는 요청 앞에서 무너지지 않기', desc: '로드밸런싱, CDN, 캐시 계층, 레이트리밋, 백프레셔까지 — 트래픽이 10배가 되어도 버티는 구조를 설계합니다.', level: '심화', Component: Lv4HighTrafficArchitecturePage },
|
||||
{ slug: 'lv4-kubernetes-cloud-native', icon: '☸️', title: '쿠버네티스와 클라우드 네이티브 — 선언하면 알아서 굴러가는 인프라', desc: 'docker compose 다음 단계. 파드·서비스·디플로이먼트부터 오토스케일링까지, 선언형 운영의 사고방식을 익힙니다.', level: '심화', Component: Lv4KubernetesCloudNativePage },
|
||||
{ slug: 'lv4-observability-metrics-tracing', icon: '🔭', title: '관측성 — 보이지 않으면 고칠 수 없다', desc: '메트릭·로그·트레이스 세 기둥으로 분산 시스템 내부를 들여다봅니다. \'요청이 어디서 느려졌나\'에 답하는 기술.', level: '심화', Component: Lv4ObservabilityMetricsTracingPage },
|
||||
{ slug: 'lv4-sre-incident-response', icon: '🚨', 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-chat', icon: '💬', title: '시스템 설계 훈련 2 — 실시간 채팅 시스템', desc: '연결이 오래 유지되는 실시간 시스템의 설계. WebSocket, 메시지 순서, 읽음 표시, 오프라인 동기화까지 파고듭니다.', level: '프로젝트', Component: Lv4SystemDesignChatPage },
|
||||
{ slug: 'lv4-system-design-newsfeed', icon: '📰', title: '시스템 설계 훈련 3 — 뉴스피드와 타임라인', desc: '푸시 vs 풀, 팬아웃, 랭킹, 셀럽 문제 — 읽기 폭발형 시스템의 끝판왕 설계로 3부작을 완성합니다.', level: '프로젝트', Component: Lv4SystemDesignNewsfeedPage },
|
||||
{ slug: 'lv4-tech-leadership-adr', icon: '🧭', title: '기술 리더십과 ADR — 결정을 설계하고 기록하는 사람', desc: '특급 과정의 마무리. 아키텍처 결정 기록(ADR), 설계 리뷰 문화, 트레이드오프 커뮤니케이션으로 팀을 움직이는 개발자가 됩니다.', level: '심화', Component: Lv4TechLeadershipAdrPage },
|
||||
{ slug: 'lv4-distributed-systems-basics', Icon: Network, title: '분산 시스템 첫걸음 — 왜 서버 한 대로는 안 될까', desc: '서버가 여러 대가 되는 순간 생기는 근본 문제들 — CAP, 일관성 모델, 부분 실패를 몸으로 이해합니다.', level: '심화', Component: Lv4DistributedSystemsBasicsPage },
|
||||
{ slug: 'lv4-message-queue-event-driven', Icon: Inbox, title: '메시지 큐와 이벤트 드리븐 — 시스템을 느슨하게 잇는 법', desc: '동기 호출의 한계를 넘어 Kafka 같은 메시지 브로커로 시스템을 분리하고, 이벤트 중심으로 사고하는 법을 배웁니다.', level: '심화', Component: Lv4MessageQueueEventDrivenPage },
|
||||
{ slug: 'lv4-monolith-vs-microservices', Icon: Boxes, title: '모놀리스 vs 마이크로서비스 — 쪼개는 것의 진짜 비용', desc: '유행이 아니라 트레이드오프로 아키텍처를 고릅니다. 서비스 경계 긋기, 분산 트랜잭션, 그리고 \'쪼개지 않을 용기\'까지.', level: '심화', Component: Lv4MonolithVsMicroservicesPage },
|
||||
{ slug: 'lv4-db-replication-sharding', Icon: DatabaseBackup, title: 'DB 레플리케이션과 샤딩 — 데이터베이스를 늘리는 두 가지 축', desc: '읽기를 늘리는 복제, 쓰기를 늘리는 샤딩. 복제 지연과 핫 샤드 같은 부작용까지 다루는 데이터 확장 설계.', level: '심화', Component: Lv4DbReplicationShardingPage },
|
||||
{ slug: 'lv4-high-traffic-architecture', Icon: Gauge, title: '대용량 트래픽 설계 — 밀려오는 요청 앞에서 무너지지 않기', desc: '로드밸런싱, CDN, 캐시 계층, 레이트리밋, 백프레셔까지 — 트래픽이 10배가 되어도 버티는 구조를 설계합니다.', level: '심화', Component: Lv4HighTrafficArchitecturePage },
|
||||
{ slug: 'lv4-kubernetes-cloud-native', Icon: Hexagon, title: '쿠버네티스와 클라우드 네이티브 — 선언하면 알아서 굴러가는 인프라', desc: 'docker compose 다음 단계. 파드·서비스·디플로이먼트부터 오토스케일링까지, 선언형 운영의 사고방식을 익힙니다.', level: '심화', Component: Lv4KubernetesCloudNativePage },
|
||||
{ slug: 'lv4-observability-metrics-tracing', Icon: Activity, title: '관측성 — 보이지 않으면 고칠 수 없다', desc: '메트릭·로그·트레이스 세 기둥으로 분산 시스템 내부를 들여다봅니다. \'요청이 어디서 느려졌나\'에 답하는 기술.', level: '심화', Component: Lv4ObservabilityMetricsTracingPage },
|
||||
{ slug: 'lv4-sre-incident-response', Icon: Siren, title: 'SRE와 장애 대응 — 무너져도 빨리 일어나는 팀', desc: 'SLO와 에러 버짓으로 신뢰성을 숫자로 관리하고, 장애를 침착하게 지휘하고, 비난 없는 포스트모템으로 배우는 법.', level: '심화', Component: Lv4SreIncidentResponsePage },
|
||||
{ slug: 'lv4-system-design-url-shortener', Icon: Link, title: '시스템 설계 훈련 1 — URL 단축기를 처음부터 끝까지', desc: '가장 고전적인 설계 문제로 \'요구사항 → 용량 산정 → API → 데이터 → 확장\'의 설계 루틴을 몸에 새깁니다.', level: '프로젝트', Component: Lv4SystemDesignUrlShortenerPage },
|
||||
{ slug: 'lv4-system-design-chat', Icon: MessageSquare, title: '시스템 설계 훈련 2 — 실시간 채팅 시스템', desc: '연결이 오래 유지되는 실시간 시스템의 설계. WebSocket, 메시지 순서, 읽음 표시, 오프라인 동기화까지 파고듭니다.', level: '프로젝트', Component: Lv4SystemDesignChatPage },
|
||||
{ slug: 'lv4-system-design-newsfeed', Icon: Newspaper, title: '시스템 설계 훈련 3 — 뉴스피드와 타임라인', desc: '푸시 vs 풀, 팬아웃, 랭킹, 셀럽 문제 — 읽기 폭발형 시스템의 끝판왕 설계로 3부작을 완성합니다.', level: '프로젝트', Component: Lv4SystemDesignNewsfeedPage },
|
||||
{ slug: 'lv4-tech-leadership-adr', Icon: Signpost, title: '기술 리더십과 ADR — 결정을 설계하고 기록하는 사람', desc: '특급 과정의 마무리. 아키텍처 결정 기록(ADR), 설계 리뷰 문화, 트레이드오프 커뮤니케이션으로 팀을 움직이는 개발자가 됩니다.', level: '심화', Component: Lv4TechLeadershipAdrPage },
|
||||
],
|
||||
}],
|
||||
};
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
// 학습 센터 코스 진도 요약 위젯도 함께 보여준다.
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Award, BookA, BookOpen, Compass, Palette, PartyPopper } from 'lucide-react';
|
||||
import client from '../api/client';
|
||||
import { useAuth } from '../AuthContext';
|
||||
import Card from '../components/Card';
|
||||
@ -45,7 +46,9 @@ function CourseProgressWidget() {
|
||||
return (
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<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>
|
||||
@ -88,17 +91,19 @@ function CourseProgressWidget() {
|
||||
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={{ 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>
|
||||
<span style={{ marginLeft: 'auto', color: 'var(--primary)', fontWeight: 700 }}>→</span>
|
||||
</div>
|
||||
</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>
|
||||
)}
|
||||
</Card>
|
||||
@ -211,22 +216,29 @@ export default function DashboardPage() {
|
||||
{/* 학습 센터 코스 진도 요약 */}
|
||||
<CourseProgressWidget />
|
||||
|
||||
{/* 빠른 이동 — 용어 사전과 내 학습 증명서. 자주 찾는 두 곳을 카드로. */}
|
||||
{/* 빠른 이동 — 자주 찾는 곳을 카드로. */}
|
||||
<div className="grid-cards" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', marginBottom: 16 }}>
|
||||
<Link to="/glossary" style={{ color: 'inherit' }}>
|
||||
<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>
|
||||
<p className="muted" style={{ fontSize: 13, marginTop: 4 }}>모르는 용어가 나오면 여기서 검색하세요.</p>
|
||||
</div>
|
||||
</Link>
|
||||
<Link to="/certificate" style={{ color: 'inherit' }}>
|
||||
<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>
|
||||
<p className="muted" style={{ fontSize: 13, marginTop: 4 }}>지금까지의 성취를 증명서로 — 인쇄·PDF 저장.</p>
|
||||
</div>
|
||||
</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 className="tabs">
|
||||
|
||||
242
frontend/src/pages/DesignPage.jsx
Normal file
242
frontend/src/pages/DesignPage.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -141,7 +141,11 @@ export default function LearnHubPage() {
|
||||
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
|
||||
style={{
|
||||
fontSize: 13,
|
||||
|
||||
@ -1,7 +1,30 @@
|
||||
/* 이 파일이 하는 일: AWESOMEDEV 브랜드 색·폰트를 CSS 변수(토큰)로 정의하고,
|
||||
버튼/카드/입력창 같은 공통 클래스를 제공한다.
|
||||
학습 포인트: 색을 컴포넌트마다 하드코딩하지 않고 :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 {
|
||||
--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);
|
||||
--code-bg: #F1F4F9;
|
||||
--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가 다크 모드면 이 블록이 변수 값만 바꿔치기한다.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user