@kood/claude-code 0.1.5 → 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 +105 -259
- 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/guides/best-practices.md +3 -8
- package/templates/tanstack-start/docs/guides/env-setup.md +3 -3
- 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 +13 -113
- 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 -73
- 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 +41 -172
- 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
|
+
- [ ] 제목/본문 명확한 크기 차이
|
|
@@ -304,7 +304,6 @@ const UsersPage = (): JSX.Element => {
|
|
|
304
304
|
import { useState, useMemo, useEffect, useCallback } from 'react'
|
|
305
305
|
import { useParams, useNavigate } from '@tanstack/react-router'
|
|
306
306
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
307
|
-
import { useServerFn } from '@tanstack/react-start'
|
|
308
307
|
import { useAuthStore } from '@/stores/auth'
|
|
309
308
|
import { getUsers, createUser, deleteUser } from '@/services/user'
|
|
310
309
|
import type { User } from '@/types'
|
|
@@ -339,24 +338,20 @@ export const useUsers = (): UseUsersReturn => {
|
|
|
339
338
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
340
339
|
// 3. React Query (useQuery → useMutation)
|
|
341
340
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
342
|
-
const getUsersFn = useServerFn(getUsers)
|
|
343
|
-
const createUserFn = useServerFn(createUser)
|
|
344
|
-
const deleteUserFn = useServerFn(deleteUser)
|
|
345
|
-
|
|
346
341
|
const { data: users, isLoading, error } = useQuery({
|
|
347
342
|
queryKey: ['users'],
|
|
348
|
-
queryFn: () =>
|
|
343
|
+
queryFn: () => getUsers(),
|
|
349
344
|
})
|
|
350
345
|
|
|
351
346
|
const createMutation = useMutation({
|
|
352
|
-
mutationFn:
|
|
347
|
+
mutationFn: createUser,
|
|
353
348
|
onSuccess: () => {
|
|
354
349
|
queryClient.invalidateQueries({ queryKey: ['users'] })
|
|
355
350
|
},
|
|
356
351
|
})
|
|
357
352
|
|
|
358
353
|
const deleteMutation = useMutation({
|
|
359
|
-
mutationFn:
|
|
354
|
+
mutationFn: deleteUser,
|
|
360
355
|
onSuccess: () => {
|
|
361
356
|
queryClient.invalidateQueries({ queryKey: ['users'] })
|
|
362
357
|
},
|
|
@@ -226,7 +226,7 @@ export const clientEnv = parseClientEnv()
|
|
|
226
226
|
```typescript
|
|
227
227
|
// app/server-functions/users.ts
|
|
228
228
|
import { createServerFn } from '@tanstack/react-start'
|
|
229
|
-
import { getServerEnv } from '
|
|
229
|
+
import { getServerEnv } from '@/config/env'
|
|
230
230
|
|
|
231
231
|
export const getUsers = createServerFn({ method: 'GET' })
|
|
232
232
|
.handler(async () => {
|
|
@@ -248,7 +248,7 @@ export const getUsers = createServerFn({ method: 'GET' })
|
|
|
248
248
|
|
|
249
249
|
```tsx
|
|
250
250
|
// app/components/AppHeader.tsx
|
|
251
|
-
import { clientEnv } from '
|
|
251
|
+
import { clientEnv } from '@/config/env'
|
|
252
252
|
|
|
253
253
|
export const AppHeader = () => {
|
|
254
254
|
return (
|
|
@@ -282,7 +282,7 @@ const appName = import.meta.env.VITE_APP_NAME // ✅ VITE_ 접두사만
|
|
|
282
282
|
|
|
283
283
|
```typescript
|
|
284
284
|
// app/lib/auth.ts
|
|
285
|
-
import { getServerEnv } from '
|
|
285
|
+
import { getServerEnv } from '@/config/env'
|
|
286
286
|
|
|
287
287
|
export const getAuthConfig = () => {
|
|
288
288
|
const env = getServerEnv()
|
|
@@ -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
|
```
|