@kood/claude-code 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +21 -243
- package/package.json +1 -1
- package/templates/hono/CLAUDE.md +10 -6
- package/templates/hono/docs/deployment/index.md +5 -0
- package/templates/hono/docs/library/hono/index.md +6 -0
- package/templates/hono/docs/library/prisma/index.md +3 -0
- package/templates/npx/CLAUDE.md +8 -2
- package/templates/tanstack-start/CLAUDE.md +103 -255
- package/templates/tanstack-start/docs/deployment/cloudflare.md +37 -424
- package/templates/tanstack-start/docs/deployment/index.md +57 -286
- package/templates/tanstack-start/docs/deployment/nitro.md +36 -318
- package/templates/tanstack-start/docs/deployment/railway.md +40 -409
- package/templates/tanstack-start/docs/deployment/vercel.md +43 -465
- package/templates/tanstack-start/docs/design/accessibility.md +56 -326
- package/templates/tanstack-start/docs/design/color.md +37 -179
- package/templates/tanstack-start/docs/design/components.md +77 -311
- package/templates/tanstack-start/docs/design/index.md +24 -87
- package/templates/tanstack-start/docs/design/safe-area.md +51 -250
- package/templates/tanstack-start/docs/design/spacing.md +57 -276
- package/templates/tanstack-start/docs/design/tailwind-setup.md +45 -359
- package/templates/tanstack-start/docs/design/typography.md +40 -284
- package/templates/tanstack-start/docs/library/better-auth/2fa.md +27 -115
- package/templates/tanstack-start/docs/library/better-auth/advanced.md +22 -105
- package/templates/tanstack-start/docs/library/better-auth/index.md +17 -66
- package/templates/tanstack-start/docs/library/better-auth/plugins.md +11 -88
- package/templates/tanstack-start/docs/library/better-auth/session.md +12 -92
- package/templates/tanstack-start/docs/library/better-auth/setup.md +9 -91
- package/templates/tanstack-start/docs/library/prisma/cloudflare-d1.md +30 -358
- package/templates/tanstack-start/docs/library/prisma/config.md +27 -327
- package/templates/tanstack-start/docs/library/prisma/crud.md +46 -174
- package/templates/tanstack-start/docs/library/prisma/index.md +23 -113
- package/templates/tanstack-start/docs/library/prisma/relations.md +31 -153
- package/templates/tanstack-start/docs/library/prisma/schema.md +40 -217
- package/templates/tanstack-start/docs/library/prisma/setup.md +12 -112
- package/templates/tanstack-start/docs/library/prisma/transactions.md +20 -110
- package/templates/tanstack-start/docs/library/tanstack-query/index.md +12 -99
- package/templates/tanstack-start/docs/library/tanstack-query/invalidation.md +28 -107
- package/templates/tanstack-start/docs/library/tanstack-query/optimistic-updates.md +44 -146
- package/templates/tanstack-start/docs/library/tanstack-query/setup.md +11 -70
- package/templates/tanstack-start/docs/library/tanstack-query/use-mutation.md +33 -127
- package/templates/tanstack-start/docs/library/tanstack-query/use-query.md +49 -149
- package/templates/tanstack-start/docs/library/tanstack-start/auth-patterns.md +19 -112
- package/templates/tanstack-start/docs/library/tanstack-start/index.md +33 -80
- package/templates/tanstack-start/docs/library/tanstack-start/middleware.md +28 -106
- package/templates/tanstack-start/docs/library/tanstack-start/routing.md +21 -118
- package/templates/tanstack-start/docs/library/tanstack-start/server-functions.md +34 -246
- package/templates/tanstack-start/docs/library/tanstack-start/setup.md +6 -39
- package/templates/tanstack-start/docs/library/zod/basic-types.md +33 -145
- package/templates/tanstack-start/docs/library/zod/complex-types.md +32 -156
- package/templates/tanstack-start/docs/library/zod/index.md +22 -150
- package/templates/tanstack-start/docs/library/zod/transforms.md +20 -129
- package/templates/tanstack-start/docs/library/zod/validation.md +39 -155
- package/templates/hono/docs/commands/git.md +0 -145
- package/templates/hono/docs/mcp/context7.md +0 -106
- package/templates/hono/docs/mcp/index.md +0 -176
- package/templates/hono/docs/mcp/sequential-thinking.md +0 -101
- package/templates/hono/docs/mcp/serena.md +0 -269
- package/templates/hono/docs/mcp/sgrep.md +0 -105
- package/templates/hono/docs/skills/gemini-review/SKILL.md +0 -220
- package/templates/hono/docs/skills/gemini-review/references/checklists.md +0 -136
- package/templates/hono/docs/skills/gemini-review/references/prompt-templates.md +0 -303
- package/templates/npx/docs/commands/git.md +0 -145
- package/templates/npx/docs/mcp/index.md +0 -60
- package/templates/npx/docs/skills/gemini-review/SKILL.md +0 -220
- package/templates/npx/docs/skills/gemini-review/references/checklists.md +0 -134
- package/templates/npx/docs/skills/gemini-review/references/prompt-templates.md +0 -301
- package/templates/tanstack-start/docs/commands/git.md +0 -145
- package/templates/tanstack-start/docs/mcp/context7.md +0 -204
- package/templates/tanstack-start/docs/mcp/index.md +0 -177
- package/templates/tanstack-start/docs/mcp/sequential-thinking.md +0 -180
- package/templates/tanstack-start/docs/mcp/serena.md +0 -269
- package/templates/tanstack-start/docs/mcp/sgrep.md +0 -174
- package/templates/tanstack-start/docs/skills/gemini-review/SKILL.md +0 -220
- package/templates/tanstack-start/docs/skills/gemini-review/references/checklists.md +0 -144
- package/templates/tanstack-start/docs/skills/gemini-review/references/prompt-templates.md +0 -292
|
@@ -1,324 +1,80 @@
|
|
|
1
1
|
# 타이포그래피
|
|
2
2
|
|
|
3
|
-
> **상위 문서**: [UI/UX 디자인 가이드](./index.md)
|
|
4
|
-
|
|
5
|
-
타이포그래피(Typography)는 텍스트를 읽기 쉽고 시각적으로 매력적으로 만드는 기술입니다.
|
|
6
|
-
|
|
7
3
|
## 기본 원칙
|
|
4
|
+
- 폰트 2-3개만 (제목용, 본문용, 코드용)
|
|
5
|
+
- 크기로 계층 표현
|
|
6
|
+
- 일관된 스타일
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
> 폰트가 많을수록 디자인은 산만해집니다.
|
|
12
|
-
|
|
13
|
-
```
|
|
14
|
-
권장 구성:
|
|
15
|
-
- 제목용 폰트 1개
|
|
16
|
-
- 본문용 폰트 1개
|
|
17
|
-
- (선택) 코드용 폰트 1개
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
### 2. 크기로 계층 표현
|
|
21
|
-
|
|
22
|
-
> 제목과 본문의 크기 차이로 중요도를 표현합니다.
|
|
23
|
-
|
|
24
|
-
```
|
|
25
|
-
h1 (제목) ━━━━━━━━━━━━━━━━━━━━━━━ 가장 큼
|
|
26
|
-
h2 (부제목) ━━━━━━━━━━━━━━━━━
|
|
27
|
-
h3 (소제목) ━━━━━━━━━━━━
|
|
28
|
-
p (본문) ━━━━━━━━ 기준 크기
|
|
29
|
-
small ━━━━━━ 가장 작음
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
### 3. 일관된 스타일
|
|
33
|
-
|
|
34
|
-
> 같은 역할의 텍스트는 항상 같은 스타일을 사용합니다.
|
|
35
|
-
|
|
36
|
-
## 폰트 선택 가이드
|
|
37
|
-
|
|
38
|
-
### Sans-serif (산세리프) - 본문에 권장
|
|
39
|
-
|
|
40
|
-
획의 끝에 장식이 없는 깔끔한 폰트입니다.
|
|
41
|
-
|
|
42
|
-
| 폰트 | 특징 | 추천 용도 |
|
|
43
|
-
|------|------|----------|
|
|
44
|
-
| **Inter** | 현대적, 가독성 뛰어남 | 웹 전반 |
|
|
45
|
-
| **Pretendard** | 한글 최적화, Inter 호환 | 한글 프로젝트 |
|
|
46
|
-
| **Noto Sans KR** | 한글/영문 균형 | 다국어 프로젝트 |
|
|
47
|
-
| **Roboto** | Google 스타일 | Material Design |
|
|
48
|
-
| **SF Pro** | Apple 스타일 | iOS/macOS 앱 |
|
|
49
|
-
|
|
50
|
-
### Serif (세리프) - 제목에 선택적
|
|
51
|
-
|
|
52
|
-
획의 끝에 장식(세리프)이 있는 전통적인 폰트입니다.
|
|
53
|
-
|
|
54
|
-
| 폰트 | 특징 | 추천 용도 |
|
|
55
|
-
|------|------|----------|
|
|
56
|
-
| **Georgia** | 클래식, 신뢰감 | 블로그, 뉴스 |
|
|
57
|
-
| **Merriweather** | 가독성 높음 | 긴 글 읽기 |
|
|
58
|
-
| **Noto Serif KR** | 한글 세리프 | 격식있는 제목 |
|
|
59
|
-
|
|
60
|
-
### Monospace (고정폭) - 코드용
|
|
8
|
+
## 폰트 선택
|
|
61
9
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
| 폰트 | 특징 |
|
|
10
|
+
| 폰트 | 용도 |
|
|
65
11
|
|------|------|
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
|
|
|
69
|
-
|
|
70
|
-
## 폰트 크기 시스템
|
|
12
|
+
| Pretendard | 한글 프로젝트 (권장) |
|
|
13
|
+
| Inter | 영문, 웹 전반 |
|
|
14
|
+
| Noto Sans KR | 다국어 프로젝트 |
|
|
15
|
+
| JetBrains Mono | 코드 |
|
|
71
16
|
|
|
72
|
-
|
|
17
|
+
## 크기 시스템
|
|
73
18
|
|
|
74
19
|
```css
|
|
75
|
-
|
|
76
|
-
text-
|
|
77
|
-
text-
|
|
78
|
-
text-
|
|
79
|
-
text-
|
|
80
|
-
text-
|
|
81
|
-
text-
|
|
82
|
-
text-
|
|
83
|
-
text-
|
|
84
|
-
text-5xl 3rem 48px 히어로 제목
|
|
20
|
+
text-xs 12px 작은 레이블
|
|
21
|
+
text-sm 14px 보조 텍스트
|
|
22
|
+
text-base 16px 본문 (기본)
|
|
23
|
+
text-lg 18px 강조 본문
|
|
24
|
+
text-xl 20px 소제목 (h4)
|
|
25
|
+
text-2xl 24px 섹션 제목 (h3)
|
|
26
|
+
text-3xl 30px 부제목 (h2)
|
|
27
|
+
text-4xl 36px 페이지 제목 (h1)
|
|
28
|
+
text-5xl 48px 히어로
|
|
85
29
|
```
|
|
86
30
|
|
|
87
|
-
### 실제 적용 예시
|
|
88
|
-
|
|
89
31
|
```tsx
|
|
90
|
-
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
<h2 className="text-2xl font-semibold">최근 활동</h2>
|
|
95
|
-
|
|
96
|
-
// 카드 제목
|
|
97
|
-
<h3 className="text-xl font-medium">사용자 통계</h3>
|
|
98
|
-
|
|
99
|
-
// 본문
|
|
100
|
-
<p className="text-base">오늘 방문자 수는 1,234명입니다.</p>
|
|
101
|
-
|
|
102
|
-
// 보조 텍스트
|
|
103
|
-
<span className="text-sm text-gray-500">5분 전 업데이트</span>
|
|
32
|
+
<h1 className="text-4xl font-bold">제목</h1>
|
|
33
|
+
<h2 className="text-2xl font-semibold">섹션</h2>
|
|
34
|
+
<p className="text-base">본문</p>
|
|
35
|
+
<span className="text-sm text-gray-500">보조</span>
|
|
104
36
|
```
|
|
105
37
|
|
|
106
38
|
## 줄 간격 (Line Height)
|
|
107
39
|
|
|
108
|
-
### 왜 중요한가?
|
|
109
|
-
|
|
110
|
-
줄 간격이 좁으면 답답하고, 너무 넓으면 텍스트가 흩어져 보입니다.
|
|
111
|
-
|
|
112
|
-
```
|
|
113
|
-
줄 간격 너무 좁음 (1.2) 적절한 줄 간격 (1.5)
|
|
114
|
-
━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━
|
|
115
|
-
이 텍스트는 줄 간격이 이 텍스트는 줄 간격이
|
|
116
|
-
너무 좁아서 읽기가 적절해서 읽기가
|
|
117
|
-
불편합니다. 편합니다.
|
|
118
|
-
━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
### Tailwind 줄 간격
|
|
122
|
-
|
|
123
40
|
```css
|
|
124
|
-
leading-
|
|
125
|
-
leading-
|
|
126
|
-
leading-snug 1.375 부제목
|
|
127
|
-
leading-normal 1.5 본문 (기본, 권장)
|
|
41
|
+
leading-tight 1.25 제목
|
|
42
|
+
leading-normal 1.5 본문 (권장)
|
|
128
43
|
leading-relaxed 1.625 긴 글
|
|
129
|
-
leading-loose 2 매우 여유로운 글
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
### 텍스트 길이별 권장 줄 간격
|
|
133
|
-
|
|
134
|
-
```tsx
|
|
135
|
-
// 짧은 제목 - 타이트하게
|
|
136
|
-
<h1 className="text-4xl leading-tight">한 줄 제목</h1>
|
|
137
|
-
|
|
138
|
-
// 긴 본문 - 여유롭게
|
|
139
|
-
<p className="text-base leading-relaxed">
|
|
140
|
-
긴 본문 텍스트는 줄 간격이 넉넉해야
|
|
141
|
-
읽기가 편합니다.
|
|
142
|
-
</p>
|
|
143
44
|
```
|
|
144
45
|
|
|
145
|
-
##
|
|
146
|
-
|
|
147
|
-
### Tailwind 굵기 옵션
|
|
46
|
+
## 굵기 (Font Weight)
|
|
148
47
|
|
|
149
48
|
```css
|
|
150
|
-
font-
|
|
151
|
-
font-
|
|
152
|
-
font-
|
|
153
|
-
font-
|
|
154
|
-
font-medium 500 약간 강조
|
|
155
|
-
font-semibold 600 부제목, 버튼
|
|
156
|
-
font-bold 700 제목, 강조
|
|
157
|
-
font-extrabold 800 히어로 제목
|
|
158
|
-
font-black 900 임팩트 제목
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
### 계층별 권장 굵기
|
|
162
|
-
|
|
163
|
-
```tsx
|
|
164
|
-
// 메인 제목 - 볼드
|
|
165
|
-
<h1 className="text-4xl font-bold">제목</h1>
|
|
166
|
-
|
|
167
|
-
// 부제목 - 세미볼드
|
|
168
|
-
<h2 className="text-2xl font-semibold">부제목</h2>
|
|
169
|
-
|
|
170
|
-
// 본문 - 일반
|
|
171
|
-
<p className="text-base font-normal">본문 텍스트</p>
|
|
172
|
-
|
|
173
|
-
// 강조 - 미디엄
|
|
174
|
-
<strong className="font-medium">중요한 내용</strong>
|
|
49
|
+
font-normal 400 본문
|
|
50
|
+
font-medium 500 약간 강조
|
|
51
|
+
font-semibold 600 부제목, 버튼
|
|
52
|
+
font-bold 700 제목
|
|
175
53
|
```
|
|
176
54
|
|
|
177
55
|
## 가독성 최적화
|
|
178
56
|
|
|
179
|
-
### 줄 길이 (Line Length)
|
|
180
|
-
|
|
181
|
-
> 한 줄에 너무 많은 글자는 읽기 어렵습니다.
|
|
182
|
-
|
|
183
|
-
```
|
|
184
|
-
권장: 45-75자 (영문 기준)
|
|
185
|
-
한글: 25-35자
|
|
186
|
-
|
|
187
|
-
Tailwind로 제한:
|
|
188
|
-
max-w-prose → 65ch (약 65자)
|
|
189
|
-
max-w-xl → 36rem
|
|
190
|
-
max-w-2xl → 42rem
|
|
191
|
-
```
|
|
192
|
-
|
|
193
57
|
```tsx
|
|
194
|
-
//
|
|
58
|
+
// 줄 길이 제한 (45-75자)
|
|
195
59
|
<article className="max-w-prose mx-auto">
|
|
196
|
-
<p>이 텍스트는 적절한 줄 길이로 제한되어 읽기 편합니다.</p>
|
|
197
|
-
</article>
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
### 글자 간격 (Letter Spacing)
|
|
201
|
-
|
|
202
|
-
```css
|
|
203
|
-
tracking-tighter -0.05em 대형 제목
|
|
204
|
-
tracking-tight -0.025em 제목
|
|
205
|
-
tracking-normal 0 본문 (기본)
|
|
206
|
-
tracking-wide 0.025em 대문자, 작은 텍스트
|
|
207
|
-
tracking-wider 0.05em 레이블
|
|
208
|
-
tracking-widest 0.1em 버튼 텍스트
|
|
209
|
-
```
|
|
210
60
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
<
|
|
214
|
-
|
|
215
|
-
// 작은 레이블 - 약간 넓게
|
|
216
|
-
<span className="text-xs tracking-wide uppercase">NEW</span>
|
|
61
|
+
// 글자 간격
|
|
62
|
+
<h1 className="text-5xl tracking-tight">히어로</h1>
|
|
63
|
+
<span className="text-xs tracking-wide uppercase">LABEL</span>
|
|
217
64
|
```
|
|
218
65
|
|
|
219
|
-
##
|
|
220
|
-
|
|
221
|
-
### 완성된 텍스트 스타일 세트
|
|
222
|
-
|
|
223
|
-
```tsx
|
|
224
|
-
// src/components/ui/typography.tsx
|
|
225
|
-
|
|
226
|
-
// 페이지 제목
|
|
227
|
-
export const PageTitle = ({ children }) => (
|
|
228
|
-
<h1 className="text-4xl font-bold tracking-tight text-gray-900 dark:text-white">
|
|
229
|
-
{children}
|
|
230
|
-
</h1>
|
|
231
|
-
)
|
|
232
|
-
|
|
233
|
-
// 섹션 제목
|
|
234
|
-
export const SectionTitle = ({ children }) => (
|
|
235
|
-
<h2 className="text-2xl font-semibold text-gray-900 dark:text-white">
|
|
236
|
-
{children}
|
|
237
|
-
</h2>
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
// 카드 제목
|
|
241
|
-
export const CardTitle = ({ children }) => (
|
|
242
|
-
<h3 className="text-lg font-medium text-gray-900 dark:text-white">
|
|
243
|
-
{children}
|
|
244
|
-
</h3>
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
// 본문
|
|
248
|
-
export const Body = ({ children }) => (
|
|
249
|
-
<p className="text-base leading-relaxed text-gray-700 dark:text-gray-300">
|
|
250
|
-
{children}
|
|
251
|
-
</p>
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
// 보조 텍스트
|
|
255
|
-
export const Caption = ({ children }) => (
|
|
256
|
-
<span className="text-sm text-gray-500 dark:text-gray-400">
|
|
257
|
-
{children}
|
|
258
|
-
</span>
|
|
259
|
-
)
|
|
260
|
-
|
|
261
|
-
// 레이블
|
|
262
|
-
export const Label = ({ children }) => (
|
|
263
|
-
<span className="text-xs font-medium uppercase tracking-wide text-gray-500">
|
|
264
|
-
{children}
|
|
265
|
-
</span>
|
|
266
|
-
)
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
## 폰트 설정
|
|
270
|
-
|
|
271
|
-
### Tailwind CSS v4 폰트 설정
|
|
66
|
+
## Tailwind 폰트 설정
|
|
272
67
|
|
|
273
68
|
```css
|
|
274
|
-
/* src/styles/app.css */
|
|
275
|
-
@import "tailwindcss";
|
|
276
|
-
|
|
277
69
|
@theme {
|
|
278
|
-
/* 폰트 패밀리 */
|
|
279
70
|
--font-sans: "Pretendard", "Inter", system-ui, sans-serif;
|
|
280
|
-
--font-mono: "JetBrains Mono",
|
|
71
|
+
--font-mono: "JetBrains Mono", monospace;
|
|
281
72
|
}
|
|
282
73
|
```
|
|
283
74
|
|
|
284
|
-
### Next.js / TanStack Start 폰트 로딩
|
|
285
|
-
|
|
286
|
-
```tsx
|
|
287
|
-
// Google Fonts 사용 시
|
|
288
|
-
// src/routes/__root.tsx
|
|
289
|
-
import { Link } from '@tanstack/react-router'
|
|
290
|
-
|
|
291
|
-
export const Route = createRootRoute({
|
|
292
|
-
component: () => (
|
|
293
|
-
<html>
|
|
294
|
-
<head>
|
|
295
|
-
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
296
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
|
|
297
|
-
<link
|
|
298
|
-
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
|
299
|
-
rel="stylesheet"
|
|
300
|
-
/>
|
|
301
|
-
</head>
|
|
302
|
-
<body className="font-sans">
|
|
303
|
-
<Outlet />
|
|
304
|
-
</body>
|
|
305
|
-
</html>
|
|
306
|
-
),
|
|
307
|
-
})
|
|
308
|
-
```
|
|
309
|
-
|
|
310
75
|
## 체크리스트
|
|
311
76
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
- [ ]
|
|
315
|
-
- [ ]
|
|
316
|
-
- [ ] 본문 줄 간격 1.5 이상
|
|
317
|
-
- [ ] 제목과 본문의 명확한 크기 차이
|
|
318
|
-
|
|
319
|
-
### 권장 사항
|
|
320
|
-
|
|
321
|
-
- [ ] 한글 최적화 폰트 사용 (Pretendard, Noto Sans KR)
|
|
322
|
-
- [ ] 텍스트 컨테이너 최대 너비 제한
|
|
323
|
-
- [ ] 일관된 텍스트 스타일 컴포넌트 사용
|
|
324
|
-
- [ ] 다크 모드 텍스트 색상 대응
|
|
77
|
+
- [ ] 폰트 2-3개 이하
|
|
78
|
+
- [ ] 본문 16px 이상
|
|
79
|
+
- [ ] 줄 간격 1.5 이상
|
|
80
|
+
- [ ] 제목/본문 명확한 크기 차이
|
|
@@ -1,136 +1,48 @@
|
|
|
1
1
|
# Better Auth - 2단계 인증 (2FA)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## 서버 설정
|
|
3
|
+
## 서버
|
|
6
4
|
|
|
7
5
|
```typescript
|
|
8
|
-
import { betterAuth } from 'better-auth'
|
|
9
6
|
import { twoFactor } from 'better-auth/plugins'
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
},
|
|
23
|
-
period: 300, // 5분
|
|
24
|
-
length: 6,
|
|
25
|
-
},
|
|
26
|
-
backupCodeLength: 10,
|
|
27
|
-
backupCodeCount: 10,
|
|
28
|
-
}),
|
|
29
|
-
],
|
|
30
|
-
})
|
|
8
|
+
plugins: [
|
|
9
|
+
twoFactor({
|
|
10
|
+
issuer: 'My App',
|
|
11
|
+
otpOptions: {
|
|
12
|
+
async sendOTP({ user, otp }) { await sendEmail({ to: user.email, html: `Code: ${otp}` }) },
|
|
13
|
+
period: 300, length: 6,
|
|
14
|
+
},
|
|
15
|
+
backupCodeLength: 10,
|
|
16
|
+
backupCodeCount: 10,
|
|
17
|
+
}),
|
|
18
|
+
]
|
|
31
19
|
```
|
|
32
20
|
|
|
33
|
-
## 클라이언트
|
|
21
|
+
## 클라이언트
|
|
34
22
|
|
|
35
23
|
```typescript
|
|
36
24
|
import { twoFactorClient } from 'better-auth/client/plugins'
|
|
37
|
-
|
|
38
|
-
const authClient = createAuthClient({
|
|
39
|
-
plugins: [
|
|
40
|
-
twoFactorClient({
|
|
41
|
-
twoFactorPage: '/two-factor',
|
|
42
|
-
}),
|
|
43
|
-
],
|
|
44
|
-
})
|
|
25
|
+
plugins: [twoFactorClient({ twoFactorPage: '/two-factor' })]
|
|
45
26
|
```
|
|
46
27
|
|
|
47
|
-
##
|
|
28
|
+
## 사용법
|
|
48
29
|
|
|
49
30
|
```typescript
|
|
50
|
-
//
|
|
51
|
-
const { data } = await authClient.twoFactor.enable({
|
|
52
|
-
|
|
53
|
-
})
|
|
54
|
-
console.log('TOTP URI:', data.totpURI)
|
|
55
|
-
console.log('Backup codes:', data.backupCodes)
|
|
56
|
-
```
|
|
31
|
+
// 활성화
|
|
32
|
+
const { data } = await authClient.twoFactor.enable({ password })
|
|
33
|
+
// data.totpURI, data.backupCodes
|
|
57
34
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
```typescript
|
|
61
|
-
// TOTP 코드 검증
|
|
62
|
-
await authClient.twoFactor.verifyTotp({
|
|
63
|
-
code: '123456',
|
|
64
|
-
})
|
|
65
|
-
```
|
|
35
|
+
// TOTP 검증
|
|
36
|
+
await authClient.twoFactor.verifyTotp({ code: '123456' })
|
|
66
37
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
// OTP 전송
|
|
38
|
+
// OTP 전송 및 검증
|
|
71
39
|
await authClient.twoFactor.sendOtp()
|
|
40
|
+
await authClient.twoFactor.verifyOtp({ code })
|
|
72
41
|
|
|
73
|
-
//
|
|
74
|
-
await authClient.twoFactor.
|
|
75
|
-
|
|
76
|
-
})
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
## 백업 코드
|
|
80
|
-
|
|
81
|
-
```typescript
|
|
82
|
-
// 백업 코드 사용
|
|
83
|
-
await authClient.twoFactor.useBackupCode({
|
|
84
|
-
code: 'ABCD-1234-EFGH',
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
// 백업 코드 재생성
|
|
88
|
-
const { data: newCodes } = await authClient.twoFactor.regenerateBackupCodes({
|
|
89
|
-
password: 'userPassword123',
|
|
90
|
-
})
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## 2FA 비활성화
|
|
94
|
-
|
|
95
|
-
```typescript
|
|
96
|
-
await authClient.twoFactor.disable({
|
|
97
|
-
password: 'userPassword123',
|
|
98
|
-
})
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
## 2FA 페이지 구현
|
|
102
|
-
|
|
103
|
-
```tsx
|
|
104
|
-
// pages/two-factor.tsx
|
|
105
|
-
import { useState } from 'react'
|
|
106
|
-
import { authClient } from '@/lib/auth-client'
|
|
107
|
-
|
|
108
|
-
export default function TwoFactorPage() {
|
|
109
|
-
const [code, setCode] = useState('')
|
|
110
|
-
const [error, setError] = useState('')
|
|
111
|
-
|
|
112
|
-
const handleVerify = async () => {
|
|
113
|
-
try {
|
|
114
|
-
await authClient.twoFactor.verifyTotp({ code })
|
|
115
|
-
// 성공 시 대시보드로 이동
|
|
116
|
-
window.location.href = '/dashboard'
|
|
117
|
-
} catch (e) {
|
|
118
|
-
setError('Invalid code')
|
|
119
|
-
}
|
|
120
|
-
}
|
|
42
|
+
// 백업 코드
|
|
43
|
+
await authClient.twoFactor.useBackupCode({ code: 'ABCD-1234' })
|
|
44
|
+
await authClient.twoFactor.regenerateBackupCodes({ password })
|
|
121
45
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
<h1>2단계 인증</h1>
|
|
125
|
-
<input
|
|
126
|
-
type="text"
|
|
127
|
-
value={code}
|
|
128
|
-
onChange={(e) => setCode(e.target.value)}
|
|
129
|
-
placeholder="6자리 코드 입력"
|
|
130
|
-
/>
|
|
131
|
-
<button onClick={handleVerify}>확인</button>
|
|
132
|
-
{error && <p style={{ color: 'red' }}>{error}</p>}
|
|
133
|
-
</div>
|
|
134
|
-
)
|
|
135
|
-
}
|
|
46
|
+
// 비활성화
|
|
47
|
+
await authClient.twoFactor.disable({ password })
|
|
136
48
|
```
|
|
@@ -1,138 +1,55 @@
|
|
|
1
1
|
# Better Auth - 고급 기능
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## CAPTCHA 보호
|
|
3
|
+
## CAPTCHA
|
|
6
4
|
|
|
7
5
|
```typescript
|
|
8
|
-
|
|
6
|
+
// 서버
|
|
9
7
|
import { captcha } from 'better-auth/plugins'
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
siteKey: process.env.RECAPTCHA_SITE_KEY!,
|
|
16
|
-
secretKey: process.env.RECAPTCHA_SECRET_KEY!,
|
|
17
|
-
protectEndpoints: ['/sign-up/email', '/sign-in/email'],
|
|
18
|
-
}),
|
|
19
|
-
],
|
|
20
|
-
})
|
|
8
|
+
plugins: [captcha({
|
|
9
|
+
provider: 'recaptcha',
|
|
10
|
+
siteKey: SITE_KEY, secretKey: SECRET_KEY,
|
|
11
|
+
protectEndpoints: ['/sign-up/email', '/sign-in/email'],
|
|
12
|
+
})]
|
|
21
13
|
|
|
22
14
|
// 클라이언트
|
|
23
15
|
import { captchaClient } from 'better-auth/client/plugins'
|
|
24
|
-
|
|
25
|
-
const authClient = createAuthClient({
|
|
26
|
-
plugins: [
|
|
27
|
-
captchaClient({
|
|
28
|
-
siteKey: process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY!,
|
|
29
|
-
}),
|
|
30
|
-
],
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
// CAPTCHA는 보호된 요청에 자동 포함됨
|
|
34
|
-
await authClient.signUp.email({
|
|
35
|
-
email: 'user@example.com',
|
|
36
|
-
password: 'SecurePassword123!',
|
|
37
|
-
})
|
|
16
|
+
plugins: [captchaClient({ siteKey: SITE_KEY })]
|
|
38
17
|
```
|
|
39
18
|
|
|
40
|
-
## SSO
|
|
19
|
+
## SSO
|
|
41
20
|
|
|
42
21
|
```typescript
|
|
43
|
-
|
|
44
|
-
const res = await authClient.signIn.sso({
|
|
45
|
-
providerId: 'example-provider-id',
|
|
46
|
-
callbackURL: '/dashboard',
|
|
47
|
-
})
|
|
22
|
+
await authClient.signIn.sso({ providerId: 'provider-id', callbackURL: '/dashboard' })
|
|
48
23
|
```
|
|
49
24
|
|
|
50
|
-
## SIWE (
|
|
51
|
-
|
|
52
|
-
### 서버 설정
|
|
25
|
+
## SIWE (Ethereum)
|
|
53
26
|
|
|
54
27
|
```typescript
|
|
55
|
-
|
|
28
|
+
// 서버
|
|
56
29
|
import { siwe } from 'better-auth/plugins'
|
|
30
|
+
plugins: [siwe({ domain: 'example.com', uri: 'https://example.com' })]
|
|
57
31
|
|
|
58
|
-
|
|
59
|
-
plugins: [
|
|
60
|
-
siwe({
|
|
61
|
-
domain: 'example.com',
|
|
62
|
-
uri: 'https://example.com',
|
|
63
|
-
statement: 'Sign in with your Ethereum account',
|
|
64
|
-
}),
|
|
65
|
-
],
|
|
66
|
-
})
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
### 클라이언트 사용
|
|
70
|
-
|
|
71
|
-
```typescript
|
|
72
|
-
import { siweClient } from 'better-auth/client/plugins'
|
|
73
|
-
import { ethers } from 'ethers'
|
|
74
|
-
|
|
75
|
-
const authClient = createAuthClient({
|
|
76
|
-
plugins: [siweClient()],
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
// Nonce 가져오기
|
|
32
|
+
// 클라이언트
|
|
80
33
|
const { data: nonce } = await authClient.siwe.getNonce()
|
|
81
|
-
|
|
82
|
-
// 메시지 생성 및 서명
|
|
83
|
-
const provider = new ethers.BrowserProvider(window.ethereum)
|
|
84
|
-
const signer = await provider.getSigner()
|
|
85
|
-
const address = await signer.getAddress()
|
|
86
|
-
|
|
87
|
-
const message = await authClient.siwe.prepareMessage({
|
|
88
|
-
address,
|
|
89
|
-
nonce: nonce.nonce,
|
|
90
|
-
})
|
|
91
|
-
|
|
34
|
+
const message = await authClient.siwe.prepareMessage({ address, nonce: nonce.nonce })
|
|
92
35
|
const signature = await signer.signMessage(message)
|
|
93
|
-
|
|
94
|
-
// 검증 및 로그인
|
|
95
|
-
await authClient.siwe.signIn({
|
|
96
|
-
message,
|
|
97
|
-
signature,
|
|
98
|
-
})
|
|
36
|
+
await authClient.siwe.signIn({ message, signature })
|
|
99
37
|
```
|
|
100
38
|
|
|
101
39
|
## Stateless 모드
|
|
102
40
|
|
|
103
|
-
데이터베이스 없이 사용:
|
|
104
|
-
|
|
105
41
|
```typescript
|
|
106
|
-
|
|
107
|
-
|
|
42
|
+
// DB 없이 소셜 로그인만
|
|
108
43
|
export const auth = betterAuth({
|
|
109
|
-
|
|
110
|
-
socialProviders: {
|
|
111
|
-
google: {
|
|
112
|
-
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
113
|
-
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
114
|
-
},
|
|
115
|
-
},
|
|
44
|
+
socialProviders: { google: { clientId, clientSecret } },
|
|
116
45
|
})
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
### Redis와 함께 하이브리드 모드
|
|
120
|
-
|
|
121
|
-
```typescript
|
|
122
|
-
import { betterAuth } from 'better-auth'
|
|
123
|
-
import { redis } from './redis'
|
|
124
46
|
|
|
47
|
+
// Redis 하이브리드
|
|
125
48
|
export const auth = betterAuth({
|
|
126
49
|
secondaryStorage: {
|
|
127
|
-
get:
|
|
128
|
-
set:
|
|
129
|
-
delete:
|
|
130
|
-
},
|
|
131
|
-
session: {
|
|
132
|
-
cookieCache: {
|
|
133
|
-
maxAge: 5 * 60, // 5분
|
|
134
|
-
refreshCache: false,
|
|
135
|
-
},
|
|
50
|
+
get: (key) => redis.get(key),
|
|
51
|
+
set: (key, value, ttl) => redis.set(key, value, 'EX', ttl),
|
|
52
|
+
delete: (key) => redis.del(key),
|
|
136
53
|
},
|
|
137
54
|
})
|
|
138
55
|
```
|