@kood/claude-code 0.2.0 → 0.2.2
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 +62 -18
- package/package.json +1 -1
- package/templates/.claude/agents/code-reviewer.md +31 -0
- package/templates/.claude/agents/debug-detective.md +37 -0
- package/templates/.claude/agents/refactor-advisor.md +44 -0
- package/templates/.claude/agents/test-writer.md +41 -0
- package/templates/.claude/skills/frontend-design/SKILL.md +310 -0
- package/templates/.claude/skills/frontend-design/references/animation-patterns.md +446 -0
- package/templates/.claude/skills/frontend-design/references/colors-2026.md +244 -0
- package/templates/.claude/skills/frontend-design/references/typography-2026.md +302 -0
- package/templates/.claude/skills/gemini-review/SKILL.md +1 -1
- package/templates/hono/docs/library/drizzle/cloudflare-d1.md +247 -0
- package/templates/hono/docs/library/drizzle/config.md +167 -0
- package/templates/hono/docs/library/drizzle/index.md +259 -0
- package/templates/tanstack-start/docs/library/drizzle/cloudflare-d1.md +146 -0
- package/templates/tanstack-start/docs/library/drizzle/config.md +118 -0
- package/templates/tanstack-start/docs/library/drizzle/crud.md +205 -0
- package/templates/tanstack-start/docs/library/drizzle/index.md +79 -0
- package/templates/tanstack-start/docs/library/drizzle/relations.md +202 -0
- package/templates/tanstack-start/docs/library/drizzle/schema.md +154 -0
- package/templates/tanstack-start/docs/library/drizzle/setup.md +95 -0
- package/templates/tanstack-start/docs/library/drizzle/transactions.md +127 -0
- package/templates/tanstack-start/docs/library/tanstack-router/error-handling.md +204 -0
- package/templates/tanstack-start/docs/library/tanstack-router/hooks.md +195 -0
- package/templates/tanstack-start/docs/library/tanstack-router/index.md +150 -0
- package/templates/tanstack-start/docs/library/tanstack-router/navigation.md +150 -0
- package/templates/tanstack-start/docs/library/tanstack-router/route-context.md +203 -0
- package/templates/tanstack-start/docs/library/tanstack-router/search-params.md +213 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# Typography 2026 Guide
|
|
2
|
+
|
|
3
|
+
## 핵심 원칙
|
|
4
|
+
|
|
5
|
+
1. **Variable Fonts 사용** - 단일 파일로 다양한 웨이트
|
|
6
|
+
2. **명확한 위계** - Display, Body, Mono 구분
|
|
7
|
+
3. **독창적 선택** - 일반적인 폰트 회피
|
|
8
|
+
4. **성능 최적화** - font-display: swap
|
|
9
|
+
|
|
10
|
+
## Variable Fonts
|
|
11
|
+
|
|
12
|
+
### 설정
|
|
13
|
+
|
|
14
|
+
```css
|
|
15
|
+
@font-face {
|
|
16
|
+
font-family: 'Display';
|
|
17
|
+
src: url('/fonts/display-variable.woff2') format('woff2');
|
|
18
|
+
font-weight: 100 900; /* 가변 범위 */
|
|
19
|
+
font-style: normal;
|
|
20
|
+
font-display: swap; /* 필수 */
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@font-face {
|
|
24
|
+
font-family: 'Body';
|
|
25
|
+
src: url('/fonts/body-variable.woff2') format('woff2');
|
|
26
|
+
font-weight: 300 700;
|
|
27
|
+
font-style: normal;
|
|
28
|
+
font-display: swap;
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 사용
|
|
33
|
+
|
|
34
|
+
```css
|
|
35
|
+
:root {
|
|
36
|
+
--font-display: 'Display', serif;
|
|
37
|
+
--font-body: 'Body', sans-serif;
|
|
38
|
+
--font-mono: 'JetBrains Mono', monospace;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
h1 {
|
|
42
|
+
font-family: var(--font-display);
|
|
43
|
+
font-weight: 800; /* 가변 웨이트 */
|
|
44
|
+
font-variation-settings: 'wght' 800; /* 세밀 조절 */
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 타입 스케일
|
|
49
|
+
|
|
50
|
+
### Fluid Typography
|
|
51
|
+
|
|
52
|
+
```css
|
|
53
|
+
:root {
|
|
54
|
+
/* Fluid scale - clamp(min, preferred, max) */
|
|
55
|
+
--text-xs: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem);
|
|
56
|
+
--text-sm: clamp(0.875rem, 0.8rem + 0.35vw, 1rem);
|
|
57
|
+
--text-base: clamp(1rem, 0.9rem + 0.5vw, 1.125rem);
|
|
58
|
+
--text-lg: clamp(1.125rem, 1rem + 0.6vw, 1.25rem);
|
|
59
|
+
--text-xl: clamp(1.25rem, 1.1rem + 0.75vw, 1.5rem);
|
|
60
|
+
--text-2xl: clamp(1.5rem, 1.2rem + 1.5vw, 2rem);
|
|
61
|
+
--text-3xl: clamp(2rem, 1.5rem + 2.5vw, 3rem);
|
|
62
|
+
--text-4xl: clamp(2.5rem, 1.8rem + 3.5vw, 4rem);
|
|
63
|
+
--text-5xl: clamp(3rem, 2rem + 5vw, 6rem);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Line Height
|
|
68
|
+
|
|
69
|
+
```css
|
|
70
|
+
:root {
|
|
71
|
+
--leading-tight: 1.1; /* 대형 헤드라인 */
|
|
72
|
+
--leading-snug: 1.25; /* 소형 헤드라인 */
|
|
73
|
+
--leading-normal: 1.5; /* 본문 */
|
|
74
|
+
--leading-relaxed: 1.75; /* 긴 텍스트 */
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Letter Spacing
|
|
79
|
+
|
|
80
|
+
```css
|
|
81
|
+
:root {
|
|
82
|
+
--tracking-tight: -0.02em; /* 대형 타이틀 */
|
|
83
|
+
--tracking-normal: 0; /* 기본 */
|
|
84
|
+
--tracking-wide: 0.05em; /* 캡션, 라벨 */
|
|
85
|
+
--tracking-wider: 0.1em; /* 올캡스 */
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## 폰트 페어링
|
|
90
|
+
|
|
91
|
+
### 클래식 에디토리얼
|
|
92
|
+
|
|
93
|
+
| 용도 | 폰트 | 웨이트 |
|
|
94
|
+
|------|------|--------|
|
|
95
|
+
| Display | Playfair Display | 400-800 |
|
|
96
|
+
| Body | Source Sans 3 | 300-600 |
|
|
97
|
+
| Mono | IBM Plex Mono | 400 |
|
|
98
|
+
|
|
99
|
+
```css
|
|
100
|
+
/* CDN */
|
|
101
|
+
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400..800&family=Source+Sans+3:wght@300..600&display=swap');
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 모던 볼드
|
|
105
|
+
|
|
106
|
+
| 용도 | 폰트 | 웨이트 |
|
|
107
|
+
|------|------|--------|
|
|
108
|
+
| Display | Clash Display | 500-700 |
|
|
109
|
+
| Body | Satoshi | 400-700 |
|
|
110
|
+
| Mono | Fira Code | 400-500 |
|
|
111
|
+
|
|
112
|
+
```css
|
|
113
|
+
/* Fontshare */
|
|
114
|
+
@import url('https://api.fontshare.com/v2/css?f[]=clash-display@500,600,700&f[]=satoshi@400,500,700&display=swap');
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 따뜻한 친근함
|
|
118
|
+
|
|
119
|
+
| 용도 | 폰트 | 웨이트 |
|
|
120
|
+
|------|------|--------|
|
|
121
|
+
| Display | Fraunces | 400-700 |
|
|
122
|
+
| Body | Work Sans | 300-600 |
|
|
123
|
+
| Mono | JetBrains Mono | 400 |
|
|
124
|
+
|
|
125
|
+
### 테크 미니멀
|
|
126
|
+
|
|
127
|
+
| 용도 | 폰트 | 웨이트 |
|
|
128
|
+
|------|------|--------|
|
|
129
|
+
| Display | Space Mono | 400-700 |
|
|
130
|
+
| Body | DM Sans | 400-600 |
|
|
131
|
+
| Mono | Space Mono | 400 |
|
|
132
|
+
|
|
133
|
+
### 럭셔리 정제
|
|
134
|
+
|
|
135
|
+
| 용도 | 폰트 | 웨이트 |
|
|
136
|
+
|------|------|--------|
|
|
137
|
+
| Display | Cormorant Garamond | 300-600 |
|
|
138
|
+
| Body | Lora | 400-600 |
|
|
139
|
+
| Mono | Victor Mono | 400 |
|
|
140
|
+
|
|
141
|
+
### 크리에이티브
|
|
142
|
+
|
|
143
|
+
| 용도 | 폰트 | 웨이트 |
|
|
144
|
+
|------|------|--------|
|
|
145
|
+
| Display | Syne | 400-800 |
|
|
146
|
+
| Body | General Sans | 400-600 |
|
|
147
|
+
| Mono | Monaspace | 400 |
|
|
148
|
+
|
|
149
|
+
## 금지 폰트
|
|
150
|
+
|
|
151
|
+
| 폰트 | 이유 |
|
|
152
|
+
|------|------|
|
|
153
|
+
| Inter | AI 슬롭의 대명사 |
|
|
154
|
+
| Roboto | 무개성, 과다 사용 |
|
|
155
|
+
| Arial | 시스템 기본, 무특징 |
|
|
156
|
+
| Helvetica | 지루함 |
|
|
157
|
+
| Space Grotesk | AI 생성물 단골 |
|
|
158
|
+
| Montserrat | 과다 사용 |
|
|
159
|
+
| Poppins | 과다 사용 |
|
|
160
|
+
| Open Sans | 무개성 |
|
|
161
|
+
|
|
162
|
+
**예외**: Inter는 독창적인 Display 폰트와 함께 Body로 사용 시 허용
|
|
163
|
+
|
|
164
|
+
## 실전 패턴
|
|
165
|
+
|
|
166
|
+
### 기본 설정
|
|
167
|
+
|
|
168
|
+
```css
|
|
169
|
+
html {
|
|
170
|
+
font-family: var(--font-body);
|
|
171
|
+
font-size: var(--text-base);
|
|
172
|
+
line-height: var(--leading-normal);
|
|
173
|
+
-webkit-font-smoothing: antialiased;
|
|
174
|
+
-moz-osx-font-smoothing: grayscale;
|
|
175
|
+
text-rendering: optimizeLegibility;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
h1, h2, h3, h4, h5, h6 {
|
|
179
|
+
font-family: var(--font-display);
|
|
180
|
+
line-height: var(--leading-tight);
|
|
181
|
+
letter-spacing: var(--tracking-tight);
|
|
182
|
+
text-wrap: balance; /* 2026 표준 */
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
p {
|
|
186
|
+
text-wrap: pretty; /* 고아 방지 */
|
|
187
|
+
max-width: 65ch; /* 가독성 */
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
code, pre {
|
|
191
|
+
font-family: var(--font-mono);
|
|
192
|
+
font-size: 0.9em;
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### 대형 헤드라인
|
|
197
|
+
|
|
198
|
+
```css
|
|
199
|
+
.hero-title {
|
|
200
|
+
font-family: var(--font-display);
|
|
201
|
+
font-size: var(--text-5xl);
|
|
202
|
+
font-weight: 800;
|
|
203
|
+
line-height: var(--leading-tight);
|
|
204
|
+
letter-spacing: var(--tracking-tight);
|
|
205
|
+
|
|
206
|
+
/* 실험적 효과 */
|
|
207
|
+
font-variation-settings: 'wght' 800, 'wdth' 110;
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### 캡션/라벨
|
|
212
|
+
|
|
213
|
+
```css
|
|
214
|
+
.label {
|
|
215
|
+
font-family: var(--font-body);
|
|
216
|
+
font-size: var(--text-xs);
|
|
217
|
+
font-weight: 500;
|
|
218
|
+
letter-spacing: var(--tracking-wide);
|
|
219
|
+
text-transform: uppercase;
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### 인용문
|
|
224
|
+
|
|
225
|
+
```css
|
|
226
|
+
blockquote {
|
|
227
|
+
font-family: var(--font-display);
|
|
228
|
+
font-size: var(--text-xl);
|
|
229
|
+
font-style: italic;
|
|
230
|
+
font-weight: 400;
|
|
231
|
+
line-height: var(--leading-snug);
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## 성능 최적화
|
|
236
|
+
|
|
237
|
+
### 서브셋팅
|
|
238
|
+
|
|
239
|
+
```css
|
|
240
|
+
/* 한글 서브셋 */
|
|
241
|
+
@font-face {
|
|
242
|
+
font-family: 'Korean';
|
|
243
|
+
src: url('/fonts/korean-subset.woff2') format('woff2');
|
|
244
|
+
unicode-range: U+AC00-D7AF; /* 한글 음절 */
|
|
245
|
+
font-display: swap;
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### 프리로드
|
|
250
|
+
|
|
251
|
+
```html
|
|
252
|
+
<link rel="preload" href="/fonts/display.woff2" as="font" type="font/woff2" crossorigin>
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Font Loading API
|
|
256
|
+
|
|
257
|
+
```js
|
|
258
|
+
// 폰트 로드 후 클래스 추가
|
|
259
|
+
document.fonts.ready.then(() => {
|
|
260
|
+
document.documentElement.classList.add('fonts-loaded');
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
```css
|
|
265
|
+
/* FOUT 방지 */
|
|
266
|
+
html:not(.fonts-loaded) body {
|
|
267
|
+
opacity: 0;
|
|
268
|
+
}
|
|
269
|
+
html.fonts-loaded body {
|
|
270
|
+
opacity: 1;
|
|
271
|
+
transition: opacity 0.3s;
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## 무료 폰트 소스
|
|
276
|
+
|
|
277
|
+
| 소스 | URL | 특징 |
|
|
278
|
+
|------|-----|------|
|
|
279
|
+
| Google Fonts | fonts.google.com | 최대 규모, Variable 지원 |
|
|
280
|
+
| Fontshare | fontshare.com | 고품질 무료 |
|
|
281
|
+
| Font Squirrel | fontsquirrel.com | 웹폰트 생성기 |
|
|
282
|
+
| Bunny Fonts | fonts.bunny.net | GDPR 준수 |
|
|
283
|
+
|
|
284
|
+
## 한글 폰트
|
|
285
|
+
|
|
286
|
+
| 폰트 | 스타일 | 용도 |
|
|
287
|
+
|------|--------|------|
|
|
288
|
+
| Pretendard | 고딕 | 본문, UI |
|
|
289
|
+
| SUIT | 고딕 | 모던 UI |
|
|
290
|
+
| Noto Sans KR | 고딕 | 범용 |
|
|
291
|
+
| Nanum Myeongjo | 명조 | 에디토리얼 |
|
|
292
|
+
| KoPub 바탕 | 명조 | 본문 |
|
|
293
|
+
| 마루 부리 | 명조 | 독특한 디스플레이 |
|
|
294
|
+
|
|
295
|
+
```css
|
|
296
|
+
/* Pretendard Variable */
|
|
297
|
+
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/variable/pretendardvariable.css');
|
|
298
|
+
|
|
299
|
+
:root {
|
|
300
|
+
--font-korean: 'Pretendard Variable', 'Pretendard', sans-serif;
|
|
301
|
+
}
|
|
302
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gemini-review
|
|
3
|
-
description: Google Gemini CLI를 활용한 코드 리뷰 및 계획 검증 스킬. 구현 계획 검토, 코드 리뷰, 아키텍처 논의 시 Gemini를 세컨드 오피니언으로 활용. gemini-2.5-pro 모델 사용 (무료: 60req/min, 1000req/day).
|
|
3
|
+
description: "Google Gemini CLI를 활용한 코드 리뷰 및 계획 검증 스킬. 구현 계획 검토, 코드 리뷰, 아키텍처 논의 시 Gemini를 세컨드 오피니언으로 활용. gemini-2.5-pro 모델 사용 (무료: 60req/min, 1000req/day)."
|
|
4
4
|
license: Complete terms in LICENSE.txt
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# Drizzle - Cloudflare D1
|
|
2
|
+
|
|
3
|
+
> SQLite 기반 서버리스 데이터베이스
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 주의사항
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
D1은 트랜잭션 미지원 (ACID 보장 X)
|
|
11
|
+
Drizzle Kit 마이그레이션 → wrangler CLI로 적용
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Quick Setup
|
|
17
|
+
|
|
18
|
+
### 설치
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install drizzle-orm
|
|
22
|
+
npm install -D drizzle-kit wrangler
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 스키마 정의
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// src/db/schema.ts
|
|
29
|
+
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core'
|
|
30
|
+
|
|
31
|
+
export const users = sqliteTable('users', {
|
|
32
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
33
|
+
email: text('email').notNull().unique(),
|
|
34
|
+
name: text('name'),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
export type User = typeof users.$inferSelect
|
|
38
|
+
export type NewUser = typeof users.$inferInsert
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### drizzle.config.ts
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { defineConfig } from 'drizzle-kit'
|
|
45
|
+
|
|
46
|
+
export default defineConfig({
|
|
47
|
+
dialect: 'sqlite',
|
|
48
|
+
schema: './src/db/schema.ts',
|
|
49
|
+
out: './drizzle/migrations',
|
|
50
|
+
driver: 'd1-http',
|
|
51
|
+
dbCredentials: {
|
|
52
|
+
accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
|
|
53
|
+
databaseId: process.env.CLOUDFLARE_DATABASE_ID!,
|
|
54
|
+
token: process.env.CLOUDFLARE_D1_TOKEN!,
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Hono + D1
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import { Hono } from 'hono'
|
|
63
|
+
import { drizzle } from 'drizzle-orm/d1'
|
|
64
|
+
import * as schema from './db/schema'
|
|
65
|
+
|
|
66
|
+
type Bindings = { DB: D1Database }
|
|
67
|
+
|
|
68
|
+
const app = new Hono<{ Bindings: Bindings }>()
|
|
69
|
+
|
|
70
|
+
app.get('/users', async (c) => {
|
|
71
|
+
const db = drizzle(c.env.DB, { schema })
|
|
72
|
+
const users = await db.select().from(schema.users)
|
|
73
|
+
return c.json(users)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
export default app
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## D1 데이터베이스 생성
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npx wrangler d1 create my-database
|
|
85
|
+
# database_id = "xxxx-xxxx-xxxx-xxxx"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### wrangler.jsonc
|
|
89
|
+
|
|
90
|
+
```jsonc
|
|
91
|
+
{
|
|
92
|
+
"name": "hono-d1-app",
|
|
93
|
+
"main": "src/index.ts",
|
|
94
|
+
"compatibility_date": "2025-01-01",
|
|
95
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
96
|
+
"d1_databases": [{
|
|
97
|
+
"binding": "DB",
|
|
98
|
+
"database_name": "my-database",
|
|
99
|
+
"database_id": "YOUR_DATABASE_ID",
|
|
100
|
+
"migrations_dir": "drizzle/migrations"
|
|
101
|
+
}]
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 마이그레이션 워크플로우
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# 1. 마이그레이션 파일 생성
|
|
111
|
+
npx drizzle-kit generate
|
|
112
|
+
|
|
113
|
+
# 2. 로컬 적용
|
|
114
|
+
npx wrangler d1 migrations apply my-database --local
|
|
115
|
+
|
|
116
|
+
# 3. 원격 적용
|
|
117
|
+
npx wrangler d1 migrations apply my-database --remote
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## 완전한 CRUD API
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import { Hono } from 'hono'
|
|
126
|
+
import { zValidator } from '@hono/zod-validator'
|
|
127
|
+
import { z } from 'zod'
|
|
128
|
+
import { HTTPException } from 'hono/http-exception'
|
|
129
|
+
import { drizzle } from 'drizzle-orm/d1'
|
|
130
|
+
import { eq } from 'drizzle-orm'
|
|
131
|
+
import * as schema from './db/schema'
|
|
132
|
+
|
|
133
|
+
type Bindings = { DB: D1Database }
|
|
134
|
+
type Variables = { db: ReturnType<typeof drizzle> }
|
|
135
|
+
|
|
136
|
+
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()
|
|
137
|
+
|
|
138
|
+
// Drizzle 미들웨어
|
|
139
|
+
app.use('*', async (c, next) => {
|
|
140
|
+
c.set('db', drizzle(c.env.DB, { schema }))
|
|
141
|
+
await next()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
app.get('/users', async (c) => {
|
|
145
|
+
const db = c.get('db')
|
|
146
|
+
const users = await db.select().from(schema.users)
|
|
147
|
+
return c.json(users)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
app.post('/users',
|
|
151
|
+
zValidator('json', z.object({ email: z.email(), name: z.string().optional() })),
|
|
152
|
+
async (c) => {
|
|
153
|
+
const db = c.get('db')
|
|
154
|
+
const data = c.req.valid('json')
|
|
155
|
+
|
|
156
|
+
const existing = await db.select().from(schema.users).where(eq(schema.users.email, data.email))
|
|
157
|
+
if (existing.length > 0) {
|
|
158
|
+
throw new HTTPException(409, { message: 'Email already exists' })
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const [user] = await db.insert(schema.users).values(data).returning()
|
|
162
|
+
return c.json(user, 201)
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
app.get('/users/:id', async (c) => {
|
|
167
|
+
const db = c.get('db')
|
|
168
|
+
const id = Number(c.req.param('id'))
|
|
169
|
+
|
|
170
|
+
const [user] = await db.select().from(schema.users).where(eq(schema.users.id, id))
|
|
171
|
+
if (!user) {
|
|
172
|
+
throw new HTTPException(404, { message: 'User not found' })
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return c.json(user)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
app.put('/users/:id',
|
|
179
|
+
zValidator('json', z.object({ name: z.string() })),
|
|
180
|
+
async (c) => {
|
|
181
|
+
const db = c.get('db')
|
|
182
|
+
const id = Number(c.req.param('id'))
|
|
183
|
+
const data = c.req.valid('json')
|
|
184
|
+
|
|
185
|
+
const [user] = await db.update(schema.users)
|
|
186
|
+
.set(data)
|
|
187
|
+
.where(eq(schema.users.id, id))
|
|
188
|
+
.returning()
|
|
189
|
+
|
|
190
|
+
if (!user) {
|
|
191
|
+
throw new HTTPException(404, { message: 'User not found' })
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return c.json(user)
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
app.delete('/users/:id', async (c) => {
|
|
199
|
+
const db = c.get('db')
|
|
200
|
+
const id = Number(c.req.param('id'))
|
|
201
|
+
|
|
202
|
+
await db.delete(schema.users).where(eq(schema.users.id, id))
|
|
203
|
+
return c.json({ success: true })
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
export default app
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## 로컬 개발
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
# Worker 실행
|
|
215
|
+
npx wrangler dev
|
|
216
|
+
|
|
217
|
+
# D1 조회
|
|
218
|
+
npx wrangler d1 execute my-database --local \
|
|
219
|
+
--command "SELECT * FROM users;"
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## 배포
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
npx drizzle-kit generate
|
|
228
|
+
npx wrangler d1 migrations apply my-database --remote
|
|
229
|
+
npx wrangler deploy
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## 제한사항
|
|
235
|
+
|
|
236
|
+
| 항목 | D1 |
|
|
237
|
+
|------|-----|
|
|
238
|
+
| 트랜잭션 | 미지원 |
|
|
239
|
+
| 마이그레이션 | wrangler 사용 |
|
|
240
|
+
| 접속 방식 | HTTP (D1 binding) |
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 관련 문서
|
|
245
|
+
|
|
246
|
+
- [Drizzle 개요](./index.md)
|
|
247
|
+
- [Cloudflare 배포](../../deployment/cloudflare.md)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Drizzle - Config 파일
|
|
2
|
+
|
|
3
|
+
> drizzle.config.ts 설정
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 기본 설정
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// drizzle.config.ts
|
|
11
|
+
import { defineConfig } from 'drizzle-kit'
|
|
12
|
+
|
|
13
|
+
export default defineConfig({
|
|
14
|
+
dialect: 'postgresql',
|
|
15
|
+
schema: './src/db/schema/index.ts',
|
|
16
|
+
out: './drizzle/migrations',
|
|
17
|
+
dbCredentials: {
|
|
18
|
+
url: process.env.DATABASE_URL!,
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 파일 위치
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
프로젝트/
|
|
29
|
+
├── drizzle.config.ts # 프로젝트 루트
|
|
30
|
+
├── drizzle/
|
|
31
|
+
│ └── migrations/ # 마이그레이션 파일
|
|
32
|
+
├── src/
|
|
33
|
+
│ └── db/
|
|
34
|
+
│ ├── index.ts # Drizzle client
|
|
35
|
+
│ └── schema/ # 스키마 정의
|
|
36
|
+
│ ├── index.ts
|
|
37
|
+
│ ├── user.ts
|
|
38
|
+
│ └── post.ts
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## PostgreSQL
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { defineConfig } from 'drizzle-kit'
|
|
47
|
+
|
|
48
|
+
export default defineConfig({
|
|
49
|
+
dialect: 'postgresql',
|
|
50
|
+
schema: './src/db/schema/index.ts',
|
|
51
|
+
out: './drizzle/migrations',
|
|
52
|
+
dbCredentials: {
|
|
53
|
+
url: process.env.DATABASE_URL!,
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## MySQL
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { defineConfig } from 'drizzle-kit'
|
|
64
|
+
|
|
65
|
+
export default defineConfig({
|
|
66
|
+
dialect: 'mysql',
|
|
67
|
+
schema: './src/db/schema/index.ts',
|
|
68
|
+
out: './drizzle/migrations',
|
|
69
|
+
dbCredentials: {
|
|
70
|
+
url: process.env.DATABASE_URL!,
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## SQLite
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { defineConfig } from 'drizzle-kit'
|
|
81
|
+
|
|
82
|
+
export default defineConfig({
|
|
83
|
+
dialect: 'sqlite',
|
|
84
|
+
schema: './src/db/schema/index.ts',
|
|
85
|
+
out: './drizzle/migrations',
|
|
86
|
+
dbCredentials: {
|
|
87
|
+
url: './sqlite.db',
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Cloudflare D1
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { defineConfig } from 'drizzle-kit'
|
|
98
|
+
|
|
99
|
+
export default defineConfig({
|
|
100
|
+
dialect: 'sqlite',
|
|
101
|
+
schema: './src/db/schema/index.ts',
|
|
102
|
+
out: './drizzle/migrations',
|
|
103
|
+
driver: 'd1-http',
|
|
104
|
+
dbCredentials: {
|
|
105
|
+
accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
|
|
106
|
+
databaseId: process.env.CLOUDFLARE_DATABASE_ID!,
|
|
107
|
+
token: process.env.CLOUDFLARE_D1_TOKEN!,
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 설정 옵션
|
|
115
|
+
|
|
116
|
+
| 옵션 | 설명 |
|
|
117
|
+
|------|------|
|
|
118
|
+
| `dialect` | DB 종류 (postgresql, mysql, sqlite) |
|
|
119
|
+
| `schema` | 스키마 파일 경로 |
|
|
120
|
+
| `out` | 마이그레이션 출력 폴더 |
|
|
121
|
+
| `dbCredentials` | DB 연결 정보 |
|
|
122
|
+
| `driver` | 드라이버 (d1-http 등) |
|
|
123
|
+
| `verbose` | 상세 로그 출력 |
|
|
124
|
+
| `strict` | 엄격 모드 |
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 환경변수 로드
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
// drizzle.config.ts
|
|
132
|
+
import 'dotenv/config'
|
|
133
|
+
import { defineConfig } from 'drizzle-kit'
|
|
134
|
+
|
|
135
|
+
export default defineConfig({
|
|
136
|
+
dialect: 'postgresql',
|
|
137
|
+
schema: './src/db/schema/index.ts',
|
|
138
|
+
out: './drizzle/migrations',
|
|
139
|
+
dbCredentials: {
|
|
140
|
+
url: process.env.DATABASE_URL!,
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## 다중 스키마 파일
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import { defineConfig } from 'drizzle-kit'
|
|
151
|
+
|
|
152
|
+
export default defineConfig({
|
|
153
|
+
dialect: 'postgresql',
|
|
154
|
+
schema: './src/db/schema/*.ts', // glob 패턴
|
|
155
|
+
out: './drizzle/migrations',
|
|
156
|
+
dbCredentials: {
|
|
157
|
+
url: process.env.DATABASE_URL!,
|
|
158
|
+
},
|
|
159
|
+
})
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 관련 문서
|
|
165
|
+
|
|
166
|
+
- [Drizzle 개요](./index.md)
|
|
167
|
+
- [Cloudflare D1](./cloudflare-d1.md)
|