@kood/claude-code 0.6.6 → 0.7.0

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.
Files changed (170) hide show
  1. package/dist/index.js +7 -1
  2. package/package.json +1 -1
  3. package/templates/.claude/agents/analyst.md +5 -0
  4. package/templates/.claude/agents/architect.md +5 -0
  5. package/templates/.claude/agents/build-fixer.md +1 -0
  6. package/templates/.claude/agents/code-reviewer.md +1 -0
  7. package/templates/.claude/agents/critic.md +4 -0
  8. package/templates/.claude/agents/deep-executor.md +1 -0
  9. package/templates/.claude/agents/dependency-manager.md +2 -0
  10. package/templates/.claude/agents/deployment-validator.md +2 -0
  11. package/templates/.claude/agents/designer.md +2 -0
  12. package/templates/.claude/agents/document-writer.md +3 -0
  13. package/templates/.claude/agents/explore.md +1 -0
  14. package/templates/.claude/agents/git-operator.md +2 -0
  15. package/templates/.claude/agents/implementation-executor.md +2 -0
  16. package/templates/.claude/agents/ko-to-en-translator.md +3 -0
  17. package/templates/.claude/agents/lint-fixer.md +2 -0
  18. package/templates/.claude/agents/planner.md +3 -0
  19. package/templates/.claude/agents/pm.md +349 -0
  20. package/templates/.claude/agents/qa-tester.md +1 -0
  21. package/templates/.claude/agents/refactor-advisor.md +4 -0
  22. package/templates/.claude/agents/researcher.md +9 -1
  23. package/templates/.claude/agents/scientist.md +1 -0
  24. package/templates/.claude/agents/security-reviewer.md +1 -0
  25. package/templates/.claude/agents/tdd-guide.md +1 -0
  26. package/templates/.claude/agents/vision.md +1 -0
  27. package/templates/.claude/instructions/agent-patterns/agent-teams-usage.md +376 -0
  28. package/templates/.claude/instructions/sourcing/reliable-search.md +49 -2
  29. package/templates/.claude/scripts/agent-teams/check-availability.sh +238 -0
  30. package/templates/.claude/scripts/agent-teams/setup-tmux.sh +125 -0
  31. package/templates/.claude/skills/agent-teams-setup/SKILL.md +460 -0
  32. package/templates/.claude/skills/brainstorm/SKILL.md +1 -0
  33. package/templates/.claude/skills/bug-fix/SKILL.md +1 -0
  34. package/templates/.claude/skills/crawler/SKILL.md +2 -0
  35. package/templates/.claude/skills/docs-creator/SKILL.md +1 -0
  36. package/templates/.claude/skills/docs-fetch/SKILL.md +6 -4
  37. package/templates/.claude/skills/docs-refactor/SKILL.md +1 -0
  38. package/templates/.claude/skills/elon-musk/SKILL.md +1 -0
  39. package/templates/.claude/skills/execute/SKILL.md +1 -0
  40. package/templates/.claude/skills/feedback/SKILL.md +1 -0
  41. package/templates/.claude/skills/figma-to-code/SKILL.md +1 -0
  42. package/templates/.claude/skills/genius-thinking/SKILL.md +1 -0
  43. package/templates/.claude/skills/global-uiux-design/SKILL.md +1 -0
  44. package/templates/.claude/skills/korea-uiux-design/SKILL.md +1 -0
  45. package/templates/.claude/skills/nextjs-react-best-practices/SKILL.md +1 -0
  46. package/templates/.claude/skills/plan/SKILL.md +1 -0
  47. package/templates/.claude/skills/prd/SKILL.md +1 -0
  48. package/templates/.claude/skills/project-optimizer/AGENTS.md +275 -0
  49. package/templates/.claude/skills/project-optimizer/SKILL.md +375 -0
  50. package/templates/.claude/skills/project-optimizer/rules/arch-config-centralize.md +66 -0
  51. package/templates/.claude/skills/project-optimizer/rules/arch-hot-path.md +35 -0
  52. package/templates/.claude/skills/project-optimizer/rules/arch-interface-segregation.md +51 -0
  53. package/templates/.claude/skills/project-optimizer/rules/arch-module-boundary.md +42 -0
  54. package/templates/.claude/skills/project-optimizer/rules/build-cache.md +57 -0
  55. package/templates/.claude/skills/project-optimizer/rules/build-code-split.md +56 -0
  56. package/templates/.claude/skills/project-optimizer/rules/build-incremental.md +65 -0
  57. package/templates/.claude/skills/project-optimizer/rules/build-minify.md +61 -0
  58. package/templates/.claude/skills/project-optimizer/rules/build-tree-shake.md +60 -0
  59. package/templates/.claude/skills/project-optimizer/rules/code-complexity.md +65 -0
  60. package/templates/.claude/skills/project-optimizer/rules/code-dead-elimination.md +32 -0
  61. package/templates/.claude/skills/project-optimizer/rules/code-duplication.md +54 -0
  62. package/templates/.claude/skills/project-optimizer/rules/code-error-handling.md +75 -0
  63. package/templates/.claude/skills/project-optimizer/rules/code-naming.md +52 -0
  64. package/templates/.claude/skills/project-optimizer/rules/concurrency-defer-await.md +54 -0
  65. package/templates/.claude/skills/project-optimizer/rules/concurrency-parallel.md +90 -0
  66. package/templates/.claude/skills/project-optimizer/rules/concurrency-pipeline.md +68 -0
  67. package/templates/.claude/skills/project-optimizer/rules/concurrency-pool.md +68 -0
  68. package/templates/.claude/skills/project-optimizer/rules/deps-lightweight-alt.md +37 -0
  69. package/templates/.claude/skills/project-optimizer/rules/deps-peer-align.md +44 -0
  70. package/templates/.claude/skills/project-optimizer/rules/deps-security-audit.md +45 -0
  71. package/templates/.claude/skills/project-optimizer/rules/deps-unused-removal.md +25 -0
  72. package/templates/.claude/skills/project-optimizer/rules/deps-version-pin.md +40 -0
  73. package/templates/.claude/skills/project-optimizer/rules/dx-ci-speed.md +47 -0
  74. package/templates/.claude/skills/project-optimizer/rules/dx-dev-server.md +35 -0
  75. package/templates/.claude/skills/project-optimizer/rules/dx-lint-config.md +36 -0
  76. package/templates/.claude/skills/project-optimizer/rules/dx-test-coverage.md +34 -0
  77. package/templates/.claude/skills/project-optimizer/rules/dx-type-safety.md +49 -0
  78. package/templates/.claude/skills/project-optimizer/rules/io-batch-queries.md +67 -0
  79. package/templates/.claude/skills/project-optimizer/rules/io-cache-layer.md +67 -0
  80. package/templates/.claude/skills/project-optimizer/rules/io-connection-reuse.md +67 -0
  81. package/templates/.claude/skills/project-optimizer/rules/io-serialize-minimal.md +61 -0
  82. package/templates/.claude/skills/project-optimizer/rules/io-stream.md +75 -0
  83. package/templates/.claude/skills/project-optimizer/rules/memory-bounded-cache.md +65 -0
  84. package/templates/.claude/skills/project-optimizer/rules/memory-large-data.md +64 -0
  85. package/templates/.claude/skills/project-optimizer/rules/memory-lazy-init.md +78 -0
  86. package/templates/.claude/skills/project-optimizer/rules/memory-leak-prevention.md +79 -0
  87. package/templates/.claude/skills/project-optimizer/rules/memory-pool-reuse.md +70 -0
  88. package/templates/.claude/skills/ralph/SKILL.md +1 -0
  89. package/templates/.claude/skills/refactor/SKILL.md +1 -0
  90. package/templates/.claude/skills/research/SKILL.md +1 -0
  91. package/templates/.claude/skills/sql-optimizer/SKILL.md +438 -0
  92. package/templates/.claude/skills/sql-optimizer/orm-patterns.md +218 -0
  93. package/templates/.claude/skills/startup-validator/SKILL.md +1 -0
  94. package/templates/.claude/skills/tanstack-start-react-best-practices/AGENTS.md +53 -14
  95. package/templates/.claude/skills/tanstack-start-react-best-practices/SKILL.md +94 -27
  96. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/bundle-defer-third-party.md +42 -19
  97. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/client-optimistic-updates.md +109 -0
  98. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/client-suspense-query.md +74 -0
  99. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/client-use-hook.md +81 -0
  100. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/rerender-react-compiler.md +81 -0
  101. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-beforeload-auth.md +121 -0
  102. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-file-conventions.md +104 -0
  103. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-link-navigation.md +119 -0
  104. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-nested-layouts.md +155 -0
  105. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-path-params.md +89 -0
  106. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-pending-component.md +110 -0
  107. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-preload-strategy.md +91 -0
  108. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-router-context.md +120 -0
  109. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-search-params.md +114 -0
  110. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-deferred-data.md +1 -1
  111. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-error-boundaries.md +79 -0
  112. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-middleware.md +85 -0
  113. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-serialization.md +56 -21
  114. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-streaming.md +84 -0
  115. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-validator.md +71 -0
  116. package/templates/.claude/skills/tauri-react-best-practices/AGENTS.md +527 -0
  117. package/templates/.claude/skills/tauri-react-best-practices/SKILL.md +571 -0
  118. package/templates/.claude/skills/tauri-react-best-practices/rules/bundle-barrel-imports.md +140 -0
  119. package/templates/.claude/skills/tauri-react-best-practices/rules/bundle-cargo-profile.md +96 -0
  120. package/templates/.claude/skills/tauri-react-best-practices/rules/bundle-frontend-treeshake.md +242 -0
  121. package/templates/.claude/skills/tauri-react-best-practices/rules/bundle-lazy-components.md +255 -0
  122. package/templates/.claude/skills/tauri-react-best-practices/rules/bundle-remove-unused-commands.md +160 -0
  123. package/templates/.claude/skills/tauri-react-best-practices/rules/deploy-ci-pipeline.md +269 -0
  124. package/templates/.claude/skills/tauri-react-best-practices/rules/deploy-signing.md +207 -0
  125. package/templates/.claude/skills/tauri-react-best-practices/rules/deploy-updater.md +226 -0
  126. package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-async-commands.md +172 -0
  127. package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-batch-commands.md +133 -0
  128. package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-binary-response.md +198 -0
  129. package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-channel-streaming.md +186 -0
  130. package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-error-handling.md +250 -0
  131. package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-type-safe.md +227 -0
  132. package/templates/.claude/skills/tauri-react-best-practices/rules/perf-derived-state.md +231 -0
  133. package/templates/.claude/skills/tauri-react-best-practices/rules/perf-functional-setstate.md +191 -0
  134. package/templates/.claude/skills/tauri-react-best-practices/rules/perf-index-maps.md +276 -0
  135. package/templates/.claude/skills/tauri-react-best-practices/rules/perf-lazy-state-init.md +196 -0
  136. package/templates/.claude/skills/tauri-react-best-practices/rules/plugin-lifecycle.md +265 -0
  137. package/templates/.claude/skills/tauri-react-best-practices/rules/plugin-mobile-compat.md +199 -0
  138. package/templates/.claude/skills/tauri-react-best-practices/rules/plugin-permission-scope.md +193 -0
  139. package/templates/.claude/skills/tauri-react-best-practices/rules/react-error-boundary.md +239 -0
  140. package/templates/.claude/skills/tauri-react-best-practices/rules/react-event-listener.md +151 -0
  141. package/templates/.claude/skills/tauri-react-best-practices/rules/react-file-src.md +155 -0
  142. package/templates/.claude/skills/tauri-react-best-practices/rules/react-invoke-hook.md +139 -0
  143. package/templates/.claude/skills/tauri-react-best-practices/rules/react-optimistic-update.md +211 -0
  144. package/templates/.claude/skills/tauri-react-best-practices/rules/security-capability-split.md +205 -0
  145. package/templates/.claude/skills/tauri-react-best-practices/rules/security-csp.md +207 -0
  146. package/templates/.claude/skills/tauri-react-best-practices/rules/security-least-privilege.md +106 -0
  147. package/templates/.claude/skills/tauri-react-best-practices/rules/security-no-wildcard.md +253 -0
  148. package/templates/.claude/skills/tauri-react-best-practices/rules/security-scope-paths.md +160 -0
  149. package/templates/.claude/skills/tauri-react-best-practices/rules/state-async-mutex.md +270 -0
  150. package/templates/.claude/skills/tauri-react-best-practices/rules/state-mutex-pattern.md +265 -0
  151. package/templates/.claude/skills/tauri-react-best-practices/rules/state-react-sync.md +375 -0
  152. package/templates/.claude/skills/tauri-react-best-practices/rules/state-single-container.md +275 -0
  153. package/templates/tanstack-start/docs/architecture.md +238 -167
  154. package/templates/tanstack-start/docs/library/tanstack-router/error-handling.md +777 -38
  155. package/templates/tanstack-start/docs/library/tanstack-router/hooks.md +549 -37
  156. package/templates/tanstack-start/docs/library/tanstack-router/index.md +895 -111
  157. package/templates/tanstack-start/docs/library/tanstack-router/navigation.md +641 -43
  158. package/templates/tanstack-start/docs/library/tanstack-router/route-context.md +889 -38
  159. package/templates/tanstack-start/docs/library/tanstack-router/search-params.md +891 -29
  160. package/templates/tanstack-start/docs/library/tanstack-start/auth-patterns.md +972 -36
  161. package/templates/tanstack-start/docs/library/tanstack-start/index.md +1525 -881
  162. package/templates/tanstack-start/docs/library/tanstack-start/middleware.md +1099 -20
  163. package/templates/tanstack-start/docs/library/tanstack-start/routing.md +796 -30
  164. package/templates/tanstack-start/docs/library/tanstack-start/server-functions.md +953 -35
  165. package/templates/tanstack-start/docs/library/tanstack-start/setup.md +371 -15
  166. package/templates/tauri/CLAUDE.md +189 -0
  167. package/templates/tauri/docs/guides/distribution.md +261 -0
  168. package/templates/tauri/docs/guides/getting-started.md +302 -0
  169. package/templates/tauri/docs/guides/mobile.md +288 -0
  170. package/templates/tauri/docs/library/tauri/index.md +510 -0
@@ -1,70 +1,1006 @@
1
1
  # TanStack Start - 인증 패턴
2
2
 
3
- <patterns>
3
+ > Better Auth 통합 및 인증 구현 (v1.159.4)
4
+
5
+ ---
6
+
7
+ <overview>
8
+
9
+ ## 인증 시스템 구성
10
+
11
+ | 계층 | 담당 | 예시 |
12
+ |------|------|------|
13
+ | **인증 라이브러리** | 세션/토큰 관리 | Better Auth, Clerk, WorkOS, Auth.js |
14
+ | **Server Functions** | 로그인/로그아웃 | `login()`, `logout()`, `register()` |
15
+ | **미들웨어** | 인증 검증 | `authMiddleware` |
16
+ | **라우트 보호** | 접근 제어 | `beforeLoad`, `_authed` 패턴 |
17
+ | **컴포넌트** | 로그인 폼 | `LoginForm`, 인증 UI |
18
+
19
+ ### 인증 vs 인가
20
+
21
+ - **인증 (Authentication)**: 이 사용자가 누구인가? (로그인/로그아웃)
22
+ - **인가 (Authorization)**: 이 사용자가 무엇을 할 수 있는가? (권한/역할)
23
+
24
+ ### 아키텍처 모델
25
+
26
+ | 영역 | 역할 | 예시 |
27
+ |------|------|------|
28
+ | **서버 사이드 (보안)** | 세션 저장/검증, 자격 증명 확인, DB 작업, 토큰 생성 | Server Functions, 미들웨어 |
29
+ | **클라이언트 사이드 (공개)** | 인증 상태 관리, 라우트 보호 UI, 리다이렉트 | beforeLoad, 컴포넌트 |
30
+ | **이소모픽 (양쪽)** | 라우트 로더 인증 체크, 공유 검증 로직 | Loader, 스키마 |
31
+
32
+ </overview>
33
+
34
+ ---
35
+
36
+ <auth_options>
37
+
38
+ ## 인증 옵션 비교
39
+
40
+ ### 호스팅 솔루션
41
+
42
+ | 솔루션 | 특징 |
43
+ |--------|------|
44
+ | **[Clerk](https://clerk.dev)** | UI 컴포넌트, 소셜 로그인 20+, MFA, 조직/팀 지원 |
45
+ | **[WorkOS](https://workos.com)** | SSO (SAML/OIDC), Directory Sync, SOC 2/GDPR 준수 |
46
+
47
+ ### OSS 솔루션
48
+
49
+ | 솔루션 | 특징 |
50
+ |--------|------|
51
+ | **[Better Auth](https://www.better-auth.com/)** | TypeScript-first, 오픈소스 |
52
+ | **[Auth.js](https://authjs.dev/)** | 80+ OAuth 프로바이더, 커뮤니티 주도 |
53
+ | **[Supabase Auth](https://supabase.com/auth)** | Firebase 대안, 내장 인증 |
54
+
55
+ ### DIY 구현
56
+
57
+ - **전체 제어**: 인증 흐름 완전 커스터마이징
58
+ - **벤더 종속 없음**: 인증 로직과 사용자 데이터 소유
59
+ - **비용 제어**: 사용자당 과금 없음
60
+
61
+ </auth_options>
62
+
63
+ ---
64
+
65
+ <session_management>
66
+
67
+ ## 세션 관리 패턴
68
+
69
+ ### HTTP-Only 쿠키 (권장)
70
+
71
+ TanStack Start 내장 세션 관리:
72
+
73
+ ```typescript
74
+ // utils/session.ts
75
+ import { useSession } from '@tanstack/react-start/server'
76
+
77
+ type SessionData = {
78
+ userId?: string
79
+ email?: string
80
+ role?: string
81
+ }
82
+
83
+ export function useAppSession() {
84
+ return useSession<SessionData>({
85
+ name: 'app-session',
86
+ password: process.env.SESSION_SECRET!, // 32자 이상
87
+ cookie: {
88
+ secure: process.env.NODE_ENV === 'production',
89
+ sameSite: 'lax',
90
+ httpOnly: true,
91
+ },
92
+ })
93
+ }
94
+ ```
95
+
96
+ ### 세션 패턴 비교
97
+
98
+ | 패턴 | 장점 | 단점 |
99
+ |------|------|------|
100
+ | **HTTP-Only 쿠키** | 가장 안전, 브라우저 자동 처리, CSRF 보호 | 전통적 웹앱에 적합 |
101
+ | **JWT 토큰** | Stateless, API-first에 적합 | XSS 취약, refresh token 관리 필요 |
102
+ | **서버 사이드 세션** | 즉시 세션 철회, 중앙 제어 | Redis/DB 스토리지 필요 |
103
+
104
+ </session_management>
105
+
106
+ ---
107
+
108
+ <better_auth_setup>
109
+
110
+ ## Better Auth 설정
111
+
112
+ ### 설치
113
+
114
+ ```bash
115
+ npm install better-auth
116
+ npm install -D @types/better-auth
117
+ ```
118
+
119
+ ### 기본 설정
120
+
121
+ ```typescript
122
+ // lib/auth.ts
123
+ import { betterAuth } from 'better-auth'
124
+ import { prismaAdapter } from 'better-auth/adapters/prisma'
125
+ import { prisma } from '@/database/prisma'
126
+
127
+ export const auth = betterAuth({
128
+ database: prismaAdapter(prisma),
129
+ emailAndPassword: {
130
+ enabled: true,
131
+ minPasswordLength: 8,
132
+ },
133
+ session: {
134
+ expiresIn: 60 * 60 * 24 * 7, // 7일
135
+ updateAge: 60 * 60 * 24, // 1일마다 세션 갱신
136
+ cookieCache: {
137
+ enabled: true,
138
+ maxAge: 5 * 60, // 5분
139
+ },
140
+ },
141
+ socialProviders: {
142
+ google: {
143
+ clientId: process.env.GOOGLE_CLIENT_ID || '',
144
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET || '',
145
+ },
146
+ },
147
+ })
148
+
149
+ // 타입 내보내기
150
+ export type Session = typeof auth.$Infer.Session
151
+ export type User = typeof auth.$Infer.User
152
+ ```
153
+
154
+ ### Prisma 마이그레이션
155
+
156
+ ```bash
157
+ npx better-auth-cli migration run
158
+ npx prisma migrate dev --name init
159
+ ```
160
+
161
+ </better_auth_setup>
162
+
163
+ ---
164
+
165
+ <server_functions>
166
+
167
+ ## 인증 Server Functions
168
+
169
+ ### 로그인 (Login)
4
170
 
5
171
  ```typescript
6
- // 로그인
172
+ // functions/auth.ts
173
+ import { createServerFn } from '@tanstack/react-start'
174
+ import { redirect } from '@tanstack/react-router'
175
+ import { z } from 'zod'
176
+ import { auth } from '@/lib/auth'
177
+
178
+ const loginSchema = z.object({
179
+ email: z.string().email('Invalid email address'),
180
+ password: z.string().min(8, 'Password must be at least 8 characters'),
181
+ })
182
+
183
+ export const login = createServerFn({ method: 'POST' })
184
+ .inputValidator(loginSchema)
185
+ .handler(async ({ data, request }): Promise<never> => {
186
+ try {
187
+ const result = await auth.api.signInEmail({
188
+ email: data.email,
189
+ password: data.password,
190
+ headers: request.headers,
191
+ })
192
+
193
+ if (!result.user) {
194
+ throw new Error('Invalid email or password')
195
+ }
196
+
197
+ throw redirect({ to: '/dashboard' })
198
+ } catch (error) {
199
+ if (error instanceof Response) throw error
200
+ throw new Error('Login failed')
201
+ }
202
+ })
203
+ ```
204
+
205
+ ### 로그아웃 (Logout)
206
+
207
+ ```typescript
208
+ export const logout = createServerFn({ method: 'POST' })
209
+ .handler(async ({ request }): Promise<never> => {
210
+ await auth.api.signOut({
211
+ headers: request.headers,
212
+ })
213
+
214
+ throw redirect({ to: '/' })
215
+ })
216
+ ```
217
+
218
+ ### 회원가입 (Register)
219
+
220
+ ```typescript
221
+ const registerSchema = z.object({
222
+ email: z.string().email('Invalid email address'),
223
+ password: z.string().min(8, 'Password must be at least 8 characters'),
224
+ name: z.string().min(1, 'Name is required'),
225
+ })
226
+
227
+ export const register = createServerFn({ method: 'POST' })
228
+ .inputValidator(registerSchema)
229
+ .handler(async ({ data, request }): Promise<never> => {
230
+ try {
231
+ const existingUser = await prisma.user.findUnique({
232
+ where: { email: data.email },
233
+ })
234
+
235
+ if (existingUser) {
236
+ throw new Error('Email already registered')
237
+ }
238
+
239
+ const result = await auth.api.signUpEmail({
240
+ email: data.email,
241
+ password: data.password,
242
+ name: data.name,
243
+ headers: request.headers,
244
+ })
245
+
246
+ if (!result.user) {
247
+ throw new Error('Registration failed')
248
+ }
249
+
250
+ throw redirect({ to: '/dashboard' })
251
+ } catch (error) {
252
+ if (error instanceof Response) throw error
253
+ throw new Error('Registration failed')
254
+ }
255
+ })
256
+ ```
257
+
258
+ ### 현재 사용자 (Get Current User)
259
+
260
+ ```typescript
261
+ export const getCurrentUser = createServerFn({ method: 'GET' })
262
+ .handler(async ({ request }): Promise<Session['user'] | null> => {
263
+ const session = await auth.api.getSession({
264
+ headers: request.headers,
265
+ })
266
+
267
+ return session?.user ?? null
268
+ })
269
+ ```
270
+
271
+ ### DIY 인증 (Better Auth 없이)
272
+
273
+ ```typescript
274
+ import bcrypt from 'bcryptjs'
275
+
7
276
  export const loginFn = createServerFn({ method: 'POST' })
8
277
  .inputValidator((data: { email: string; password: string }) => data)
9
278
  .handler(async ({ data }) => {
10
279
  const user = await authenticateUser(data.email, data.password)
11
- if (!user) return { error: 'Invalid credentials' }
280
+
281
+ if (!user) {
282
+ return { error: 'Invalid credentials' }
283
+ }
284
+
12
285
  const session = await useAppSession()
13
- await session.update({ userId: user.id, email: user.email })
286
+ await session.update({
287
+ userId: user.id,
288
+ email: user.email,
289
+ })
290
+
14
291
  throw redirect({ to: '/dashboard' })
15
292
  })
16
293
 
17
- // 로그아웃
18
- export const logoutFn = createServerFn({ method: 'POST' })
19
- .handler(async () => {
20
- const session = await useAppSession()
21
- await session.clear()
22
- throw redirect({ to: '/' })
294
+ async function authenticateUser(email: string, password: string) {
295
+ const user = await getUserByEmail(email)
296
+ if (!user) return null
297
+
298
+ const isValid = await bcrypt.compare(password, user.password)
299
+ return isValid ? user : null
300
+ }
301
+ ```
302
+
303
+ ### 비밀번호 변경 / 재설정
304
+
305
+ ```typescript
306
+ const changePasswordSchema = z.object({
307
+ currentPassword: z.string().min(8),
308
+ newPassword: z.string().min(8),
309
+ confirmPassword: z.string().min(8),
310
+ }).refine((data) => data.newPassword === data.confirmPassword, {
311
+ message: 'Passwords do not match',
312
+ path: ['confirmPassword'],
313
+ })
314
+
315
+ export const changePassword = createServerFn({ method: 'POST' })
316
+ .middleware([authMiddleware])
317
+ .inputValidator(changePasswordSchema)
318
+ .handler(async ({ data, context }): Promise<{ success: true }> => {
319
+ await auth.api.changePassword({
320
+ newPassword: data.newPassword,
321
+ })
322
+
323
+ return { success: true }
23
324
  })
24
325
 
25
- // 현재 사용자
26
- export const getCurrentUserFn = createServerFn({ method: 'GET' })
27
- .handler(async () => {
28
- const session = await useAppSession()
29
- if (!session.data.userId) return null
30
- return getUserById(session.data.userId)
326
+ // 비밀번호 재설정 요청
327
+ export const requestPasswordReset = createServerFn({ method: 'POST' })
328
+ .inputValidator(z.object({ email: z.string().email() }))
329
+ .handler(async ({ data }): Promise<{ sent: true }> => {
330
+ const user = await prisma.user.findUnique({
331
+ where: { email: data.email },
332
+ })
333
+
334
+ if (user) {
335
+ const resetToken = generateResetToken()
336
+ await prisma.passwordReset.create({
337
+ data: {
338
+ userId: user.id,
339
+ token: resetToken,
340
+ expiresAt: new Date(Date.now() + 1000 * 60 * 60),
341
+ },
342
+ })
343
+ await sendPasswordResetEmail(user.email, resetToken)
344
+ }
345
+
346
+ return { sent: true } // 이메일 존재 여부 노출 방지
31
347
  })
348
+ ```
349
+
350
+ </server_functions>
351
+
352
+ ---
353
+
354
+ <auth_middleware>
355
+
356
+ ## 인증 미들웨어
357
+
358
+ ### 기본 인증 미들웨어
359
+
360
+ ```typescript
361
+ // middleware/auth.ts
362
+ import { createMiddleware } from '@tanstack/react-start'
363
+ import { redirect } from '@tanstack/react-router'
364
+ import { auth } from '@/lib/auth'
32
365
 
33
- // 인증 미들웨어
34
366
  export const authMiddleware = createMiddleware({ type: 'function' })
35
- .server(async ({ next }) => {
36
- const session = await useAppSession()
37
- if (!session.data.userId) throw redirect({ to: '/login' })
38
- const user = await getUserById(session.data.userId)
39
- return next({ context: { user } })
367
+ .server(async ({ next, request }) => {
368
+ const session = await auth.api.getSession({
369
+ headers: request.headers,
370
+ })
371
+
372
+ if (!session?.user) {
373
+ throw redirect({
374
+ to: '/login',
375
+ search: { returnUrl: new URL(request.url).pathname },
376
+ })
377
+ }
378
+
379
+ return next({ context: { user: session.user } })
380
+ })
381
+ ```
382
+
383
+ ### 역할 기반 미들웨어
384
+
385
+ ```typescript
386
+ // middleware/roles.ts
387
+
388
+ export const adminMiddleware = createMiddleware({ type: 'function' })
389
+ .server(async ({ next, request }) => {
390
+ const session = await auth.api.getSession({
391
+ headers: request.headers,
392
+ })
393
+
394
+ if (session?.user?.role !== 'ADMIN') {
395
+ throw new Error('Forbidden: Admin access only')
396
+ }
397
+
398
+ return next({ context: { user: session.user } })
399
+ })
400
+
401
+ export const moderatorMiddleware = createMiddleware({ type: 'function' })
402
+ .server(async ({ next, request }) => {
403
+ const session = await auth.api.getSession({
404
+ headers: request.headers,
405
+ })
406
+
407
+ const allowedRoles = ['ADMIN', 'MODERATOR']
408
+ if (!session?.user || !allowedRoles.includes(session.user.role)) {
409
+ throw new Error('Forbidden: Insufficient permissions')
410
+ }
411
+
412
+ return next({ context: { user: session.user } })
413
+ })
414
+ ```
415
+
416
+ </auth_middleware>
417
+
418
+ ---
419
+
420
+ <protected_server_functions>
421
+
422
+ ## 보호된 Server Functions
423
+
424
+ ### 인증 필수
425
+
426
+ ```typescript
427
+ // functions/posts.ts
428
+
429
+ const createPostSchema = z.object({
430
+ title: z.string().min(1).max(200),
431
+ content: z.string().min(1).max(10000),
432
+ })
433
+
434
+ export const createPost = createServerFn({ method: 'POST' })
435
+ .middleware([authMiddleware])
436
+ .inputValidator(createPostSchema)
437
+ .handler(async ({ data, context }): Promise<Post> => {
438
+ return prisma.post.create({
439
+ data: {
440
+ ...data,
441
+ authorId: context.user.id,
442
+ published: false,
443
+ },
444
+ })
40
445
  })
41
446
 
42
- // Server Function에 적용
43
- export const protectedFn = createServerFn({ method: 'GET' })
447
+ export const getMyPosts = createServerFn({ method: 'GET' })
44
448
  .middleware([authMiddleware])
45
- .handler(async ({ context }) => ({ user: context.user }))
449
+ .handler(async ({ context }): Promise<Post[]> => {
450
+ return prisma.post.findMany({
451
+ where: { authorId: context.user.id },
452
+ orderBy: { createdAt: 'desc' },
453
+ })
454
+ })
455
+
456
+ export const deletePost = createServerFn({ method: 'DELETE' })
457
+ .middleware([authMiddleware])
458
+ .inputValidator(z.object({ id: z.string() }))
459
+ .handler(async ({ data, context }): Promise<{ success: true }> => {
460
+ const post = await prisma.post.findUnique({
461
+ where: { id: data.id },
462
+ })
463
+
464
+ if (post?.authorId !== context.user.id && context.user.role !== 'ADMIN') {
465
+ throw new Error('Forbidden: Cannot delete this post')
466
+ }
467
+
468
+ await prisma.post.delete({ where: { id: data.id } })
469
+ return { success: true }
470
+ })
471
+ ```
472
+
473
+ ### 관리자 전용
46
474
 
47
- // 보호된 라우트
475
+ ```typescript
476
+ export const deleteAnyUser = createServerFn({ method: 'DELETE' })
477
+ .middleware([adminMiddleware])
478
+ .inputValidator(z.object({ id: z.string() }))
479
+ .handler(async ({ data }): Promise<{ success: true }> => {
480
+ await prisma.user.delete({ where: { id: data.id } })
481
+ return { success: true }
482
+ })
483
+ ```
484
+
485
+ </protected_server_functions>
486
+
487
+ ---
488
+
489
+ <route_protection>
490
+
491
+ ## 라우트 보호
492
+
493
+ ### beforeLoad로 인증 체크
494
+
495
+ ```tsx
48
496
  export const Route = createFileRoute('/dashboard')({
49
- beforeLoad: async () => {
50
- const user = await getCurrentUserFn()
51
- if (!user) throw redirect({ to: '/login' })
497
+ beforeLoad: async ({ location }): Promise<{ user: Session['user'] }> => {
498
+ const user = await getCurrentUser()
499
+
500
+ if (!user) {
501
+ throw redirect({
502
+ to: '/login',
503
+ search: { redirect: location.href },
504
+ })
505
+ }
506
+
52
507
  return { user }
53
508
  },
54
- component: () => {
55
- const { user } = Route.useRouteContext()
56
- return <h1>Welcome, {user.name}!</h1>
509
+ component: DashboardPage,
510
+ })
511
+
512
+ const DashboardPage = (): JSX.Element => {
513
+ const { user } = Route.useRouteContext()
514
+ return <h1>Welcome, {user?.name}!</h1>
515
+ }
516
+ ```
517
+
518
+ ### _authed 패턴 (Layout Route 보호 - 권장)
519
+
520
+ 전체 라우트 서브트리를 한번에 보호:
521
+
522
+ ```tsx
523
+ // routes/_authed.tsx (pathless layout)
524
+ import { createFileRoute, Outlet } from '@tanstack/react-router'
525
+ import { redirect } from '@tanstack/react-router'
526
+
527
+ export const Route = createFileRoute('/_authed')({
528
+ beforeLoad: async ({ location }): Promise<{ user: Session['user'] }> => {
529
+ const user = await getCurrentUser()
530
+
531
+ if (!user) {
532
+ throw redirect({
533
+ to: '/login',
534
+ search: { redirect: location.href },
535
+ })
536
+ }
537
+
538
+ return { user }
57
539
  },
540
+ component: AuthedLayout,
541
+ })
542
+
543
+ const AuthedLayout = (): JSX.Element => {
544
+ const { user } = Route.useRouteContext()
545
+
546
+ return (
547
+ <div>
548
+ <header>
549
+ <div>Welcome, {user?.name}!</div>
550
+ <LogoutButton />
551
+ </header>
552
+ <Outlet />
553
+ </div>
554
+ )
555
+ }
556
+
557
+ // routes/_authed/dashboard.tsx -> /dashboard (자동 보호)
558
+ export const Route = createFileRoute('/_authed/dashboard')({
559
+ component: DashboardPage,
58
560
  })
59
561
 
60
- // Better Auth 통합
562
+ // routes/_authed/settings.tsx -> /settings (자동 보호)
563
+ export const Route = createFileRoute('/_authed/settings')({
564
+ component: SettingsPage,
565
+ })
566
+ ```
567
+
568
+ ### 권한별 라우트
569
+
570
+ ```tsx
571
+ export const Route = createFileRoute('/admin')({
572
+ beforeLoad: async ({ context }): Promise<{ user: Session['user'] }> => {
573
+ const user = await getCurrentUser()
574
+
575
+ if (!user || user.role !== 'ADMIN') {
576
+ throw redirect({ to: '/' })
577
+ }
578
+
579
+ return { user }
580
+ },
581
+ component: AdminPage,
582
+ })
583
+ ```
584
+
585
+ ### RBAC (역할 기반 접근 제어)
586
+
587
+ ```typescript
588
+ export const roles = {
589
+ USER: 'user',
590
+ ADMIN: 'admin',
591
+ MODERATOR: 'moderator',
592
+ } as const
593
+
594
+ type Role = (typeof roles)[keyof typeof roles]
595
+
596
+ export function hasPermission(userRole: Role, requiredRole: Role): boolean {
597
+ const hierarchy = {
598
+ [roles.USER]: 0,
599
+ [roles.MODERATOR]: 1,
600
+ [roles.ADMIN]: 2,
601
+ }
602
+
603
+ return hierarchy[userRole] >= hierarchy[requiredRole]
604
+ }
605
+
606
+ // 사용
607
+ export const Route = createFileRoute('/_authed/admin/')({
608
+ beforeLoad: async ({ context }) => {
609
+ if (!hasPermission(context.user.role, roles.ADMIN)) {
610
+ throw redirect({ to: '/unauthorized' })
611
+ }
612
+ },
613
+ })
614
+ ```
615
+
616
+ </route_protection>
617
+
618
+ ---
619
+
620
+ <login_form>
621
+
622
+ ## 로그인 폼 (TanStack Query)
623
+
624
+ ```tsx
625
+ // routes/login.tsx
626
+ import { useMutation } from '@tanstack/react-query'
627
+ import { createFileRoute } from '@tanstack/react-router'
628
+ import { useState } from 'react'
629
+
630
+ export const Route = createFileRoute('/login')({
631
+ component: LoginPage,
632
+ })
633
+
634
+ const LoginPage = (): JSX.Element => {
635
+ const [formData, setFormData] = useState({
636
+ email: '',
637
+ password: '',
638
+ })
639
+ const [error, setError] = useState<string | null>(null)
640
+
641
+ const mutation = useMutation({
642
+ mutationFn: (data: typeof formData) => login(data),
643
+ onError: (err) => {
644
+ setError((err as Error).message)
645
+ },
646
+ })
647
+
648
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
649
+ const { name, value } = e.target
650
+ setFormData((prev) => ({ ...prev, [name]: value }))
651
+ }
652
+
653
+ const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
654
+ e.preventDefault()
655
+ setError(null)
656
+ mutation.mutate(formData)
657
+ }
658
+
659
+ return (
660
+ <div style={{ maxWidth: '400px', margin: '0 auto', padding: '2rem' }}>
661
+ <h1>Login</h1>
662
+
663
+ <form onSubmit={handleSubmit}>
664
+ <div style={{ marginBottom: '1rem' }}>
665
+ <label htmlFor="email">Email:</label>
666
+ <input
667
+ id="email"
668
+ name="email"
669
+ type="email"
670
+ value={formData.email}
671
+ onChange={handleChange}
672
+ disabled={mutation.isPending}
673
+ required
674
+ />
675
+ </div>
676
+
677
+ <div style={{ marginBottom: '1rem' }}>
678
+ <label htmlFor="password">Password:</label>
679
+ <input
680
+ id="password"
681
+ name="password"
682
+ type="password"
683
+ value={formData.password}
684
+ onChange={handleChange}
685
+ disabled={mutation.isPending}
686
+ required
687
+ />
688
+ </div>
689
+
690
+ {error && <p style={{ color: 'red' }}>{error}</p>}
691
+
692
+ <button type="submit" disabled={mutation.isPending}>
693
+ {mutation.isPending ? 'Logging in...' : 'Login'}
694
+ </button>
695
+ </form>
696
+
697
+ <p>
698
+ No account? <a href="/register">Register</a>
699
+ </p>
700
+ </div>
701
+ )
702
+ }
703
+ ```
704
+
705
+ </login_form>
706
+
707
+ ---
708
+
709
+ <register_form>
710
+
711
+ ## 회원가입 폼
712
+
713
+ ```tsx
714
+ // routes/register.tsx
715
+ import { useMutation } from '@tanstack/react-query'
716
+ import { createFileRoute } from '@tanstack/react-router'
717
+ import { useState } from 'react'
718
+
719
+ export const Route = createFileRoute('/register')({
720
+ component: RegisterPage,
721
+ })
722
+
723
+ const RegisterPage = (): JSX.Element => {
724
+ const [formData, setFormData] = useState({
725
+ name: '',
726
+ email: '',
727
+ password: '',
728
+ confirmPassword: '',
729
+ })
730
+ const [error, setError] = useState<string | null>(null)
731
+
732
+ const mutation = useMutation({
733
+ mutationFn: (data) => register(data),
734
+ onError: (err) => {
735
+ setError((err as Error).message)
736
+ },
737
+ })
738
+
739
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
740
+ const { name, value } = e.target
741
+ setFormData((prev) => ({ ...prev, [name]: value }))
742
+ }
743
+
744
+ const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
745
+ e.preventDefault()
746
+ setError(null)
747
+
748
+ if (formData.password !== formData.confirmPassword) {
749
+ setError('Passwords do not match')
750
+ return
751
+ }
752
+
753
+ const { confirmPassword, ...registerData } = formData
754
+ mutation.mutate(registerData)
755
+ }
756
+
757
+ return (
758
+ <div style={{ maxWidth: '400px', margin: '0 auto', padding: '2rem' }}>
759
+ <h1>Register</h1>
760
+
761
+ <form onSubmit={handleSubmit}>
762
+ <div style={{ marginBottom: '1rem' }}>
763
+ <label htmlFor="name">Name:</label>
764
+ <input id="name" name="name" type="text" value={formData.name} onChange={handleChange} disabled={mutation.isPending} required />
765
+ </div>
766
+
767
+ <div style={{ marginBottom: '1rem' }}>
768
+ <label htmlFor="email">Email:</label>
769
+ <input id="email" name="email" type="email" value={formData.email} onChange={handleChange} disabled={mutation.isPending} required />
770
+ </div>
771
+
772
+ <div style={{ marginBottom: '1rem' }}>
773
+ <label htmlFor="password">Password:</label>
774
+ <input id="password" name="password" type="password" value={formData.password} onChange={handleChange} disabled={mutation.isPending} required />
775
+ </div>
776
+
777
+ <div style={{ marginBottom: '1rem' }}>
778
+ <label htmlFor="confirmPassword">Confirm Password:</label>
779
+ <input id="confirmPassword" name="confirmPassword" type="password" value={formData.confirmPassword} onChange={handleChange} disabled={mutation.isPending} required />
780
+ </div>
781
+
782
+ {error && <p style={{ color: 'red' }}>{error}</p>}
783
+
784
+ <button type="submit" disabled={mutation.isPending}>
785
+ {mutation.isPending ? 'Registering...' : 'Register'}
786
+ </button>
787
+ </form>
788
+
789
+ <p>
790
+ Already have an account? <a href="/login">Login</a>
791
+ </p>
792
+ </div>
793
+ )
794
+ }
795
+ ```
796
+
797
+ </register_form>
798
+
799
+ ---
800
+
801
+ <session_context>
802
+
803
+ ## Session Context 제공
804
+
805
+ ```tsx
806
+ // lib/session-context.tsx
807
+ import { createContext, useContext } from 'react'
808
+ import { Session } from '@/lib/auth'
809
+
810
+ const SessionContext = createContext<{
811
+ user: Session['user'] | null
812
+ isLoading: boolean
813
+ } | null>(null)
814
+
815
+ export const SessionProvider = ({
816
+ children,
817
+ }: {
818
+ children: React.ReactNode
819
+ }): JSX.Element => {
820
+ const { data: user, isLoading } = useQuery({
821
+ queryKey: ['currentUser'],
822
+ queryFn: () => getCurrentUser(),
823
+ staleTime: Infinity,
824
+ })
825
+
826
+ return (
827
+ <SessionContext.Provider value={{ user: user || null, isLoading }}>
828
+ {children}
829
+ </SessionContext.Provider>
830
+ )
831
+ }
832
+
833
+ export const useSession = () => {
834
+ const context = useContext(SessionContext)
835
+ if (!context) {
836
+ throw new Error('useSession must be used within SessionProvider')
837
+ }
838
+ return context
839
+ }
840
+ ```
841
+
842
+ ### 상태 관리 패턴 비교
843
+
844
+ | 패턴 | 설명 | 적합한 경우 |
845
+ |------|------|-----------|
846
+ | **서버 주도 (권장)** | 매 요청마다 서버에서 인증 상태 확인 | SSR, 최고 보안 |
847
+ | **Context 기반** | 클라이언트에서 인증 상태 관리 | 서드파티 인증 (Auth0, Firebase) |
848
+ | **하이브리드** | 서버 초기 상태 + 클라이언트 업데이트 | 보안과 UX 균형 |
849
+
850
+ </session_context>
851
+
852
+ ---
853
+
854
+ <oauth_integration>
855
+
856
+ ## OAuth 통합 (Google)
857
+
858
+ ```typescript
859
+ // lib/auth.ts
61
860
  export const auth = betterAuth({
62
861
  database: prismaAdapter(prisma),
63
862
  emailAndPassword: { enabled: true },
863
+ socialProviders: {
864
+ google: {
865
+ clientId: process.env.GOOGLE_CLIENT_ID || '',
866
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET || '',
867
+ },
868
+ },
64
869
  })
870
+ ```
65
871
 
66
- export const getSession = createServerFn({ method: 'GET' })
67
- .handler(async ({ request }) => auth.api.getSession({ headers: request.headers }))
872
+ ```tsx
873
+ const GoogleLoginButton = (): JSX.Element => {
874
+ return (
875
+ <form action={auth.api.signInSocial('google')}>
876
+ <button type="submit">Login with Google</button>
877
+ </form>
878
+ )
879
+ }
68
880
  ```
69
881
 
70
- </patterns>
882
+ </oauth_integration>
883
+
884
+ ---
885
+
886
+ <security_best_practices>
887
+
888
+ ## 보안 모범 사례
889
+
890
+ ### 비밀번호 보안
891
+
892
+ ```typescript
893
+ import bcrypt from 'bcryptjs'
894
+
895
+ const saltRounds = 12 // 보안 요구에 맞게 조정
896
+ const hashedPassword = await bcrypt.hash(password, saltRounds)
897
+ ```
898
+
899
+ ### 세션 보안
900
+
901
+ ```typescript
902
+ export function useAppSession() {
903
+ return useSession({
904
+ name: 'app-session',
905
+ password: process.env.SESSION_SECRET!, // 32자 이상
906
+ cookie: {
907
+ secure: process.env.NODE_ENV === 'production', // HTTPS only
908
+ sameSite: 'lax', // CSRF 보호
909
+ httpOnly: true, // XSS 보호
910
+ maxAge: 7 * 24 * 60 * 60, // 7일
911
+ },
912
+ })
913
+ }
914
+ ```
915
+
916
+ ### Rate Limiting
917
+
918
+ ```typescript
919
+ const loginAttempts = new Map<string, { count: number; resetTime: number }>()
920
+
921
+ export const rateLimitLogin = (ip: string): boolean => {
922
+ const now = Date.now()
923
+ const attempts = loginAttempts.get(ip)
924
+
925
+ if (!attempts || now > attempts.resetTime) {
926
+ loginAttempts.set(ip, { count: 1, resetTime: now + 15 * 60 * 1000 })
927
+ return true
928
+ }
929
+
930
+ if (attempts.count >= 5) {
931
+ return false // 너무 많은 시도
932
+ }
933
+
934
+ attempts.count++
935
+ return true
936
+ }
937
+ ```
938
+
939
+ ### 입력 검증
940
+
941
+ ```typescript
942
+ import { z } from 'zod'
943
+
944
+ const loginSchema = z.object({
945
+ email: z.string().email().max(255),
946
+ password: z.string().min(8).max(100),
947
+ })
948
+
949
+ export const loginFn = createServerFn({ method: 'POST' })
950
+ .inputValidator(loginSchema)
951
+ .handler(async ({ data }) => {
952
+ // data는 검증 완료 상태
953
+ })
954
+ ```
955
+
956
+ ### 보안 체크리스트
957
+
958
+ | 항목 | 구현 |
959
+ |------|------|
960
+ | HTTPS | 프로덕션에서 필수 |
961
+ | HTTP-Only 쿠키 | 가능한 경우 항상 사용 |
962
+ | 서버 입력 검증 | 모든 입력을 서버에서 검증 |
963
+ | 시크릿 보호 | 서버 전용 함수에서만 사용 |
964
+ | Rate Limiting | 인증 엔드포인트에 적용 |
965
+ | CSRF 보호 | 폼 제출 시 적용 |
966
+
967
+ </security_best_practices>
968
+
969
+ ---
970
+
971
+ <best_practices>
972
+
973
+ ## 인증 모범 사례
974
+
975
+ | 원칙 | 구현 |
976
+ |------|------|
977
+ | **Server Functions 사용** | 로그인/로그아웃은 Server Function |
978
+ | **미들웨어 분리** | 인증/권한 검증은 미들웨어 |
979
+ | **beforeLoad 사용** | 라우트 접근 전 인증 체크 |
980
+ | **_authed 패턴** | Layout Route로 라우트 서브트리 한번에 보호 |
981
+ | **TanStack Query** | 로그인 폼은 useMutation |
982
+ | **환경변수 보안** | Better Auth secrets는 .env |
983
+ | **에러 처리** | 명확한 에러 메시지 제공 |
984
+ | **세션 갱신** | 주기적 세션 갱신 설정 |
985
+ | **이메일 미노출** | 비밀번호 재설정 시 이메일 존재 여부 노출 방지 |
986
+
987
+ </best_practices>
988
+
989
+ ---
990
+
991
+ <version_info>
992
+
993
+ **Version:** TanStack Start/Router v1.159.4 with Better Auth latest
994
+
995
+ **Key Points:**
996
+ - Better Auth 2.x for session management
997
+ - TanStack Start 내장 useSession 지원 (HTTP-Only 쿠키)
998
+ - Server Functions for auth actions
999
+ - Middleware for role-based access
1000
+ - beforeLoad for route protection
1001
+ - _authed Pathless Layout 패턴 (권장)
1002
+ - TanStack Query for form handling
1003
+ - RBAC 역할 계층 패턴
1004
+ - Rate Limiting, CSRF, XSS 보호
1005
+
1006
+ </version_info>