@axboot-mcp/mcp-server 1.0.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 (78) hide show
  1. package/CLAUDE.md +119 -0
  2. package/MCP_TOOL_PLAN.md +710 -0
  3. package/MCP_USAGE.md +914 -0
  4. package/README.md +168 -0
  5. package/REPOSITORY_CONVENTIONS.md +250 -0
  6. package/SEARCH_PARAMS_MCP_TOOL_COMPLETE_PLAN.md +646 -0
  7. package/SEARCH_PARAMS_PLAN.md +2570 -0
  8. package/STORE_PATTERNS.md +1178 -0
  9. package/debug-dto.js +72 -0
  10. package/generate-banner-store.js +62 -0
  11. package/generation-plan.json +2176 -0
  12. package/generation-results.json +1817 -0
  13. package/package.json +45 -0
  14. package/scripts/batch-generate-all.js +159 -0
  15. package/scripts/batch-generate-mcp.js +329 -0
  16. package/scripts/batch-generate-stores-v2.js +272 -0
  17. package/scripts/batch-generate-stores.js +179 -0
  18. package/scripts/batch-plan.json +3810 -0
  19. package/scripts/batch-process.py +90 -0
  20. package/scripts/batch-regenerate.js +356 -0
  21. package/scripts/direct-generate.js +227 -0
  22. package/scripts/execute-batches.js +1911 -0
  23. package/scripts/generate-all-stores.js +144 -0
  24. package/scripts/generate-stores-mcp.js +161 -0
  25. package/scripts/generate-stores-v2.js +450 -0
  26. package/scripts/generate-stores-v3.js +412 -0
  27. package/scripts/generate-stores-v4.js +521 -0
  28. package/scripts/generate-stores.js +382 -0
  29. package/scripts/repos-to-process.json +1899 -0
  30. package/src/config/nh-layout-patterns.ts +166 -0
  31. package/src/docs/HOOK_GENERATION_PLAN.md +2226 -0
  32. package/src/docs/NH_STORE_PATTERNS.md +297 -0
  33. package/src/docs/README.md +216 -0
  34. package/src/docs/index.ts +28 -0
  35. package/src/docs/loader.ts +568 -0
  36. package/src/docs/patterns.json +419 -0
  37. package/src/docs/practical-examples.md +732 -0
  38. package/src/docs/quick-start.md +257 -0
  39. package/src/docs/requirements-analysis-guide.md +364 -0
  40. package/src/docs/rules.json +321 -0
  41. package/src/docs/store-pattern-analysis.md +664 -0
  42. package/src/docs/store-patterns-rules.md +1168 -0
  43. package/src/docs/store-patterns-usage-guide.md +1835 -0
  44. package/src/docs/troubleshooting.md +544 -0
  45. package/src/docs/type-selection-guide.md +572 -0
  46. package/src/docs//354/202/254/354/232/251/353/262/225/AntD-/354/273/264/355/217/254/353/204/214/355/212/270-/354/202/254/354/232/251/353/262/225.md +1515 -0
  47. package/src/docs//354/202/254/354/232/251/353/262/225/DataGrid-/354/202/254/354/232/251/353/262/225.md +866 -0
  48. package/src/docs//354/202/254/354/232/251/353/262/225/FormItem-/354/202/254/354/232/251/353/262/225.md +903 -0
  49. package/src/docs//354/202/254/354/232/251/353/262/225/FormModal-/354/202/254/354/232/251/353/262/225.md +1155 -0
  50. package/src/docs//354/202/254/354/232/251/353/262/225/MCP-/353/260/224/354/235/264/353/270/214/354/275/224/353/224/251-/352/260/200/354/235/264/353/223/234.md +1133 -0
  51. package/src/docs//354/202/254/354/232/251/353/262/225/MSW-Mock-/353/215/260/354/235/264/355/204/260-/354/202/254/354/232/251/353/262/225.md +579 -0
  52. package/src/docs//354/202/254/354/232/251/353/262/225/Search-/354/273/264/355/217/254/353/204/214/355/212/270-/354/202/254/354/232/251/353/262/225.md +738 -0
  53. package/src/docs//354/202/254/354/232/251/353/262/225/Store-/355/214/250/355/204/264-/354/202/254/354/232/251/353/262/225.md +1135 -0
  54. package/src/docs//354/202/254/354/232/251/353/262/225//355/231/224/353/251/264/352/265/254/354/204/261-/355/203/200/354/236/205/353/263/204-/352/260/234/353/260/234/354/210/234/354/204/234.md +1805 -0
  55. package/src/docs//354/202/254/354/232/251/353/262/225//355/231/224/353/251/264/355/203/200/354/236/205/353/263/204-/352/260/234/353/260/234-/355/224/204/353/241/254/355/224/204/355/212/270-/352/260/200/354/235/264/353/223/234.md +946 -0
  56. package/src/docs//354/202/254/354/232/251/353/262/225//355/231/225/354/236/245/355/231/224/353/251/264/355/203/200/354/236/205/353/263/204-/354/203/201/354/204/270-/355/224/204/353/241/254/355/224/204/355/212/270/352/260/200/354/235/264/353/223/234.md +2422 -0
  57. package/src/features/store-features.ts +232 -0
  58. package/src/handlers/analyze-requirements.ts +403 -0
  59. package/src/handlers/analyze.ts +1373 -0
  60. package/src/handlers/generate-from-requirements.ts +250 -0
  61. package/src/handlers/generate-hook.ts +950 -0
  62. package/src/handlers/generate-interactive.ts +840 -0
  63. package/src/handlers/generate-listdatagrid.ts +521 -0
  64. package/src/handlers/generate-multi-stores.ts +577 -0
  65. package/src/handlers/generate-requirements-from-layout.ts +160 -0
  66. package/src/handlers/generate-search-params.ts +717 -0
  67. package/src/handlers/generate.ts +911 -0
  68. package/src/handlers/list-templates.ts +104 -0
  69. package/src/handlers/scan-metadata.ts +485 -0
  70. package/src/handlers/suggest-layout.ts +326 -0
  71. package/src/index.ts +959 -0
  72. package/src/prompts/search-params.md +793 -0
  73. package/src/templates/index.ts +107 -0
  74. package/src/templates/unified.ts +462 -0
  75. package/store-generation-error-patterns.md +225 -0
  76. package/test/useAgentStore.ts +136 -0
  77. package/test-server.js +78 -0
  78. package/tsconfig.json +20 -0
@@ -0,0 +1,2226 @@
1
+ # React Hook 자동 생성 기능 계획서
2
+
3
+ ## 개요
4
+
5
+ 리포지토리 인터페이스를 분석하여 리액트 훅 파일을 자동 생성하는 MCP 툴을 추가합니다.
6
+
7
+ ---
8
+
9
+ ## 1. 기능 목표
10
+
11
+ ### 1.0 선행 작업: 인터페이스 타입 분석 (최우선)
12
+
13
+ 훅 생성을 위해 **반드시** 인터페이스 파일을 분석하여 요청/응답 타입을 확인해야 합니다.
14
+
15
+ **분석 순서:**
16
+ 1. Repository 파일에서 메서드 추출
17
+ 2. 각 메서드의 요청 타입(`RequestType`) 추출
18
+ 3. 각 메서드의 응답 타입(`ResponseType`) 추출
19
+ 4. 응답 타입에서 DTO 타입 추출 (`ds: Type[]` → `Type`)
20
+ 5. 페이징 여부 확인 (`page:` 필드 존재 여부)
21
+ 6. dateRange 패턴 확인 (`bgnDtm/endDtm` 쌍)
22
+
23
+ **타입 추출 예시:**
24
+ ```typescript
25
+ // Repository 파일
26
+ async postMemberListMember(params: PostMemberListMemberRequest): Promise<PostMemberListMemberResponse>
27
+
28
+ // 분석 결과
29
+ {
30
+ methodName: "postMemberListMember",
31
+ requestType: "PostMemberListMemberRequest",
32
+ responseType: "PostMemberListMemberResponse",
33
+ dtoType: "MemberRes", // Response에서 ds: MemberRes[] 추출
34
+ hasPage: true, // Response에 page: 필드 있음
35
+ hasDateRange: true, // Request에 bgnDtm/endDtm 쌍 있음
36
+ dateRangeFields: { startField: "bgnDtm", endField: "endDtm" }
37
+ }
38
+ ```
39
+
40
+ ### 1.1 대상 API 필터링
41
+
42
+ | 포함 키워드 | 제외 키워드 |
43
+ |-------------|-------------|
44
+ | 조회, 목록 | 저장, 삭제, 수정, 엑셀, 업로드, 다운로드 |
45
+
46
+ **예시:**
47
+ ```typescript
48
+ /* 회원 목록 */ // ✅ 포함
49
+ async postMemberListMember() // ✅ 포함
50
+ /* 회원 저장 */ // ❌ 제외
51
+ async postMemberSave() // ❌ 제외
52
+ /* 엑셀 다운로드 */ // ❌ 제외
53
+ async postMemberExcel() // ❌ 제외
54
+ ```
55
+
56
+ ### 1.2 생성되는 훅 구조
57
+
58
+ **핵심 원칙:** 각 메서드의 응답 타입을 분석하여 `hasPage` 여부에 따라 **가변적으로** `page`, `setPage`를 생성합니다.
59
+
60
+ #### Case 1: 페이징 있는 경우
61
+
62
+ ```typescript
63
+ export function useMemberService(params?: ListRequest) {
64
+ // 상태
65
+ const [list, setList] = useState<MemberRes[]>([]);
66
+ const [listSpinning, setListSpinning] = useState(false);
67
+
68
+ // ✅ 페이징 있는 경우 (hasPage: true)
69
+ const [page, setPage] = useState<AXDGPage>({
70
+ totalPages: 0,
71
+ totalElements: 0,
72
+ currentPage: 0,
73
+ pageSize: 0,
74
+ });
75
+
76
+ // dateRange 있는 경우
77
+ interface ListRequest extends Partial<PostMemberListMemberRequest> {
78
+ dateRange?: [string, string];
79
+ }
80
+
81
+ const getList = useCallback(async (listParams: ListRequest) => {
82
+ setListSpinning(true);
83
+ try {
84
+ const data = await MemberService.postMemberListMember(deleteEmptyValue(listParams));
85
+ setList(data.ds);
86
+
87
+ // ✅ hasPage: true이면 setPage 호출
88
+ setPage({
89
+ totalPages: data.page.pageCount,
90
+ totalElements: data.page.totalCount,
91
+ currentPage: data.page.pageNumber,
92
+ pageSize: data.page.pageSize,
93
+ });
94
+ } catch (err) {
95
+ await errorHandling(err);
96
+ } finally {
97
+ setListSpinning(false);
98
+ }
99
+ }, []);
100
+
101
+ // ✅ hasPage: true이면 return에 page 포함
102
+ return {
103
+ list,
104
+ page, // ✅ 포함
105
+ listSpinning,
106
+ getList,
107
+ };
108
+ }
109
+ ```
110
+
111
+ #### Case 2: 페이징 없는 경우
112
+
113
+ ```typescript
114
+ export function useCodeService() {
115
+ const [list, setList] = useState<Code[]>([]);
116
+ const [listSpinning, setListSpinning] = useState(false);
117
+
118
+ // ❌ 페이징 없으면 page, setPage 생성 안 함
119
+
120
+ const getList = useCallback(async (params?: GetCodeListRequest) => {
121
+ setListSpinning(true);
122
+ try {
123
+ const data = await CodeService.getCodeList(deleteEmptyValue(params));
124
+ setList(data.ds);
125
+
126
+ // ❌ hasPage: false이면 setPage 호출 안 함
127
+ } catch (err) {
128
+ await errorHandling(err);
129
+ } finally {
130
+ setListSpinning(false);
131
+ }
132
+ }, []);
133
+
134
+ // ❌ hasPage: false이면 return에 page 미포함
135
+ return {
136
+ list,
137
+ // page, // ❌ 미포함
138
+ listSpinning,
139
+ getList,
140
+ };
141
+ }
142
+ ```
143
+
144
+ #### Case 3: 여러 API - 페이징 섞인 경우
145
+
146
+ ```typescript
147
+ export function useMemberService() {
148
+ // 첫 번째: 페이징 있음
149
+ const [list, setList] = useState<MemberRes[]>([]);
150
+ const [listSpinning, setListSpinning] = useState(false);
151
+ const [page, setPage] = useState<AXDGPage>({...}); // ✅ 있음
152
+
153
+ // 두 번째: 페이징 없음
154
+ const [adminList, setAdminList] = useState<AdminRes[]>([]);
155
+ const [adminSpinning, setAdminSpinning] = useState(false);
156
+ // ❌ adminPage, setAdminPage 없음
157
+
158
+ const getList = useCallback(async (params?: ListRequest) => {
159
+ setListSpinning(true);
160
+ try {
161
+ const data = await MemberService.postMemberListMember(deleteEmptyValue(params));
162
+ setList(data.ds);
163
+ setPage({ ... }); // ✅ setPage 호출
164
+ } catch (err) {
165
+ await errorHandling(err);
166
+ } finally {
167
+ setListSpinning(false);
168
+ }
169
+ }, []);
170
+
171
+ const getAdminList = useCallback(async (params?: AdminListRequest) => {
172
+ setAdminSpinning(true);
173
+ try {
174
+ const data = await MemberService.postMemberListAdmin(deleteEmptyValue(params));
175
+ setAdminList(data.ds);
176
+ // ❌ setPage 없음
177
+ } catch (err) {
178
+ await errorHandling(err);
179
+ } finally {
180
+ setAdminSpinning(false);
181
+ }
182
+ }, []);
183
+
184
+ return {
185
+ list,
186
+ page, // ✅ 첫 번째는 포함
187
+ listSpinning,
188
+ getList,
189
+ adminList,
190
+ // adminPage, // ❌ 두 번째는 미포함
191
+ adminSpinning,
192
+ getAdminList,
193
+ };
194
+ }
195
+ ```
196
+
197
+ ---
198
+
199
+ ## 2. 여러 목록 API 처리
200
+
201
+ ### 2.1 네이밍 규칙 (옵션 1: 메서드명 기반 자동 네이밍)
202
+
203
+ | 메서드명 | 리스트 변수명 | 함수명 |
204
+ |----------|---------------|--------|
205
+ | `postMemberListMember` | `list` (기본) | `getList` |
206
+ | `postMemberListAdmin` | `adminList` | `getAdminList` |
207
+ | `postMemberListActive` | `activeList` | `getActiveList` |
208
+ | `postMemberListByStatus` | `byStatusList` | `getByStatusList` |
209
+ | `postCategoryList` | `categoryList` | `getCategoryList` |
210
+
211
+ **규칙:**
212
+ - `post{Entity}List{Suffix}` → `{suffix}List` (camelCase)
213
+ - `post{Entity}List` (접미사 없음) → `list` (기본)
214
+
215
+ ### 2.2 여러 API가 있는 경우 생성 예시
216
+
217
+ **중요:** 각 메서드별로 `hasPage` 여부를 확인하여 **가변적으로** `page`, `setPage`를 생성합니다.
218
+
219
+ ```typescript
220
+ export function useMemberService() {
221
+ // 첫 번째 목록 (기본) - 페이징 있음 (hasPage: true)
222
+ const [list, setList] = useState<MemberRes[]>([]);
223
+ const [listSpinning, setListSpinning] = useState(false);
224
+ const [page, setPage] = useState<AXDGPage>({...}); // ✅ 있음
225
+
226
+ // 두 번째 목록 - 페이징 없음 (hasPage: false)
227
+ const [adminList, setAdminList] = useState<AdminRes[]>([]);
228
+ const [adminSpinning, setAdminSpinning] = useState(false);
229
+ // ❌ adminPage, setAdminPage 없음
230
+
231
+ // 세 번째 목록 - 페이징 있음 (hasPage: true)
232
+ const [activeList, setActiveList] = useState<ActiveRes[]>([]);
233
+ const [activeSpinning, setActiveSpinning] = useState(false);
234
+ const [activePage, setActivePage] = useState<AXDGPage>({...}); // ✅ 있음
235
+
236
+ // 각각의 getList 함수
237
+ const getList = useCallback(async (params?: ListRequest) => {
238
+ setListSpinning(true);
239
+ try {
240
+ const data = await MemberService.postMemberListMember(deleteEmptyValue(params));
241
+ setList(data.ds);
242
+ setPage({ // ✅ hasPage: true이면 호출
243
+ totalPages: data.page.pageCount,
244
+ totalElements: data.page.totalCount,
245
+ currentPage: data.page.pageNumber,
246
+ pageSize: data.page.pageSize,
247
+ });
248
+ } catch (err) {
249
+ await errorHandling(err);
250
+ } finally {
251
+ setListSpinning(false);
252
+ }
253
+ }, []);
254
+
255
+ const getAdminList = useCallback(async (params?: AdminListRequest) => {
256
+ setAdminSpinning(true);
257
+ try {
258
+ const data = await MemberService.postMemberListAdmin(deleteEmptyValue(params));
259
+ setAdminList(data.ds);
260
+ // ❌ hasPage: false이면 setPage 없음
261
+ } catch (err) {
262
+ await errorHandling(err);
263
+ } finally {
264
+ setAdminSpinning(false);
265
+ }
266
+ }, []);
267
+
268
+ const getActiveList = useCallback(async (params?: ActiveListRequest) => {
269
+ setActiveSpinning(true);
270
+ try {
271
+ const data = await MemberService.postMemberListActive(deleteEmptyValue(params));
272
+ setActiveList(data.ds);
273
+ setActivePage({ // ✅ hasPage: true이면 호출
274
+ totalPages: data.page.pageCount,
275
+ totalElements: data.page.totalCount,
276
+ currentPage: data.page.pageNumber,
277
+ pageSize: data.page.pageSize,
278
+ });
279
+ } catch (err) {
280
+ await errorHandling(err);
281
+ } finally {
282
+ setActiveSpinning(false);
283
+ }
284
+ }, []);
285
+
286
+ return {
287
+ list,
288
+ page, // ✅ hasPage: true
289
+ listSpinning,
290
+ getList,
291
+ adminList,
292
+ // adminPage, // ❌ hasPage: false
293
+ adminSpinning,
294
+ getAdminList,
295
+ activeList,
296
+ activePage, // ✅ hasPage: true
297
+ activeSpinning,
298
+ getActiveList,
299
+ };
300
+ }
301
+ ```
302
+
303
+ **페이징 처리 규칙:**
304
+ | 메서드 | hasPage | 상태 생성 | setPage 호출 | return 포함 |
305
+ |--------|---------|-----------|--------------|-------------|
306
+ | getList | true | ✅ | ✅ | ✅ |
307
+ | getAdminList | false | ❌ | ❌ | ❌ |
308
+ | getActiveList | true | ✅ | ✅ | ✅ |
309
+
310
+ ### 2.3 단건(Detail) API 네이밍 규칙
311
+
312
+ **메서드 패턴 분류:**
313
+
314
+ | 구분 | 메서드 패턴 | 예시 | 응답 구조 |
315
+ |------|-------------|------|-----------|
316
+ | 목록 | `post{Entity}List{Suffix}` | `postMemberListMember` | `ds: Type[]` |
317
+ | 단건 | `post{Entity}{Suffix}` | `postMemberDetail`, `postMemberInfo` | `rs: Type` |
318
+ | 단건 | `get{Entity}{Suffix}` | `getMemberDetail`, `getMemberInfo` | `rs: Type` |
319
+ | 단건 | `post{Entity}` | `postMember` | `rs: Type` |
320
+ | 단건 | `get{Entity}` | `getMember` | `rs: Type` |
321
+
322
+ **단건 네이밍 규칙:**
323
+
324
+ | 메서드명 | 변수명 | 함수명 | Spinning |
325
+ |----------|--------|--------|----------|
326
+ | `postMemberDetail` | `detail` | `getDetail` | `detailSpinning` |
327
+ | `postMemberInfo` | `info` | `getInfo` | `infoSpinning` |
328
+ | `postMemberProfile` | `profile` | `getProfile` | `profileSpinning` |
329
+ | `postMember` | `member` | `getMember` | `memberSpinning` |
330
+ | `getMemberDetail` | `detail` | `getDetail` | `detailSpinning` |
331
+ | `getMember` | `member` | `getMember` | `memberSpinning` |
332
+
333
+ **단건 네이밍 추출 로직:**
334
+
335
+ ```typescript
336
+ /**
337
+ * 메서드명에서 단건 변수명 추출
338
+ * postMemberDetail → detail
339
+ * postMemberInfo → info
340
+ * postMemberProfile → profile
341
+ * getMember → member
342
+ */
343
+ function extractDetailName(methodName: string, entityName: string): string {
344
+ // post{Entity}{Suffix} 패턴
345
+ const postPattern = new RegExp(`post${entityName}(.*)`);
346
+ const postMatch = methodName.match(postPattern);
347
+
348
+ if (postMatch) {
349
+ const suffix = postMatch[1];
350
+ if (!suffix) return entityName.toLowerCase(); // postMember → member
351
+
352
+ // 첫 글자 소문자로 변환 (camelCase)
353
+ return suffix.charAt(0).toLowerCase() + suffix.slice(1);
354
+ }
355
+
356
+ // get{Entity}{Suffix} 패턴
357
+ const getPattern = new RegExp(`get${entityName}(.*)`);
358
+ const getMatch = methodName.match(getPattern);
359
+
360
+ if (getMatch) {
361
+ const suffix = getMatch[1];
362
+ if (!suffix) return entityName.toLowerCase(); // getMember → member
363
+
364
+ return suffix.charAt(0).toLowerCase() + suffix.slice(1);
365
+ }
366
+
367
+ return 'detail'; // 기본값
368
+ }
369
+
370
+ /**
371
+ * 메서드명에서 단건 함수명 추출
372
+ * postMemberDetail → getDetail
373
+ * postMemberInfo → getInfo
374
+ * postMemberProfile → getProfile
375
+ * getMember → getMember
376
+ */
377
+ function extractDetailFunctionName(methodName: string, entityName: string): string {
378
+ const detailName = extractDetailName(methodName, entityName);
379
+
380
+ // detail → getDetail, info → getInfo, member → getMember
381
+ return 'get' + detailName.charAt(0).toUpperCase() + detailName.slice(1);
382
+ }
383
+ ```
384
+
385
+ ### 2.4 중복 처리 규칙 (옵션 C: 중복 시만 구분)
386
+
387
+ **중복 발생 예시:**
388
+
389
+ ```typescript
390
+ // 중복 가능성
391
+ async postMemberDetail(...) // → detail, getDetail
392
+ async getMemberDetail(...) // → detail, getDetail (중복!)
393
+ ```
394
+
395
+ **중복 처리 로직:**
396
+
397
+ ```typescript
398
+ /**
399
+ * 중복 확인 후 접두사 추가
400
+ * 1. 기본 네이밍으로 모든 메서드의 이름을 생성
401
+ * 2. 중복이 있는 메서드에만 접두사 추가 (post → post, get → get)
402
+ */
403
+ function resolveDuplicates(methods: Method[]): Method[] {
404
+ const nameMap = new Map<string, Method[]>();
405
+
406
+ // 1. 기본 네이밍으로 이름 생성
407
+ methods.forEach(method => {
408
+ const baseName = extractDetailName(method.name, entityName);
409
+ if (!nameMap.has(baseName)) {
410
+ nameMap.set(baseName, []);
411
+ }
412
+ nameMap.get(baseName)!.push(method);
413
+ });
414
+
415
+ // 2. 중복이 있는 경우 접두사 추가
416
+ nameMap.forEach((duplicates, baseName) => {
417
+ if (duplicates.length > 1) {
418
+ // 중복이 있으면 접두사 추가
419
+ duplicates.forEach(method => {
420
+ const prefix = method.name.startsWith('post') ? 'post' : 'get';
421
+ method.variableName = prefix + baseName.charAt(0).toUpperCase() + baseName.slice(1);
422
+ method.functionName = 'get' + method.variableName.charAt(0).toUpperCase() + method.variableName.slice(1);
423
+ });
424
+ }
425
+ });
426
+
427
+ return methods;
428
+ }
429
+ ```
430
+
431
+ **중복 처리 결과:**
432
+
433
+ | 메서드명 | 기본 네이밍 | 중복处理后 |
434
+ |----------|-------------|-----------|
435
+ | `postMemberDetail` | `detail`, `getDetail` | `postDetail`, `getPostDetail` |
436
+ | `getMemberDetail` | `detail`, `getDetail` | `detail`, `getDetail` |
437
+ | `postMemberInfo` | `info`, `getInfo` | `info`, `getInfo` (중복 없음) |
438
+
439
+ ### 2.5 목록과 단건 혼합 예시
440
+
441
+ **입력:**
442
+ ```typescript
443
+ // MemberRepository.ts
444
+
445
+ /* 목록 API */
446
+ async postMemberListMember(...) : Promise<PostMemberListMemberResponse>
447
+ // Response: { ds: MemberRes[]; page: {...} } → 목록, hasPage: true
448
+
449
+ async postMemberListAdmin(...) : Promise<PostMemberListAdminResponse>
450
+ // Response: { ds: AdminRes[]; } → 목록, hasPage: false
451
+
452
+ /* 단건 API */
453
+ async postMemberDetail(...) : Promise<PostMemberDetailResponse>
454
+ // Response: { rs: MemberDetailRes; } → 단건
455
+
456
+ async postMemberInfo(...) : Promise<PostMemberInfoResponse>
457
+ // Response: { rs: MemberInfoRes; } → 단건
458
+
459
+ async postMemberProfile(...) : Promise<PostMemberProfileResponse>
460
+ // Response: { rs: MemberProfileRes; } → 단건
461
+
462
+ async getMember(...) : Promise<GetMemberResponse>
463
+ // Response: { rs: MemberRes; } → 단건
464
+ ```
465
+
466
+ **생성되는 훅:**
467
+
468
+ ```typescript
469
+ export function useMemberService() {
470
+ // ===== 목록 (List) =====
471
+ const [list, setList] = useState<MemberRes[]>([]);
472
+ const [listSpinning, setListSpinning] = useState(false);
473
+ const [page, setPage] = useState<AXDGPage>({...}); // ✅ hasPage: true
474
+
475
+ const [adminList, setAdminList] = useState<AdminRes[]>([]);
476
+ const [adminSpinning, setAdminSpinning] = useState(false);
477
+ // ❌ adminPage 없음 (hasPage: false)
478
+
479
+ // ===== 단건 (Detail) =====
480
+ const [detail, setDetail] = useState<MemberDetailRes>();
481
+ const [detailSpinning, setDetailSpinning] = useState(false);
482
+
483
+ const [info, setInfo] = useState<MemberInfoRes>();
484
+ const [infoSpinning, setInfoSpinning] = useState(false);
485
+
486
+ const [profile, setProfile] = useState<MemberProfileRes>();
487
+ const [profileSpinning, setProfileSpinning] = useState(false);
488
+
489
+ const [member, setMember] = useState<MemberRes>();
490
+ const [memberSpinning, setMemberSpinning] = useState(false);
491
+
492
+ // ===== 목록 함수 =====
493
+ const getList = useCallback(async (params?: ListRequest) => {
494
+ setListSpinning(true);
495
+ try {
496
+ const data = await MemberService.postMemberListMember(deleteEmptyValue(params));
497
+ setList(data.ds);
498
+ setPage({ ... }); // ✅ hasPage: true
499
+ } catch (err) {
500
+ await errorHandling(err);
501
+ } finally {
502
+ setListSpinning(false);
503
+ }
504
+ }, []);
505
+
506
+ const getAdminList = useCallback(async (params?: AdminListRequest) => {
507
+ setAdminSpinning(true);
508
+ try {
509
+ const data = await MemberService.postMemberListAdmin(deleteEmptyValue(params));
510
+ setAdminList(data.ds);
511
+ } catch (err) {
512
+ await errorHandling(err);
513
+ } finally {
514
+ setAdminSpinning(false);
515
+ }
516
+ }, []);
517
+
518
+ // ===== 단건 함수 =====
519
+ const getDetail = useCallback(async (params?: DetailRequest) => {
520
+ setDetailSpinning(true);
521
+ try {
522
+ const data = await MemberService.postMemberDetail(deleteEmptyValue(params));
523
+ setDetail(data.rs);
524
+ } catch (err) {
525
+ await errorHandling(err);
526
+ } finally {
527
+ setDetailSpinning(false);
528
+ }
529
+ }, []);
530
+
531
+ const getInfo = useCallback(async (params?: InfoRequest) => {
532
+ setInfoSpinning(true);
533
+ try {
534
+ const data = await MemberService.postMemberInfo(deleteEmptyValue(params));
535
+ setInfo(data.rs);
536
+ } catch (err) {
537
+ await errorHandling(err);
538
+ } finally {
539
+ setInfoSpinning(false);
540
+ }
541
+ }, []);
542
+
543
+ const getProfile = useCallback(async (params?: ProfileRequest) => {
544
+ setProfileSpinning(true);
545
+ try {
546
+ const data = await MemberService.postMemberProfile(deleteEmptyValue(params));
547
+ setProfile(data.rs);
548
+ } catch (err) {
549
+ await errorHandling(err);
550
+ } finally {
551
+ setProfileSpinning(false);
552
+ }
553
+ }, []);
554
+
555
+ const getMember = useCallback(async (params?: MemberRequest) => {
556
+ setMemberSpinning(true);
557
+ try {
558
+ const data = await MemberService.getMember(deleteEmptyValue(params));
559
+ setMember(data.rs);
560
+ } catch (err) {
561
+ await errorHandling(err);
562
+ } finally {
563
+ setMemberSpinning(false);
564
+ }
565
+ }, []);
566
+
567
+ return {
568
+ // 목록
569
+ list,
570
+ page, // ✅ hasPage: true
571
+ listSpinning,
572
+ getList,
573
+ adminList,
574
+ // adminPage, // ❌ hasPage: false
575
+ adminSpinning,
576
+ getAdminList,
577
+ // 단건
578
+ detail,
579
+ detailSpinning,
580
+ getDetail,
581
+ info,
582
+ infoSpinning,
583
+ getInfo,
584
+ profile,
585
+ profileSpinning,
586
+ getProfile,
587
+ member,
588
+ memberSpinning,
589
+ getMember,
590
+ };
591
+ }
592
+ ```
593
+
594
+ **목록 vs 단건 차이점:**
595
+
596
+ | 구분 | 상태 타입 | 응답 필드 | 페이징 |
597
+ |------|-----------|-----------|--------|
598
+ | 목록 | `Type[]` | `data.ds` | `page` 상태 생성 |
599
+ | 단건 | `Type \| undefined` | `data.rs` | 페이지 없음 |
600
+
601
+ ---
602
+
603
+ ## 3. Spinning 상태 관리 (옵션 B: 개별 spinning)
604
+
605
+ 각 목록마다 개별 spinning 상태를 관리합니다.
606
+
607
+ ```typescript
608
+ const [listSpinning, setListSpinning] = useState(false);
609
+ const [adminSpinning, setAdminSpinning] = useState(false);
610
+ const [activeSpinning, setActiveSpinning] = useState(false);
611
+ ```
612
+
613
+ **장점:**
614
+ - 각 API의 로딩 상태를 독립적으로 관리
615
+ - UI에서 정확한 로딩 표시 가능
616
+
617
+ ---
618
+
619
+ ## 4. DateRange 처리
620
+
621
+ 요청 타입에 `bgnDtm/endDtm` 또는 `startDateTime/endDateTime` 쌍이 있는 경우 자동으로 `dateRange` 필드를 추가합니다.
622
+
623
+ ```typescript
624
+ interface ListRequest extends Partial<PostMemberListMemberRequest> {
625
+ dateRange?: [string, string];
626
+ }
627
+
628
+ const getList = useCallback(async (listParams: ListRequest) => {
629
+ setListSpinning(true);
630
+ try {
631
+ const data = await MemberService.postMemberListMember(
632
+ deleteEmptyValue({
633
+ ...listParams,
634
+ ...dateRangeToDts(listParams.dateRange, {
635
+ bgnDtm: DT_FORMAT.DATETIME_HHMMSS,
636
+ endDtm: DT_FORMAT.DATETIME_HHMMSS,
637
+ }),
638
+ }),
639
+ );
640
+ setList(data.ds);
641
+ // ...
642
+ } catch (err) {
643
+ await errorHandling(err);
644
+ } finally {
645
+ setListSpinning(false);
646
+ }
647
+ }, []);
648
+ ```
649
+
650
+ ---
651
+
652
+ ## 5. 출력 경로 처리
653
+
654
+ ### 5.1 기본 경로
655
+
656
+ 출력 경로를 지정하지 않은 경우 `src/hooks`에 생성합니다.
657
+
658
+ ```
659
+ src/hooks/useMemberService.ts
660
+ src/hooks/useCategoryService.ts
661
+ ```
662
+
663
+ ### 5.2 사용자 지정 경로
664
+
665
+ 출력 경로를 지정한 경우 해당 폴더에 생성합니다.
666
+
667
+ ```
668
+ src/hooks/member/useMemberService.ts
669
+ src/hooks/product/useProductService.ts
670
+ ```
671
+
672
+ ### 5.3 index.ts 자동 생성
673
+
674
+ 대상 폴더에 `index.ts`가 없으면 자동으로 생성하고 export를 추가합니다.
675
+
676
+ ```typescript
677
+ // src/hooks/index.ts
678
+ export * from './useMemberService';
679
+ export * from './useCategoryService';
680
+ export * from './useProductService';
681
+ ```
682
+
683
+ ### 5.4 기존 파일 처리
684
+
685
+ 기존 파일이 있으면 덮어씁니다.
686
+
687
+ ---
688
+
689
+ ## 6. 구현 파일 구조
690
+
691
+ ```
692
+ src/
693
+ ├── handlers/
694
+ │ └── generate-hook.ts # 새로 추가
695
+ ├── templates/
696
+ │ └── hook-template.ts # 새로 추가
697
+ └── index.ts # 도구 등록
698
+ ```
699
+
700
+ ---
701
+
702
+ ## 7. MCP 도구 사양
703
+
704
+ ### 7.1 도구 이름
705
+
706
+ `generate-hook`
707
+
708
+ ### 7.2 파라미터
709
+
710
+ | 파라미터 | 타입 | 필수 | 설명 | 기본값 |
711
+ |----------|------|------|------|--------|
712
+ | `interfacePath` | string | ✅ | 인터페이스 Repository 파일 경로 (절대 경로) | - |
713
+ | `outputPath` | string | ❌ | 생성할 훅 파일 경로 (절대 경로) | `src/hooks/use{Entity}Service.ts` |
714
+
715
+ ### 7.3 사용 예시
716
+
717
+ ```typescript
718
+ // 기본 경로 (src/hooks)
719
+ {
720
+ "interfacePath": "/path/to/repository/MemberRepository.ts"
721
+ }
722
+ // → src/hooks/useMemberService.ts 생성
723
+
724
+ // 사용자 지정 경로
725
+ {
726
+ "interfacePath": "/path/to/repository/MemberRepository.ts",
727
+ "outputPath": "/path/to/src/hooks/member/useMemberService.ts"
728
+ }
729
+ // → 지정한 경로에 생성
730
+ ```
731
+
732
+ ---
733
+
734
+ ## 8. 네이밍 추출 로직
735
+
736
+ ### 8.1 리스트 변수명 추출
737
+
738
+ ```typescript
739
+ /**
740
+ * 메서드명에서 리스트 변수명 추출
741
+ * postMemberListMember → memberList (접미사가 Entity면 기본값 list)
742
+ * postMemberListAdmin → adminList
743
+ * postMemberList → list (접미사 없으면 기본값)
744
+ */
745
+ function extractListName(methodName: string, entityName: string): string {
746
+ const pattern = new RegExp(`post${entityName}List(.*)`);
747
+ const match = methodName.match(pattern);
748
+
749
+ if (match) {
750
+ const suffix = match[1];
751
+ if (!suffix) return 'list'; // 접미사 없으면 기본값
752
+
753
+ // 접미사가 Entity명과 같으면 기본값 (postMemberListMember → list)
754
+ if (suffix.toLowerCase() === entityName.toLowerCase()) {
755
+ return 'list';
756
+ }
757
+
758
+ // 첫 글자 소문자로 변환 (camelCase)
759
+ return suffix.charAt(0).toLowerCase() + suffix.slice(1) + 'List';
760
+ }
761
+
762
+ return 'list';
763
+ }
764
+ ```
765
+
766
+ ### 8.2 함수명 추출
767
+
768
+ ```typescript
769
+ /**
770
+ * 메서드명에서 함수명 추출
771
+ * postMemberListMember → getList (접미사가 Entity면 getList)
772
+ * postMemberListAdmin → getAdminList
773
+ * postMemberList → getList
774
+ */
775
+ function extractFunctionName(methodName: string, entityName: string): string {
776
+ const pattern = new RegExp(`post${entityName}List(.*)`);
777
+ const match = methodName.match(pattern);
778
+
779
+ if (match) {
780
+ const suffix = match[1];
781
+ if (!suffix) return 'getList';
782
+
783
+ // 접미사가 Entity명과 같으면 getList
784
+ if (suffix.toLowerCase() === entityName.toLowerCase()) {
785
+ return 'getList';
786
+ }
787
+
788
+ // 첫 글자 대문자로 변환 (PascalCase)
789
+ return 'get' + suffix.charAt(0).toUpperCase() + suffix.slice(1);
790
+ }
791
+
792
+ return 'getList';
793
+ }
794
+ ```
795
+
796
+ ### 8.2 단건(Detail) 변수명 추출
797
+
798
+ ```typescript
799
+ /**
800
+ * 메서드명에서 단건 변수명 추출
801
+ * postMemberDetail → detail
802
+ * postMemberInfo → info
803
+ * postMemberProfile → profile
804
+ * postMember → member
805
+ * getMemberDetail → detail
806
+ * getMember → member
807
+ */
808
+ function extractDetailName(methodName: string, entityName: string): string {
809
+ // post{Entity}{Suffix} 패턴
810
+ const postPattern = new RegExp(`post${entityName}(.*)`);
811
+ const postMatch = methodName.match(postPattern);
812
+
813
+ if (postMatch) {
814
+ const suffix = postMatch[1];
815
+ if (!suffix) return entityName.toLowerCase(); // postMember → member
816
+
817
+ // 첫 글자 소문자로 변환 (camelCase)
818
+ return suffix.charAt(0).toLowerCase() + suffix.slice(1);
819
+ }
820
+
821
+ // get{Entity}{Suffix} 패턴
822
+ const getPattern = new RegExp(`get${entityName}(.*)`);
823
+ const getMatch = methodName.match(getPattern);
824
+
825
+ if (getMatch) {
826
+ const suffix = getMatch[1];
827
+ if (!suffix) return entityName.toLowerCase(); // getMember → member
828
+
829
+ return suffix.charAt(0).toLowerCase() + suffix.slice(1);
830
+ }
831
+
832
+ return 'detail'; // 기본값
833
+ }
834
+ ```
835
+
836
+ ### 8.3 단건(Detail) 함수명 추출
837
+
838
+ ```typescript
839
+ /**
840
+ * 메서드명에서 단건 함수명 추출
841
+ * postMemberDetail → getDetail
842
+ * postMemberInfo → getInfo
843
+ * postMember → getMember
844
+ * getMemberDetail → getDetail
845
+ */
846
+ function extractDetailFunctionName(methodName: string, entityName: string): string {
847
+ const detailName = extractDetailName(methodName, entityName);
848
+
849
+ // detail → getDetail, info → getInfo, member → getMember
850
+ return 'get' + detailName.charAt(0).toUpperCase() + detailName.slice(1);
851
+ }
852
+ ```
853
+
854
+ ### 8.4 중복 처리 로직
855
+
856
+ ```typescript
857
+ /**
858
+ * 중복 확인 후 접두사 추가
859
+ * 1. 기본 네이밍으로 모든 메서드의 이름을 생성
860
+ * 2. 중복이 있는 메서드에만 접두사 추가
861
+ */
862
+ function resolveDuplicateNames(methods: MethodInfo[]): MethodInfo[] {
863
+ const nameMap = new Map<string, MethodInfo[]>();
864
+
865
+ // 1. 기본 네이밍으로 이름 생성 후 그룹화
866
+ methods.forEach(method => {
867
+ const baseName = method.isListMethod
868
+ ? extractListName(method.name, entityName)
869
+ : extractDetailName(method.name, entityName);
870
+
871
+ if (!nameMap.has(baseName)) {
872
+ nameMap.set(baseName, []);
873
+ }
874
+ nameMap.get(baseName)!.push(method);
875
+ });
876
+
877
+ // 2. 중복이 있는 경우 접두사 추가
878
+ nameMap.forEach((duplicates, baseName) => {
879
+ if (duplicates.length > 1) {
880
+ // 중복이 있으면 접두사 추가 (post/get 또는 List/Detail 구분)
881
+ duplicates.forEach((method, index) => {
882
+ if (method.isListMethod) {
883
+ // 목록은 접미사로 구분
884
+ method.listName = baseName + (index + 1);
885
+ method.functionName = 'get' + baseName.charAt(0).toUpperCase() + baseName.slice(1) + (index + 1);
886
+ } else {
887
+ // 단건은 post/get 접두사로 구분
888
+ const prefix = method.name.startsWith('post') ? 'post' : 'get';
889
+ method.detailName = prefix + baseName.charAt(0).toUpperCase() + baseName.slice(1);
890
+ method.functionName = 'get' + method.detailName.charAt(0).toUpperCase() + method.detailName.slice(1);
891
+ }
892
+ });
893
+ }
894
+ });
895
+
896
+ return methods;
897
+ }
898
+ ```
899
+
900
+ ---
901
+
902
+ ## 9. 템플릿 변수
903
+
904
+ ```typescript
905
+ interface HookTemplateVariables {
906
+ // 서비스 정보
907
+ serviceName: string; // MemberService
908
+ entityName: string; // Member
909
+ hookName: string; // useMemberService
910
+
911
+ // 메서드 정보 (목록 + 단건)
912
+ methods: Array<{
913
+ // 공통
914
+ name: string; // postMemberListMember, postMemberDetail
915
+ functionName: string; // getList, getDetail
916
+ requestType: string; // PostMemberListMemberRequest, PostMemberDetailRequest
917
+ responseType: string; // PostMemberListMemberResponse, PostMemberDetailResponse
918
+ dtoType: string; // MemberRes, MemberDetailRes
919
+
920
+ // 목록(List) 전용
921
+ isListMethod: boolean; // true: 목록, false: 단건
922
+ listName?: string; // list, adminList (목록인 경우)
923
+ hasPage: boolean; // 페이징 여부 (가변 처리 결정)
924
+
925
+ // 단건(Detail) 전용
926
+ detailName?: string; // detail, info, member (단건인 경우)
927
+
928
+ // dateRange 정보
929
+ hasDateRange: boolean;
930
+ dateRangeStartField?: string; // bgnDtm
931
+ dateRangeEndField?: string; // endDtm
932
+ }>;
933
+
934
+ // 분류된 메서드
935
+ listMethods: typeof methods; // isListMethod: true인 메서드들
936
+ detailMethods: typeof methods; // isListMethod: false인 메서드들
937
+ }
938
+ ```
939
+
940
+ ### 9.1 목록/단건에 따른 가변 처리 로직
941
+
942
+ ```typescript
943
+ // 템플릿 내부 처리 로직 (pseudo code)
944
+
945
+ // ===== 목록(List) 메서드 처리 =====
946
+ listMethods.forEach(method => {
947
+ // 1. 상태 선언
948
+ const [listName, setListName] = useState<DtoType[]>([]);
949
+ const [listNameSpinning, setListNameSpinning] = useState(false);
950
+
951
+ // 2. 페이징 상태 (hasPage: true인 경우만)
952
+ if (method.hasPage) {
953
+ const [page, setPage] = useState<AXDGPage>({...});
954
+ }
955
+
956
+ // 3. useCallback
957
+ const callback = useCallback(async (params) => {
958
+ setListNameSpinning(true);
959
+ try {
960
+ const data = await Service.method(deleteEmptyValue(params));
961
+ setListName(data.ds); // 목록은 data.ds 사용
962
+
963
+ if (method.hasPage) {
964
+ setPage({ ... }); // 가변 처리
965
+ }
966
+ } catch (err) {
967
+ await errorHandling(err);
968
+ } finally {
969
+ setListNameSpinning(false);
970
+ }
971
+ }, []);
972
+
973
+ // 4. return 값
974
+ if (method.hasPage) {
975
+ return { listName, page, listNameSpinning, functionName };
976
+ } else {
977
+ return { listName, listNameSpinning, functionName };
978
+ }
979
+ });
980
+
981
+ // ===== 단건(Detail) 메서드 처리 =====
982
+ detailMethods.forEach(method => {
983
+ // 1. 상태 선언
984
+ const [detailName, setDetailName] = useState<DtoType>();
985
+ const [detailNameSpinning, setDetailNameSpinning] = useState(false);
986
+
987
+ // 2. useCallback
988
+ const callback = useCallback(async (params) => {
989
+ setDetailNameSpinning(true);
990
+ try {
991
+ const data = await Service.method(deleteEmptyValue(params));
992
+ setDetailName(data.rs); // 단건은 data.rs 사용
993
+ } catch (err) {
994
+ await errorHandling(err);
995
+ } finally {
996
+ setDetailNameSpinning(false);
997
+ }
998
+ }, []);
999
+
1000
+ // 3. return 값 (단건은 페이징 없음)
1001
+ return { detailName, detailNameSpinning, functionName };
1002
+ });
1003
+ ```
1004
+
1005
+ ### 9.2 응답 필드에 따른 분류
1006
+
1007
+ ```typescript
1008
+ /**
1009
+ * 응답 인터페이스에서 필드 구조 분석
1010
+ * data.ds[] → 목록 (isListMethod: true)
1011
+ * data.rs → 단건 (isListMethod: false)
1012
+ */
1013
+ function classifyMethodByResponse(responseType: string): 'list' | 'detail' {
1014
+ const responseInterface = findInterface(content, responseType);
1015
+
1016
+ if (responseInterface) {
1017
+ // ds: Type[] 형태 → 목록
1018
+ if (/ds:\s*\w+\[\]/.test(responseInterface)) {
1019
+ return 'list';
1020
+ }
1021
+ // rs: Type 형태 → 단건
1022
+ if (/rs:\s*\w+/.test(responseInterface)) {
1023
+ return 'detail';
1024
+ }
1025
+ }
1026
+
1027
+ return 'list'; // 기본값
1028
+ }
1029
+ ```
1030
+
1031
+ ---
1032
+
1033
+ ## 10. 구현 우선순위
1034
+
1035
+ ### 최우선: 인터페이스 타입 분석
1036
+
1037
+ **모든 작업의 선행 조건:**
1038
+ 1. ✅ 인터페이스 Repository 파일 파싱
1039
+ 2. ✅ 메서드 추출 (주석, 시그니처)
1040
+ 3. ✅ 요청 타입(`RequestType`) 추출
1041
+ 4. ✅ 응답 타입(`ResponseType`) 추출
1042
+ 5. ✅ 응답 타입에서 DTO 타입 추출 (`ds: Type[]` → `Type`)
1043
+ 6. ✅ 페이징 여부 확인 (`page:` 필드)
1044
+ 7. ✅ dateRange 패턴 확인 (`bgnDtm/endDtm` 쌍)
1045
+
1046
+ ### 이후 작업 순서
1047
+
1048
+ 1. ✅ 기본 훅 템플릿 생성 (단일 API)
1049
+ 2. ✅ 여러 API 네이밍 로직 구현
1050
+ 3. ✅ 개별 spinning 상태 추가
1051
+ 4. ✅ **hasPage에 따른 가변 처리** (page, setPage)
1052
+ 5. ✅ dateRange 처리
1053
+ 6. ✅ 출력 경로 처리 (기본/사용자 지정)
1054
+ 7. ✅ index.ts 자동 생성 및 export
1055
+ 8. ✅ 제외 키워드 필터링
1056
+ 9. ✅ ESLint, Prettier 자동 적용
1057
+
1058
+ ---
1059
+
1060
+ ## 11. 테스트 시나리오
1061
+
1062
+ ### 11.1 페이징 유무에 따른 가변 처리 확인
1063
+
1064
+ **입력 (페이징 있음):**
1065
+ ```typescript
1066
+ // MemberRepository.ts
1067
+ /* 회원 목록 */
1068
+ async postMemberListMember(params: PostMemberListMemberRequest): Promise<PostMemberListMemberResponse>
1069
+
1070
+ // Response 인터페이스
1071
+ export interface PostMemberListMemberResponse extends DataGridPageResponse {
1072
+ ds: MemberRes[];
1073
+ page: { pageCount: number; totalCount: number; pageNumber: number; pageSize: number; };
1074
+ }
1075
+ ```
1076
+
1077
+ **분석 결과:**
1078
+ ```typescript
1079
+ {
1080
+ methodName: "postMemberListMember",
1081
+ hasPage: true, // ✅ page: 필드 있음
1082
+ }
1083
+ ```
1084
+
1085
+ **출력 (페이징 있음):**
1086
+ ```typescript
1087
+ // useMemberService.ts
1088
+ export function useMemberService(params?: ListRequest) {
1089
+ const [list, setList] = useState<MemberRes[]>([]);
1090
+ const [listSpinning, setListSpinning] = useState(false);
1091
+ const [page, setPage] = useState<AXDGPage>({...}); // ✅ 있음
1092
+
1093
+ const getList = useCallback(async (listParams: ListRequest) => {
1094
+ setListSpinning(true);
1095
+ try {
1096
+ const data = await MemberService.postMemberListMember(deleteEmptyValue(listParams));
1097
+ setList(data.ds);
1098
+ setPage({ ... }); // ✅ 호출
1099
+ } catch (err) {
1100
+ await errorHandling(err);
1101
+ } finally {
1102
+ setListSpinning(false);
1103
+ }
1104
+ }, []);
1105
+
1106
+ return { list, page, listSpinning, getList }; // ✅ page 포함
1107
+ }
1108
+ ```
1109
+
1110
+ ---
1111
+
1112
+ **입력 (페이징 없음):**
1113
+ ```typescript
1114
+ // CodeRepository.ts
1115
+ /* 코드 목록 */
1116
+ async getCodeList(params: GetCodeListRequest): Promise<GetCodeListResponse>
1117
+
1118
+ // Response 인터페이스
1119
+ export interface GetCodeListResponse {
1120
+ ds: Code[];
1121
+ // page: 필드 없음
1122
+ }
1123
+ ```
1124
+
1125
+ **분석 결과:**
1126
+ ```typescript
1127
+ {
1128
+ methodName: "getCodeList",
1129
+ hasPage: false, // ❌ page: 필드 없음
1130
+ }
1131
+ ```
1132
+
1133
+ **출력 (페이징 없음):**
1134
+ ```typescript
1135
+ // useCodeService.ts
1136
+ export function useCodeService() {
1137
+ const [list, setList] = useState<Code[]>([]);
1138
+ const [listSpinning, setListSpinning] = useState(false);
1139
+ // ❌ page, setPage 없음
1140
+
1141
+ const getList = useCallback(async (params?: GetCodeListRequest) => {
1142
+ setListSpinning(true);
1143
+ try {
1144
+ const data = await CodeService.getCodeList(deleteEmptyValue(params));
1145
+ setList(data.ds);
1146
+ // ❌ setPage 없음
1147
+ } catch (err) {
1148
+ await errorHandling(err);
1149
+ } finally {
1150
+ setListSpinning(false);
1151
+ }
1152
+ }, []);
1153
+
1154
+ return { list, listSpinning, getList }; // ❌ page 미포함
1155
+ }
1156
+ ```
1157
+
1158
+ ### 11.2 여러 목록 API (페이징 섞인 경우)
1159
+
1160
+ **입력:**
1161
+ ```typescript
1162
+ // MemberRepository.ts
1163
+ /* 회원 목록 */
1164
+ async postMemberListMember(params: PostMemberListMemberRequest): Promise<PostMemberListMemberResponse> { ... }
1165
+ // Response: { ds: MemberRes[]; page: {...} } → hasPage: true
1166
+
1167
+ /* 관리자 목록 */
1168
+ async postMemberListAdmin(params: PostMemberListAdminRequest): Promise<PostMemberListAdminResponse> { ... }
1169
+ // Response: { ds: AdminRes[]; } → hasPage: false
1170
+
1171
+ /* 활성 회원 목록 */
1172
+ async postMemberListActive(params: PostMemberListActiveRequest): Promise<PostMemberListActiveResponse> { ... }
1173
+ // Response: { ds: ActiveRes[]; page: {...} } → hasPage: true
1174
+ ```
1175
+
1176
+ **출력 (가변 처리 적용):**
1177
+ ```typescript
1178
+ // useMemberService.ts
1179
+ export function useMemberService() {
1180
+ const [list, setList] = useState<MemberRes[]>([]);
1181
+ const [listSpinning, setListSpinning] = useState(false);
1182
+ const [page, setPage] = useState<AXDGPage>({...}); // ✅ 있음 (hasPage: true)
1183
+
1184
+ const [adminList, setAdminList] = useState<AdminRes[]>([]);
1185
+ const [adminSpinning, setAdminSpinning] = useState(false);
1186
+ // ❌ adminPage, setAdminPage 없음 (hasPage: false)
1187
+
1188
+ const [activeList, setActiveList] = useState<ActiveRes[]>([]);
1189
+ const [activeSpinning, setActiveSpinning] = useState(false);
1190
+ const [activePage, setActivePage] = useState<AXDGPage>({...}); // ✅ 있음 (hasPage: true)
1191
+
1192
+ const getList = useCallback(async (params?: ListRequest) => {
1193
+ setListSpinning(true);
1194
+ try {
1195
+ const data = await MemberService.postMemberListMember(deleteEmptyValue(params));
1196
+ setList(data.ds);
1197
+ setPage({ ... }); // ✅ 호출 (hasPage: true)
1198
+ } catch (err) {
1199
+ await errorHandling(err);
1200
+ } finally {
1201
+ setListSpinning(false);
1202
+ }
1203
+ }, []);
1204
+
1205
+ const getAdminList = useCallback(async (params?: AdminListRequest) => {
1206
+ setAdminSpinning(true);
1207
+ try {
1208
+ const data = await MemberService.postMemberListAdmin(deleteEmptyValue(params));
1209
+ setAdminList(data.ds);
1210
+ // ❌ setPage 없음 (hasPage: false)
1211
+ } catch (err) {
1212
+ await errorHandling(err);
1213
+ } finally {
1214
+ setAdminSpinning(false);
1215
+ }
1216
+ }, []);
1217
+
1218
+ const getActiveList = useCallback(async (params?: ActiveListRequest) => {
1219
+ setActiveSpinning(true);
1220
+ try {
1221
+ const data = await MemberService.postMemberListActive(deleteEmptyValue(params));
1222
+ setActiveList(data.ds);
1223
+ setActivePage({ ... }); // ✅ 호출 (hasPage: true)
1224
+ } catch (err) {
1225
+ await errorHandling(err);
1226
+ } finally {
1227
+ setActiveSpinning(false);
1228
+ }
1229
+ }, []);
1230
+
1231
+ return {
1232
+ list,
1233
+ page, // ✅ 포함 (hasPage: true)
1234
+ listSpinning,
1235
+ getList,
1236
+ adminList,
1237
+ // adminPage, // ❌ 미포함 (hasPage: false)
1238
+ adminSpinning,
1239
+ getAdminList,
1240
+ activeList,
1241
+ activePage, // ✅ 포함 (hasPage: true)
1242
+ activeSpinning,
1243
+ getActiveList,
1244
+ };
1245
+ }
1246
+ ```
1247
+
1248
+ **페이징 처리 규칙:**
1249
+ | 메서드 | hasPage | 상태 생성 | setPage 호출 | return 포함 |
1250
+ |--------|---------|-----------|--------------|-------------|
1251
+ | getList | true | ✅ | ✅ | ✅ |
1252
+ | getAdminList | false | ❌ | ❌ | ❌ |
1253
+ | getActiveList | true | ✅ | ✅ | ✅ |
1254
+
1255
+ ### 11.3 제외 키워드가 포함된 API
1256
+
1257
+ **입력:**
1258
+ ```typescript
1259
+ // MemberRepository.ts
1260
+ /* 회원 목록 */
1261
+ async postMemberListMember(params: PostMemberListMemberRequest): Promise<PostMemberListMemberResponse> { ... }
1262
+ // Response: { ds: MemberRes[]; page: {...} } → hasPage: true
1263
+
1264
+ /* 회원 저장 */ // ❌ "저장" 키워드로 제외
1265
+ async postMemberSave(params: PostMemberSaveRequest) { ... }
1266
+
1267
+ /* 회원 삭제 */ // ❌ "삭제" 키워드로 제외
1268
+ async postMemberDelete(params: PostMemberDeleteRequest) { ... }
1269
+
1270
+ /* 엑셀 다운로드 */ // ❌ "엑셀", "다운로드" 키워드로 제외
1271
+ async postMemberExcel(params: PostMemberExcelRequest) { ... }
1272
+ ```
1273
+
1274
+ **분석 결과:**
1275
+ ```typescript
1276
+ // 포함 API만 필터링
1277
+ {
1278
+ includedMethods: [
1279
+ { name: "postMemberListMember", hasPage: true }
1280
+ ],
1281
+ excludedMethods: [
1282
+ { name: "postMemberSave", reason: "저장" },
1283
+ { name: "postMemberDelete", reason: "삭제" },
1284
+ { name: "postMemberExcel", reason: "엑셀, 다운로드" }
1285
+ ]
1286
+ }
1287
+ ```
1288
+
1289
+ **출력 (목록/조회 API만 포함):**
1290
+ ```typescript
1291
+ // useMemberService.ts
1292
+ export function useMemberService(params?: ListRequest) {
1293
+ const [list, setList] = useState<MemberRes[]>([]);
1294
+ const [listSpinning, setListSpinning] = useState(false);
1295
+ const [page, setPage] = useState<AXDGPage>({...}); // ✅ hasPage: true
1296
+
1297
+ const getList = useCallback(async (listParams: ListRequest) => {
1298
+ setListSpinning(true);
1299
+ try {
1300
+ const data = await MemberService.postMemberListMember(deleteEmptyValue(listParams));
1301
+ setList(data.ds);
1302
+ setPage({ ... }); // ✅ hasPage: true
1303
+ } catch (err) {
1304
+ await errorHandling(err);
1305
+ } finally {
1306
+ setListSpinning(false);
1307
+ }
1308
+ }, []);
1309
+
1310
+ return { list, page, listSpinning, getList };
1311
+ }
1312
+
1313
+ // ❌ 저장, 삭제, 엑셀 API는 훅에 포함되지 않음
1314
+ ```
1315
+
1316
+ ### 11.4 목록과 단건 혼합
1317
+
1318
+ **입력:**
1319
+ ```typescript
1320
+ // MemberRepository.ts
1321
+
1322
+ /* 목록 API */
1323
+ /* 회원 목록 */
1324
+ async postMemberListMember(params: PostMemberListMemberRequest): Promise<PostMemberListMemberResponse>
1325
+ // Response: { ds: MemberRes[]; page: {...} } → 목록, hasPage: true
1326
+
1327
+ /* 관리자 목록 */
1328
+ async postMemberListAdmin(params: PostMemberListAdminRequest): Promise<PostMemberListAdminResponse>
1329
+ // Response: { ds: AdminRes[]; } → 목록, hasPage: false
1330
+
1331
+ /* 단건 API */
1332
+ /* 회원 상세 */
1333
+ async postMemberDetail(params: PostMemberDetailRequest): Promise<PostMemberDetailResponse>
1334
+ // Response: { rs: MemberDetailRes; } → 단건
1335
+
1336
+ /* 회원 정보 */
1337
+ async postMemberInfo(params: PostMemberInfoRequest): Promise<PostMemberInfoResponse>
1338
+ // Response: { rs: MemberInfoRes; } → 단건
1339
+
1340
+ /* 회원 프로필 */
1341
+ async postMemberProfile(params: PostMemberProfileRequest): Promise<PostMemberProfileResponse>
1342
+ // Response: { rs: MemberProfileRes; } → 단건
1343
+
1344
+ /* 회원 조회 (단건) */
1345
+ async getMember(params: GetMemberRequest): Promise<GetMemberResponse>
1346
+ // Response: { rs: MemberRes; } → 단건
1347
+ ```
1348
+
1349
+ **분석 결과:**
1350
+ ```typescript
1351
+ {
1352
+ listMethods: [
1353
+ { name: "postMemberListMember", hasPage: true, listName: "list", functionName: "getList" },
1354
+ { name: "postMemberListAdmin", hasPage: false, listName: "adminList", functionName: "getAdminList" }
1355
+ ],
1356
+ detailMethods: [
1357
+ { name: "postMemberDetail", detailName: "detail", functionName: "getDetail" },
1358
+ { name: "postMemberInfo", detailName: "info", functionName: "getInfo" },
1359
+ { name: "postMemberProfile", detailName: "profile", functionName: "getProfile" },
1360
+ { name: "getMember", detailName: "member", functionName: "getMember" }
1361
+ ]
1362
+ }
1363
+ ```
1364
+
1365
+ **출력 (목록 + 단건 혼합):**
1366
+ ```typescript
1367
+ // useMemberService.ts
1368
+ export function useMemberService() {
1369
+ // ===== 목록 (List) =====
1370
+ const [list, setList] = useState<MemberRes[]>([]);
1371
+ const [listSpinning, setListSpinning] = useState(false);
1372
+ const [page, setPage] = useState<AXDGPage>({...}); // ✅ hasPage: true
1373
+
1374
+ const [adminList, setAdminList] = useState<AdminRes[]>([]);
1375
+ const [adminSpinning, setAdminSpinning] = useState(false);
1376
+ // ❌ adminPage 없음 (hasPage: false)
1377
+
1378
+ // ===== 단건 (Detail) =====
1379
+ const [detail, setDetail] = useState<MemberDetailRes>();
1380
+ const [detailSpinning, setDetailSpinning] = useState(false);
1381
+
1382
+ const [info, setInfo] = useState<MemberInfoRes>();
1383
+ const [infoSpinning, setInfoSpinning] = useState(false);
1384
+
1385
+ const [profile, setProfile] = useState<MemberProfileRes>();
1386
+ const [profileSpinning, setProfileSpinning] = useState(false);
1387
+
1388
+ const [member, setMember] = useState<MemberRes>();
1389
+ const [memberSpinning, setMemberSpinning] = useState(false);
1390
+
1391
+ // ===== 목록 함수 =====
1392
+ const getList = useCallback(async (params?: ListRequest) => {
1393
+ setListSpinning(true);
1394
+ try {
1395
+ const data = await MemberService.postMemberListMember(deleteEmptyValue(params));
1396
+ setList(data.ds); // 목록은 data.ds
1397
+ setPage({ ... }); // ✅ hasPage: true
1398
+ } catch (err) {
1399
+ await errorHandling(err);
1400
+ } finally {
1401
+ setListSpinning(false);
1402
+ }
1403
+ }, []);
1404
+
1405
+ const getAdminList = useCallback(async (params?: AdminListRequest) => {
1406
+ setAdminSpinning(true);
1407
+ try {
1408
+ const data = await MemberService.postMemberListAdmin(deleteEmptyValue(params));
1409
+ setAdminList(data.ds); // 목록은 data.ds
1410
+ } catch (err) {
1411
+ await errorHandling(err);
1412
+ } finally {
1413
+ setAdminSpinning(false);
1414
+ }
1415
+ }, []);
1416
+
1417
+ // ===== 단건 함수 =====
1418
+ const getDetail = useCallback(async (params?: DetailRequest) => {
1419
+ setDetailSpinning(true);
1420
+ try {
1421
+ const data = await MemberService.postMemberDetail(deleteEmptyValue(params));
1422
+ setDetail(data.rs); // 단건은 data.rs
1423
+ } catch (err) {
1424
+ await errorHandling(err);
1425
+ } finally {
1426
+ setDetailSpinning(false);
1427
+ }
1428
+ }, []);
1429
+
1430
+ const getInfo = useCallback(async (params?: InfoRequest) => {
1431
+ setInfoSpinning(true);
1432
+ try {
1433
+ const data = await MemberService.postMemberInfo(deleteEmptyValue(params));
1434
+ setInfo(data.rs);
1435
+ } catch (err) {
1436
+ await errorHandling(err);
1437
+ } finally {
1438
+ setInfoSpinning(false);
1439
+ }
1440
+ }, []);
1441
+
1442
+ const getProfile = useCallback(async (params?: ProfileRequest) => {
1443
+ setProfileSpinning(true);
1444
+ try {
1445
+ const data = await MemberService.postMemberProfile(deleteEmptyValue(params));
1446
+ setProfile(data.rs);
1447
+ } catch (err) {
1448
+ await errorHandling(err);
1449
+ } finally {
1450
+ setProfileSpinning(false);
1451
+ }
1452
+ }, []);
1453
+
1454
+ const getMember = useCallback(async (params?: MemberRequest) => {
1455
+ setMemberSpinning(true);
1456
+ try {
1457
+ const data = await MemberService.getMember(deleteEmptyValue(params));
1458
+ setMember(data.rs);
1459
+ } catch (err) {
1460
+ await errorHandling(err);
1461
+ } finally {
1462
+ setMemberSpinning(false);
1463
+ }
1464
+ }, []);
1465
+
1466
+ return {
1467
+ // 목록
1468
+ list,
1469
+ page, // ✅ hasPage: true
1470
+ listSpinning,
1471
+ getList,
1472
+ adminList,
1473
+ // adminPage, // ❌ hasPage: false
1474
+ adminSpinning,
1475
+ getAdminList,
1476
+ // 단건
1477
+ detail,
1478
+ detailSpinning,
1479
+ getDetail,
1480
+ info,
1481
+ infoSpinning,
1482
+ getInfo,
1483
+ profile,
1484
+ profileSpinning,
1485
+ getProfile,
1486
+ member,
1487
+ memberSpinning,
1488
+ getMember,
1489
+ };
1490
+ }
1491
+ ```
1492
+
1493
+ ### 11.5 중복 발생 시 처리 예시
1494
+
1495
+ **입력 (중복 발생):**
1496
+ ```typescript
1497
+ // MemberRepository.ts
1498
+ /* 회원 상세 (post) */
1499
+ async postMemberDetail(...) : Promise<PostMemberDetailResponse>
1500
+
1501
+ /* 회원 상세 (get) */
1502
+ async getMemberDetail(...) : Promise<GetMemberDetailResponse>
1503
+ ```
1504
+
1505
+ **분석 결과 (중복 감지):**
1506
+ ```typescript
1507
+ {
1508
+ duplicates: [
1509
+ { name: "postMemberDetail", baseName: "detail", functionName: "getDetail" },
1510
+ { name: "getMemberDetail", baseName: "detail", functionName: "getDetail" }
1511
+ ]
1512
+ }
1513
+ ```
1514
+
1515
+ **출력 (중복 처리 - 옵션 C: 중복 시만 구분):**
1516
+ ```typescript
1517
+ // useMemberService.ts
1518
+ export function useMemberService() {
1519
+ // 중복 처리: post 접두사 추가
1520
+ const [postDetail, setPostDetail] = useState<PostMemberDetailRes>();
1521
+ const [postDetailSpinning, setPostDetailSpinning] = useState(false);
1522
+
1523
+ // 중복 없음: 기본 네이밍
1524
+ const [detail, setDetail] = useState<GetMemberDetailRes>();
1525
+ const [detailSpinning, setDetailSpinning] = useState(false);
1526
+
1527
+ const getPostDetail = useCallback(async (params?: PostDetailRequest) => {
1528
+ setPostDetailSpinning(true);
1529
+ try {
1530
+ const data = await MemberService.postMemberDetail(deleteEmptyValue(params));
1531
+ setPostDetail(data.rs);
1532
+ } catch (err) {
1533
+ await errorHandling(err);
1534
+ } finally {
1535
+ setPostDetailSpinning(false);
1536
+ }
1537
+ }, []);
1538
+
1539
+ const getDetail = useCallback(async (params?: DetailRequest) => {
1540
+ setDetailSpinning(true);
1541
+ try {
1542
+ const data = await MemberService.getMemberDetail(deleteEmptyValue(params));
1543
+ setDetail(data.rs);
1544
+ } catch (err) {
1545
+ await errorHandling(err);
1546
+ } finally {
1547
+ setDetailSpinning(false);
1548
+ }
1549
+ }, []);
1550
+
1551
+ return {
1552
+ postDetail,
1553
+ postDetailSpinning,
1554
+ getPostDetail, // 중복 처리: getPostDetail
1555
+ detail,
1556
+ detailSpinning,
1557
+ getDetail, // 기본 유지
1558
+ };
1559
+ }
1560
+ ```
1561
+
1562
+ ---
1563
+
1564
+ ## 12. 공통 Import
1565
+
1566
+ ```typescript
1567
+ import { AXDGPage } from "@axboot/datagrid";
1568
+ import { useCallback, useState } from "react";
1569
+ import { {Service}, {ResponseType}, {RequestType} } from "../services";
1570
+ import { dateRangeToDts, errorHandling } from "../utils";
1571
+ import { deleteEmptyValue } from "../@core/utils/object";
1572
+ import { DT_FORMAT } from "../@types";
1573
+ ```
1574
+
1575
+ ---
1576
+
1577
+ ## 13. 추가 고려사항
1578
+
1579
+ ### 13.1 주석 처리된 옵션 기능 (작업자 판단 후 사용)
1580
+
1581
+ **원칙:** 생성 시 아래 내용을 **무조건 주석 처리**하여 포함하고, 작업자가 필요시 주석을 해제하여 사용합니다.
1582
+
1583
+ #### 13.1.1 자동 초기화 (주석 처리)
1584
+
1585
+ 훅 생성 시 마운트 시 자동으로 데이터를 조회하는 코드를 주석 처리하여 포함합니다.
1586
+
1587
+ ```typescript
1588
+ export function useMemberService(params?: ListRequest) {
1589
+ // ... 상태 선언, useCallback 등
1590
+
1591
+ // ===== 옵션: 자동 초기화 (필요시 주석 해제) =====
1592
+ // React.useEffect(() => {
1593
+ // (async () => {
1594
+ // await getList(params);
1595
+ // })();
1596
+ // }, [getList, params]);
1597
+ // ===============================================
1598
+
1599
+ return {
1600
+ list,
1601
+ page,
1602
+ listSpinning,
1603
+ getList,
1604
+ };
1605
+ }
1606
+ ```
1607
+
1608
+ **작업자 가이드:**
1609
+ - 컴포넌트 마운트 시 자동으로 데이터를 조회하려면 주석 해제
1610
+ - `params`가 변경될 때마다 재조회됨
1611
+
1612
+ #### 13.1.2 i18n 사용 (주석 처리)
1613
+
1614
+ 훅 생성 시 i18n 사용 코드를 주석 처리하여 포함합니다.
1615
+
1616
+ ```typescript
1617
+ export function useMemberService(params?: ListRequest) {
1618
+ // ===== 옵션: i18n 사용 (필요시 주석 해제) =====
1619
+ // const { t } = useI18n();
1620
+ // ==============================================
1621
+
1622
+ // ... 상태 선언, useCallback 등
1623
+
1624
+ return {
1625
+ list,
1626
+ page,
1627
+ listSpinning,
1628
+ getList,
1629
+ };
1630
+ }
1631
+ ```
1632
+
1633
+ **작업자 가이드:**
1634
+ - 다국어 처리가 필요한 경우 주석 해제 후 `t()` 함수 사용
1635
+
1636
+ #### 13.1.3 전체 옵션 포함 예시
1637
+
1638
+ ```typescript
1639
+ export function useMemberService(params?: ListRequest) {
1640
+ // ===== 옵션: i18n 사용 (필요시 주석 해제) =====
1641
+ // const { t } = useI18n();
1642
+ // ==============================================
1643
+
1644
+ const [list, setList] = useState<MemberRes[]>([]);
1645
+ const [listSpinning, setListSpinning] = useState(false);
1646
+ const [page, setPage] = useState<AXDGPage>({...});
1647
+
1648
+ const getList = useCallback(async (listParams: ListRequest) => {
1649
+ setListSpinning(true);
1650
+ try {
1651
+ const data = await MemberService.postMemberListMember(deleteEmptyValue(listParams));
1652
+ setList(data.ds);
1653
+ setPage({ ... });
1654
+ } catch (err) {
1655
+ await errorHandling(err);
1656
+ } finally {
1657
+ setListSpinning(false);
1658
+ }
1659
+ }, []);
1660
+
1661
+ // ===== 옵션: 자동 초기화 (필요시 주석 해제) =====
1662
+ // React.useEffect(() => {
1663
+ // (async () => {
1664
+ // await getList(params);
1665
+ // })();
1666
+ // }, [getList, params]);
1667
+ // ===============================================
1668
+
1669
+ return {
1670
+ list,
1671
+ page,
1672
+ listSpinning,
1673
+ getList,
1674
+ };
1675
+ }
1676
+ ```
1677
+
1678
+ ### 13.2 옵션 기능 포함 여부 결정 기준
1679
+
1680
+ | 옵션 | 포함 여부 | 주석 처리 | 비고 |
1681
+ |------|-----------|-----------|------|
1682
+ | 자동 초기화 | ✅ 항상 포함 | ✅ 주석 처리 | 작업자가 필요 시 해제 |
1683
+ | i18n 사용 | ✅ 항상 포함 | ✅ 주석 처리 | 작업자가 필요 시 해제 |
1684
+ | 페이징 | ✅ hasPage에 따라 가변 | ❌ 주석 없음 | 자동 생성 |
1685
+ | dateRange | ✅ 패턴에 따라 가변 | ❌ 주석 없음 | 자동 생성 |
1686
+
1687
+ ---
1688
+
1689
+ ## 14. 타입 체크 및 자동 수정
1690
+
1691
+ ### 14.1 타입 체크 흐름
1692
+
1693
+ ```
1694
+ 1. 훅 파일 생성
1695
+ 2. 프로젝트 구조 자동 분석 (tsconfig.json, package.json)
1696
+ 3. 적절한 명령으로 타입 체크 실행 (npm run type-check 또는 npx tsc --noEmit)
1697
+ 4. 타입 오류가 있으면 AI가 1차 자동 수정 시도
1698
+ 5. 수정된 코드로 재시도
1699
+ 6. 그래도 실패하면 사용자에게 위임 (결과에 타입 오류 포함)
1700
+ 7. 사용자가 판단 후 수정 요청
1701
+ ```
1702
+
1703
+ ### 14.2 재시도 로직
1704
+
1705
+ | 시도 | 동작 | 실패 시 처리 |
1706
+ |------|------|-------------|
1707
+ | 1회차 | 기본 생성 후 타입 체크 | AI 자동 수정 |
1708
+ | 2회차 | AI 수정 후 타입 체크 | 사용자에게 위임 |
1709
+ | 3회차 | (옵션) 재시도 | 사용자에게 위임 |
1710
+
1711
+ ### 14.2 프로젝트 구조 자동 분석
1712
+
1713
+ ```typescript
1714
+ /**
1715
+ * 프로젝트 루트 찾기 (tsconfig.json이 있는 위치)
1716
+ */
1717
+ async function findProjectRoot(startPath: string): Promise<string> {
1718
+ let currentDir = path.dirname(startPath);
1719
+
1720
+ // 최대 10단계 상위 폴더로 탐색
1721
+ for (let i = 0; i < 10; i++) {
1722
+ const tsconfigPath = path.join(currentDir, 'tsconfig.json');
1723
+
1724
+ try {
1725
+ await fs.access(tsconfigPath);
1726
+ return currentDir; // tsconfig.json을 찾으면 해당 경로가 프로젝트 루트
1727
+ } catch {
1728
+ // 상위 폴더로 이동
1729
+ currentDir = path.dirname(currentDir);
1730
+ if (currentDir === path.dirname(currentDir)) break;
1731
+ }
1732
+ }
1733
+
1734
+ return process.cwd(); // 기본값
1735
+ }
1736
+
1737
+ /**
1738
+ * package.json 확인하여 type-check 스크립트 확인
1739
+ */
1740
+ async function hasTypeCheckScript(projectRoot: string): Promise<boolean> {
1741
+ try {
1742
+ const packageJsonPath = path.join(projectRoot, 'package.json');
1743
+ const content = await fs.readFile(packageJsonPath, 'utf-8');
1744
+ const packageJson = JSON.parse(content);
1745
+
1746
+ return !!(packageJson.scripts && packageJson.scripts['type-check']);
1747
+ } catch {
1748
+ return false;
1749
+ }
1750
+ }
1751
+ ```
1752
+
1753
+ ### 14.3 타입 체크 실행
1754
+
1755
+ ```typescript
1756
+ /**
1757
+ * 타입 체크 실행 (프로젝트 구조 자동 분석)
1758
+ */
1759
+ async function typeCheck(filePath: string): Promise<{ success: boolean; errors?: string[] }> {
1760
+ try {
1761
+ // 1. 프로젝트 루트 찾기
1762
+ const projectRoot = await findProjectRoot(filePath);
1763
+
1764
+ // 2. package.json의 type-check 스크립트 확인
1765
+ const hasTypeCheck = await hasTypeCheckScript(projectRoot);
1766
+
1767
+ let command: string;
1768
+ let args: string[];
1769
+
1770
+ if (hasTypeCheck) {
1771
+ // package.json에 type-check 스크립트가 있으면 그것 사용
1772
+ command = 'npm';
1773
+ args = ['run', 'type-check'];
1774
+ } else {
1775
+ // 없으면 tsc 직접 실행
1776
+ command = 'npx';
1777
+ args = ['tsc', '--noEmit', filePath];
1778
+ }
1779
+
1780
+ // 3. 명령 실행 (백그라운드)
1781
+ execSync(
1782
+ `${command} ${args.join(' ')}`,
1783
+ {
1784
+ encoding: 'utf-8',
1785
+ cwd: projectRoot,
1786
+ stdio: ['ignore', 'ignore', 'pipe'],
1787
+ timeout: 30000 // 30초 타임아웃
1788
+ }
1789
+ );
1790
+
1791
+ return { success: true };
1792
+ } catch (error: any) {
1793
+ const stderr = error.stderr || error.stdout || '';
1794
+ const errors = stderr
1795
+ .split('\n')
1796
+ .filter((line: string) => line.includes('error TS'))
1797
+ .map((line: string) => line.trim())
1798
+ .filter((line: string) => line.length > 0);
1799
+ return { success: false, errors };
1800
+ }
1801
+ }
1802
+ ```
1803
+
1804
+ ### 14.4 결과 반환 (AI 1차 수정 후 사용자 위임)
1805
+
1806
+ ```typescript
1807
+ /**
1808
+ * AI 기반 타입 오류 수정
1809
+ * @param code 현재 코드
1810
+ * @param errors 타입 오류 목록
1811
+ * @param interfacePath 인터페이스 경로 (컨텍스트)
1812
+ * @returns 수정된 코드
1813
+ */
1814
+ async function fixTypeErrors(
1815
+ code: string,
1816
+ errors: string[],
1817
+ interfacePath: string
1818
+ ): Promise<string> {
1819
+ // TODO: AI 기반 수정 로직 구현
1820
+ // - 타입 오류 분석
1821
+ // - import 추가
1822
+ // - 타입 이름 수정
1823
+ // - 코드 재생성
1824
+
1825
+ // 현재는 기본 코드 반환 (추후 구현)
1826
+ return code;
1827
+ }
1828
+
1829
+ /**
1830
+ * 훅 생성 (타입 체크 + AI 1차 수정)
1831
+ */
1832
+ async function generateHookWithCheck(args: { interfacePath: string; outputPath?: string }) {
1833
+ const { interfacePath, outputPath } = args;
1834
+
1835
+ // 1. 기본 훅 생성
1836
+ let hookCode = await generateHookCode(interfacePath, outputPath);
1837
+ const finalPath = outputPath || getDefaultPath(interfacePath);
1838
+ await fs.writeFile(finalPath, hookCode);
1839
+
1840
+ // 2. 타입 체크 및 수정 (최대 3회 시도)
1841
+ const maxRetries = 3;
1842
+ let typeErrors: string[] = [];
1843
+
1844
+ for (let i = 0; i < maxRetries; i++) {
1845
+ const result = await typeCheck(finalPath);
1846
+
1847
+ if (result.success) {
1848
+ break; // 타입 체크 통과
1849
+ }
1850
+
1851
+ // 오류 수집
1852
+ typeErrors = result.errors || [];
1853
+
1854
+ // 마지막 시도이면 종료 (사용자에게 위임)
1855
+ if (i === maxRetries - 1) {
1856
+ break;
1857
+ }
1858
+
1859
+ // 1차 시도: AI 자동 수정
1860
+ if (i === 0) {
1861
+ console.log(`[AI 자동 수정] 타입 오류 ${typeErrors.length}건`);
1862
+ hookCode = await fixTypeErrors(hookCode, typeErrors, interfacePath);
1863
+ await fs.writeFile(finalPath, hookCode);
1864
+ }
1865
+ }
1866
+
1867
+ // 3. Lint 및 Prettier 실행
1868
+ await runLintAndPrettier(finalPath);
1869
+
1870
+ // 4. 결과 반환
1871
+ return {
1872
+ success: true,
1873
+ message: typeErrors.length > 0
1874
+ ? `훅 파일 생성 완료 (타입 오류 ${typeErrors.length}건, AI 수정 후 실패)`
1875
+ : '훅 파일 생성 완료 (타입 체크 통과)',
1876
+ path: finalPath,
1877
+ typeErrors: typeErrors.length > 0 ? typeErrors : undefined, // 사용자에게 위임
1878
+ aiFixed: typeErrors.length > 0, // AI 시도 표시
1879
+ };
1880
+ }
1881
+ ```
1882
+
1883
+ ### 14.5 사용자 경험
1884
+
1885
+ **타입 체크 통과:**
1886
+ ```
1887
+ Store 파일 생성 완료 (타입 체크 통과)
1888
+
1889
+ 생성된 파일: src/stores/useMemberListStore.ts
1890
+ ```
1891
+
1892
+ **AI 수정 후 통과:**
1893
+ ```
1894
+ Store 파일 생성 완료 (AI 수정 후 타입 체크 통과)
1895
+
1896
+ 생성된 파일: src/stores/useMemberListStore.ts
1897
+ ```
1898
+
1899
+ **AI 수정 후 실패 (결과만 표시, 질문 없음):**
1900
+ ```
1901
+ Store 파일 생성 완료 (타입 오류 3건)
1902
+
1903
+ 생성된 파일: src/stores/useMemberListStore.ts
1904
+
1905
+ 타입 오류:
1906
+ - error TS2304: Cannot find name 'MemberRes'.
1907
+ - error TS2339: Property 'ds' does not exist on type '...'.
1908
+ - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
1909
+ ```
1910
+
1911
+ ### 14.6 전체 흐름도
1912
+
1913
+ ```
1914
+ ┌─────────────────┐
1915
+ │ 훅 파일 생성 │
1916
+ └────────┬────────┘
1917
+
1918
+
1919
+ ┌─────────────────┐
1920
+ │ 타입 체크 실행 │
1921
+ └────────┬────────┘
1922
+
1923
+ ┌────┴────┐
1924
+ │ 통과? │
1925
+ └────┬────┘
1926
+
1927
+ ┌────┴─────────┐
1928
+ │ │
1929
+ YES NO
1930
+ │ │
1931
+ ▼ ▼
1932
+ ┌────────┐ ┌──────────┐
1933
+ │ 완료 │ │ AI 자동 │
1934
+ └────────┘ │ 수정 시도 │
1935
+ └─────┬────┘
1936
+
1937
+
1938
+ ┌──────────┐
1939
+ │ 타입 체크 │
1940
+ └─────┬────┘
1941
+
1942
+ ┌────┴────┐
1943
+ │ 통과? │
1944
+ └────┬────┘
1945
+
1946
+ ┌────┴─────┐
1947
+ │ │
1948
+ YES NO
1949
+ │ │
1950
+ ▼ ▼
1951
+ ┌────────┐ ┌──────────┐
1952
+ │ 완료 │ │ 결과 반환 │
1953
+ └────────┘ │ (오류만) │
1954
+ └──────────┘
1955
+ }
1956
+ ```
1957
+
1958
+ ### 14.5 사용자 경험
1959
+
1960
+ **타입 체크 통과:**
1961
+ ```
1962
+ 훅 파일 생성 완료 (타입 체크 통과)
1963
+
1964
+ 생성된 파일: src/hooks/useMemberService.ts
1965
+ ```
1966
+
1967
+ **타입 오류 있음:**
1968
+ ```
1969
+ 훅 파일 생성 완료 (타입 오류 3건)
1970
+
1971
+ 생성된 파일: src/hooks/useMemberService.ts
1972
+
1973
+ 타입 오류:
1974
+ - error TS2304: Cannot find name 'MemberRes'.
1975
+ - error TS2339: Property 'ds' does not exist on type '...'.
1976
+ - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
1977
+
1978
+ 수정하시겠습니까?
1979
+ ```
1980
+
1981
+ ### 14.6 타입 오류 수정 예시
1982
+
1983
+ **생성된 코드 (타입 오류):**
1984
+ ```typescript
1985
+ // ❌ MemberRes를 찾을 수 없음
1986
+ const [list, setList] = useState<MemberRes[]>([]);
1987
+ ```
1988
+
1989
+ **수정된 코드:**
1990
+ ```typescript
1991
+ // ✅ 올바른 타입으로 수정
1992
+ const [list, setList] = useState<Member[]>([]);
1993
+ ```
1994
+
1995
+ ### 14.7 타입 체크 명령어
1996
+
1997
+ ```bash
1998
+ # 프로젝트에 type-check 스크립트가 있으면
1999
+ npm run type-check
2000
+
2001
+ # 없으면 직접 실행
2002
+ npx tsc --noEmit src/hooks/useMemberService.ts
2003
+ ```
2004
+
2005
+ ### 14.8 타입 오류 패턴별 수정 로직
2006
+
2007
+ | 오류 패턴 | 수정 방법 |
2008
+ |----------|----------|
2009
+ | `Could not find namespace 'XXX'` | import 추가 또는 타입 경로 수정 |
2010
+ | `Property 'xxx' does not exist on type 'XXX'` | 타입 정의 수정 |
2011
+ | `Type 'XXX' is not assignable to type 'YYY'` | 타입 호환성 수정 |
2012
+ | `Module '"XXX"' has no exported member 'YYY'` | import 경로 또는 이름 수정 |
2013
+
2014
+ ---
2015
+ | 2회차 | 오류 수정 후 재생성 |
2016
+ | 3회차 | 마지막 시도 후 결과 반환 |
2017
+
2018
+ ### 14.5 타입 체크 명령어
2019
+
2020
+ ```bash
2021
+ # 특정 파일 타입 체크
2022
+ npx tsc --noEmit src/hooks/useMemberService.ts
2023
+
2024
+ # 프로젝트 전체 타입 체크
2025
+ npx tsc --noEmit
2026
+
2027
+ # 또는
2028
+ npm run type-check # package.json에 설정된 경우
2029
+ ```
2030
+
2031
+ ### 14.6 타입 오류 패턴별 수정 로직
2032
+
2033
+ | 오류 패턴 | 수정 방법 |
2034
+ |----------|----------|
2035
+ | `Could not find namespace 'XXX'` | import 추가 또는 타입 경로 수정 |
2036
+ | `Property 'xxx' does not exist on type 'XXX'` | 타입 정의 수정 |
2037
+ | `Type 'XXX' is not assignable to type 'YYY'` | 타입 호환성 수정 |
2038
+ | `Module '"XXX"' has no exported member 'YYY'` | import 경로 또는 이름 수정 |
2039
+
2040
+ ---
2041
+
2042
+ ## 15. Store와 Hook 동시 생성 (결과 요약)
2043
+
2044
+ ### 15.1 사용자 시나리오
2045
+
2046
+ 사용자가 단일 요청으로 Store와 Hook을 동시에 생성할 때, 각각의 개별 결과가 아닌 **요약된 결과**를 보여줍니다.
2047
+
2048
+ ```typescript
2049
+ // 사용자 요청
2050
+ generateStoreAndHook({ interfacePath: "/path/to/MemberRepository.ts" })
2051
+ ```
2052
+
2053
+ ### 15.2 결과 요약 형식
2054
+
2055
+ **성공 시:**
2056
+ ```
2057
+ ✅ Store 및 Hook 생성 완료
2058
+
2059
+ 생성된 파일:
2060
+ - src/stores/useMemberListStore.ts (Store)
2061
+ - src/hooks/useMemberService.ts (Hook)
2062
+
2063
+ Store: 타입 체크 통과
2064
+ Hook: 타입 체크 통과
2065
+ ```
2066
+
2067
+ **부분 실패 시 (Hook 타입 오류):**
2068
+ ```
2069
+ ⚠️ Store 및 Hook 생성 완료 (일부 타입 오류)
2070
+
2071
+ 생성된 파일:
2072
+ - src/stores/useMemberListStore.ts (Store)
2073
+ - src/hooks/useMemberService.ts (Hook)
2074
+
2075
+ Store: 타입 체크 통과
2076
+ Hook: 타입 오류 2건
2077
+
2078
+ Hook 타입 오류:
2079
+ - error TS2304: Cannot find name 'MemberRes'.
2080
+ - error TS2339: Property 'ds' does not exist on type '...'.
2081
+ ```
2082
+
2083
+ ### 15.3 MCP 도구 설계
2084
+
2085
+ #### 옵션 A: 통합 도구
2086
+
2087
+ ```typescript
2088
+ // 도구: generate-store-and-hook
2089
+ {
2090
+ "name": "generate-store-and-hook",
2091
+ "description": "Store와 Hook을 동시에 생성합니다",
2092
+ "inputSchema": {
2093
+ "type": "object",
2094
+ "properties": {
2095
+ "interfacePath": { "type": "string" },
2096
+ "storeOutputPath": { "type": "string" },
2097
+ "hookOutputPath": { "type": "string" },
2098
+ "storeType": { "type": "number" }
2099
+ }
2100
+ }
2101
+ }
2102
+ ```
2103
+
2104
+ #### 옵션 B: 개별 도구 (현재 방식)
2105
+
2106
+ ```typescript
2107
+ // 사용자가 각각 호출
2108
+ generateStore({ interfacePath, outputPath: "stores/useMemberListStore.ts" })
2109
+ generateHook({ interfacePath, outputPath: "hooks/useMemberService.ts" })
2110
+ ```
2111
+
2112
+ ### 15.4 결과 요약 로직
2113
+
2114
+ ```typescript
2115
+ /**
2116
+ * Store와 Hook 동시 생성 (결과 요약)
2117
+ */
2118
+ async function generateStoreAndHook(args: {
2119
+ interfacePath: string;
2120
+ storeOutputPath?: string;
2121
+ hookOutputPath?: string;
2122
+ storeType?: number;
2123
+ }) {
2124
+ const { interfacePath, storeOutputPath, hookOutputPath, storeType = 1 } = args;
2125
+
2126
+ // 1. Store 생성
2127
+ const storeResult = await generateStore({
2128
+ interfacePath,
2129
+ outputPath: storeOutputPath || getDefaultStorePath(interfacePath),
2130
+ storeType,
2131
+ });
2132
+
2133
+ // 2. Hook 생성
2134
+ const hookResult = await generateHook({
2135
+ interfacePath,
2136
+ outputPath: hookOutputPath || getDefaultHookPath(interfacePath),
2137
+ });
2138
+
2139
+ // 3. 결과 요약
2140
+ const storeData = JSON.parse(storeResult.content[0].text);
2141
+ const hookData = JSON.parse(hookResult.content[0].text);
2142
+
2143
+ // 4. 요약된 결과 반환
2144
+ return {
2145
+ content: [{
2146
+ type: 'text',
2147
+ text: formatSummary(storeData, hookData),
2148
+ }],
2149
+ };
2150
+ }
2151
+
2152
+ /**
2153
+ * 결과 요약 포맷팅
2154
+ */
2155
+ function formatSummary(storeData: any, hookData: any): string {
2156
+ const lines: string[] = [];
2157
+
2158
+ // 헤더
2159
+ const hasErrors = storeData.typeErrors || hookData.typeErrors;
2160
+ const icon = hasErrors ? '⚠️' : '✅';
2161
+ const title = hasErrors ? 'Store 및 Hook 생성 완료 (일부 타입 오류)' : 'Store 및 Hook 생성 완료';
2162
+
2163
+ lines.push(`${icon} ${title}\n`);
2164
+
2165
+ // 생성된 파일
2166
+ lines.push('생성된 파일:');
2167
+ lines.push(`- ${storeData.outputPath} (Store)`);
2168
+ lines.push(`- ${hookData.path} (Hook)\n`);
2169
+
2170
+ // Store 상태
2171
+ const storeStatus = storeData.typeErrors
2172
+ ? `Store: 타입 오류 ${storeData.typeErrors.length}건`
2173
+ : 'Store: 타입 체크 통과';
2174
+ lines.push(storeStatus);
2175
+
2176
+ // Hook 상태
2177
+ const hookStatus = hookData.typeErrors
2178
+ ? `Hook: 타입 오류 ${hookData.typeErrors.length}건`
2179
+ : 'Hook: 타입 체크 통과';
2180
+ lines.push(hookStatus);
2181
+
2182
+ // 타입 오류가 있으면 추가
2183
+ const allErrors = [
2184
+ ...(storeData.typeErrors || []).map((e: string) => `[Store] ${e}`),
2185
+ ...(hookData.typeErrors || []).map((e: string) => `[Hook] ${e}`),
2186
+ ];
2187
+
2188
+ if (allErrors.length > 0) {
2189
+ lines.push('\n타입 오류:');
2190
+ allErrors.forEach(error => lines.push(`- ${error}`));
2191
+ }
2192
+
2193
+ return lines.join('\n');
2194
+ }
2195
+ ```
2196
+
2197
+ ### 15.5 사용자 경험 비교
2198
+
2199
+ | 방식 | 장점 | 단점 |
2200
+ |------|------|------|
2201
+ | **개별 결과** | 각 파일의 상세 결과 확인 | 결과가 길어짐, 전체 현황 파악 어려움 |
2202
+ | **요약 결과** | 전체 현황 한눈에 파악 | 개별 상세 정보는 별도 확인 필요 |
2203
+
2204
+ ### 15.6 구현 우선순위
2205
+
2206
+ 1. ✅ Store 생성 결과에 `typeErrors` 필드 추가
2207
+ 2. ✅ Hook 생성 결과에 `typeErrors` 필드 추가
2208
+ 3. ⏳ 통합 도구 `generate-store-and-hook` 구현
2209
+ 4. ⏳ 결과 요약 함수 `formatSummary` 구현
2210
+
2211
+ ---
2212
+
2213
+ ## 16. 향후 확장 가능성
2214
+
2215
+ 1. **커스텀 훅 옵션**: 사용자가 네이밍 규칙, spinning 관리 방식 등을 선택 가능
2216
+ 2. **타입스크립트 엄격 모드**: 더 엄격한 타입 검증
2217
+ 3. **테스트 코드 자동 생성**: 훅에 대한 테스트 코드 자동 생성
2218
+ 4. **문서 자동 생성**: 훅 사용법에 대한 문서 자동 생성
2219
+
2220
+ ---
2221
+
2222
+ ## 17. 참고 자료
2223
+
2224
+ - NH-FE-B 리액트 훅 패턴 분석 결과
2225
+ - 기존 Store 생성 템플릿
2226
+ - React Hooks 공식 문서