byuckchon-frontend-cli 1.7.0 β 1.9.1
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 +36 -0
- package/bin/index.js +1 -1
- package/package.json +2 -1
- package/src/commands/adopt.js +24 -1
- package/src/commands/chat.js +121 -27
- package/src/config/index.js +6 -0
- package/src/context/conventions.js +114 -0
- package/src/generators/apiConventionDoc.js +49 -0
- package/src/generators/createBcConfig.js +4 -0
- package/src/generators/createProject.js +3 -0
- package/src/history/management.js +30 -0
- package/src/history/store.js +33 -5
- package/src/ui/ChatApp.js +186 -7
- package/templates/conventions/api-codegen.md +400 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
# π API μ½λ μμ± κ°μ΄λ (for AI)
|
|
2
|
+
|
|
3
|
+
> μ΄ λ¬Έμλ **Swagger(OpenAPI) JSON μ κΈ°λ°μΌλ‘ API μ½λλ₯Ό μμ±**ν λ λ°λΌμΌ νλ 컨벀μ
μ΄λ€.
|
|
4
|
+
> μ¬μ©μκ° Swagger JSON(λλ μλν¬μΈνΈ)μ μ 곡νλ©΄, AI λ μ΄ λ¬Έμμ κ·μΉμ λ§μΆ°
|
|
5
|
+
> `api / zod / type / service / index` νμΌμ μμ±νλ€.
|
|
6
|
+
>
|
|
7
|
+
> μ€ν: `@tanstack/react-query` + `axios`. **React / Next.js 곡μ©**μ΄λ©°, μ°¨μ΄λ Β§0 μ "μμΉ"λΏμ΄λ€.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## β³ μ¬μ κ·μΉ β μ½λ μ§κΈ° μ μ
|
|
12
|
+
|
|
13
|
+
1. **μΆμΈ‘ κΈμ§.** Swagger μ μλ νλ/μλν¬μΈνΈλ₯Ό μμλ‘ λ§λ€μ§ μλλ€.
|
|
14
|
+
μ€ν€λ§κ° λͺ¨νΈνλ©΄ κΈ°μ‘΄ 리μμ€ ν΄λ(μ: `user/`)λ₯Ό λ¨Όμ `read_file` λ‘ νμΈνκ³ ,
|
|
15
|
+
κ·Έλλ λΆλͺ
ννλ©΄ μ¬μ©μμκ² ν λ² λ¬»λλ€.
|
|
16
|
+
2. **κΈ°μ‘΄ μ½λ μ°μ .** κ³΅μ© μ νΈ(`cacheConfig`, `queryKey`, `captureSentryError`, `metaSchema`,
|
|
17
|
+
`PaginationParams` λ±)μ μλ‘ λ§λ€μ§ λ§κ³ κ·Έλλ‘ κ°μ Έλ€ μ΄λ€. import κ²½λ‘κ° νμ€μΉ μμΌλ©΄
|
|
18
|
+
`search_code` λ‘ μ€μ export μμΉλ₯Ό νμΈνλ€.
|
|
19
|
+
3. **5νμΌ μΈνΈλ νμ ν¨κ».** `api / zod / type / service / index` λ₯Ό ν λ²μ μμ±νλ€.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 0. μμΉ & ν΄λ ꡬ쑰 β οΈ (React vs Next μ°¨μ΄λ μ¬κΈ°λΏ)
|
|
24
|
+
|
|
25
|
+
API μ½λμ **λ£¨νΈ μμΉλ νλ μμν¬λ§λ€ λ€λ₯΄λ€.**
|
|
26
|
+
|
|
27
|
+
| νλ μμν¬ | API λ£¨νΈ |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| **React** (Vite/CRA λ±) | `src/api` |
|
|
30
|
+
| **Next.js** | `lib/api` |
|
|
31
|
+
|
|
32
|
+
> κ·Έ μΈ ν΄λ/νμΌ κ΅¬μ‘°μ κ·μΉμ **μμ ν λμΌ**νλ€. μλ μμλ `src/api` κΈ°μ€μ΄λ©°,
|
|
33
|
+
> Next λ©΄ `src/api` λ₯Ό `lib/api` λ‘ λ°κΏ μ½μΌλ©΄ λλ€.
|
|
34
|
+
|
|
35
|
+
리μμ€(λλ©μΈ) νλλΉ ν΄λ νλ. ν΄λλͺ
μ **μλ¬Έμ**(μ¬λ¬ λ¨μ΄λ kebab-case: `favorite-stores`).
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
src/api/ # (Next: lib/api/)
|
|
39
|
+
βββ instance.ts # axios μΈμ€ν΄μ€ (interceptor ν¬ν¨)
|
|
40
|
+
βββ index.ts # λͺ¨λ API λͺ¨λ export
|
|
41
|
+
βββ user/ # 리μμ€ ν΄λ (μλ¬Έμ)
|
|
42
|
+
βββ user.api.ts # axios νΈμΆ ν¨μ (μμ ν¨μ)
|
|
43
|
+
βββ user.zod.ts # μλ΅/μμ² zod μ€ν€λ§
|
|
44
|
+
βββ user.type.ts # zod λ‘λΆν° μΆλ‘ ν νμ
+ μ
λ ₯ νμ
|
|
45
|
+
βββ user.service.ts # react-query ν
(use~) β λΉμ¦λμ€ λ‘μ§
|
|
46
|
+
βββ index.ts # μΈλΆ λ
ΈμΆ (service, type λ§)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- νμΌ prefix λ ν΄λλͺ
κ³Ό κ°λ€ (`user/` β `user.api.ts`, `user.zod.ts` ...).
|
|
50
|
+
- `RESOURCE` μμλ `/api/<path>` ννλ‘ `.api.ts` μλ¨μ λλ€.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 1. κ°μ₯ λ¨Όμ β "next(무νμ€ν¬λ‘€)" μΈμ§ νλ¨νλΌ
|
|
55
|
+
|
|
56
|
+
> μ¬κΈ°μ λ§νλ "next" λ **νλ μμν¬ Next.js κ° μλλΌ**, **컀μ κΈ°λ° λ¬΄νμ€ν¬λ‘€ ν¨ν΄**μ κ°λ¦¬ν¨λ€.
|
|
57
|
+
|
|
58
|
+
μ½λλ₯Ό μ§κΈ° μ μ **ν΄λΉ μλν¬μΈνΈκ° 컀μ κΈ°λ° λ¬΄νμ€ν¬λ‘€ API μΈμ§** λ¨Όμ νλ¨νλ€.
|
|
59
|
+
|
|
60
|
+
### "next" λ‘ νλ¨νλ κΈ°μ€ (μλ μ€ νλλΌλ ν΄λΉνλ©΄ next)
|
|
61
|
+
|
|
62
|
+
- μμ² νλΌλ―Έν°μ `cursor`, `limit` (λλ `page`, `size` λ± νμ΄μ§λ€μ΄μ
νλΌλ―Έν°) κ° μλ€.
|
|
63
|
+
- μλ΅ λ³Έλ¬Έμ `items`(λ°°μ΄) + `meta`(`hasNextPage`, `nextCursor`) κ΅¬μ‘°κ° μλ€.
|
|
64
|
+
- "λͺ©λ‘μ μ€ν¬λ‘€νλ©° λ λΆλ¬μ€λ" 리μ€νΈ μ‘°ν μλν¬μΈνΈλ€.
|
|
65
|
+
|
|
66
|
+
| νλ¨ | μ¬μ© ν
| μ°Έκ³ |
|
|
67
|
+
| --- | --- | --- |
|
|
68
|
+
| **next O** (무νμ€ν¬λ‘€) | `useInfiniteQuery` | [Β§5](#5-next무νμ€ν¬λ‘€-ν
νλ¦Ώ) |
|
|
69
|
+
| **next X** (μΌλ°) | `useQuery` / `useMutation` | [Β§4](#4-μΌλ°next-x-ν
νλ¦Ώ) |
|
|
70
|
+
|
|
71
|
+
> λ¨κ±΄ μ‘°νΒ·μμ±Β·μμ Β·μμ , νμ΄μ§λ€μ΄μ
μλ μ 체 λͺ©λ‘μ λͺ¨λ **next X (μΌλ°)**.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 2. κ° νμΌ μμ± κ·μΉ
|
|
76
|
+
|
|
77
|
+
### 2-1. `user.api.ts` β μμ νΈμΆ ν¨μ
|
|
78
|
+
|
|
79
|
+
- `import baseInstance from '../instance';` μ¬μ© (axios μΈμ€ν΄μ€).
|
|
80
|
+
- ν¨μλ `async`, λ΄λΆμμ `const { data } = await baseInstance.X(...)` ν `return data;`.
|
|
81
|
+
- μλ΅ λ³Έλ¬Έμ΄ μλ κ²½μ°(`204 No Content`) λ `return data` μλ΅ κ°λ₯.
|
|
82
|
+
- 쿼리μ€νΈλ§μ `{ params }`, path νλΌλ―Έν°λ ν
νλ¦Ώ 리ν°λ΄.
|
|
83
|
+
- **μ¬κΈ°μλ zod νμ±μ νμ§ μλλ€.** (νμ±μ service μ queryFn μ±
μ)
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import baseInstance from '../instance';
|
|
87
|
+
|
|
88
|
+
const RESOURCE = '/api/user';
|
|
89
|
+
|
|
90
|
+
export const getUserList = async () => {
|
|
91
|
+
const { data } = await baseInstance.get(RESOURCE);
|
|
92
|
+
|
|
93
|
+
return data;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const getUser = async (userId: string) => {
|
|
97
|
+
const { data } = await baseInstance.get(`${RESOURCE}/${userId}`);
|
|
98
|
+
|
|
99
|
+
return data;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const deleteUser = async (userId: string) => {
|
|
103
|
+
const { data } = await baseInstance.delete(`${RESOURCE}/${userId}`);
|
|
104
|
+
|
|
105
|
+
return data;
|
|
106
|
+
};
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 2-2. `user.zod.ts` β μ€ν€λ§
|
|
110
|
+
|
|
111
|
+
- `import { z } from 'zod';`
|
|
112
|
+
- **λ¨μΌ μμ΄ν
μ€ν€λ§**(`userItemSchema`)λ₯Ό λ¨Όμ μ μνκ³ , 리μ€νΈλ `z.array(...)` λ‘ μ‘°ν©νλ€.
|
|
113
|
+
- μ¬μ¬μ© κ°λ₯ν μμ μ€ν€λ§λ λ³λ `const` λ‘ λΆλ¦¬.
|
|
114
|
+
- Swagger νμ
β zod λ§€ν:
|
|
115
|
+
- `string` β `z.string()`, `integer/number` β `z.number()`, `boolean` β `z.boolean()`
|
|
116
|
+
- `nullable: true` β `.nullable()` / `required` μ μμΌλ©΄ `.optional()` (λ λ€λ©΄ `.nullable().optional()`)
|
|
117
|
+
- `enum` β `z.enum([...] as const)` (μ«μ enum μ `z.union([z.literal(1), ...])`)
|
|
118
|
+
- `format: date-time` β `z.string().datetime()` (κ°μ ISO λ¬Έμμ΄ μ μ§)
|
|
119
|
+
- μ μ½(min/max/length) μ΄ λͺ
μλλ©΄ λ°μ
|
|
120
|
+
- **`$ref` / `allOf` / `oneOf`**:
|
|
121
|
+
- `$ref` β μ°Έμ‘° λμ μ€ν€λ§λ₯Ό λ¨Όμ μ μ ν μ¬μ¬μ©
|
|
122
|
+
- `allOf` β `baseSchema.merge(extraSchema)` λλ `.and(...)`
|
|
123
|
+
- `oneOf`/`anyOf` β `z.union([...])`, discriminator μμΌλ©΄ `z.discriminatedUnion(...)`
|
|
124
|
+
- **next μλ΅**μ κ³΅ν΅ `metaSchema` μ¬μ©: `import { metaSchema } from '@/lib';`
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { z } from 'zod';
|
|
128
|
+
|
|
129
|
+
export const userItemSchema = z.object({
|
|
130
|
+
userId: z.string(),
|
|
131
|
+
name: z.string(),
|
|
132
|
+
email: z.string().nullable(),
|
|
133
|
+
isActive: z.boolean(),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
export const userListSchema = z.array(userItemSchema);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### 2-3. `user.type.ts` β νμ
|
|
140
|
+
|
|
141
|
+
- μλ΅ νμ
μ **zod μ€ν€λ§μμ μΆλ‘ **: `export type X = z.infer<typeof xSchema>;`
|
|
142
|
+
- μ
λ ₯(μμ²) νμ
μ μ§μ μ μ. μΈμκ° 2κ° μ΄μμΈ λ³κ²½μ κ°μ²΄ μ
λ ₯ νμ
μΌλ‘ λ¬Άλλ€.
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { z } from 'zod';
|
|
146
|
+
import { userItemSchema } from './user.zod';
|
|
147
|
+
|
|
148
|
+
export type UserItem = z.infer<typeof userItemSchema>;
|
|
149
|
+
|
|
150
|
+
export type UpdateUser = {
|
|
151
|
+
userId: string;
|
|
152
|
+
name: string;
|
|
153
|
+
};
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 2-4. `user.service.ts` β react-query ν
(λΉμ¦λμ€ λ‘μ§)
|
|
157
|
+
|
|
158
|
+
κ³΅ν΅ import:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { cacheConfig, captureSentryError, queryKey } from '@/lib';
|
|
162
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
κ·μΉ:
|
|
166
|
+
|
|
167
|
+
- ν
μ΄λ¦: μ‘°ν `useGetUser` / `useGetUserList`, λ³κ²½ `useCreateUser` / `useUpdateUser` / `useDeleteUser`.
|
|
168
|
+
- **μ‘°ν(useQuery)** μ `queryFn` μμ zod `safeParse` λ‘ κ²μ¦.
|
|
169
|
+
- μ€ν¨ μ `console.error(...)` ν **μλ³Έ `data` κ·Έλλ‘ λ°ν**(throw κΈμ§).
|
|
170
|
+
- μ±κ³΅ μ `parsed.data` λ°ν.
|
|
171
|
+
- **λ³κ²½(useMutation)** μ `onSuccess` μμ κ΄λ ¨ `queryKey` λ₯Ό `invalidateQueries`.
|
|
172
|
+
- λͺ¨λ ν
μ `onError` μμ `captureSentryError(error, { location, action })`.
|
|
173
|
+
- `location` = ν
μ΄λ¦(`'useDeleteUser'`), `action` = νΈμΆ ν¨μλͺ
(`'deleteUser'`).
|
|
174
|
+
- μ‘°ν ν
μ λ§μ§λ§μ `...cacheConfig.<tier>` + `...options` νΌμΉ¨.
|
|
175
|
+
- μΊμ tier: μμ£Ό λ°λ `realtime`/`shortLived`, λ³΄ν΅ `mediumLived`, μ μ λ°λ `longLived`, λΆλ³ `immutable`.
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
export const useGetUserList = (options?: Record<string, any>) => {
|
|
179
|
+
return useQuery({
|
|
180
|
+
queryKey: queryKey.user.list,
|
|
181
|
+
queryFn: async () => {
|
|
182
|
+
const data = await getUserList();
|
|
183
|
+
const parsed = userListSchema.safeParse(data);
|
|
184
|
+
|
|
185
|
+
if (!parsed.success) {
|
|
186
|
+
console.error('User list validation error:', parsed.error);
|
|
187
|
+
|
|
188
|
+
return data;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return parsed.data;
|
|
192
|
+
},
|
|
193
|
+
...cacheConfig.longLived,
|
|
194
|
+
...options,
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export const useDeleteUser = () => {
|
|
199
|
+
const queryClient = useQueryClient();
|
|
200
|
+
|
|
201
|
+
return useMutation({
|
|
202
|
+
mutationFn: (userId: string) => deleteUser(userId),
|
|
203
|
+
onSuccess: () => {
|
|
204
|
+
queryClient.invalidateQueries({ queryKey: queryKey.user.all });
|
|
205
|
+
},
|
|
206
|
+
onError: (error) => {
|
|
207
|
+
captureSentryError(error, { location: 'useDeleteUser', action: 'deleteUser' });
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### 2-5. `index.ts` (리μμ€) β λ
ΈμΆ
|
|
214
|
+
|
|
215
|
+
- **`service` μ `type` λ§** μ¬λ
ΈμΆ (`api`, `zod` λ λ
ΈμΆνμ§ μμ).
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
export * from './user.service';
|
|
219
|
+
export * from './user.type';
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### 2-6. `src/api/index.ts` (루νΈ) β λͺ¨λ λͺ¨λ export
|
|
223
|
+
|
|
224
|
+
- μ 리μμ€λ₯Ό μΆκ°νλ©΄ ν μ€ μΆκ°νλ€.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
export * from './user';
|
|
228
|
+
// export * from './order';
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### 2-7. `src/api/instance.ts` β axios μΈμ€ν΄μ€
|
|
232
|
+
|
|
233
|
+
- μ΄λ―Έ μμΌλ©΄ **건λλ¦¬μ§ μλλ€.** baseURL/interceptor μ€μ μ΄ μ¬κΈ° λͺ¨μ¬ μλ€.
|
|
234
|
+
- μ 리μμ€λ νμ μ΄ `baseInstance` λ₯Ό import ν΄μ μ΄λ€.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## 3. queryKey λ±λ‘ κ·μΉ
|
|
239
|
+
|
|
240
|
+
κ³΅μ© `queryKey` κ°μ²΄μ 리μμ€ νλͺ©μ μΆκ°νλ€.
|
|
241
|
+
|
|
242
|
+
- `all` μ 무ν¨ν(invalidate) κΈ°μ€ μ΅μμ ν€. λ³κ²½ ν
μ λ³΄ν΅ `queryKey.<resource>.all` 무ν¨ν.
|
|
243
|
+
- νμ ν€λ `['<resource>', '<scope>']`. νλΌλ―Έν°κ° λ€μ΄κ°λ©΄ ν¨μνμΌλ‘.
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
user: Object.freeze({
|
|
247
|
+
all: ['user'],
|
|
248
|
+
list: ['user', 'list'],
|
|
249
|
+
detail: (id: string) => ['user', 'detail', id],
|
|
250
|
+
}),
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## 4. μΌλ°(next X) ν
νλ¦Ώ
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
// api
|
|
259
|
+
export const getUser = async (params?: SomeParams) => {
|
|
260
|
+
const { data } = await baseInstance.get(RESOURCE, { params });
|
|
261
|
+
return data;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// service
|
|
265
|
+
export const useGetUser = (options?: Record<string, any>) => {
|
|
266
|
+
return useQuery({
|
|
267
|
+
queryKey: queryKey.user.list,
|
|
268
|
+
queryFn: async () => {
|
|
269
|
+
const data = await getUser();
|
|
270
|
+
const parsed = userSchema.safeParse(data);
|
|
271
|
+
if (!parsed.success) {
|
|
272
|
+
console.error('User validation error:', parsed.error);
|
|
273
|
+
return data;
|
|
274
|
+
}
|
|
275
|
+
return parsed.data;
|
|
276
|
+
},
|
|
277
|
+
...cacheConfig.mediumLived,
|
|
278
|
+
...options,
|
|
279
|
+
});
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// λ³κ²½
|
|
283
|
+
export const useCreateUser = () => {
|
|
284
|
+
const queryClient = useQueryClient();
|
|
285
|
+
|
|
286
|
+
return useMutation({
|
|
287
|
+
mutationFn: (payload: CreateUserPayload) => createUser(payload),
|
|
288
|
+
onSuccess: () => {
|
|
289
|
+
queryClient.invalidateQueries({ queryKey: queryKey.user.all });
|
|
290
|
+
},
|
|
291
|
+
onError: (error) => {
|
|
292
|
+
captureSentryError(error, { location: 'useCreateUser', action: 'createUser' });
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
};
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
> μΈμκ° 2κ° μ΄μμ΄λ©΄ κ°μ²΄λ‘ λ¬Άμ΄ `*.type.ts` μ μ
λ ₯ νμ
μ μ μνκ³ κ΅¬μ‘°λΆν΄λ‘ λ°λλ€.
|
|
299
|
+
> μ: `mutationFn: ({ userId, name }: UpdateUser) => updateUser(userId, name)`
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## 5. next(무νμ€ν¬λ‘€) ν
νλ¦Ώ
|
|
304
|
+
|
|
305
|
+
νλ¨ κ²°κ³Όκ° **next** μΌ λλ§ μ¬μ©. ν΅μ¬μ `useInfiniteQuery` + κ³΅ν΅ `metaSchema`.
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
// api (cursor κΈ°λ°)
|
|
309
|
+
import { PaginationParams } from '@/lib';
|
|
310
|
+
|
|
311
|
+
export const getUser = async (params: PaginationParams) => {
|
|
312
|
+
const { data } = await baseInstance.get(RESOURCE, { params });
|
|
313
|
+
return data;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// zod (items + meta)
|
|
317
|
+
import { metaSchema } from '@/lib';
|
|
318
|
+
|
|
319
|
+
export const userItemSchema = z.object({ /* ... */ });
|
|
320
|
+
export const getUserResponseSchema = z.object({
|
|
321
|
+
items: z.array(userItemSchema),
|
|
322
|
+
meta: metaSchema, // { hasNextPage, nextCursor }
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// type
|
|
326
|
+
export type GetUserResponse = z.infer<typeof getUserResponseSchema>;
|
|
327
|
+
|
|
328
|
+
// service
|
|
329
|
+
import { useInfiniteQuery } from '@tanstack/react-query';
|
|
330
|
+
|
|
331
|
+
export const useGetUser = (options?: Record<string, any>) => {
|
|
332
|
+
const defaultParams: PaginationParams = { limit: 10, cursor: undefined };
|
|
333
|
+
|
|
334
|
+
return useInfiniteQuery<GetUserResponse, Error, GetUserResponse['items']>({
|
|
335
|
+
queryKey: queryKey.user.list,
|
|
336
|
+
queryFn: async ({ pageParam }) => {
|
|
337
|
+
const params: PaginationParams = { ...defaultParams, ...(pageParam || {}) };
|
|
338
|
+
const data = await getUser(params);
|
|
339
|
+
const parsed = getUserResponseSchema.safeParse(data);
|
|
340
|
+
if (!parsed.success) {
|
|
341
|
+
console.error('User validation error:', parsed.error);
|
|
342
|
+
return data;
|
|
343
|
+
}
|
|
344
|
+
return parsed.data;
|
|
345
|
+
},
|
|
346
|
+
getNextPageParam: (lastPage) =>
|
|
347
|
+
lastPage?.meta?.hasNextPage
|
|
348
|
+
? { ...defaultParams, cursor: lastPage.meta.nextCursor }
|
|
349
|
+
: undefined,
|
|
350
|
+
select: (data) => data.pages.flatMap((page) => page?.items ?? []),
|
|
351
|
+
initialPageParam: defaultParams,
|
|
352
|
+
...cacheConfig.longLived,
|
|
353
|
+
...options,
|
|
354
|
+
});
|
|
355
|
+
};
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
- `select` λ‘ `pages` λ₯Ό νννν΄ μ»΄ν¬λνΈλ ννν λ°°μ΄λ§ λ°λλ€.
|
|
359
|
+
- `meta.hasNextPage` κ° falsy λ©΄ `getNextPageParam` μ `undefined`(λ€μ νμ΄μ§ μμ).
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## 6. λ€μ΄λ° & μ€νμΌ μμ½
|
|
364
|
+
|
|
365
|
+
- ν΄λ: μλ¬Έμ / kebab-case (`user`, `favorite-stores`). νμΌ prefix = ν΄λλͺ
.
|
|
366
|
+
- ν¨μ: λμ¬ + 리μμ€ (`getUserList`, `createUser`, `updateUser`).
|
|
367
|
+
- ν
: `use` + ν¨μ μλ―Έ (`useGetUserList`, `useCreateUser`).
|
|
368
|
+
- `RESOURCE` μμλ‘ baseURL κ²½λ‘ κ΄λ¦¬, λμ κ²½λ‘λ ν
νλ¦Ώ 리ν°λ΄.
|
|
369
|
+
- μ‘°νλ zod κ²μ¦(μ€ν¨ μ μλ³Έ λ°ν), λ³κ²½μ invalidate + Sentry.
|
|
370
|
+
- `index.ts` λ service/type λ§ λ
ΈμΆ, λ£¨νΈ `index.ts` μ 리μμ€ ν μ€ μΆκ°.
|
|
371
|
+
- λ€μ¬μ°κΈ° 2μΉΈ, μΈλ―Έμ½λ‘ μ¬μ©, import κ·Έλ£Ή: μΈλΆ β `@/...` β μλκ²½λ‘.
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## 7. μμ± μ 체ν¬λ¦¬μ€νΈ β
|
|
376
|
+
|
|
377
|
+
1. [ ] μμΉλ₯Ό λ§μ·λ€ (React `src/api` / Next `lib/api`).
|
|
378
|
+
2. [ ] μλν¬μΈνΈκ° **next(무νμ€ν¬λ‘€)** μΈμ§ νλ¨νλ€. (Β§1)
|
|
379
|
+
3. [ ] `api / zod / type / service / index` 5νμΌμ λͺ¨λ λ§λ€μλ€.
|
|
380
|
+
4. [ ] Swagger μλ΅μ zod λ‘ μ νν λ§€ν(nullable/optional/enum/date/$ref).
|
|
381
|
+
5. [ ] νμ
μ `z.infer` λ‘ μΆλ‘ .
|
|
382
|
+
6. [ ] μ‘°ν ν
queryFn μμ `safeParse` ν μ€ν¨ μ μλ³Έ λ°ν.
|
|
383
|
+
7. [ ] λ³κ²½ ν
μ `invalidateQueries` + `captureSentryError`.
|
|
384
|
+
8. [ ] `queryKey` μ 리μμ€ ν€(`all` ν¬ν¨) μΆκ°.
|
|
385
|
+
9. [ ] 리μμ€ `index.ts` + λ£¨νΈ `index.ts` λ
ΈμΆ μΆκ°.
|
|
386
|
+
10. [ ] next λ©΄ `useInfiniteQuery` + `metaSchema` + `select` ννν μ μ©.
|
|
387
|
+
11. [ ] κ³΅μ© μ νΈμ μ¬μ¬μ©νκ³ , Swagger μ μλ νλλ₯Ό μΆμΈ‘μΌλ‘ λ§λ€μ§ μμλ€.
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
## 8. μμ£Ό νλ μ€μ (νμ§ λ§ κ²) π«
|
|
392
|
+
|
|
393
|
+
- β `*.api.ts` μμ zod νμ± (νμ±μ service μ queryFn μ±
μ).
|
|
394
|
+
- β `index.ts` μμ `api`/`zod` λ
ΈμΆ (service/type λ§).
|
|
395
|
+
- β μ‘°ν ν
μμ κ²μ¦ μ€ν¨ μ throw (μλ³Έ λ°νμ΄ κ·μΉ).
|
|
396
|
+
- β `captureSentryError` / `invalidateQueries` λλ½.
|
|
397
|
+
- β Swagger μ `nullable` 무μνκ³ νμλ‘ μ μΈ.
|
|
398
|
+
- β μΌλ° λͺ©λ‘μΈλ° `useInfiniteQuery` μ¬μ© (λλ κ·Έ λ°λ).
|
|
399
|
+
- β κ³΅μ© νμ
/μ νΈ(`metaSchema`, `PaginationParams`)μ μ€λ³΅ μ¬μ μ.
|
|
400
|
+
- β React μΈλ° `lib/api`, Next μΈλ° `src/api` μ λ§λλ μμΉ μ€μ.
|