@kood/claude-code 0.1.2 → 0.1.4
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 +129 -5
- package/package.json +2 -2
- package/templates/hono/CLAUDE.md +20 -2
- package/templates/hono/docs/architecture/architecture.md +909 -0
- package/templates/hono/docs/commands/git.md +275 -0
- package/templates/hono/docs/deployment/cloudflare.md +527 -190
- package/templates/hono/docs/deployment/docker.md +514 -0
- package/templates/hono/docs/deployment/index.md +179 -214
- package/templates/hono/docs/deployment/railway.md +416 -0
- package/templates/hono/docs/deployment/vercel.md +567 -0
- package/templates/hono/docs/library/ai-sdk/index.md +427 -0
- package/templates/hono/docs/library/ai-sdk/openrouter.md +479 -0
- package/templates/hono/docs/library/ai-sdk/providers.md +468 -0
- package/templates/hono/docs/library/ai-sdk/streaming.md +447 -0
- package/templates/hono/docs/library/ai-sdk/structured-output.md +493 -0
- package/templates/hono/docs/library/ai-sdk/tools.md +513 -0
- package/templates/hono/docs/library/hono/env-setup.md +458 -0
- package/templates/hono/docs/library/hono/index.md +1 -3
- package/templates/hono/docs/library/pino/index.md +437 -0
- package/templates/hono/docs/library/prisma/cloudflare-d1.md +503 -0
- package/templates/hono/docs/library/prisma/config.md +362 -0
- package/templates/hono/docs/library/prisma/index.md +86 -13
- package/templates/hono/docs/skills/gemini-review/SKILL.md +116 -116
- package/templates/hono/docs/skills/gemini-review/references/checklists.md +125 -125
- package/templates/hono/docs/skills/gemini-review/references/prompt-templates.md +191 -191
- package/templates/npx/CLAUDE.md +309 -0
- package/templates/npx/docs/commands/git.md +275 -0
- package/templates/npx/docs/library/commander/index.md +164 -0
- package/templates/npx/docs/library/fs-extra/index.md +171 -0
- package/templates/npx/docs/library/prompts/index.md +253 -0
- package/templates/npx/docs/mcp/index.md +60 -0
- package/templates/npx/docs/skills/gemini-review/SKILL.md +220 -0
- package/templates/npx/docs/skills/gemini-review/references/checklists.md +134 -0
- package/templates/npx/docs/skills/gemini-review/references/prompt-templates.md +301 -0
- package/templates/tanstack-start/CLAUDE.md +43 -5
- package/templates/tanstack-start/docs/architecture/architecture.md +134 -4
- package/templates/tanstack-start/docs/commands/git.md +275 -0
- package/templates/tanstack-start/docs/deployment/cloudflare.md +223 -50
- package/templates/tanstack-start/docs/deployment/index.md +320 -30
- package/templates/tanstack-start/docs/deployment/nitro.md +195 -14
- package/templates/tanstack-start/docs/deployment/railway.md +302 -150
- package/templates/tanstack-start/docs/deployment/vercel.md +345 -75
- package/templates/tanstack-start/docs/guides/best-practices.md +203 -1
- package/templates/tanstack-start/docs/guides/env-setup.md +450 -0
- package/templates/tanstack-start/docs/library/ai-sdk/hooks.md +472 -0
- package/templates/tanstack-start/docs/library/ai-sdk/index.md +264 -0
- package/templates/tanstack-start/docs/library/ai-sdk/openrouter.md +371 -0
- package/templates/tanstack-start/docs/library/ai-sdk/providers.md +403 -0
- package/templates/tanstack-start/docs/library/ai-sdk/streaming.md +320 -0
- package/templates/tanstack-start/docs/library/ai-sdk/structured-output.md +454 -0
- package/templates/tanstack-start/docs/library/ai-sdk/tools.md +473 -0
- package/templates/tanstack-start/docs/library/pino/index.md +320 -0
- package/templates/tanstack-start/docs/library/prisma/cloudflare-d1.md +404 -0
- package/templates/tanstack-start/docs/library/prisma/config.md +377 -0
- package/templates/tanstack-start/docs/library/prisma/index.md +3 -5
- package/templates/tanstack-start/docs/library/prisma/schema.md +123 -25
- package/templates/tanstack-start/docs/library/prisma/setup.md +0 -7
- package/templates/tanstack-start/docs/library/tanstack-start/server-functions.md +80 -2
- package/templates/tanstack-start/docs/skills/gemini-review/SKILL.md +116 -116
- package/templates/tanstack-start/docs/skills/gemini-review/references/checklists.md +138 -144
- package/templates/tanstack-start/docs/skills/gemini-review/references/prompt-templates.md +186 -187
- package/templates/hono/docs/git/index.md +0 -180
- package/templates/tanstack-start/docs/git/index.md +0 -203
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# Pino - Structured Logging
|
|
2
|
+
|
|
3
|
+
> 초고속 JSON 로거 (Server Functions 전용)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## ⚠️ 중요: Server Functions에서만 사용
|
|
8
|
+
|
|
9
|
+
Pino는 **Server Functions에서만** 사용해야 합니다. 클라이언트 컴포넌트에서는 사용하지 마세요.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
✅ src/functions/*.ts → Pino 사용 가능
|
|
13
|
+
✅ routes/*/-functions/*.ts → Pino 사용 가능
|
|
14
|
+
❌ routes/*.tsx (컴포넌트) → Pino 사용 금지 (console.log 사용)
|
|
15
|
+
❌ components/*.tsx → Pino 사용 금지
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 왜 console.log 대신 Pino를 사용해야 하나요?
|
|
21
|
+
|
|
22
|
+
| 항목 | console.log | Pino |
|
|
23
|
+
|------|-------------|------|
|
|
24
|
+
| **출력 형식** | 비정형 텍스트 | 구조화된 JSON |
|
|
25
|
+
| **성능** | 느림 | 초고속 (30x 빠름) |
|
|
26
|
+
| **로그 레벨** | 없음 | debug, info, warn, error, fatal |
|
|
27
|
+
| **컨텍스트 추가** | 수동 문자열 연결 | 자동 객체 바인딩 |
|
|
28
|
+
| **프로덕션 분석** | 어려움 | JSON 파싱으로 쉬움 |
|
|
29
|
+
| **Child Logger** | 불가 | 요청별 컨텍스트 분리 |
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 설치
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# pino + pino-pretty (개발용 포맷터)
|
|
37
|
+
npm install pino pino-pretty
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 기본 설정
|
|
43
|
+
|
|
44
|
+
### lib/logger.ts
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import pino from 'pino'
|
|
48
|
+
|
|
49
|
+
const isDev = process.env.NODE_ENV === 'development'
|
|
50
|
+
|
|
51
|
+
export const logger = pino({
|
|
52
|
+
level: isDev ? 'debug' : 'info',
|
|
53
|
+
transport: isDev
|
|
54
|
+
? {
|
|
55
|
+
target: 'pino-pretty',
|
|
56
|
+
options: {
|
|
57
|
+
colorize: true,
|
|
58
|
+
translateTime: 'SYS:yyyy-mm-dd HH:MM:ss',
|
|
59
|
+
ignore: 'pid,hostname',
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
: undefined,
|
|
63
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 환경별 출력
|
|
67
|
+
|
|
68
|
+
**개발 환경** (pino-pretty):
|
|
69
|
+
```
|
|
70
|
+
2024-01-15 10:30:45 INFO: User created successfully
|
|
71
|
+
userId: "user_123"
|
|
72
|
+
email: "user@example.com"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**프로덕션** (JSON):
|
|
76
|
+
```json
|
|
77
|
+
{"level":30,"time":1705312245000,"msg":"User created successfully","userId":"user_123","email":"user@example.com"}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Server Functions에서 사용
|
|
83
|
+
|
|
84
|
+
### 기본 사용법
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// functions/user.ts
|
|
88
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
89
|
+
import { logger } from '@/lib/logger'
|
|
90
|
+
import { prisma } from '@/database/prisma'
|
|
91
|
+
import { createUserSchema } from '@/validators/user'
|
|
92
|
+
import { authMiddleware } from '@/middleware/auth'
|
|
93
|
+
|
|
94
|
+
export const createUser = createServerFn({ method: 'POST' })
|
|
95
|
+
.middleware([authMiddleware])
|
|
96
|
+
.inputValidator(createUserSchema)
|
|
97
|
+
.handler(async ({ data, context }) => {
|
|
98
|
+
logger.info({ email: data.email }, 'Creating new user')
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const user = await prisma.user.create({ data })
|
|
102
|
+
logger.info({ userId: user.id }, 'User created successfully')
|
|
103
|
+
return user
|
|
104
|
+
} catch (error) {
|
|
105
|
+
logger.error({ error, email: data.email }, 'Failed to create user')
|
|
106
|
+
throw error
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Child Logger로 요청 컨텍스트 추가
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// functions/user.ts
|
|
115
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
116
|
+
import { logger } from '@/lib/logger'
|
|
117
|
+
import { prisma } from '@/database/prisma'
|
|
118
|
+
|
|
119
|
+
export const getUser = createServerFn({ method: 'GET' })
|
|
120
|
+
.middleware([authMiddleware])
|
|
121
|
+
.handler(async ({ context }) => {
|
|
122
|
+
// 요청별 컨텍스트가 포함된 child logger 생성
|
|
123
|
+
const log = logger.child({
|
|
124
|
+
requestId: context.requestId,
|
|
125
|
+
userId: context.userId,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
log.info('Fetching user profile')
|
|
129
|
+
|
|
130
|
+
const user = await prisma.user.findUnique({
|
|
131
|
+
where: { id: context.userId },
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
if (!user) {
|
|
135
|
+
log.warn('User not found')
|
|
136
|
+
throw new Error('User not found')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
log.debug({ user }, 'User fetched successfully')
|
|
140
|
+
return user
|
|
141
|
+
})
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**출력 예시**:
|
|
145
|
+
```json
|
|
146
|
+
{"level":30,"time":1705312245000,"requestId":"req_abc123","userId":"user_456","msg":"Fetching user profile"}
|
|
147
|
+
{"level":20,"time":1705312245050,"requestId":"req_abc123","userId":"user_456","user":{"id":"user_456","name":"John"},"msg":"User fetched successfully"}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 로그 레벨
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
logger.trace('매우 상세한 디버깅 정보') // level: 10
|
|
156
|
+
logger.debug('디버깅 정보') // level: 20
|
|
157
|
+
logger.info('일반 정보') // level: 30
|
|
158
|
+
logger.warn('경고') // level: 40
|
|
159
|
+
logger.error('에러') // level: 50
|
|
160
|
+
logger.fatal('치명적 에러') // level: 60
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### 레벨별 사용 가이드
|
|
164
|
+
|
|
165
|
+
| 레벨 | 용도 | 예시 |
|
|
166
|
+
|------|------|------|
|
|
167
|
+
| `trace` | 매우 상세한 흐름 추적 | 함수 진입/종료, 변수 값 |
|
|
168
|
+
| `debug` | 개발 중 디버깅 | 쿼리 결과, 중간 계산값 |
|
|
169
|
+
| `info` | 주요 이벤트 | 사용자 생성, 결제 완료 |
|
|
170
|
+
| `warn` | 주의 필요 | 재시도 발생, 느린 쿼리 |
|
|
171
|
+
| `error` | 에러 발생 | API 실패, 예외 처리 |
|
|
172
|
+
| `fatal` | 시스템 중단 | 서버 시작 실패 |
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## 구조화된 로깅 패턴
|
|
177
|
+
|
|
178
|
+
### 객체와 메시지 함께 로깅
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// ✅ 올바른 패턴: 객체 먼저, 메시지 나중
|
|
182
|
+
logger.info({ userId: '123', action: 'login' }, 'User logged in')
|
|
183
|
+
|
|
184
|
+
// ❌ 잘못된 패턴: 문자열 연결
|
|
185
|
+
logger.info('User ' + userId + ' logged in') // 구조화되지 않음
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### 에러 로깅
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
try {
|
|
192
|
+
await riskyOperation()
|
|
193
|
+
} catch (error) {
|
|
194
|
+
// ✅ err 키로 에러 객체 전달 (스택 트레이스 자동 포함)
|
|
195
|
+
logger.error({ err: error, context: 'riskyOperation' }, 'Operation failed')
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**출력**:
|
|
200
|
+
```json
|
|
201
|
+
{
|
|
202
|
+
"level": 50,
|
|
203
|
+
"time": 1705312245000,
|
|
204
|
+
"err": {
|
|
205
|
+
"type": "Error",
|
|
206
|
+
"message": "Connection refused",
|
|
207
|
+
"stack": "Error: Connection refused\n at ..."
|
|
208
|
+
},
|
|
209
|
+
"context": "riskyOperation",
|
|
210
|
+
"msg": "Operation failed"
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## 고급 설정
|
|
217
|
+
|
|
218
|
+
### pino-pretty 전체 옵션
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
// lib/logger.ts
|
|
222
|
+
import pino from 'pino'
|
|
223
|
+
|
|
224
|
+
export const logger = pino({
|
|
225
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
226
|
+
transport:
|
|
227
|
+
process.env.NODE_ENV === 'development'
|
|
228
|
+
? {
|
|
229
|
+
target: 'pino-pretty',
|
|
230
|
+
options: {
|
|
231
|
+
colorize: true,
|
|
232
|
+
translateTime: 'SYS:yyyy-mm-dd HH:MM:ss.l',
|
|
233
|
+
ignore: 'pid,hostname',
|
|
234
|
+
singleLine: false,
|
|
235
|
+
levelFirst: true,
|
|
236
|
+
messageFormat: '{levelLabel} - {msg}',
|
|
237
|
+
customColors: 'info:blue,warn:yellow,error:red,fatal:bgRed',
|
|
238
|
+
errorProps: 'stack,code,cause',
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
: undefined,
|
|
242
|
+
})
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### TypeScript 타입 강제
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
// types/pino.d.ts
|
|
249
|
+
declare module 'pino' {
|
|
250
|
+
interface LogFnFields {
|
|
251
|
+
userId?: string
|
|
252
|
+
requestId?: string
|
|
253
|
+
action?: string
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
// 사용 시 타입 검사
|
|
260
|
+
logger.info({ userId: '123' }, 'User action') // ✅
|
|
261
|
+
logger.info({ userId: 123 }, 'User action') // ❌ 타입 에러
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## 프로덕션 설정
|
|
267
|
+
|
|
268
|
+
### 환경 변수
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
# .env
|
|
272
|
+
LOG_LEVEL=info # production
|
|
273
|
+
# LOG_LEVEL=debug # staging
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### 멀티 스트림 (파일 + stdout)
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
import pino from 'pino'
|
|
280
|
+
|
|
281
|
+
const streams = [
|
|
282
|
+
{ level: 'info', stream: process.stdout },
|
|
283
|
+
{ level: 'error', stream: pino.destination('./logs/error.log') },
|
|
284
|
+
]
|
|
285
|
+
|
|
286
|
+
export const logger = pino(
|
|
287
|
+
{ level: 'debug' },
|
|
288
|
+
pino.multistream(streams)
|
|
289
|
+
)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## ❌ 하지 말아야 할 것
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
// ❌ 클라이언트 컴포넌트에서 pino 사용 금지
|
|
298
|
+
const MyComponent = () => {
|
|
299
|
+
logger.info('Component rendered') // ❌ 서버에서만 동작
|
|
300
|
+
return <div>Hello</div>
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ❌ 민감한 정보 로깅 금지
|
|
304
|
+
logger.info({ password: user.password }, 'User logged in') // ❌
|
|
305
|
+
|
|
306
|
+
// ❌ 문자열 연결 사용 금지
|
|
307
|
+
logger.info('User ' + userId + ' created at ' + new Date()) // ❌
|
|
308
|
+
|
|
309
|
+
// ❌ console.log와 혼용 금지 (Server Functions에서)
|
|
310
|
+
console.log('Processing...') // ❌ pino 사용하세요
|
|
311
|
+
logger.info('Processing...') // ✅
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## 관련 문서
|
|
317
|
+
|
|
318
|
+
- [Server Functions](../tanstack-start/server-functions.md)
|
|
319
|
+
- [미들웨어](../tanstack-start/middleware.md)
|
|
320
|
+
- [아키텍처](../../architecture/architecture.md)
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
# Prisma - Cloudflare D1
|
|
2
|
+
|
|
3
|
+
> **상위 문서**: [Prisma](./index.md)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## ⚠️ 주의사항
|
|
8
|
+
|
|
9
|
+
Cloudflare D1은 **SQLite 기반 서버리스 데이터베이스**입니다. 일반 Prisma 마이그레이션과 **다른 워크플로우**를 사용합니다.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
⚠️ D1은 트랜잭션을 지원하지 않음 (ACID 보장 X)
|
|
13
|
+
⚠️ prisma migrate dev 사용 불가 - wrangler CLI 사용
|
|
14
|
+
⚠️ Prisma ORM D1 지원은 Preview 상태
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 🚀 Quick Reference
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// D1 Adapter 설정
|
|
23
|
+
import { PrismaClient } from './generated/prisma'
|
|
24
|
+
import { PrismaD1 } from '@prisma/adapter-d1'
|
|
25
|
+
|
|
26
|
+
export interface Env {
|
|
27
|
+
DB: D1Database
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default {
|
|
31
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
32
|
+
const adapter = new PrismaD1(env.DB)
|
|
33
|
+
const prisma = new PrismaClient({ adapter })
|
|
34
|
+
|
|
35
|
+
const users = await prisma.user.findMany()
|
|
36
|
+
return new Response(JSON.stringify(users))
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### schema.prisma (D1용)
|
|
42
|
+
|
|
43
|
+
```prisma
|
|
44
|
+
generator client {
|
|
45
|
+
provider = "prisma-client"
|
|
46
|
+
output = "../src/generated/prisma"
|
|
47
|
+
runtime = "cloudflare" // D1 필수!
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
datasource db {
|
|
51
|
+
provider = "sqlite" // D1은 SQLite 기반
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
model User {
|
|
55
|
+
id Int @id @default(autoincrement())
|
|
56
|
+
email String @unique
|
|
57
|
+
name String?
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 설치
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# 필수 패키지
|
|
67
|
+
npm install @prisma/client @prisma/adapter-d1
|
|
68
|
+
npm install -D prisma wrangler
|
|
69
|
+
|
|
70
|
+
# Prisma 초기화
|
|
71
|
+
npx prisma init --datasource-provider sqlite
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## D1 데이터베이스 생성
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Cloudflare D1 데이터베이스 생성
|
|
80
|
+
npx wrangler d1 create my-database
|
|
81
|
+
|
|
82
|
+
# 출력 예시:
|
|
83
|
+
# ✅ Successfully created DB 'my-database'
|
|
84
|
+
# database_id = "xxxx-xxxx-xxxx-xxxx"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## wrangler.jsonc 설정
|
|
90
|
+
|
|
91
|
+
```jsonc
|
|
92
|
+
{
|
|
93
|
+
"$schema": "node_modules/wrangler/config-schema.json",
|
|
94
|
+
"name": "my-worker",
|
|
95
|
+
"main": "src/index.ts",
|
|
96
|
+
"compatibility_date": "2025-01-01",
|
|
97
|
+
"d1_databases": [
|
|
98
|
+
{
|
|
99
|
+
"binding": "DB",
|
|
100
|
+
"database_name": "my-database",
|
|
101
|
+
"database_id": "YOUR_DATABASE_ID"
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Schema 설정
|
|
110
|
+
|
|
111
|
+
### schema.prisma
|
|
112
|
+
|
|
113
|
+
```prisma
|
|
114
|
+
generator client {
|
|
115
|
+
provider = "prisma-client"
|
|
116
|
+
output = "../src/generated/prisma"
|
|
117
|
+
runtime = "cloudflare"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
datasource db {
|
|
121
|
+
provider = "sqlite"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
model User {
|
|
125
|
+
id Int @id @default(autoincrement())
|
|
126
|
+
email String @unique
|
|
127
|
+
name String?
|
|
128
|
+
posts Post[]
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
model Post {
|
|
132
|
+
id Int @id @default(autoincrement())
|
|
133
|
+
title String
|
|
134
|
+
content String?
|
|
135
|
+
published Boolean @default(false)
|
|
136
|
+
author User @relation(fields: [authorId], references: [id])
|
|
137
|
+
authorId Int
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 마이그레이션 워크플로우
|
|
144
|
+
|
|
145
|
+
D1은 일반 Prisma 마이그레이션(`prisma migrate dev`)을 사용하지 않습니다.
|
|
146
|
+
|
|
147
|
+
### 1단계: 마이그레이션 디렉토리 생성
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
mkdir -p prisma/migrations
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 2단계: 마이그레이션 파일 생성 (wrangler)
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
npx wrangler d1 migrations create my-database create_user_table
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 3단계: SQL 생성 (prisma migrate diff)
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# 초기 마이그레이션 (빈 스키마에서)
|
|
163
|
+
npx prisma migrate diff \
|
|
164
|
+
--from-empty \
|
|
165
|
+
--to-schema-datamodel prisma/schema.prisma \
|
|
166
|
+
--script \
|
|
167
|
+
--output prisma/migrations/0001_create_user_table.sql
|
|
168
|
+
|
|
169
|
+
# 후속 마이그레이션 (로컬 D1에서)
|
|
170
|
+
npx prisma migrate diff \
|
|
171
|
+
--from-local-d1 \
|
|
172
|
+
--to-schema-datamodel prisma/schema.prisma \
|
|
173
|
+
--script \
|
|
174
|
+
--output prisma/migrations/0002_add_post_table.sql
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### 4단계: 마이그레이션 적용
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# 로컬 데이터베이스에 적용
|
|
181
|
+
npx wrangler d1 migrations apply my-database --local
|
|
182
|
+
|
|
183
|
+
# 원격(프로덕션) 데이터베이스에 적용
|
|
184
|
+
npx wrangler d1 migrations apply my-database --remote
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 생성된 SQL 예시
|
|
188
|
+
|
|
189
|
+
```sql
|
|
190
|
+
-- 0001_create_user_table.sql
|
|
191
|
+
-- CreateTable
|
|
192
|
+
CREATE TABLE "User" (
|
|
193
|
+
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
194
|
+
"email" TEXT NOT NULL,
|
|
195
|
+
"name" TEXT
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
-- CreateIndex
|
|
199
|
+
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Prisma Client 설정
|
|
205
|
+
|
|
206
|
+
### Cloudflare Worker에서 사용
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// src/index.ts
|
|
210
|
+
import { PrismaClient } from './generated/prisma'
|
|
211
|
+
import { PrismaD1 } from '@prisma/adapter-d1'
|
|
212
|
+
|
|
213
|
+
export interface Env {
|
|
214
|
+
DB: D1Database
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export default {
|
|
218
|
+
async fetch(
|
|
219
|
+
request: Request,
|
|
220
|
+
env: Env,
|
|
221
|
+
ctx: ExecutionContext
|
|
222
|
+
): Promise<Response> {
|
|
223
|
+
const adapter = new PrismaD1(env.DB)
|
|
224
|
+
const prisma = new PrismaClient({ adapter })
|
|
225
|
+
|
|
226
|
+
const url = new URL(request.url)
|
|
227
|
+
|
|
228
|
+
if (url.pathname === '/users' && request.method === 'GET') {
|
|
229
|
+
const users = await prisma.user.findMany()
|
|
230
|
+
return new Response(JSON.stringify(users), {
|
|
231
|
+
headers: { 'Content-Type': 'application/json' },
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (url.pathname === '/users' && request.method === 'POST') {
|
|
236
|
+
const body = await request.json()
|
|
237
|
+
const user = await prisma.user.create({
|
|
238
|
+
data: body,
|
|
239
|
+
})
|
|
240
|
+
return new Response(JSON.stringify(user), {
|
|
241
|
+
status: 201,
|
|
242
|
+
headers: { 'Content-Type': 'application/json' },
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return new Response('Not Found', { status: 404 })
|
|
247
|
+
},
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## TanStack Start + D1 통합
|
|
254
|
+
|
|
255
|
+
D1은 Cloudflare Workers 환경에서 실행되므로, TanStack Start의 Cloudflare 배포와 함께 사용합니다.
|
|
256
|
+
|
|
257
|
+
### Server Function에서 사용
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
// lib/prisma.ts
|
|
261
|
+
import { PrismaClient } from './generated/prisma'
|
|
262
|
+
import { PrismaD1 } from '@prisma/adapter-d1'
|
|
263
|
+
|
|
264
|
+
export const createPrismaClient = (db: D1Database) => {
|
|
265
|
+
const adapter = new PrismaD1(db)
|
|
266
|
+
return new PrismaClient({ adapter })
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Server Function
|
|
270
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
271
|
+
|
|
272
|
+
export const getUsers = createServerFn({ method: 'GET' })
|
|
273
|
+
.handler(async ({ context }) => {
|
|
274
|
+
// context에서 D1 바인딩 접근 필요
|
|
275
|
+
const prisma = createPrismaClient(context.env.DB)
|
|
276
|
+
return prisma.user.findMany()
|
|
277
|
+
})
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## 로컬 개발
|
|
283
|
+
|
|
284
|
+
### 로컬 D1로 개발
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
# 로컬 Worker 실행 (D1 포함)
|
|
288
|
+
npx wrangler dev
|
|
289
|
+
|
|
290
|
+
# 로컬 D1에 데이터 삽입
|
|
291
|
+
npx wrangler d1 execute my-database --local \
|
|
292
|
+
--command "INSERT INTO User (email, name) VALUES ('test@example.com', 'Test User');"
|
|
293
|
+
|
|
294
|
+
# 로컬 D1 조회
|
|
295
|
+
npx wrangler d1 execute my-database --local \
|
|
296
|
+
--command "SELECT * FROM User;"
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## 배포
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
# Prisma Client 생성
|
|
305
|
+
npx prisma generate
|
|
306
|
+
|
|
307
|
+
# 원격 D1에 마이그레이션 적용
|
|
308
|
+
npx wrangler d1 migrations apply my-database --remote
|
|
309
|
+
|
|
310
|
+
# Worker 배포
|
|
311
|
+
npx wrangler deploy
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## ⚠️ 제한사항
|
|
317
|
+
|
|
318
|
+
### 트랜잭션 미지원
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
// ❌ 트랜잭션이 무시됨 - ACID 보장 안됨
|
|
322
|
+
await prisma.$transaction([
|
|
323
|
+
prisma.user.create({ data: { email: 'user1@example.com' } }),
|
|
324
|
+
prisma.post.create({ data: { title: 'Post', authorId: 1 } }),
|
|
325
|
+
])
|
|
326
|
+
|
|
327
|
+
// ❌ Interactive 트랜잭션도 미지원
|
|
328
|
+
await prisma.$transaction(async (tx) => {
|
|
329
|
+
// 각 쿼리가 독립적으로 실행됨
|
|
330
|
+
})
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### D1 vs 일반 SQLite 차이점
|
|
334
|
+
|
|
335
|
+
| 항목 | 일반 SQLite | Cloudflare D1 |
|
|
336
|
+
|------|-------------|---------------|
|
|
337
|
+
| 마이그레이션 | `prisma migrate dev` | `wrangler d1 migrations` |
|
|
338
|
+
| 트랜잭션 | ✅ 지원 | ❌ 미지원 |
|
|
339
|
+
| 데이터베이스 위치 | 로컬 파일 | Cloudflare Edge |
|
|
340
|
+
| 접속 방식 | 직접 연결 | HTTP (어댑터) |
|
|
341
|
+
| 환경 | 로컬 `.wrangler/state` | 원격 Cloudflare |
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## prisma.config.ts (선택사항)
|
|
346
|
+
|
|
347
|
+
Prisma CLI에서 D1에 직접 접근이 필요한 경우:
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
// prisma.config.ts
|
|
351
|
+
import path from 'node:path'
|
|
352
|
+
import type { PrismaConfig } from 'prisma'
|
|
353
|
+
import { PrismaD1 } from '@prisma/adapter-d1'
|
|
354
|
+
|
|
355
|
+
export default {
|
|
356
|
+
experimental: {
|
|
357
|
+
adapter: true,
|
|
358
|
+
},
|
|
359
|
+
engine: 'js',
|
|
360
|
+
schema: path.join('prisma', 'schema.prisma'),
|
|
361
|
+
async adapter() {
|
|
362
|
+
return new PrismaD1({
|
|
363
|
+
CLOUDFLARE_D1_TOKEN: process.env.CLOUDFLARE_D1_TOKEN,
|
|
364
|
+
CLOUDFLARE_ACCOUNT_ID: process.env.CLOUDFLARE_ACCOUNT_ID,
|
|
365
|
+
CLOUDFLARE_DATABASE_ID: process.env.CLOUDFLARE_DATABASE_ID,
|
|
366
|
+
})
|
|
367
|
+
},
|
|
368
|
+
} satisfies PrismaConfig
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## 마이그레이션 명령어 요약
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
# D1 데이터베이스 생성
|
|
377
|
+
npx wrangler d1 create my-database
|
|
378
|
+
|
|
379
|
+
# 마이그레이션 파일 생성
|
|
380
|
+
npx wrangler d1 migrations create my-database migration_name
|
|
381
|
+
|
|
382
|
+
# SQL 생성 (초기)
|
|
383
|
+
npx prisma migrate diff --from-empty --to-schema-datamodel prisma/schema.prisma --script
|
|
384
|
+
|
|
385
|
+
# SQL 생성 (후속)
|
|
386
|
+
npx prisma migrate diff --from-local-d1 --to-schema-datamodel prisma/schema.prisma --script
|
|
387
|
+
|
|
388
|
+
# 로컬 적용
|
|
389
|
+
npx wrangler d1 migrations apply my-database --local
|
|
390
|
+
|
|
391
|
+
# 원격 적용
|
|
392
|
+
npx wrangler d1 migrations apply my-database --remote
|
|
393
|
+
|
|
394
|
+
# Prisma Client 생성
|
|
395
|
+
npx prisma generate
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## 참고 자료
|
|
401
|
+
|
|
402
|
+
- [Prisma D1 공식 문서](https://www.prisma.io/docs/orm/overview/databases/cloudflare-d1)
|
|
403
|
+
- [Cloudflare D1 문서](https://developers.cloudflare.com/d1/)
|
|
404
|
+
- [Prisma D1 가이드](https://www.prisma.io/guides/cloudflare-d1)
|