@01.software/sdk 0.1.0-dev.260119.0a66443 → 0.1.0-dev.260210.4ecca43

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/README.md CHANGED
@@ -1,59 +1,42 @@
1
1
  # @01.software/sdk
2
2
 
3
- 01.software 플랫폼의 공식 TypeScript SDK입니다. Supabase 스타일의 직관적인 API와 강력한 타입 안전성을 제공하며, 브라우저와 서버 환경을 모두 지원합니다.
3
+ Official TypeScript SDK for the 01.software platform.
4
4
 
5
- ## 주요 기능
6
-
7
- - 🔒 **타입 안전성**: TypeScript 기반의 완전한 타입 추론 및 discriminated union
8
- - 🌐 **멀티 환경**: 브라우저와 서버 환경 모두 지원
9
- - 🎯 **직관적 API**: Supabase 스타일의 쿼리 빌더
10
- - ⚡ **React Query 통합**: 데이터 페칭 및 캐싱 최적화
11
- - 🔄 **Webhook 처리**: 타입 안전한 webhook 핸들링
12
- - 📦 **37개 컬렉션**: 브랜드, 상품, 주문, 콘텐츠 등 포괄적 지원
13
- - 🔁 **자동 재시도**: Exponential backoff 전략 기반 재시도 로직
14
- - 🐛 **디버그 모드**: 요청/응답/에러 선택적 로깅
15
- - ⚠️ **향상된 에러 처리**: 사용자 친화적 에러 메시지 및 제안
16
-
17
- ## 설치
5
+ ## Installation
18
6
 
19
7
  ```bash
20
8
  npm install @01.software/sdk
21
- # 또는
22
- yarn add @01.software/sdk
23
- # 또는
9
+ # or
24
10
  pnpm add @01.software/sdk
25
11
  ```
26
12
 
27
- ## 빠른 시작
13
+ ## Features
14
+
15
+ - Full TypeScript type inference
16
+ - Browser and server environment support
17
+ - React Query integration
18
+ - Automatic retry with exponential backoff
19
+ - Webhook handling
28
20
 
29
- ### 브라우저 환경
21
+ ## Getting Started
22
+
23
+ ### Browser Client
30
24
 
31
25
  ```typescript
32
26
  import { createBrowserClient } from '@01.software/sdk'
33
27
 
34
28
  const client = createBrowserClient({
35
- clientKey: process.env.NEXT_PUBLIC_SOFTWARE_CLIENT_KEY
29
+ clientKey: process.env.NEXT_PUBLIC_SOFTWARE_CLIENT_KEY,
36
30
  })
37
31
 
38
- // 쿼리 빌더로 데이터 조회
39
- const { data: products } = await client.from('products').find({
32
+ // Query data
33
+ const { data } = await client.from('products').find({
40
34
  limit: 10,
41
- where: { status: { equals: 'published' } }
35
+ where: { status: { equals: 'published' } },
42
36
  })
43
-
44
- // React Query 훅 사용
45
- function ProductList() {
46
- const { data, isLoading } = client.query.useQuery({
47
- collection: 'products',
48
- options: { limit: 10 }
49
- })
50
-
51
- if (isLoading) return <div>Loading...</div>
52
- return <div>{data?.map(p => p.name)}</div>
53
- }
54
37
  ```
55
38
 
56
- ### 서버 환경
39
+ ### Server Client
57
40
 
58
41
  ```typescript
59
42
  import { createServerClient } from '@01.software/sdk'
@@ -63,476 +46,240 @@ const client = createServerClient({
63
46
  secretKey: process.env.SOFTWARE_SECRET_KEY,
64
47
  })
65
48
 
66
- // 주문 생성
49
+ // Create order (server only)
67
50
  const order = await client.api.createOrder({
68
51
  paymentId: 'pay_123',
69
52
  orderNumber: generateOrderNumber(),
70
53
  email: 'user@example.com',
71
- orderProducts: [
72
- {
73
- product: 'product_id',
74
- variant: 'variant_id',
75
- quantity: 1,
76
- price: 10000,
77
- },
78
- ],
54
+ orderProducts: [...],
79
55
  totalAmount: 10000,
80
56
  })
81
57
  ```
82
58
 
83
- ## 핵심 개념
84
-
85
- ### 1. 클라이언트
59
+ ## API
86
60
 
87
- #### BrowserClient
88
-
89
- 브라우저 환경에서 사용하는 클라이언트로, React Query 통합과 쿼리 빌더를 제공합니다.
61
+ ### Client Configuration
90
62
 
91
63
  ```typescript
92
64
  const client = createBrowserClient({
93
- clientKey: 'your-client-key',
94
- baseUrl: 'https://api.example.com', // 선택사항
95
- debug: true, // 디버그 모드 활성화
96
- retry: {
97
- maxRetries: 3, // 최대 재시도 횟수
98
- retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 10000), // Exponential backoff
99
- },
100
- errorLogger: {
101
- log: (error) => console.error('SDK Error:', error), // 커스텀 에러 로거
102
- },
65
+ clientKey: string, // Required
66
+ baseUrl?: string, // API URL (optional)
67
+ debug?: boolean | DebugOptions,
68
+ retry?: RetryOptions,
69
+ errorLogger?: ErrorLogger,
103
70
  })
104
71
  ```
105
72
 
106
- **주요 속성:**
107
-
108
- - `query`: React Query 훅 (`useQuery`, `useSuspenseQuery`, `useQueryById`, `useInfiniteQuery`)
109
- - `collections`: 컬렉션 API 클라이언트
110
- - `from()`: Supabase 스타일 쿼리 빌더
73
+ | Option | Type | Description |
74
+ | ----------- | ------------------------- | ---------------------------- |
75
+ | `clientKey` | `string` | API client key |
76
+ | `secretKey` | `string` | API secret key (server only) |
77
+ | `baseUrl` | `string` | API base URL |
78
+ | `debug` | `boolean \| DebugOptions` | Enable debug logging |
79
+ | `retry` | `RetryOptions` | Retry configuration |
111
80
 
112
- #### ServerClient
81
+ ### Query Builder
113
82
 
114
- 서버 환경에서 사용하는 클라이언트로, API 클라이언트를 추가로 제공합니다.
83
+ Access collections via `client.from(collection)` method.
115
84
 
116
85
  ```typescript
117
- const client = createServerClient({
118
- clientKey: 'your-client-key',
119
- secretKey: 'your-secret-key', // 서버 전용
120
- })
121
- ```
122
-
123
- **추가 속성:**
124
-
125
- - `api`: 주문 및 트랜잭션 API (`createOrder`, `updateOrder`, `updateTransaction`)
126
-
127
- ### 2. 쿼리 빌더
128
-
129
- Supabase 스타일의 타입 안전한 쿼리 빌더를 제공합니다.
130
-
131
- ```typescript
132
- // 목록 조회
86
+ // List query
133
87
  const { data } = await client.from('products').find({
134
88
  limit: 20,
135
89
  page: 1,
136
90
  sort: '-createdAt',
137
- where: {
138
- status: { equals: 'published' },
139
- price: { greater_than: 1000 },
140
- },
91
+ where: { status: { equals: 'published' } },
141
92
  })
142
93
 
143
- // ID로 조회
144
- const { data: product } = await client.from('products').findById('product_id')
94
+ // Single item query
95
+ const { data } = await client.from('products').findById('id')
145
96
 
146
- // 생성
147
- const { data: newProduct } = await client.from('products').create({
148
- name: '새 상품',
149
- price: 10000,
150
- })
97
+ // Create (server only)
98
+ const { data } = await client.from('products').create({ name: 'Product' })
151
99
 
152
- // 수정
153
- const { data: updatedProduct } = await client
154
- .from('products')
155
- .update('product_id', {
156
- name: '수정된 상품명',
157
- })
100
+ // Update (server only)
101
+ const { data } = await client.from('products').update('id', { name: 'Updated' })
158
102
 
159
- // 삭제
160
- await client.from('products').remove('product_id')
103
+ // Delete (server only)
104
+ await client.from('products').remove('id')
161
105
  ```
162
106
 
163
- ### 3. React Query 훅
107
+ ### API Response Structure
164
108
 
165
- 데이터 페칭과 캐싱을 위한 React Query 통합을 제공합니다.
109
+ All SDK methods return a standardized response format:
166
110
 
167
111
  ```typescript
168
- // 컬렉션 목록 조회
169
- const { data, isLoading, error, refetch } = client.query.useQuery({
112
+ // Success Response
113
+ interface ApiSuccessResponse<T> {
114
+ data: T // Actual data
115
+ success: true
116
+ message?: string // Optional message from server
117
+ pagination?: PaginationMeta // For list queries
118
+ }
119
+
120
+ // Error Response
121
+ interface ApiErrorResponse {
122
+ data: null
123
+ success: false
124
+ error: {
125
+ code: string
126
+ message: string
127
+ details?: unknown
128
+ }
129
+ }
130
+ ```
131
+
132
+ **Payload CMS API Mapping:**
133
+
134
+ | Operation | Payload Response | SDK Response |
135
+ |-----------|-----------------|--------------|
136
+ | `find()` | `{docs: [], totalDocs, ...}` | `{data: [], success: true, pagination: {...}}` |
137
+ | `findById()` | `{...document}` | `{data: {...}, success: true}` |
138
+ | `create()` | `{doc: {...}, message}` | `{data: {...}, success: true, message}` |
139
+ | `update()` | `{doc: {...}, message}` | `{data: {...}, success: true, message}` |
140
+ | `remove()` | `{doc: {...}, message}` | `{data: {...}, success: true, message}` |
141
+
142
+ ### React Query Hooks
143
+
144
+ ```typescript
145
+ // List query
146
+ const { data, isLoading } = client.query.useQuery({
170
147
  collection: 'products',
171
- options: {
172
- limit: 10,
173
- where: { status: { equals: 'published' } },
174
- },
148
+ options: { limit: 10 },
175
149
  })
176
150
 
177
- // Suspense 모드 (서버 컴포넌트에서 사용)
151
+ // Suspense mode
178
152
  const { data } = client.query.useSuspenseQuery({
179
153
  collection: 'products',
180
154
  options: { limit: 10 },
181
155
  })
182
156
 
183
- // 단일 항목 조회 (ID 기반)
184
- const { data: product } = client.query.useQueryById({
157
+ // Query by ID
158
+ const { data } = client.query.useQueryById({
185
159
  collection: 'products',
186
160
  id: 'product_id',
187
161
  })
188
162
 
189
- // 무한 스크롤
163
+ // Infinite scroll
190
164
  const { data, fetchNextPage, hasNextPage } = client.query.useInfiniteQuery({
191
165
  collection: 'products',
192
166
  options: { limit: 20 },
193
167
  })
194
168
  ```
195
169
 
196
- ### 4. Webhook 처리
170
+ ### Server API
197
171
 
198
- Next.js API Route에서 타입 안전한 webhook 처리를 제공합니다.
172
+ Available only in ServerClient.
199
173
 
200
174
  ```typescript
201
- // app/api/webhook/route.ts
202
- import { handleWebhook } from '@01.software/sdk'
175
+ // Create order
176
+ await client.api.createOrder(data)
203
177
 
204
- export async function POST(request: Request) {
205
- return handleWebhook(request, async (event) => {
206
- console.log('Collection:', event.collection)
207
- console.log('Operation:', event.operation) // 'create' | 'update' | 'delete'
208
- console.log('Data:', event.data)
209
-
210
- if (event.collection === 'orders') {
211
- // 주문 관련 처리
212
- if (event.operation === 'update') {
213
- console.log('Order updated:', event.data.id)
214
- }
215
- }
216
- })
217
- }
178
+ // Update order
179
+ await client.api.updateOrder(id, data)
180
+
181
+ // Update transaction
182
+ await client.api.updateTransaction(id, data)
218
183
  ```
219
184
 
220
- 타입 안전한 컬렉션별 핸들러:
185
+ ### Webhook
221
186
 
222
187
  ```typescript
223
- import { createTypedWebhookHandler } from '@01.software/sdk'
224
-
225
- const handleOrderWebhook = createTypedWebhookHandler(
226
- 'orders',
227
- async (event) => {
228
- // event.data의 타입이 Order로 자동 추론됨
229
- console.log('Order:', event.data.orderNumber)
230
- },
231
- )
188
+ import { handleWebhook, createTypedWebhookHandler } from '@01.software/sdk'
232
189
 
190
+ // Basic handler
233
191
  export async function POST(request: Request) {
234
- return handleWebhook(request, handleOrderWebhook)
192
+ return handleWebhook(request, async (event) => {
193
+ console.log(event.collection, event.operation, event.data)
194
+ })
235
195
  }
236
- ```
237
-
238
- ## API 레퍼런스
239
-
240
- ### 컬렉션 목록
241
-
242
- SDK는 37개의 컬렉션을 지원합니다:
243
-
244
- **브랜드 관련**
245
-
246
- - `brands`, `brand-logos`, `brand-og-images`, `brand-settings`, `brand-secret-keys`
247
196
 
248
- **상품 관련**
249
-
250
- - `products`, `product-variants`, `product-options`, `product-categories`, `product-tags`, `product-images`
251
-
252
- **주문 관련**
253
-
254
- - `orders`, `order-products`, `returns`, `return-products`, `transactions`
255
-
256
- **콘텐츠 관련**
257
-
258
- - `posts`, `post-categories`, `post-tags`, `post-images`
259
- - `documents`, `document-images`
260
- - `entities`, `entity-categories`, `entity-tags`, `entity-images`
261
-
262
- **미디어 관련**
263
-
264
- - `playlists`, `playlist-images`, `musics`
265
- - `galleries`, `gallery-images`
266
-
267
- **기타**
197
+ // Type-safe handler
198
+ const handler = createTypedWebhookHandler('orders', async (event) => {
199
+ // event.data is typed as Order
200
+ console.log(event.data.orderNumber)
201
+ })
202
+ ```
268
203
 
269
- - `links`, `link-images`, `nodes`, `forms`
204
+ ## Supported Collections
270
205
 
271
- ### 유틸리티 함수
206
+ | Category | Collections |
207
+ | -------- | ---------------------------------------------------------------------------------------------------------------------------------- |
208
+ | Tenant | `tenants`, `tenant-metadata`, `tenant-logos`, `tenant-og-images` |
209
+ | Products | `products`, `product-variants`, `product-options`, `product-categories`, `product-tags`, `product-images`, `brands`, `brand-logos` |
210
+ | Orders | `orders`, `order-products`, `returns`, `return-products`, `transactions` |
211
+ | Content | `posts`, `post-categories`, `post-tags`, `post-images`, `documents`, `document-categories`, `document-images` |
212
+ | Media | `playlists`, `playlist-images`, `musics`, `galleries`, `gallery-images`, `media` |
272
213
 
273
- #### generateOrderNumber()
214
+ ## Utilities
274
215
 
275
- 날짜 기반 주문 번호를 생성합니다.
216
+ ### generateOrderNumber
276
217
 
277
218
  ```typescript
278
219
  import { generateOrderNumber } from '@01.software/sdk'
279
220
 
280
221
  const orderNumber = generateOrderNumber()
281
- // 예: "260107123456" (YYMMDDRRRRRR 형식)
222
+ // "260121123456" (YYMMDDRRRRRR format)
282
223
  ```
283
224
 
284
- #### formatOrderName()
285
-
286
- 상품 옵션 배열을 주문명으로 포맷팅합니다.
225
+ ### formatOrderName
287
226
 
288
227
  ```typescript
289
228
  import { formatOrderName } from '@01.software/sdk'
290
229
 
291
- const orderName = formatOrderName([
292
- { product: { name: '상품 A' }, variant: { name: '옵션 1' } },
293
- ])
294
- // "상품 A"
230
+ formatOrderName([{ product: { name: 'Product A' } }])
231
+ // "Product A"
295
232
 
296
- const orderName2 = formatOrderName([
297
- { product: { name: '상품 A' }, variant: { name: '옵션 1' } },
298
- { product: { name: '상품 B' }, variant: { name: '옵션 2' } },
233
+ formatOrderName([
234
+ { product: { name: 'Product A' } },
235
+ { product: { name: 'Product B' } },
299
236
  ])
300
- // "상품 A 1"
237
+ // "Product A and 1 more"
301
238
  ```
302
239
 
303
- ### React 컴포넌트
240
+ ### RichTextContent
304
241
 
305
- #### RichTextContent
242
+ React component for rendering Payload CMS Lexical rich text.
306
243
 
307
- Payload CMS의 Lexical 리치 텍스트를 렌더링합니다.
308
-
309
- ```typescript
244
+ ```tsx
310
245
  import { RichTextContent } from '@01.software/sdk'
311
246
 
312
- function Article({ content }) {
313
- return (
314
- <RichTextContent
315
- data={content}
316
- className="prose"
317
- internalDocToHref={(args) => `/posts/${args.doc.slug}`}
318
- blocks={{
319
- Iframe: ({ url }) => <iframe src={url} />,
320
- Player: ({ videoId }) => <VideoPlayer id={videoId} />
321
- }}
322
- />
323
- )
324
- }
325
- ```
326
-
327
- ## 고급 사용법
328
-
329
- ### 디버그 모드
330
-
331
- 개발 중 요청/응답을 로깅하여 디버깅을 쉽게 할 수 있습니다.
332
-
333
- ```typescript
334
- // 전체 디버그 모드
335
- const client = createBrowserClient({
336
- clientKey: 'your-key',
337
- debug: true, // 모든 요청/응답/에러 로깅
338
- })
339
-
340
- // 선택적 디버그 모드
341
- const client = createBrowserClient({
342
- clientKey: 'your-key',
343
- debug: {
344
- logRequests: true, // 요청만 로깅
345
- logResponses: false, // 응답 로깅 안 함
346
- logErrors: true, // 에러만 로깅
347
- },
348
- })
247
+ ;<RichTextContent
248
+ data={content}
249
+ className="prose"
250
+ internalDocToHref={(args) => `/posts/${args.doc.slug}`}
251
+ blocks={{
252
+ Iframe: ({ url }) => <iframe src={url} />,
253
+ Player: ({ videoId }) => <VideoPlayer id={videoId} />,
254
+ }}
255
+ />
349
256
  ```
350
257
 
351
- ### 재시도 로직
352
-
353
- 네트워크 오류나 서버 에러 시 자동으로 재시도합니다.
354
-
355
- ```typescript
356
- const client = createBrowserClient({
357
- clientKey: 'your-key',
358
- retry: {
359
- maxRetries: 3, // 최대 재시도 횟수 (기본값: 3)
360
- retryableStatuses: [408, 429, 500, 502, 503, 504], // 재시도할 HTTP 상태 코드
361
- retryDelay: (attempt) => {
362
- // Exponential backoff: 1초 → 2초 → 4초 → 8초 (최대 10초)
363
- return Math.min(1000 * 2 ** attempt, 10000)
364
- },
365
- },
366
- })
367
- ```
368
-
369
- **기본 동작:**
370
-
371
- - 네트워크 에러는 항상 재시도
372
- - 408, 429, 500, 502, 503, 504 상태 코드는 재시도
373
- - 401, 404 등 클라이언트 에러는 재시도하지 않음
374
- - Exponential backoff 전략 사용
375
-
376
- ### 쿼리 옵션
377
-
378
- ```typescript
379
- // 정렬
380
- const { data } = await client.from('products').find({
381
- sort: '-createdAt', // 내림차순
382
- })
383
-
384
- // 페이지네이션
385
- const { data, pagination } = await client.from('products').find({
386
- page: 2,
387
- limit: 20,
388
- })
389
- console.log(pagination.hasNextPage)
390
-
391
- // 복잡한 필터
392
- const { data } = await client.from('products').find({
393
- where: {
394
- and: [
395
- { status: { equals: 'published' } },
396
- { price: { greater_than: 1000, less_than: 50000 } },
397
- {
398
- or: [
399
- { category: { equals: 'electronics' } },
400
- { tags: { contains: 'featured' } },
401
- ],
402
- },
403
- ],
404
- },
405
- })
406
- ```
407
-
408
- ### React Query 옵션
409
-
410
- ```typescript
411
- const { data } = client.query.useQuery(
412
- {
413
- collection: 'products',
414
- options: { limit: 10 },
415
- },
416
- {
417
- staleTime: 5 * 60 * 1000, // 5분
418
- gcTime: 10 * 60 * 1000, // 10분 (React Query v5에서 cacheTime → gcTime)
419
- refetchOnWindowFocus: false,
420
- },
421
- )
422
- ```
423
-
424
- ### 에러 처리
425
-
426
- SDK는 사용자 친화적인 에러 메시지와 제안을 제공합니다.
258
+ ## Error Handling
427
259
 
428
260
  ```typescript
429
261
  import {
430
- SDKError,
431
- ApiError,
432
- NetworkError,
433
- ValidationError,
434
- TimeoutError,
435
262
  isNetworkError,
263
+ isSuccessResponse,
264
+ isErrorResponse,
436
265
  } from '@01.software/sdk'
437
266
 
438
- try {
439
- const { data } = await client.from('products').find()
440
- } catch (error) {
441
- if (isNetworkError(error)) {
442
- // 사용자 친화적 메시지
443
- console.error(error.getUserMessage())
444
- // "네트워크 연결을 확인해주세요."
445
-
446
- // 개발자용 메시지
447
- console.error(error.message)
448
-
449
- // 해결 제안
450
- console.log(error.suggestion)
451
- // "인터넷 연결을 확인하거나 잠시 후 다시 시도해주세요."
452
- }
453
- }
454
- ```
455
-
456
- #### 타입 안전한 응답 처리
457
-
458
- ```typescript
459
- import { isSuccessResponse, isErrorResponse } from '@01.software/sdk'
460
-
461
267
  const response = await client.from('products').find()
462
268
 
463
269
  if (isSuccessResponse(response)) {
464
- // TypeScript가 response.data의 타입을 Product[]로 추론
465
270
  console.log(response.data)
466
- console.log(response.pagination)
467
271
  } else if (isErrorResponse(response)) {
468
- // TypeScript가 response.error의 타입을 추론
469
- console.error(response.error.code)
470
272
  console.error(response.error.message)
471
273
  }
472
274
  ```
473
275
 
474
- ## 보안
276
+ Error classes: `SDKError`, `ApiError`, `NetworkError`, `ValidationError`, `TimeoutError`
475
277
 
476
- ### 환경 변수 설정
278
+ ## Environment Variables
477
279
 
478
280
  ```bash
479
- # .env.local
480
281
  NEXT_PUBLIC_SOFTWARE_CLIENT_KEY=your_client_key
481
- SOFTWARE_SECRET_KEY=your_secret_key # 서버 전용
482
- ```
483
-
484
- ### 주의사항
485
-
486
- - `clientKey`는 브라우저에 노출되어도 안전합니다.
487
- - `secretKey`는 **절대** 브라우저에 노출되어서는 안 됩니다.
488
- - `secretKey`는 서버 환경(API Route, Server Component 등)에서만 사용하세요.
489
-
490
- ## 타입 시스템
491
-
492
- 모든 컬렉션은 자동으로 타입이 추론됩니다:
493
-
494
- ```typescript
495
- // 타입이 자동으로 추론됨
496
- const { data: products } = await client.from('products').find()
497
- // products의 타입: Product[]
498
-
499
- const { data: product } = await client.from('products').findById('id')
500
- // product의 타입: Product
501
-
502
- // 생성 시에도 타입 체크
503
- await client.from('products').create({
504
- name: '상품명', // ✅ OK
505
- price: 10000, // ✅ OK
506
- invalid: 'field', // ❌ 타입 에러
507
- })
508
- ```
509
-
510
- ## 개발
511
-
512
- SDK는 포괄적인 테스트를 포함하고 있으며, 타입 안전성과 사용자 경험을 최우선으로 설계되었습니다.
513
-
514
- ### 테스트
515
-
516
- ```bash
517
- # 모든 테스트 실행
518
- pnpm test
519
-
520
- # watch 모드로 실행
521
- pnpm test:watch
282
+ SOFTWARE_SECRET_KEY=your_secret_key # Server only
522
283
  ```
523
284
 
524
- ### 주요 특징
525
-
526
- - ✅ **완전한 타입 안전성**: TypeScript 기반 타입 추론
527
- - ✅ **사용자 친화적 에러 처리**: 명확한 에러 메시지와 해결 제안
528
- - ✅ **자동 재시도**: 네트워크 오류 시 exponential backoff 전략
529
- - ✅ **디버그 모드**: 개발 중 요청/응답/에러 로깅
530
- - ✅ **React Query 통합**: 효율적인 데이터 페칭 및 캐싱
531
-
532
- ## 라이센스
533
-
534
- MIT
535
-
536
- ## 지원
537
-
538
- 문제가 발생하거나 궁금한 점이 있다면 이슈를 등록해주세요.
285
+ > `secretKey` should only be used in server environments. Never expose it to the browser.