@kood/claude-code 0.5.3 → 0.5.5
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 +552 -340
- package/package.json +1 -1
- package/templates/.claude/agents/document-writer.md +73 -306
- package/templates/.claude/instructions/agent-patterns/index.md +7 -7
- package/templates/.claude/instructions/document-templates/ralph-templates.md +71 -0
- package/templates/.claude/instructions/index.md +14 -14
- package/templates/.claude/instructions/multi-agent/agent-roster.md +14 -14
- package/templates/.claude/instructions/multi-agent/index.md +4 -4
- package/templates/.claude/skills/docs-creator/AGENTS.md +54 -176
- package/templates/.claude/skills/docs-creator/SKILL.md +98 -464
- package/templates/.claude/skills/docs-refactor/AGENTS.md +61 -190
- package/templates/.claude/skills/docs-refactor/SKILL.md +67 -443
- package/templates/.claude/skills/execute/SKILL.md +540 -13
- package/templates/.claude/skills/plan/SKILL.md +84 -18
- package/templates/.claude/skills/ralph/SKILL.md +17 -14
- package/templates/.claude/skills/refactor/AGENTS.md +269 -0
- package/templates/.claude/skills/refactor/SKILL.md +424 -66
- package/templates/.claude/skills/stitch-design/README.md +34 -0
- package/templates/.claude/skills/stitch-design/SKILL.md +213 -0
- package/templates/.claude/skills/stitch-design/examples/DESIGN.md +154 -0
- package/templates/.claude/skills/stitch-loop/README.md +54 -0
- package/templates/.claude/skills/stitch-loop/SKILL.md +316 -0
- package/templates/.claude/skills/stitch-loop/examples/SITE.md +73 -0
- package/templates/.claude/skills/stitch-loop/examples/next-prompt.md +25 -0
- package/templates/.claude/skills/stitch-loop/resources/baton-schema.md +61 -0
- package/templates/.claude/skills/stitch-loop/resources/site-template.md +104 -0
- package/templates/.claude/skills/stitch-react/README.md +36 -0
- package/templates/.claude/skills/stitch-react/SKILL.md +323 -0
- package/templates/.claude/skills/stitch-react/examples/gold-standard-card.tsx +88 -0
- package/templates/.claude/skills/stitch-react/package-lock.json +231 -0
- package/templates/.claude/skills/stitch-react/package.json +16 -0
- package/templates/.claude/skills/stitch-react/resources/architecture-checklist.md +15 -0
- package/templates/.claude/skills/stitch-react/resources/component-template.tsx +37 -0
- package/templates/.claude/skills/stitch-react/resources/stitch-api-reference.md +14 -0
- package/templates/.claude/skills/stitch-react/resources/style-guide.json +24 -0
- package/templates/.claude/skills/stitch-react/scripts/fetch-stitch.sh +30 -0
- package/templates/.claude/skills/stitch-react/scripts/validate.js +77 -0
- package/templates/hono/CLAUDE.md +28 -28
- package/templates/hono/docs/architecture.md +24 -24
- package/templates/hono/docs/deployment/cloudflare.md +18 -18
- package/templates/hono/docs/deployment/docker.md +13 -13
- package/templates/hono/docs/deployment/index.md +19 -19
- package/templates/hono/docs/deployment/railway.md +32 -32
- package/templates/hono/docs/deployment/vercel.md +29 -29
- package/templates/hono/docs/guides/conventions.md +57 -57
- package/templates/hono/docs/guides/env-setup.md +47 -47
- package/templates/hono/docs/guides/getting-started.md +27 -27
- package/templates/hono/docs/library/hono/error-handling.md +11 -11
- package/templates/hono/docs/library/hono/index.md +4 -4
- package/templates/hono/docs/library/hono/middleware.md +18 -18
- package/templates/hono/docs/library/hono/rpc.md +7 -7
- package/templates/hono/docs/library/hono/validation.md +6 -6
- package/templates/hono/docs/library/prisma/cloudflare-d1.md +29 -29
- package/templates/hono/docs/library/prisma/config.md +16 -16
- package/templates/hono/docs/library/prisma/index.md +32 -32
- package/templates/hono/docs/library/t3-env/index.md +22 -22
- package/templates/hono/docs/library/zod/index.md +31 -31
- package/templates/nextjs/CLAUDE.md +54 -54
- package/templates/nextjs/docs/architecture.md +146 -146
- package/templates/nextjs/docs/design.md +183 -183
- package/templates/nextjs/docs/guides/conventions.md +86 -86
- package/templates/nextjs/docs/guides/getting-started.md +28 -28
- package/templates/nextjs/docs/guides/routes.md +32 -32
- package/templates/nextjs/docs/library/better-auth/index.md +70 -70
- package/templates/nextjs/docs/library/nextjs/app-router.md +43 -43
- package/templates/nextjs/docs/library/nextjs/caching.md +73 -73
- package/templates/nextjs/docs/library/nextjs/index.md +51 -51
- package/templates/nextjs/docs/library/nextjs/middleware.md +41 -41
- package/templates/nextjs/docs/library/nextjs/route-handlers.md +31 -31
- package/templates/nextjs/docs/library/nextjs/server-actions.md +34 -34
- package/templates/nextjs/docs/library/prisma/cloudflare-d1.md +20 -20
- package/templates/nextjs/docs/library/prisma/config.md +18 -18
- package/templates/nextjs/docs/library/prisma/crud.md +17 -17
- package/templates/nextjs/docs/library/prisma/index.md +18 -18
- package/templates/nextjs/docs/library/prisma/relations.md +16 -16
- package/templates/nextjs/docs/library/prisma/schema.md +23 -23
- package/templates/nextjs/docs/library/prisma/setup.md +6 -6
- package/templates/nextjs/docs/library/prisma/transactions.md +10 -10
- package/templates/nextjs/docs/library/tanstack-query/index.md +6 -6
- package/templates/nextjs/docs/library/tanstack-query/invalidation.md +20 -20
- package/templates/nextjs/docs/library/tanstack-query/optimistic-updates.md +4 -4
- package/templates/nextjs/docs/library/tanstack-query/use-mutation.md +15 -15
- package/templates/nextjs/docs/library/tanstack-query/use-query.md +22 -22
- package/templates/nextjs/docs/library/zod/complex-types.md +11 -11
- package/templates/nextjs/docs/library/zod/index.md +8 -8
- package/templates/nextjs/docs/library/zod/transforms.md +11 -11
- package/templates/nextjs/docs/library/zod/validation.md +9 -9
- package/templates/npx/CLAUDE.md +38 -38
- package/templates/npx/docs/library/commander/index.md +12 -12
- package/templates/npx/docs/library/fs-extra/index.md +9 -9
- package/templates/npx/docs/library/prompts/index.md +3 -3
- package/templates/npx/docs/references/patterns.md +12 -12
- package/templates/tanstack-start/CLAUDE.md +54 -54
- package/templates/tanstack-start/docs/architecture.md +128 -128
- package/templates/tanstack-start/docs/design.md +169 -169
- package/templates/tanstack-start/docs/guides/conventions.md +43 -43
- package/templates/tanstack-start/docs/guides/env-setup.md +35 -35
- package/templates/tanstack-start/docs/guides/getting-started.md +19 -19
- package/templates/tanstack-start/docs/guides/hooks.md +45 -45
- package/templates/tanstack-start/docs/guides/routes.md +54 -54
- package/templates/tanstack-start/docs/guides/services.md +45 -45
- package/templates/tanstack-start/docs/library/prisma/cloudflare-d1.md +19 -19
- package/templates/tanstack-start/docs/library/prisma/config.md +16 -16
- package/templates/tanstack-start/docs/library/prisma/crud.md +17 -17
- package/templates/tanstack-start/docs/library/prisma/relations.md +16 -16
- package/templates/tanstack-start/docs/library/prisma/schema.md +23 -23
- package/templates/tanstack-start/docs/library/prisma/setup.md +6 -6
- package/templates/tanstack-start/docs/library/prisma/transactions.md +10 -10
- package/templates/tanstack-start/docs/library/tanstack-query/invalidation.md +19 -19
- package/templates/tanstack-start/docs/library/tanstack-query/optimistic-updates.md +4 -4
- package/templates/tanstack-start/docs/library/tanstack-query/use-mutation.md +14 -14
- package/templates/tanstack-start/docs/library/tanstack-query/use-query.md +21 -21
- package/templates/tanstack-start/docs/library/tanstack-router/error-handling.md +9 -9
- package/templates/tanstack-start/docs/library/tanstack-router/hooks.md +11 -11
- package/templates/tanstack-start/docs/library/tanstack-router/navigation.md +17 -17
- package/templates/tanstack-start/docs/library/tanstack-router/route-context.md +5 -5
- package/templates/tanstack-start/docs/library/tanstack-router/search-params.md +10 -10
- package/templates/tanstack-start/docs/library/tanstack-start/auth-patterns.md +8 -8
- package/templates/tanstack-start/docs/library/tanstack-start/middleware.md +9 -9
- package/templates/tanstack-start/docs/library/tanstack-start/routing.md +6 -6
- package/templates/tanstack-start/docs/library/tanstack-start/server-functions.md +18 -18
- package/templates/tanstack-start/docs/library/tanstack-start/setup.md +4 -4
- package/templates/tanstack-start/docs/library/zod/complex-types.md +11 -11
- package/templates/tanstack-start/docs/library/zod/transforms.md +11 -11
- package/templates/tanstack-start/docs/library/zod/validation.md +9 -9
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<Link to="/products" search={{ page: 1, sort: 'newest' }}>Products</Link>
|
|
10
10
|
<Link to="/products" search={prev => ({ ...prev, page: 2 })}>Next</Link>
|
|
11
11
|
|
|
12
|
-
// Active
|
|
12
|
+
// Active 스타일
|
|
13
13
|
<Link
|
|
14
14
|
to="/about"
|
|
15
15
|
activeProps={{ className: 'text-blue-500 font-bold' }}
|
|
@@ -33,11 +33,11 @@ const Component = () => {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// Preloading
|
|
36
|
-
<Link to="/posts" preload="intent">Posts</Link> //
|
|
37
|
-
<Link to="/dashboard" preload="render">Dash</Link> //
|
|
38
|
-
<Link to="/products" preload="viewport">Prod</Link> //
|
|
36
|
+
<Link to="/posts" preload="intent">Posts</Link> // hover 시
|
|
37
|
+
<Link to="/dashboard" preload="render">Dash</Link> // 렌더링 시
|
|
38
|
+
<Link to="/products" preload="viewport">Prod</Link> // viewport 진입 시
|
|
39
39
|
|
|
40
|
-
//
|
|
40
|
+
// 조건부 네비게이션
|
|
41
41
|
const SubmitButton = () => {
|
|
42
42
|
const navigate = useNavigate()
|
|
43
43
|
const [isPending, startTransition] = useTransition()
|
|
@@ -57,24 +57,24 @@ const SubmitButton = () => {
|
|
|
57
57
|
|
|
58
58
|
<options>
|
|
59
59
|
|
|
60
|
-
| Link Props |
|
|
60
|
+
| Link Props | 타입 | 설명 |
|
|
61
61
|
|------------|------|------|
|
|
62
|
-
| `to` | string |
|
|
63
|
-
| `params` | object | Path
|
|
62
|
+
| `to` | string | 목적지 경로 |
|
|
63
|
+
| `params` | object | Path 파라미터 |
|
|
64
64
|
| `search` | object \| function | Search params |
|
|
65
65
|
| `hash` | string | Hash |
|
|
66
|
-
| `replace` | boolean |
|
|
67
|
-
| `preload` | 'intent' \| 'render' \| 'viewport' | Preload
|
|
68
|
-
| `activeProps` | object |
|
|
69
|
-
| `inactiveProps` | object |
|
|
66
|
+
| `replace` | boolean | history replace |
|
|
67
|
+
| `preload` | 'intent' \| 'render' \| 'viewport' | Preload 전략 |
|
|
68
|
+
| `activeProps` | object | Active 시 props |
|
|
69
|
+
| `inactiveProps` | object | Inactive 시 props |
|
|
70
70
|
|
|
71
|
-
| navigate
|
|
71
|
+
| navigate 옵션 | 타입 | 설명 |
|
|
72
72
|
|---------------|------|------|
|
|
73
|
-
| `to` | string |
|
|
74
|
-
| `params` | object | Path
|
|
73
|
+
| `to` | string | 목적지 경로 |
|
|
74
|
+
| `params` | object | Path 파라미터 |
|
|
75
75
|
| `search` | object \| function | Search params |
|
|
76
76
|
| `hash` | string | Hash |
|
|
77
|
-
| `replace` | boolean |
|
|
78
|
-
| `resetScroll` | boolean |
|
|
77
|
+
| `replace` | boolean | history.replace 사용 |
|
|
78
|
+
| `resetScroll` | boolean | 스크롤 리셋 |
|
|
79
79
|
|
|
80
80
|
</options>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<patterns>
|
|
4
4
|
|
|
5
5
|
```tsx
|
|
6
|
-
// beforeLoad:
|
|
6
|
+
// beforeLoad: 인증 체크 + context 추가
|
|
7
7
|
export const Route = createFileRoute('/dashboard')({
|
|
8
8
|
beforeLoad: async ({ context, location }) => {
|
|
9
9
|
if (!context.auth.isAuthenticated) {
|
|
@@ -30,7 +30,7 @@ export const Route = createFileRoute('/_authed/dashboard')({
|
|
|
30
30
|
component: DashboardPage,
|
|
31
31
|
})
|
|
32
32
|
const DashboardPage = () => {
|
|
33
|
-
const { user } = Route.useRouteContext() //
|
|
33
|
+
const { user } = Route.useRouteContext() // _authed에서 전달된 context
|
|
34
34
|
return <h1>Welcome, {user.name}!</h1>
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -75,10 +75,10 @@ routes/
|
|
|
75
75
|
|
|
76
76
|
<usage>
|
|
77
77
|
|
|
78
|
-
|
|
|
78
|
+
| 위치 | 접근 방법 |
|
|
79
79
|
|------|----------|
|
|
80
|
-
| beforeLoad | `{ context }`
|
|
81
|
-
| loader | `{ context }`
|
|
80
|
+
| beforeLoad | `{ context }` 파라미터 |
|
|
81
|
+
| loader | `{ context }` 파라미터 |
|
|
82
82
|
| component | `Route.useRouteContext()` |
|
|
83
83
|
|
|
84
84
|
</usage>
|
|
@@ -3,20 +3,20 @@
|
|
|
3
3
|
<patterns>
|
|
4
4
|
|
|
5
5
|
```tsx
|
|
6
|
-
// Zod
|
|
6
|
+
// Zod 스키마 + 라우트
|
|
7
7
|
const searchSchema = z.object({
|
|
8
|
-
page: z.number().catch(1), //
|
|
9
|
-
search: z.string().optional(), //
|
|
8
|
+
page: z.number().catch(1), // 기본값
|
|
9
|
+
search: z.string().optional(), // 선택
|
|
10
10
|
sort: z.enum(['newest', 'price']).catch('newest'),
|
|
11
|
-
tags: z.array(z.string()).catch([]), //
|
|
11
|
+
tags: z.array(z.string()).catch([]), // 배열
|
|
12
12
|
inStock: z.boolean().catch(true), // Boolean
|
|
13
|
-
from: z.string().date().optional(), //
|
|
14
|
-
minPrice: z.number().min(0).catch(0), //
|
|
13
|
+
from: z.string().date().optional(), // 날짜
|
|
14
|
+
minPrice: z.number().min(0).catch(0), // 범위
|
|
15
15
|
})
|
|
16
16
|
|
|
17
17
|
export const Route = createFileRoute('/products')({
|
|
18
18
|
validateSearch: searchSchema,
|
|
19
|
-
loaderDeps: ({ search }) => ({ search }), //
|
|
19
|
+
loaderDeps: ({ search }) => ({ search }), // search 변경 시 loader 재실행
|
|
20
20
|
loader: async ({ deps: { search } }) => fetchProducts(search),
|
|
21
21
|
component: ProductsPage,
|
|
22
22
|
})
|
|
@@ -26,11 +26,11 @@ const ProductsPage = () => {
|
|
|
26
26
|
return <div>Page: {page}, Sort: {sort}</div>
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
//
|
|
29
|
+
// Link로 업데이트
|
|
30
30
|
<Link to="/products" search={{ page: 1, sort: 'newest' }}>Reset</Link>
|
|
31
31
|
<Link to="/products" search={prev => ({ ...prev, page: 2 })}>Next</Link>
|
|
32
32
|
|
|
33
|
-
//
|
|
33
|
+
// useNavigate로 업데이트
|
|
34
34
|
const Pagination = () => {
|
|
35
35
|
const navigate = useNavigate()
|
|
36
36
|
const { page } = Route.useSearch()
|
|
@@ -48,7 +48,7 @@ const Pagination = () => {
|
|
|
48
48
|
)
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
//
|
|
51
|
+
// 실전: 필터 + 정렬 + 페이지네이션
|
|
52
52
|
const PostsPage = () => {
|
|
53
53
|
const { page, search, category, sort } = Route.useSearch()
|
|
54
54
|
const posts = Route.useLoaderData()
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
# TanStack Start -
|
|
1
|
+
# TanStack Start - 인증 패턴
|
|
2
2
|
|
|
3
3
|
<patterns>
|
|
4
4
|
|
|
5
5
|
```typescript
|
|
6
|
-
//
|
|
6
|
+
// 로그인
|
|
7
7
|
export const loginFn = createServerFn({ method: 'POST' })
|
|
8
8
|
.inputValidator((data: { email: string; password: string }) => data)
|
|
9
9
|
.handler(async ({ data }) => {
|
|
@@ -14,7 +14,7 @@ export const loginFn = createServerFn({ method: 'POST' })
|
|
|
14
14
|
throw redirect({ to: '/dashboard' })
|
|
15
15
|
})
|
|
16
16
|
|
|
17
|
-
//
|
|
17
|
+
// 로그아웃
|
|
18
18
|
export const logoutFn = createServerFn({ method: 'POST' })
|
|
19
19
|
.handler(async () => {
|
|
20
20
|
const session = await useAppSession()
|
|
@@ -22,7 +22,7 @@ export const logoutFn = createServerFn({ method: 'POST' })
|
|
|
22
22
|
throw redirect({ to: '/' })
|
|
23
23
|
})
|
|
24
24
|
|
|
25
|
-
//
|
|
25
|
+
// 현재 사용자
|
|
26
26
|
export const getCurrentUserFn = createServerFn({ method: 'GET' })
|
|
27
27
|
.handler(async () => {
|
|
28
28
|
const session = await useAppSession()
|
|
@@ -30,7 +30,7 @@ export const getCurrentUserFn = createServerFn({ method: 'GET' })
|
|
|
30
30
|
return getUserById(session.data.userId)
|
|
31
31
|
})
|
|
32
32
|
|
|
33
|
-
//
|
|
33
|
+
// 인증 미들웨어
|
|
34
34
|
export const authMiddleware = createMiddleware({ type: 'function' })
|
|
35
35
|
.server(async ({ next }) => {
|
|
36
36
|
const session = await useAppSession()
|
|
@@ -39,12 +39,12 @@ export const authMiddleware = createMiddleware({ type: 'function' })
|
|
|
39
39
|
return next({ context: { user } })
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
-
//
|
|
42
|
+
// Server Function에 적용
|
|
43
43
|
export const protectedFn = createServerFn({ method: 'GET' })
|
|
44
44
|
.middleware([authMiddleware])
|
|
45
45
|
.handler(async ({ context }) => ({ user: context.user }))
|
|
46
46
|
|
|
47
|
-
//
|
|
47
|
+
// 보호된 라우트
|
|
48
48
|
export const Route = createFileRoute('/dashboard')({
|
|
49
49
|
beforeLoad: async () => {
|
|
50
50
|
const user = await getCurrentUserFn()
|
|
@@ -57,7 +57,7 @@ export const Route = createFileRoute('/dashboard')({
|
|
|
57
57
|
},
|
|
58
58
|
})
|
|
59
59
|
|
|
60
|
-
// Better Auth
|
|
60
|
+
// Better Auth 통합
|
|
61
61
|
export const auth = betterAuth({
|
|
62
62
|
database: prismaAdapter(prisma),
|
|
63
63
|
emailAndPassword: { enabled: true },
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
# TanStack Start - Middleware
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Server Function 및 라우트에 공통 로직 적용.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 기본 패턴
|
|
6
6
|
|
|
7
7
|
```typescript
|
|
8
|
-
//
|
|
8
|
+
// 미들웨어 정의
|
|
9
9
|
const loggingMiddleware = createMiddleware({ type: 'function' })
|
|
10
10
|
.server(({ next }) => {
|
|
11
11
|
console.log('Processing request')
|
|
12
12
|
return next()
|
|
13
13
|
})
|
|
14
14
|
|
|
15
|
-
//
|
|
15
|
+
// 적용
|
|
16
16
|
const fn = createServerFn()
|
|
17
17
|
.middleware([loggingMiddleware])
|
|
18
18
|
.handler(async () => ({ message: 'Hello' }))
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
##
|
|
21
|
+
## 인증 미들웨어
|
|
22
22
|
|
|
23
23
|
```typescript
|
|
24
24
|
const authMiddleware = createMiddleware({ type: 'function' })
|
|
@@ -28,7 +28,7 @@ const authMiddleware = createMiddleware({ type: 'function' })
|
|
|
28
28
|
return next({ context: { user: session.user } })
|
|
29
29
|
})
|
|
30
30
|
|
|
31
|
-
//
|
|
31
|
+
// 사용
|
|
32
32
|
export const protectedFn = createServerFn({ method: 'GET' })
|
|
33
33
|
.middleware([authMiddleware])
|
|
34
34
|
.handler(async ({ context }) => ({ user: context.user }))
|
|
@@ -47,8 +47,8 @@ const workspaceMiddleware = createMiddleware({ type: 'function' })
|
|
|
47
47
|
```typescript
|
|
48
48
|
// src/start.ts
|
|
49
49
|
export const startInstance = createStart(() => ({
|
|
50
|
-
requestMiddleware: [globalMiddleware], //
|
|
51
|
-
functionMiddleware: [loggingMiddleware], //
|
|
50
|
+
requestMiddleware: [globalMiddleware], // 모든 요청
|
|
51
|
+
functionMiddleware: [loggingMiddleware], // 모든 Server Function
|
|
52
52
|
}))
|
|
53
53
|
```
|
|
54
54
|
|
|
@@ -57,7 +57,7 @@ export const startInstance = createStart(() => ({
|
|
|
57
57
|
```typescript
|
|
58
58
|
export const Route = createFileRoute('/hello')({
|
|
59
59
|
server: {
|
|
60
|
-
middleware: [authMiddleware], //
|
|
60
|
+
middleware: [authMiddleware], // 모든 핸들러
|
|
61
61
|
handlers: { GET: async ({ request }) => new Response('Hello') },
|
|
62
62
|
},
|
|
63
63
|
})
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<patterns>
|
|
4
4
|
|
|
5
5
|
```tsx
|
|
6
|
-
//
|
|
6
|
+
// 기본
|
|
7
7
|
export const Route = createFileRoute('/about')({ component: AboutPage })
|
|
8
8
|
|
|
9
9
|
// Loader
|
|
@@ -16,7 +16,7 @@ const Page = () => {
|
|
|
16
16
|
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
//
|
|
19
|
+
// 동적 라우트
|
|
20
20
|
export const Route = createFileRoute('/users/$id')({
|
|
21
21
|
loader: async ({ params }) => ({ user: await getUserById(params.id) }),
|
|
22
22
|
component: () => {
|
|
@@ -25,10 +25,10 @@ export const Route = createFileRoute('/users/$id')({
|
|
|
25
25
|
},
|
|
26
26
|
})
|
|
27
27
|
|
|
28
|
-
// SSR
|
|
29
|
-
ssr: true //
|
|
30
|
-
ssr: false //
|
|
31
|
-
ssr: 'data-only' //
|
|
28
|
+
// SSR 옵션
|
|
29
|
+
ssr: true // 전체 SSR (기본값)
|
|
30
|
+
ssr: false // 클라이언트만
|
|
31
|
+
ssr: 'data-only' // 데이터만 서버
|
|
32
32
|
|
|
33
33
|
// Server Routes (API)
|
|
34
34
|
export const Route = createFileRoute('/api/hello')({
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# TanStack Start - Server Functions
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
서버에서만 실행되는 타입 안전한 함수.
|
|
4
4
|
|
|
5
|
-
## ⚠️
|
|
5
|
+
## ⚠️ 필수: TanStack Query 사용
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
-
|
|
7
|
+
클라이언트 호출 시 반드시 useQuery/useMutation 사용.
|
|
8
|
+
- 자동 캐싱, 중복 요청 제거, 로딩/에러 상태 관리, invalidateQueries 동기화
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## 기본 패턴
|
|
11
11
|
|
|
12
12
|
```typescript
|
|
13
13
|
// GET
|
|
@@ -25,31 +25,31 @@ export const createUser = createServerFn({ method: 'POST' })
|
|
|
25
25
|
.handler(async ({ data }) => prisma.user.create({ data }))
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
##
|
|
28
|
+
## 컴포넌트에서 호출
|
|
29
29
|
|
|
30
30
|
```tsx
|
|
31
|
-
// ✅ useQuery (
|
|
31
|
+
// ✅ useQuery (조회)
|
|
32
32
|
const { data, isLoading } = useQuery({
|
|
33
33
|
queryKey: ['posts'],
|
|
34
34
|
queryFn: () => getServerPosts(),
|
|
35
35
|
})
|
|
36
36
|
|
|
37
|
-
// ✅ useMutation (
|
|
37
|
+
// ✅ useMutation (변경)
|
|
38
38
|
const mutation = useMutation({
|
|
39
39
|
mutationFn: createPost,
|
|
40
40
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }),
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
-
// ❌
|
|
43
|
+
// ❌ 직접 호출 금지 (캐싱 없음, 동기화 안됨)
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
##
|
|
46
|
+
## 함수 분리 규칙
|
|
47
47
|
|
|
48
48
|
```typescript
|
|
49
|
-
//
|
|
49
|
+
// 내부 헬퍼 (export 금지!)
|
|
50
50
|
const validateUserData = async (email: string) => { ... }
|
|
51
51
|
|
|
52
|
-
// Server Function (
|
|
52
|
+
// Server Function (export 가능)
|
|
53
53
|
export const createUser = createServerFn({ method: 'POST' })
|
|
54
54
|
.inputValidator(createUserSchema)
|
|
55
55
|
.handler(async ({ data }) => {
|
|
@@ -57,19 +57,19 @@ export const createUser = createServerFn({ method: 'POST' })
|
|
|
57
57
|
return prisma.user.create({ data })
|
|
58
58
|
})
|
|
59
59
|
|
|
60
|
-
// index.ts:
|
|
60
|
+
// index.ts: Server Function만 export
|
|
61
61
|
export { createUser } from './mutations'
|
|
62
|
-
// ❌ export { validateUserData }
|
|
62
|
+
// ❌ export { validateUserData } 금지
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
##
|
|
65
|
+
## 보안
|
|
66
66
|
|
|
67
67
|
```tsx
|
|
68
|
-
// ❌
|
|
68
|
+
// ❌ loader에서 환경변수 직접 사용 (노출됨)
|
|
69
69
|
loader: () => { const secret = process.env.SECRET }
|
|
70
70
|
|
|
71
|
-
// ✅
|
|
71
|
+
// ✅ Server Function 사용
|
|
72
72
|
const fn = createServerFn().handler(() => {
|
|
73
|
-
const secret = process.env.SECRET //
|
|
73
|
+
const secret = process.env.SECRET // 서버에서만
|
|
74
74
|
})
|
|
75
75
|
```
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
# TanStack Start -
|
|
1
|
+
# TanStack Start - 설치 및 설정
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 설치
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
yarn add @tanstack/react-start @tanstack/react-router vinxi
|
|
7
7
|
yarn add -D vite @vitejs/plugin-react vite-tsconfig-paths
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## 설정
|
|
11
11
|
|
|
12
12
|
```typescript
|
|
13
13
|
// vite.config.ts
|
|
@@ -36,7 +36,7 @@ export default defineConfig({
|
|
|
36
36
|
}
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
##
|
|
39
|
+
## 환경 변수 검증
|
|
40
40
|
|
|
41
41
|
```typescript
|
|
42
42
|
// lib/env.ts
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
# Zod -
|
|
1
|
+
# Zod - 복합 타입
|
|
2
2
|
|
|
3
3
|
<patterns>
|
|
4
4
|
|
|
5
5
|
```typescript
|
|
6
|
-
//
|
|
6
|
+
// 객체
|
|
7
7
|
const UserSchema = z.object({
|
|
8
8
|
name: z.string(),
|
|
9
9
|
email: z.email(),
|
|
10
10
|
age: z.number().optional(),
|
|
11
11
|
})
|
|
12
12
|
|
|
13
|
-
UserSchema.partial() //
|
|
14
|
-
UserSchema.required() //
|
|
15
|
-
UserSchema.pick({ name: true }) //
|
|
16
|
-
UserSchema.omit({ email: true }) //
|
|
13
|
+
UserSchema.partial() // 모든 필드 optional
|
|
14
|
+
UserSchema.required() // 모든 필드 required
|
|
15
|
+
UserSchema.pick({ name: true }) // 특정 필드만
|
|
16
|
+
UserSchema.omit({ email: true }) // 특정 필드 제외
|
|
17
17
|
UserSchema.extend({ role: z.enum(['admin', 'user']) })
|
|
18
18
|
UserSchema.merge(AnotherSchema)
|
|
19
19
|
|
|
20
|
-
z.strictObject({ name: z.string() }) // v4:
|
|
21
|
-
z.looseObject({ name: z.string() }) // v4:
|
|
20
|
+
z.strictObject({ name: z.string() }) // v4: 추가 키 에러
|
|
21
|
+
z.looseObject({ name: z.string() }) // v4: 추가 키 통과
|
|
22
22
|
|
|
23
|
-
//
|
|
23
|
+
// 배열/튜플
|
|
24
24
|
z.array(z.string())
|
|
25
25
|
z.array(z.number()).min(1).max(10).length(5).nonempty()
|
|
26
26
|
z.tuple([z.string(), z.number()]) // [string, number]
|
|
27
27
|
|
|
28
|
-
//
|
|
28
|
+
// 유니온
|
|
29
29
|
z.union([z.string(), z.number()])
|
|
30
30
|
z.string().or(z.number())
|
|
31
31
|
|
|
@@ -46,7 +46,7 @@ z.record(z.string(), z.object({ name: z.string() })) // { [key: string]: { name
|
|
|
46
46
|
z.map(z.string(), z.number()) // Map<string, number>
|
|
47
47
|
z.set(z.number()) // Set<number>
|
|
48
48
|
|
|
49
|
-
//
|
|
49
|
+
// 재귀
|
|
50
50
|
type Json = string | number | boolean | null | { [key: string]: Json } | Json[]
|
|
51
51
|
|
|
52
52
|
const jsonSchema: z.ZodType<Json> = z.lazy(() =>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Zod -
|
|
1
|
+
# Zod - 변환
|
|
2
2
|
|
|
3
3
|
<patterns>
|
|
4
4
|
|
|
@@ -10,7 +10,7 @@ stringToLength.parse('hello') // => 5
|
|
|
10
10
|
type In = z.input<typeof stringToLength> // string
|
|
11
11
|
type Out = z.output<typeof stringToLength> // number
|
|
12
12
|
|
|
13
|
-
// Pipe (
|
|
13
|
+
// Pipe (검증 후 변환)
|
|
14
14
|
const stringToNumber = z.string()
|
|
15
15
|
.transform((val) => parseInt(val, 10))
|
|
16
16
|
.pipe(z.number().min(0).max(100))
|
|
@@ -18,20 +18,20 @@ const stringToNumber = z.string()
|
|
|
18
18
|
stringToNumber.parse('50') // => 50
|
|
19
19
|
stringToNumber.parse('150') // throws
|
|
20
20
|
|
|
21
|
-
// Coerce (
|
|
22
|
-
z.coerce.string() //
|
|
23
|
-
z.coerce.number() //
|
|
24
|
-
z.coerce.boolean() //
|
|
25
|
-
z.coerce.date() //
|
|
26
|
-
z.coerce.bigint() //
|
|
21
|
+
// Coerce (강제 변환)
|
|
22
|
+
z.coerce.string() // 모든 값 → 문자열
|
|
23
|
+
z.coerce.number() // 모든 값 → 숫자
|
|
24
|
+
z.coerce.boolean() // 모든 값 → 불리언
|
|
25
|
+
z.coerce.date() // 모든 값 → 날짜
|
|
26
|
+
z.coerce.bigint() // 모든 값 → BigInt
|
|
27
27
|
|
|
28
28
|
z.coerce.number().parse("42") // => 42
|
|
29
29
|
z.coerce.date().parse("2021-01-01") // => Date
|
|
30
30
|
|
|
31
|
-
// v4:
|
|
31
|
+
// v4: 입력 타입이 unknown으로 변경
|
|
32
32
|
type In = z.input<typeof z.coerce.string()> // unknown
|
|
33
33
|
|
|
34
|
-
// v4
|
|
34
|
+
// v4 환경변수 불리언
|
|
35
35
|
z.stringbool() // "true"/"yes"/"1" → true, "false"/"no"/"0" → false
|
|
36
36
|
|
|
37
37
|
// Preprocess
|
|
@@ -40,7 +40,7 @@ const trimmed = z.preprocess(
|
|
|
40
40
|
z.string()
|
|
41
41
|
)
|
|
42
42
|
|
|
43
|
-
//
|
|
43
|
+
// 입력/출력 타입 분리
|
|
44
44
|
const Schema = z.object({
|
|
45
45
|
createdAt: z.string().transform((str) => new Date(str)),
|
|
46
46
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Zod -
|
|
1
|
+
# Zod - 검증
|
|
2
2
|
|
|
3
3
|
<patterns>
|
|
4
4
|
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
// Refinement (v4: message → error)
|
|
7
7
|
const PasswordSchema = z.string()
|
|
8
8
|
.min(8)
|
|
9
|
-
.refine((val) => /[A-Z]/.test(val), { error: '
|
|
10
|
-
.refine((val) => /[0-9]/.test(val), { error: '
|
|
9
|
+
.refine((val) => /[A-Z]/.test(val), { error: '대문자 필수' })
|
|
10
|
+
.refine((val) => /[0-9]/.test(val), { error: '숫자 필수' })
|
|
11
11
|
|
|
12
|
-
z.string().refine(val => val.includes("@")).min(5) // v4:
|
|
12
|
+
z.string().refine(val => val.includes("@")).min(5) // v4: refinement 후 체이닝
|
|
13
13
|
|
|
14
14
|
// Async
|
|
15
15
|
const schema = z.string().refine(async (val) => val.length <= 8)
|
|
@@ -23,20 +23,20 @@ z.object({
|
|
|
23
23
|
if (data.password !== data.confirmPassword) {
|
|
24
24
|
ctx.addIssue({
|
|
25
25
|
code: z.ZodIssueCode.custom,
|
|
26
|
-
message: '
|
|
26
|
+
message: '비밀번호 불일치',
|
|
27
27
|
path: ['confirmPassword'],
|
|
28
28
|
})
|
|
29
29
|
}
|
|
30
|
-
}) // v4: ctx.path
|
|
30
|
+
}) // v4: ctx.path 사용 불가
|
|
31
31
|
|
|
32
|
-
//
|
|
32
|
+
// 커스텀
|
|
33
33
|
const px = z.custom<`${number}px`>((val) =>
|
|
34
34
|
typeof val === 'string' && /^\d+px$/.test(val)
|
|
35
35
|
)
|
|
36
36
|
px.parse('42px') // ✅
|
|
37
37
|
px.parse('42vw') // throws
|
|
38
38
|
|
|
39
|
-
//
|
|
39
|
+
// 에러 처리
|
|
40
40
|
const result = schema.safeParse(data)
|
|
41
41
|
if (!result.success) {
|
|
42
42
|
result.error.errors.forEach((err) => {
|
|
@@ -50,7 +50,7 @@ export const createUser = createServerFn({ method: 'POST' })
|
|
|
50
50
|
.inputValidator(zodValidator(createUserSchema))
|
|
51
51
|
.handler(async ({ data }) => prisma.user.create({ data }))
|
|
52
52
|
|
|
53
|
-
//
|
|
53
|
+
// 환경 변수
|
|
54
54
|
const env = z.object({
|
|
55
55
|
NODE_ENV: z.enum(['development', 'production', 'test']),
|
|
56
56
|
DATABASE_URL: z.string().url(),
|