@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,909 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
Hono 서버 애플리케이션의 기술 아키텍처 가이드입니다.
|
|
4
|
+
|
|
5
|
+
## System Overview
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
9
|
+
│ Client (Browser/App) │
|
|
10
|
+
│ │
|
|
11
|
+
│ ┌────────────────┐ ┌────────────────┐ ┌───────────────┐ │
|
|
12
|
+
│ │ HTTP Client │───▶│ hono/client │───▶│ React UI │ │
|
|
13
|
+
│ │ (fetch/axios) │◀───│ (RPC Client) │◀───│ (Components) │ │
|
|
14
|
+
│ └────────────────┘ └───────┬────────┘ └───────────────┘ │
|
|
15
|
+
│ │ │
|
|
16
|
+
└────────────────────────────────┼─────────────────────────────────┘
|
|
17
|
+
│
|
|
18
|
+
▼
|
|
19
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
20
|
+
│ Hono Server │
|
|
21
|
+
│ │
|
|
22
|
+
│ ┌────────────────────────────────────────────────────────────┐ │
|
|
23
|
+
│ │ Middleware Stack │ │
|
|
24
|
+
│ │ - logger → 요청 로깅 │ │
|
|
25
|
+
│ │ - cors → CORS 설정 │ │
|
|
26
|
+
│ │ - secureHeaders → 보안 헤더 │ │
|
|
27
|
+
│ │ - authMiddleware → 인증 처리 │ │
|
|
28
|
+
│ └────────────────────────┬───────────────────────────────────┘ │
|
|
29
|
+
│ │ │
|
|
30
|
+
│ ┌────────────────────────▼───────────────────────────────────┐ │
|
|
31
|
+
│ │ Routes Layer │ │
|
|
32
|
+
│ │ - routes/users.ts → /users/* │ │
|
|
33
|
+
│ │ - routes/posts.ts → /posts/* │ │
|
|
34
|
+
│ │ - routes/auth.ts → /auth/* │ │
|
|
35
|
+
│ └────────────────────────┬───────────────────────────────────┘ │
|
|
36
|
+
│ │ │
|
|
37
|
+
│ ┌────────────────────────▼───────────────────────────────────┐ │
|
|
38
|
+
│ │ Validation Layer │ │
|
|
39
|
+
│ │ - @hono/zod-validator → 요청 검증 │ │
|
|
40
|
+
│ │ - Zod v4 Schemas → 타입 안전 검증 │ │
|
|
41
|
+
│ └────────────────────────┬───────────────────────────────────┘ │
|
|
42
|
+
│ │ │
|
|
43
|
+
│ ┌────────────────────────▼───────────────────────────────────┐ │
|
|
44
|
+
│ │ Services Layer │ │
|
|
45
|
+
│ │ - Business Logic │ │
|
|
46
|
+
│ │ - Data Transformation │ │
|
|
47
|
+
│ │ - Error Handling (HTTPException) │ │
|
|
48
|
+
│ └────────────────────────┬───────────────────────────────────┘ │
|
|
49
|
+
│ │ │
|
|
50
|
+
└───────────────────────────┼──────────────────────────────────────┘
|
|
51
|
+
│
|
|
52
|
+
▼
|
|
53
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
54
|
+
│ Database Layer │
|
|
55
|
+
│ │
|
|
56
|
+
│ ┌────────────────┐ ┌────────────────┐ ┌───────────────┐ │
|
|
57
|
+
│ │ Prisma Client │───▶│ PostgreSQL │ │ Cloudflare │ │
|
|
58
|
+
│ │ (ORM v7) │ │ (Primary) │ │ D1/KV/R2 │ │
|
|
59
|
+
│ └────────────────┘ └────────────────┘ └───────────────┘ │
|
|
60
|
+
│ │
|
|
61
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Project Structure
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
my-app/
|
|
68
|
+
├── src/
|
|
69
|
+
│ ├── index.ts # Entry point & App 설정
|
|
70
|
+
│ ├── routes/ # 라우트 모듈
|
|
71
|
+
│ │ ├── index.ts # 라우트 통합 (re-export)
|
|
72
|
+
│ │ ├── users.ts # /users/* 라우트
|
|
73
|
+
│ │ ├── posts.ts # /posts/* 라우트
|
|
74
|
+
│ │ └── auth.ts # /auth/* 라우트
|
|
75
|
+
│ ├── middleware/ # 커스텀 미들웨어
|
|
76
|
+
│ │ ├── index.ts # re-export
|
|
77
|
+
│ │ ├── auth.ts # 인증 미들웨어
|
|
78
|
+
│ │ ├── logger.ts # 로깅 미들웨어
|
|
79
|
+
│ │ └── rate-limit.ts # 레이트 리밋 미들웨어
|
|
80
|
+
│ ├── validators/ # Zod 스키마 정의
|
|
81
|
+
│ │ ├── index.ts # re-export
|
|
82
|
+
│ │ ├── user.ts # 사용자 스키마
|
|
83
|
+
│ │ ├── post.ts # 게시글 스키마
|
|
84
|
+
│ │ └── common.ts # 공통 스키마 (pagination 등)
|
|
85
|
+
│ ├── services/ # 비즈니스 로직 레이어
|
|
86
|
+
│ │ ├── user/
|
|
87
|
+
│ │ │ ├── index.ts # re-export
|
|
88
|
+
│ │ │ ├── queries.ts # 조회 로직
|
|
89
|
+
│ │ │ └── mutations.ts # 생성/수정/삭제 로직
|
|
90
|
+
│ │ └── post/
|
|
91
|
+
│ │ ├── index.ts
|
|
92
|
+
│ │ ├── queries.ts
|
|
93
|
+
│ │ └── mutations.ts
|
|
94
|
+
│ ├── database/ # 데이터베이스 연결
|
|
95
|
+
│ │ └── prisma.ts # Prisma Client 싱글톤
|
|
96
|
+
│ ├── types/ # 타입 정의
|
|
97
|
+
│ │ ├── index.ts # re-export
|
|
98
|
+
│ │ ├── env.d.ts # 환경변수 타입
|
|
99
|
+
│ │ └── bindings.ts # Cloudflare Bindings 타입
|
|
100
|
+
│ └── lib/ # 공통 유틸리티
|
|
101
|
+
│ ├── errors.ts # 커스텀 에러 클래스
|
|
102
|
+
│ ├── jwt.ts # JWT 유틸리티
|
|
103
|
+
│ └── utils.ts # 범용 유틸리티
|
|
104
|
+
├── prisma/
|
|
105
|
+
│ ├── schema.prisma # Prisma 스키마
|
|
106
|
+
│ └── generated/ # Prisma Client 출력
|
|
107
|
+
│ └── client/
|
|
108
|
+
├── wrangler.toml # Cloudflare Workers 설정
|
|
109
|
+
├── .dev.vars # 로컬 환경변수
|
|
110
|
+
├── package.json
|
|
111
|
+
└── tsconfig.json
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Layer Architecture
|
|
115
|
+
|
|
116
|
+
### 1. Entry Point (src/index.ts)
|
|
117
|
+
|
|
118
|
+
애플리케이션의 진입점으로, 전역 설정과 라우트 마운트를 담당합니다.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// src/index.ts
|
|
122
|
+
import { Hono } from 'hono'
|
|
123
|
+
import { logger } from 'hono/logger'
|
|
124
|
+
import { cors } from 'hono/cors'
|
|
125
|
+
import { secureHeaders } from 'hono/secure-headers'
|
|
126
|
+
import { HTTPException } from 'hono/http-exception'
|
|
127
|
+
import { users } from './routes/users'
|
|
128
|
+
import { posts } from './routes/posts'
|
|
129
|
+
import { auth } from './routes/auth'
|
|
130
|
+
|
|
131
|
+
type Bindings = {
|
|
132
|
+
DATABASE_URL: string
|
|
133
|
+
JWT_SECRET: string
|
|
134
|
+
NODE_ENV: string
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
type Variables = {
|
|
138
|
+
userId: string
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()
|
|
142
|
+
|
|
143
|
+
// 글로벌 미들웨어
|
|
144
|
+
app.use(logger())
|
|
145
|
+
app.use(secureHeaders())
|
|
146
|
+
app.use('/api/*', cors())
|
|
147
|
+
|
|
148
|
+
// 글로벌 에러 핸들러
|
|
149
|
+
app.onError((err, c) => {
|
|
150
|
+
if (err instanceof HTTPException) {
|
|
151
|
+
return c.json({ error: err.message }, err.status)
|
|
152
|
+
}
|
|
153
|
+
console.error(err)
|
|
154
|
+
return c.json({ error: 'Internal Server Error' }, 500)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// 404 핸들러
|
|
158
|
+
app.notFound((c) => {
|
|
159
|
+
return c.json({ error: 'Not Found', path: c.req.path }, 404)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// 라우트 마운트
|
|
163
|
+
app.route('/api/users', users)
|
|
164
|
+
app.route('/api/posts', posts)
|
|
165
|
+
app.route('/api/auth', auth)
|
|
166
|
+
|
|
167
|
+
// Health Check
|
|
168
|
+
app.get('/health', (c) => c.json({ status: 'ok' }))
|
|
169
|
+
|
|
170
|
+
export default app
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### 2. Routes Layer (Presentation)
|
|
174
|
+
|
|
175
|
+
파일 기반으로 라우트를 모듈화하여 관리합니다.
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
routes/
|
|
179
|
+
├── index.ts # 라우트 통합
|
|
180
|
+
├── users.ts # /users 라우트
|
|
181
|
+
├── posts.ts # /posts 라우트
|
|
182
|
+
└── auth.ts # /auth 라우트
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**특징**:
|
|
186
|
+
- 각 파일은 독립적인 Hono 인스턴스
|
|
187
|
+
- zValidator로 요청 검증
|
|
188
|
+
- 미들웨어 적용 가능
|
|
189
|
+
- RPC 타입 추론 지원
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// routes/users.ts
|
|
193
|
+
import { Hono } from 'hono'
|
|
194
|
+
import { zValidator } from '@hono/zod-validator'
|
|
195
|
+
import { authMiddleware } from '@/middleware/auth'
|
|
196
|
+
import { createUserSchema, updateUserSchema, userIdSchema } from '@/validators/user'
|
|
197
|
+
import { paginationSchema } from '@/validators/common'
|
|
198
|
+
import { getUsers, getUserById, createUser, updateUser, deleteUser } from '@/services/user'
|
|
199
|
+
|
|
200
|
+
const users = new Hono()
|
|
201
|
+
|
|
202
|
+
// 목록 조회 (공개)
|
|
203
|
+
users.get('/', zValidator('query', paginationSchema), async (c) => {
|
|
204
|
+
const { page, limit } = c.req.valid('query')
|
|
205
|
+
const result = await getUsers({ page, limit })
|
|
206
|
+
return c.json(result)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
// 단일 조회 (공개)
|
|
210
|
+
users.get('/:id', zValidator('param', userIdSchema), async (c) => {
|
|
211
|
+
const { id } = c.req.valid('param')
|
|
212
|
+
const user = await getUserById(id)
|
|
213
|
+
return c.json({ user })
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
// 생성 (인증 필요)
|
|
217
|
+
users.post(
|
|
218
|
+
'/',
|
|
219
|
+
authMiddleware,
|
|
220
|
+
zValidator('json', createUserSchema),
|
|
221
|
+
async (c) => {
|
|
222
|
+
const data = c.req.valid('json')
|
|
223
|
+
const user = await createUser(data)
|
|
224
|
+
return c.json({ user }, 201)
|
|
225
|
+
}
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
// 수정 (인증 필요)
|
|
229
|
+
users.put(
|
|
230
|
+
'/:id',
|
|
231
|
+
authMiddleware,
|
|
232
|
+
zValidator('param', userIdSchema),
|
|
233
|
+
zValidator('json', updateUserSchema),
|
|
234
|
+
async (c) => {
|
|
235
|
+
const { id } = c.req.valid('param')
|
|
236
|
+
const data = c.req.valid('json')
|
|
237
|
+
const user = await updateUser(id, data)
|
|
238
|
+
return c.json({ user })
|
|
239
|
+
}
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
// 삭제 (인증 필요)
|
|
243
|
+
users.delete(
|
|
244
|
+
'/:id',
|
|
245
|
+
authMiddleware,
|
|
246
|
+
zValidator('param', userIdSchema),
|
|
247
|
+
async (c) => {
|
|
248
|
+
const { id } = c.req.valid('param')
|
|
249
|
+
await deleteUser(id)
|
|
250
|
+
return c.json({ success: true })
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
export { users }
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### 3. Middleware Layer
|
|
258
|
+
|
|
259
|
+
요청/응답 파이프라인을 처리합니다.
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
middleware/
|
|
263
|
+
├── index.ts # re-export
|
|
264
|
+
├── auth.ts # 인증 미들웨어
|
|
265
|
+
├── logger.ts # 로깅 미들웨어
|
|
266
|
+
└── rate-limit.ts # 레이트 리밋
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**규칙**:
|
|
270
|
+
- `createMiddleware`로 타입 안전 미들웨어 작성
|
|
271
|
+
- `HTTPException`으로 에러 처리
|
|
272
|
+
- `c.set()`으로 변수 전달
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
// middleware/auth.ts
|
|
276
|
+
import { createMiddleware } from 'hono/factory'
|
|
277
|
+
import { HTTPException } from 'hono/http-exception'
|
|
278
|
+
import { verifyToken } from '@/lib/jwt'
|
|
279
|
+
|
|
280
|
+
type Env = {
|
|
281
|
+
Bindings: {
|
|
282
|
+
JWT_SECRET: string
|
|
283
|
+
}
|
|
284
|
+
Variables: {
|
|
285
|
+
userId: string
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export const authMiddleware = createMiddleware<Env>(async (c, next) => {
|
|
290
|
+
const token = c.req.header('Authorization')?.replace('Bearer ', '')
|
|
291
|
+
|
|
292
|
+
if (!token) {
|
|
293
|
+
throw new HTTPException(401, { message: 'Unauthorized' })
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const payload = await verifyToken(token, c.env.JWT_SECRET)
|
|
298
|
+
c.set('userId', payload.sub)
|
|
299
|
+
await next()
|
|
300
|
+
} catch {
|
|
301
|
+
throw new HTTPException(401, { message: 'Invalid token' })
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### 4. Validators Layer (Input Validation)
|
|
307
|
+
|
|
308
|
+
Zod v4를 사용한 타입 안전 요청 검증을 담당합니다.
|
|
309
|
+
|
|
310
|
+
```
|
|
311
|
+
validators/
|
|
312
|
+
├── index.ts # re-export
|
|
313
|
+
├── user.ts # 사용자 스키마
|
|
314
|
+
├── post.ts # 게시글 스키마
|
|
315
|
+
└── common.ts # 공통 스키마
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**Zod v4 문법**:
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
// validators/user.ts
|
|
322
|
+
import { z } from 'zod'
|
|
323
|
+
|
|
324
|
+
// ✅ Zod v4 문법
|
|
325
|
+
export const createUserSchema = z.object({
|
|
326
|
+
email: z.email(), // ✅ v4: z.email()
|
|
327
|
+
name: z.string().min(1).max(100).trim(),
|
|
328
|
+
password: z.string().min(8),
|
|
329
|
+
website: z.url().optional(), // ✅ v4: z.url()
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
export const updateUserSchema = createUserSchema.partial().omit({ password: true })
|
|
333
|
+
|
|
334
|
+
export const userIdSchema = z.object({
|
|
335
|
+
id: z.string().uuid(),
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
// 타입 추출
|
|
339
|
+
export type CreateUserInput = z.infer<typeof createUserSchema>
|
|
340
|
+
export type UpdateUserInput = z.infer<typeof updateUserSchema>
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
// validators/common.ts
|
|
345
|
+
import { z } from 'zod'
|
|
346
|
+
|
|
347
|
+
export const paginationSchema = z.object({
|
|
348
|
+
page: z.coerce.number().positive().optional().default(1),
|
|
349
|
+
limit: z.coerce.number().min(1).max(100).optional().default(10),
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
export const searchSchema = z.object({
|
|
353
|
+
q: z.string().min(1),
|
|
354
|
+
page: z.coerce.number().positive().optional(),
|
|
355
|
+
limit: z.coerce.number().max(100).optional(),
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
export type PaginationInput = z.infer<typeof paginationSchema>
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### 5. Services Layer (Business Logic)
|
|
362
|
+
|
|
363
|
+
비즈니스 로직을 라우트와 분리하여 관리합니다.
|
|
364
|
+
|
|
365
|
+
```
|
|
366
|
+
services/
|
|
367
|
+
├── user/
|
|
368
|
+
│ ├── index.ts # re-export
|
|
369
|
+
│ ├── queries.ts # 조회 로직
|
|
370
|
+
│ └── mutations.ts # 생성/수정/삭제 로직
|
|
371
|
+
└── post/
|
|
372
|
+
├── index.ts
|
|
373
|
+
├── queries.ts
|
|
374
|
+
└── mutations.ts
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
**Queries** - 읽기 작업:
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
// services/user/queries.ts
|
|
381
|
+
import { HTTPException } from 'hono/http-exception'
|
|
382
|
+
import { prisma } from '@/database/prisma'
|
|
383
|
+
import type { PaginationInput } from '@/validators/common'
|
|
384
|
+
|
|
385
|
+
export const getUsers = async ({ page, limit }: PaginationInput) => {
|
|
386
|
+
const [users, total] = await Promise.all([
|
|
387
|
+
prisma.user.findMany({
|
|
388
|
+
skip: (page - 1) * limit,
|
|
389
|
+
take: limit,
|
|
390
|
+
orderBy: { createdAt: 'desc' },
|
|
391
|
+
select: {
|
|
392
|
+
id: true,
|
|
393
|
+
email: true,
|
|
394
|
+
name: true,
|
|
395
|
+
createdAt: true,
|
|
396
|
+
},
|
|
397
|
+
}),
|
|
398
|
+
prisma.user.count(),
|
|
399
|
+
])
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
users,
|
|
403
|
+
pagination: {
|
|
404
|
+
page,
|
|
405
|
+
limit,
|
|
406
|
+
total,
|
|
407
|
+
totalPages: Math.ceil(total / limit),
|
|
408
|
+
},
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export const getUserById = async (id: string) => {
|
|
413
|
+
const user = await prisma.user.findUnique({
|
|
414
|
+
where: { id },
|
|
415
|
+
include: { posts: true },
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
if (!user) {
|
|
419
|
+
throw new HTTPException(404, { message: 'User not found' })
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return user
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
**Mutations** - 쓰기 작업:
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
// services/user/mutations.ts
|
|
430
|
+
import { HTTPException } from 'hono/http-exception'
|
|
431
|
+
import { prisma } from '@/database/prisma'
|
|
432
|
+
import type { CreateUserInput, UpdateUserInput } from '@/validators/user'
|
|
433
|
+
|
|
434
|
+
export const createUser = async (data: CreateUserInput) => {
|
|
435
|
+
// 중복 체크
|
|
436
|
+
const existing = await prisma.user.findUnique({
|
|
437
|
+
where: { email: data.email },
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
if (existing) {
|
|
441
|
+
throw new HTTPException(409, { message: 'Email already exists' })
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// 비밀번호 해싱 (실제로는 bcrypt 사용)
|
|
445
|
+
const hashedPassword = await hashPassword(data.password)
|
|
446
|
+
|
|
447
|
+
return prisma.user.create({
|
|
448
|
+
data: {
|
|
449
|
+
...data,
|
|
450
|
+
password: hashedPassword,
|
|
451
|
+
},
|
|
452
|
+
select: {
|
|
453
|
+
id: true,
|
|
454
|
+
email: true,
|
|
455
|
+
name: true,
|
|
456
|
+
createdAt: true,
|
|
457
|
+
},
|
|
458
|
+
})
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export const updateUser = async (id: string, data: UpdateUserInput) => {
|
|
462
|
+
const user = await prisma.user.findUnique({ where: { id } })
|
|
463
|
+
|
|
464
|
+
if (!user) {
|
|
465
|
+
throw new HTTPException(404, { message: 'User not found' })
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return prisma.user.update({
|
|
469
|
+
where: { id },
|
|
470
|
+
data,
|
|
471
|
+
select: {
|
|
472
|
+
id: true,
|
|
473
|
+
email: true,
|
|
474
|
+
name: true,
|
|
475
|
+
updatedAt: true,
|
|
476
|
+
},
|
|
477
|
+
})
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export const deleteUser = async (id: string) => {
|
|
481
|
+
const user = await prisma.user.findUnique({ where: { id } })
|
|
482
|
+
|
|
483
|
+
if (!user) {
|
|
484
|
+
throw new HTTPException(404, { message: 'User not found' })
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
await prisma.user.delete({ where: { id } })
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### 6. Database Layer (Data Access)
|
|
492
|
+
|
|
493
|
+
Prisma v7을 통한 데이터베이스 액세스를 담당합니다.
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
// database/prisma.ts
|
|
497
|
+
import { PrismaClient } from '../../prisma/generated/client'
|
|
498
|
+
|
|
499
|
+
const globalForPrisma = globalThis as unknown as {
|
|
500
|
+
prisma: PrismaClient | undefined
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export const prisma =
|
|
504
|
+
globalForPrisma.prisma ??
|
|
505
|
+
new PrismaClient({
|
|
506
|
+
log: process.env.NODE_ENV === 'development' ? ['query'] : [],
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
510
|
+
globalForPrisma.prisma = prisma
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
## Data Flow
|
|
515
|
+
|
|
516
|
+
### Query Flow (읽기)
|
|
517
|
+
|
|
518
|
+
```
|
|
519
|
+
1. Client sends GET request
|
|
520
|
+
│
|
|
521
|
+
▼
|
|
522
|
+
┌─────────────────┐
|
|
523
|
+
│ Middleware │ ← logger, cors, secureHeaders
|
|
524
|
+
│ Stack │
|
|
525
|
+
└────────┬────────┘
|
|
526
|
+
│
|
|
527
|
+
▼
|
|
528
|
+
┌─────────────────┐
|
|
529
|
+
│ Route Handler │ ← zValidator('query', schema)
|
|
530
|
+
└────────┬────────┘
|
|
531
|
+
│
|
|
532
|
+
▼
|
|
533
|
+
┌─────────────────┐
|
|
534
|
+
│ Service Query │ ← getUsers(), getUserById()
|
|
535
|
+
└────────┬────────┘
|
|
536
|
+
│
|
|
537
|
+
▼
|
|
538
|
+
┌─────────────────┐
|
|
539
|
+
│ Prisma Client │ ← findMany(), findUnique()
|
|
540
|
+
└────────┬────────┘
|
|
541
|
+
│
|
|
542
|
+
▼
|
|
543
|
+
┌─────────────────┐
|
|
544
|
+
│ Database │
|
|
545
|
+
└─────────────────┘
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
### Mutation Flow (쓰기)
|
|
549
|
+
|
|
550
|
+
```
|
|
551
|
+
1. Client sends POST/PUT/DELETE request
|
|
552
|
+
│
|
|
553
|
+
▼
|
|
554
|
+
┌─────────────────┐
|
|
555
|
+
│ Middleware │ ← authMiddleware (인증)
|
|
556
|
+
│ Stack │
|
|
557
|
+
└────────┬────────┘
|
|
558
|
+
│
|
|
559
|
+
▼
|
|
560
|
+
┌─────────────────┐
|
|
561
|
+
│ Zod Validation │ ← zValidator('json', schema)
|
|
562
|
+
└────────┬────────┘
|
|
563
|
+
│ Valid
|
|
564
|
+
▼
|
|
565
|
+
┌─────────────────┐
|
|
566
|
+
│ Service │ ← createUser(), updateUser()
|
|
567
|
+
│ Mutation │
|
|
568
|
+
└────────┬────────┘
|
|
569
|
+
│
|
|
570
|
+
▼
|
|
571
|
+
┌─────────────────┐
|
|
572
|
+
│ Business Logic │ ← 중복 체크, 권한 검증
|
|
573
|
+
└────────┬────────┘
|
|
574
|
+
│
|
|
575
|
+
▼
|
|
576
|
+
┌─────────────────┐
|
|
577
|
+
│ Prisma Mutation│ ← create(), update(), delete()
|
|
578
|
+
└────────┬────────┘
|
|
579
|
+
│
|
|
580
|
+
▼
|
|
581
|
+
┌─────────────────┐
|
|
582
|
+
│ Database │
|
|
583
|
+
└─────────────────┘
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### Error Flow
|
|
587
|
+
|
|
588
|
+
```
|
|
589
|
+
1. Error occurs anywhere
|
|
590
|
+
│
|
|
591
|
+
▼
|
|
592
|
+
┌─────────────────┐
|
|
593
|
+
│ HTTPException │ ← 비즈니스 에러
|
|
594
|
+
│ throw │
|
|
595
|
+
└────────┬────────┘
|
|
596
|
+
│
|
|
597
|
+
▼
|
|
598
|
+
┌─────────────────┐
|
|
599
|
+
│ app.onError() │ ← 글로벌 에러 핸들러
|
|
600
|
+
└────────┬────────┘
|
|
601
|
+
│
|
|
602
|
+
▼
|
|
603
|
+
┌─────────────────┐
|
|
604
|
+
│ JSON Response │ ← { error: message }
|
|
605
|
+
└─────────────────┘
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
## RPC Pattern (Type-safe Client)
|
|
609
|
+
|
|
610
|
+
### Server 설정
|
|
611
|
+
|
|
612
|
+
```typescript
|
|
613
|
+
// src/index.ts
|
|
614
|
+
const app = new Hono()
|
|
615
|
+
.route('/api/users', users)
|
|
616
|
+
.route('/api/posts', posts)
|
|
617
|
+
|
|
618
|
+
export type AppType = typeof app
|
|
619
|
+
export default app
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
### Client 사용
|
|
623
|
+
|
|
624
|
+
```typescript
|
|
625
|
+
// client.ts
|
|
626
|
+
import { hc } from 'hono/client'
|
|
627
|
+
import type { AppType } from './server'
|
|
628
|
+
|
|
629
|
+
const client = hc<AppType>('http://localhost:8787')
|
|
630
|
+
|
|
631
|
+
// Type-safe API 호출
|
|
632
|
+
const getUsers = async () => {
|
|
633
|
+
const res = await client.api.users.$get({
|
|
634
|
+
query: { page: '1', limit: '10' },
|
|
635
|
+
})
|
|
636
|
+
return res.json()
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const createUser = async (data: CreateUserInput) => {
|
|
640
|
+
const res = await client.api.users.$post({
|
|
641
|
+
json: data,
|
|
642
|
+
})
|
|
643
|
+
return res.json()
|
|
644
|
+
}
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
## Technology Stack
|
|
648
|
+
|
|
649
|
+
### Core Framework
|
|
650
|
+
|
|
651
|
+
| Technology | Purpose | Version |
|
|
652
|
+
|------------|---------|---------|
|
|
653
|
+
| Hono | Web Standards 서버 프레임워크 | latest |
|
|
654
|
+
| TypeScript | 타입 안전 개발 | 5.x |
|
|
655
|
+
|
|
656
|
+
### Validation
|
|
657
|
+
|
|
658
|
+
| Technology | Purpose | Version |
|
|
659
|
+
|------------|---------|---------|
|
|
660
|
+
| Zod | 스키마 검증 | **4.x** |
|
|
661
|
+
| @hono/zod-validator | Zod 미들웨어 | latest |
|
|
662
|
+
|
|
663
|
+
### Database
|
|
664
|
+
|
|
665
|
+
| Technology | Purpose | Version |
|
|
666
|
+
|------------|---------|---------|
|
|
667
|
+
| Prisma | ORM | **7.x** |
|
|
668
|
+
| PostgreSQL | Primary Database | - |
|
|
669
|
+
| Cloudflare D1 | Edge Database | - |
|
|
670
|
+
|
|
671
|
+
### Runtime
|
|
672
|
+
|
|
673
|
+
| Technology | Purpose |
|
|
674
|
+
|------------|---------|
|
|
675
|
+
| Cloudflare Workers | Edge Runtime |
|
|
676
|
+
| Node.js | 로컬 개발 |
|
|
677
|
+
| Bun | 대안 런타임 |
|
|
678
|
+
|
|
679
|
+
## TypeScript Configuration
|
|
680
|
+
|
|
681
|
+
```json
|
|
682
|
+
{
|
|
683
|
+
"compilerOptions": {
|
|
684
|
+
"target": "ES2022",
|
|
685
|
+
"lib": ["ES2022"],
|
|
686
|
+
"module": "ESNext",
|
|
687
|
+
"moduleResolution": "bundler",
|
|
688
|
+
"jsx": "react-jsx",
|
|
689
|
+
"jsxImportSource": "hono/jsx",
|
|
690
|
+
"strict": true,
|
|
691
|
+
"noUnusedLocals": true,
|
|
692
|
+
"noUnusedParameters": true,
|
|
693
|
+
"types": ["@cloudflare/workers-types"],
|
|
694
|
+
"paths": {
|
|
695
|
+
"@/*": ["./src/*"]
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
"include": ["src/**/*"]
|
|
699
|
+
}
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
## Deployment Architecture
|
|
703
|
+
|
|
704
|
+
### Cloudflare Workers
|
|
705
|
+
|
|
706
|
+
```
|
|
707
|
+
┌─────────────────────────────────────┐
|
|
708
|
+
│ Cloudflare Edge Network │
|
|
709
|
+
│ │
|
|
710
|
+
│ ┌─────────────┐ ┌─────────────┐ │
|
|
711
|
+
│ │ Workers │ │ KV │ │
|
|
712
|
+
│ │ (Hono App) │ │ (Cache) │ │
|
|
713
|
+
│ └──────┬──────┘ └─────────────┘ │
|
|
714
|
+
│ │ │
|
|
715
|
+
│ ┌──────▼──────┐ ┌─────────────┐ │
|
|
716
|
+
│ │ D1 │ │ R2 │ │
|
|
717
|
+
│ │ (Database) │ │ (Storage) │ │
|
|
718
|
+
│ └─────────────┘ └─────────────┘ │
|
|
719
|
+
│ │
|
|
720
|
+
└─────────────────────────────────────┘
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
### Node.js / Bun
|
|
724
|
+
|
|
725
|
+
```
|
|
726
|
+
┌─────────────────────────────────────┐
|
|
727
|
+
│ Server (Node/Bun) │
|
|
728
|
+
│ │
|
|
729
|
+
│ ┌─────────────────────────────┐ │
|
|
730
|
+
│ │ Hono App │ │
|
|
731
|
+
│ │ (Full-stack Server) │ │
|
|
732
|
+
│ └─────────────────────────────┘ │
|
|
733
|
+
│ │
|
|
734
|
+
└───────────────────┬─────────────────┘
|
|
735
|
+
│
|
|
736
|
+
▼
|
|
737
|
+
┌───────────────┐
|
|
738
|
+
│ Database │
|
|
739
|
+
│ (PostgreSQL) │
|
|
740
|
+
└───────────────┘
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
## Documentation Structure
|
|
744
|
+
|
|
745
|
+
```
|
|
746
|
+
docs/
|
|
747
|
+
├── library/ # 라이브러리 레퍼런스
|
|
748
|
+
│ ├── hono/ # Hono 가이드
|
|
749
|
+
│ │ ├── index.md # 기본 사용법
|
|
750
|
+
│ │ ├── middleware.md # 미들웨어
|
|
751
|
+
│ │ ├── validation.md # Zod 검증
|
|
752
|
+
│ │ ├── error-handling.md # 에러 처리
|
|
753
|
+
│ │ └── rpc.md # RPC Client
|
|
754
|
+
│ ├── prisma/ # Prisma 가이드
|
|
755
|
+
│ │ ├── index.md # 기본 사용법
|
|
756
|
+
│ │ └── cloudflare-d1.md # D1 연동
|
|
757
|
+
│ └── zod/ # Zod 가이드
|
|
758
|
+
│ └── index.md
|
|
759
|
+
├── deployment/ # 배포 가이드
|
|
760
|
+
│ ├── index.md # 배포 개요
|
|
761
|
+
│ └── cloudflare.md # Cloudflare 배포
|
|
762
|
+
├── mcp/ # MCP 도구 가이드
|
|
763
|
+
│ ├── index.md
|
|
764
|
+
│ ├── sgrep.md
|
|
765
|
+
│ ├── sequential-thinking.md
|
|
766
|
+
│ └── context7.md
|
|
767
|
+
├── skills/ # 스킬 가이드
|
|
768
|
+
│ └── gemini-review/
|
|
769
|
+
├── git/ # Git 규칙
|
|
770
|
+
│ └── index.md
|
|
771
|
+
└── architecture/ # 아키텍처
|
|
772
|
+
└── architecture.md # 시스템 아키텍처
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
## Security Considerations
|
|
776
|
+
|
|
777
|
+
### Input Validation
|
|
778
|
+
|
|
779
|
+
모든 라우트에서 Zod를 통한 입력 검증:
|
|
780
|
+
|
|
781
|
+
```typescript
|
|
782
|
+
// ✅ 올바른 패턴: zValidator 사용
|
|
783
|
+
app.post(
|
|
784
|
+
'/users',
|
|
785
|
+
zValidator('json', createUserSchema), // 자동 검증
|
|
786
|
+
(c) => {
|
|
787
|
+
const data = c.req.valid('json') // data는 이미 검증됨
|
|
788
|
+
// ...
|
|
789
|
+
}
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
// ❌ 잘못된 패턴: 수동 검증 금지
|
|
793
|
+
app.post('/users', async (c) => {
|
|
794
|
+
const body = await c.req.json()
|
|
795
|
+
if (!body.email) { // ❌ 이렇게 하지 마세요
|
|
796
|
+
return c.json({ error: 'Email required' }, 400)
|
|
797
|
+
}
|
|
798
|
+
})
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
### Error Handling
|
|
802
|
+
|
|
803
|
+
HTTPException을 통한 안전한 에러 처리:
|
|
804
|
+
|
|
805
|
+
```typescript
|
|
806
|
+
// ✅ 올바른 패턴
|
|
807
|
+
import { HTTPException } from 'hono/http-exception'
|
|
808
|
+
|
|
809
|
+
if (!user) {
|
|
810
|
+
throw new HTTPException(404, { message: 'User not found' })
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// ❌ 잘못된 패턴
|
|
814
|
+
if (!user) {
|
|
815
|
+
throw new Error('User not found') // ❌ HTTPException 사용해야 함
|
|
816
|
+
}
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
### Authentication
|
|
820
|
+
|
|
821
|
+
미들웨어를 통한 인증 처리:
|
|
822
|
+
|
|
823
|
+
```typescript
|
|
824
|
+
// ✅ 올바른 패턴: 미들웨어 사용
|
|
825
|
+
app.post('/protected', authMiddleware, (c) => {
|
|
826
|
+
const userId = c.get('userId')
|
|
827
|
+
// ...
|
|
828
|
+
})
|
|
829
|
+
|
|
830
|
+
// ❌ 잘못된 패턴: 핸들러 내부에서 인증
|
|
831
|
+
app.post('/protected', async (c) => {
|
|
832
|
+
const token = c.req.header('Authorization') // ❌ 미들웨어 사용해야 함
|
|
833
|
+
// ...
|
|
834
|
+
})
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
### Environment Variables
|
|
838
|
+
|
|
839
|
+
```typescript
|
|
840
|
+
// types/env.d.ts
|
|
841
|
+
type Bindings = {
|
|
842
|
+
DATABASE_URL: string
|
|
843
|
+
JWT_SECRET: string
|
|
844
|
+
NODE_ENV: 'development' | 'production'
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// 사용
|
|
848
|
+
const app = new Hono<{ Bindings: Bindings }>()
|
|
849
|
+
|
|
850
|
+
app.get('/', (c) => {
|
|
851
|
+
const secret = c.env.JWT_SECRET // 타입 안전
|
|
852
|
+
})
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
## Performance Considerations
|
|
856
|
+
|
|
857
|
+
### Middleware Optimization
|
|
858
|
+
|
|
859
|
+
```typescript
|
|
860
|
+
// 필요한 경로에만 미들웨어 적용
|
|
861
|
+
app.use('/api/*', cors()) // API 전용
|
|
862
|
+
app.use('/api/protected/*', authMiddleware) // 인증 필요 경로만
|
|
863
|
+
|
|
864
|
+
// 불필요한 미들웨어 중복 적용 금지
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
### Database Optimization
|
|
868
|
+
|
|
869
|
+
```typescript
|
|
870
|
+
// 필요한 필드만 선택
|
|
871
|
+
const users = await prisma.user.findMany({
|
|
872
|
+
select: { // ✅ select 사용
|
|
873
|
+
id: true,
|
|
874
|
+
name: true,
|
|
875
|
+
email: true,
|
|
876
|
+
},
|
|
877
|
+
take: 20, // ✅ 페이지네이션
|
|
878
|
+
skip: (page - 1) * 20,
|
|
879
|
+
})
|
|
880
|
+
|
|
881
|
+
// 병렬 쿼리 실행
|
|
882
|
+
const [users, total] = await Promise.all([
|
|
883
|
+
prisma.user.findMany({ take: 20 }),
|
|
884
|
+
prisma.user.count(),
|
|
885
|
+
])
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
### Response Optimization
|
|
889
|
+
|
|
890
|
+
```typescript
|
|
891
|
+
// 적절한 상태 코드 사용
|
|
892
|
+
app.post('/users', (c) => {
|
|
893
|
+
return c.json({ user }, 201) // Created
|
|
894
|
+
})
|
|
895
|
+
|
|
896
|
+
// 불필요한 데이터 제외
|
|
897
|
+
const { password, ...safeUser } = user
|
|
898
|
+
return c.json({ user: safeUser })
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
## Related Documentation
|
|
902
|
+
|
|
903
|
+
- [Hono 기본 사용법](../library/hono/index.md)
|
|
904
|
+
- [미들웨어](../library/hono/middleware.md)
|
|
905
|
+
- [Zod 검증](../library/hono/validation.md)
|
|
906
|
+
- [에러 처리](../library/hono/error-handling.md)
|
|
907
|
+
- [RPC Client](../library/hono/rpc.md)
|
|
908
|
+
- [Prisma v7](../library/prisma/index.md)
|
|
909
|
+
- [Cloudflare 배포](../deployment/cloudflare.md)
|