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.
@@ -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` 에 λ§Œλ“œλŠ” μœ„μΉ˜ μ‹€μˆ˜.