@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,950 @@
1
+ import { promises as fs } from 'fs';
2
+ import * as path from 'path';
3
+ import { execSync } from 'child_process';
4
+ import { analyzeInterface, InterfaceAnalysis, MethodInfo } from './analyze.js';
5
+
6
+ /**
7
+ * 프로젝트 루트 찾기 (tsconfig.json이 있는 위치)
8
+ */
9
+ async function findProjectRoot(startPath: string): Promise<string> {
10
+ let currentDir = path.dirname(startPath);
11
+
12
+ for (let i = 0; i < 10; i++) {
13
+ const tsconfigPath = path.join(currentDir, 'tsconfig.json');
14
+
15
+ try {
16
+ await fs.access(tsconfigPath);
17
+ return currentDir;
18
+ } catch {
19
+ const parentDir = path.dirname(currentDir);
20
+ if (parentDir === currentDir) {
21
+ break;
22
+ }
23
+ currentDir = parentDir;
24
+ }
25
+ }
26
+
27
+ return process.cwd();
28
+ }
29
+
30
+ /**
31
+ * package.json 확인하여 type-check 스크립트 확인
32
+ */
33
+ async function hasTypeCheckScript(projectRoot: string): Promise<boolean> {
34
+ try {
35
+ const packageJsonPath = path.join(projectRoot, 'package.json');
36
+ const content = await fs.readFile(packageJsonPath, 'utf-8');
37
+ const packageJson = JSON.parse(content);
38
+
39
+ return !!(packageJson.scripts && packageJson.scripts['type-check']);
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * 타입 체크 실행 (프로젝트 구조 자동 분석)
47
+ */
48
+ async function typeCheck(filePath: string): Promise<{ success: boolean; errors?: string[] }> {
49
+ try {
50
+ const projectRoot = await findProjectRoot(filePath);
51
+ const hasTypeCheck = await hasTypeCheckScript(projectRoot);
52
+
53
+ let command: string;
54
+ let args: string[];
55
+
56
+ if (hasTypeCheck) {
57
+ command = 'npm';
58
+ args = ['run', 'type-check'];
59
+ } else {
60
+ command = 'npx';
61
+ args = ['tsc', '--noEmit', filePath];
62
+ }
63
+
64
+ execSync(
65
+ `${command} ${args.join(' ')}`,
66
+ {
67
+ encoding: 'utf-8',
68
+ cwd: projectRoot,
69
+ stdio: ['ignore', 'ignore', 'pipe'],
70
+ timeout: 30000
71
+ }
72
+ );
73
+
74
+ return { success: true };
75
+ } catch (error: any) {
76
+ const stderr = error.stderr || error.stdout || '';
77
+ const errors = stderr
78
+ .split('\n')
79
+ .filter((line: string) => line.includes('error TS'))
80
+ .map((line: string) => line.trim())
81
+ .filter((line: string) => line.length > 0);
82
+ return { success: false, errors };
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Lint 및 Prettier 실행 (프로젝트 루트 기준)
88
+ */
89
+ async function runLintAndPrettier(filePath: string): Promise<void> {
90
+ // 프로젝트 루트 찾기
91
+ const projectRoot = await findProjectRoot(filePath);
92
+ const relativePath = path.relative(projectRoot, filePath);
93
+
94
+ try {
95
+ execSync(`npx prettier --write "${relativePath}"`, { cwd: projectRoot, stdio: 'ignore' });
96
+ } catch {
97
+ // Prettier 실패는 무시
98
+ }
99
+
100
+ try {
101
+ execSync(`npx eslint --fix "${relativePath}"`, { cwd: projectRoot, stdio: 'ignore' });
102
+ } catch {
103
+ // ESLint 실패는 무시
104
+ }
105
+ }
106
+
107
+ // ===== 포함/제외 키워드 =====
108
+
109
+ const INCLUDE_KEYWORDS = ['조회', '목록'];
110
+ const EXCLUDE_KEYWORDS = ['저장', '삭제', '수정', '엑셀', '업로드', '다운로드', 'excel', 'upload', 'download'];
111
+
112
+ /**
113
+ * 메서드가 훅 생성에 포함될지 확인
114
+ */
115
+ function shouldIncludeMethod(method: MethodInfo): boolean {
116
+ if (!method.comment) {
117
+ return false;
118
+ }
119
+
120
+ const commentLower = method.comment.toLowerCase();
121
+
122
+ // 제외 키워드 확인
123
+ for (const exclude of EXCLUDE_KEYWORDS) {
124
+ if (commentLower.includes(exclude)) {
125
+ return false;
126
+ }
127
+ }
128
+
129
+ // 포함 키워드 확인
130
+ for (const include of INCLUDE_KEYWORDS) {
131
+ if (commentLower.includes(include)) {
132
+ return true;
133
+ }
134
+ }
135
+
136
+ return false;
137
+ }
138
+
139
+ // ===== 네이밍 추출 =====
140
+
141
+ /**
142
+ * 리스트 변수명 추출 (옵션 1: 메서드명 기반 자동 네이밍)
143
+ * postMemberListMember → list (접미사가 Entity면 기본값)
144
+ * postMemberListAdmin → adminList
145
+ * postMemberList → list (접미사 없으면 기본값)
146
+ */
147
+ function extractListName(methodName: string, entityName: string): string {
148
+ const pattern = new RegExp(`post${entityName}List(.*)`);
149
+ const match = methodName.match(pattern);
150
+
151
+ if (match) {
152
+ const suffix = match[1];
153
+ if (!suffix) return 'list'; // 접미사 없으면 기본값
154
+
155
+ // 접미사가 Entity명과 같으면 기본값 (postMemberListMember → list)
156
+ if (suffix.toLowerCase() === entityName.toLowerCase()) {
157
+ return 'list';
158
+ }
159
+
160
+ // 첫 글자 소문자로 변환 (camelCase)
161
+ return suffix.charAt(0).toLowerCase() + suffix.slice(1) + 'List';
162
+ }
163
+
164
+ return 'list';
165
+ }
166
+
167
+ /**
168
+ * 리스트 함수명 추출
169
+ * postMemberListMember → getList (접미사가 Entity면 getList)
170
+ * postMemberListAdmin → getAdminList
171
+ * postMemberList → getList
172
+ */
173
+ function extractListFunctionName(methodName: string, entityName: string): string {
174
+ const listName = extractListName(methodName, entityName);
175
+
176
+ // list → getList, adminList → getAdminList
177
+ return 'get' + listName.charAt(0).toUpperCase() + listName.slice(1);
178
+ }
179
+
180
+ /**
181
+ * 단건 변수명 추출
182
+ * postMemberDetail → detail
183
+ * postMemberInfo → info
184
+ * postMemberProfile → profile
185
+ * postMember → member
186
+ * getMemberDetail → detail
187
+ * getMember → member
188
+ */
189
+ function extractDetailName(methodName: string, entityName: string): string {
190
+ // post{Entity}{Suffix} 패턴
191
+ const postPattern = new RegExp(`post${entityName}(.*)`);
192
+ const postMatch = methodName.match(postPattern);
193
+
194
+ if (postMatch) {
195
+ const suffix = postMatch[1];
196
+ if (!suffix) return entityName.toLowerCase(); // postMember → member
197
+
198
+ // 첫 글자 소문자로 변환 (camelCase)
199
+ return suffix.charAt(0).toLowerCase() + suffix.slice(1);
200
+ }
201
+
202
+ // get{Entity}{Suffix} 패턴
203
+ const getPattern = new RegExp(`get${entityName}(.*)`);
204
+ const getMatch = methodName.match(getPattern);
205
+
206
+ if (getMatch) {
207
+ const suffix = getMatch[1];
208
+ if (!suffix) return entityName.toLowerCase(); // getMember → member
209
+
210
+ return suffix.charAt(0).toLowerCase() + suffix.slice(1);
211
+ }
212
+
213
+ return 'detail'; // 기본값
214
+ }
215
+
216
+ /**
217
+ * 단건 함수명 추출
218
+ * postMemberDetail → getDetail
219
+ * postMemberInfo → getInfo
220
+ * postMember → getMember
221
+ * getMemberDetail → getDetail
222
+ */
223
+ function extractDetailFunctionName(methodName: string, entityName: string): string {
224
+ const detailName = extractDetailName(methodName, entityName);
225
+
226
+ // detail → getDetail, info → getInfo, member → getMember
227
+ return 'get' + detailName.charAt(0).toUpperCase() + detailName.slice(1);
228
+ }
229
+
230
+ // ===== 중복 처리 (옵션 C: 중복 시만 구분) =====
231
+
232
+ interface ProcessedMethod {
233
+ method: MethodInfo;
234
+ listName?: string;
235
+ functionName: string;
236
+ detailName?: string;
237
+ }
238
+
239
+ /**
240
+ * 중복 확인 후 접두사 추가
241
+ * 1. 기본 네이밍으로 모든 메서드의 이름을 생성
242
+ * 2. 중복이 있는 메서드에만 접두사 추가
243
+ */
244
+ function resolveDuplicateNames(processedMethods: ProcessedMethod[], entityName: string): ProcessedMethod[] {
245
+ // 이름별로 그룹화
246
+ const nameMap = new Map<string, ProcessedMethod[]>();
247
+
248
+ processedMethods.forEach(pm => {
249
+ const baseName = pm.listName || pm.detailName || '';
250
+ if (!nameMap.has(baseName)) {
251
+ nameMap.set(baseName, []);
252
+ }
253
+ nameMap.get(baseName)!.push(pm);
254
+ });
255
+
256
+ // 중복이 있는 경우 접두사 추가
257
+ nameMap.forEach((duplicates, baseName) => {
258
+ if (duplicates.length > 1) {
259
+ // 중복이 있으면 접두사 추가 (post/get 또는 List/Detail 구분)
260
+ duplicates.forEach((pm) => {
261
+ const isPost = pm.method.name.startsWith('post');
262
+ const prefix = isPost ? 'post' : 'get';
263
+ const capitalized = prefix.charAt(0).toUpperCase() + prefix.slice(1);
264
+
265
+ if (pm.listName) {
266
+ // 목록은 숫자로 구분
267
+ const idx = duplicates.indexOf(pm);
268
+ pm.listName = baseName + (idx + 1);
269
+ pm.functionName = 'get' + baseName.charAt(0).toUpperCase() + baseName.slice(1) + (idx + 1);
270
+ } else {
271
+ // 단건은 post/get 접두사로 구분
272
+ pm.detailName = capitalized + baseName.charAt(0).toUpperCase() + baseName.slice(1);
273
+ pm.functionName = 'get' + pm.detailName.charAt(0).toUpperCase() + pm.detailName.slice(1);
274
+ }
275
+ });
276
+ }
277
+ });
278
+
279
+ return processedMethods;
280
+ }
281
+
282
+ // ===== DTO 타입 추출 =====
283
+
284
+ /**
285
+ * Response 타입에서 DTO 타입 추출
286
+ * PostMemberListMemberResponse → MemberRes
287
+ * PostPbReprtListPbReprtResponse → PbReprtRes
288
+ */
289
+ function extractDtoType(responseType: string): string {
290
+ if (!responseType) return 'unknown';
291
+
292
+ // Post{Entity}List{Response} → {Entity}Res
293
+ // Post{Entity}List{Suffix}Response → {Suffix}Res
294
+ // 정규식: Post로 시작하고 List가 있고 Response로 끝나는 패턴
295
+ const listMatch = responseType.match(/Post(\w+)List(\w+)Response/);
296
+ if (listMatch) {
297
+ const entityPart = listMatch[1]; // PbReprt, Member
298
+ const suffixPart = listMatch[2]; // PbReprt, Member, Admin
299
+
300
+ // PostPbReprtListPbReprtResponse → PbReprtRes
301
+ // PostMemberListMemberResponse → MemberRes
302
+ if (suffixPart.toLowerCase() === entityPart.toLowerCase()) {
303
+ return entityPart + 'Res';
304
+ }
305
+
306
+ // PostMemberListAdminResponse → AdminRes
307
+ return suffixPart + 'Res';
308
+ }
309
+
310
+ // Post{Entity}{Suffix}Response → {Suffix}Res (단건 조회)
311
+ const detailMatch = responseType.match(/Post(\w+)(\w+)Response/);
312
+ if (detailMatch) {
313
+ const entityPart = detailMatch[1];
314
+ const suffixPart = detailMatch[2];
315
+ return suffixPart + 'Res';
316
+ }
317
+
318
+ // Get{Entity}{Detail}Response → {Entity}DetailRes
319
+ const getMatch = responseType.match(/Get(\w+)(\w+)Response/);
320
+ if (getMatch) {
321
+ return getMatch[1] + getMatch[2] + 'Res';
322
+ }
323
+
324
+ return 'unknown';
325
+ }
326
+
327
+ // ===== Request Interface에서 dateRange 패턴 추출 =====
328
+
329
+ interface DateRangePattern {
330
+ hasDateRange: boolean;
331
+ startField?: string;
332
+ endField?: string;
333
+ }
334
+
335
+ /**
336
+ * Request 타입에서 dateRange 패턴 추출
337
+ */
338
+ function extractDateRangeFromRequest(requestType: string, combinedContent: string): DateRangePattern {
339
+ if (!requestType || requestType === 'void' || requestType === 'any') {
340
+ return { hasDateRange: false };
341
+ }
342
+
343
+ // Request 인터페이스 찾기
344
+ const searchStr = `export interface ${requestType}`;
345
+ const searchIdx = combinedContent.indexOf(searchStr);
346
+
347
+ if (searchIdx === -1) {
348
+ return { hasDateRange: false };
349
+ }
350
+
351
+ const afterIdx = searchIdx + searchStr.length;
352
+ const remaining = combinedContent.slice(afterIdx);
353
+
354
+ const match = remaining.match(/^\s*(?:extends\s+[\w<>]+\s*)?\{/);
355
+ if (!match) {
356
+ return { hasDateRange: false };
357
+ }
358
+
359
+ const startIndex = afterIdx + match[0].length;
360
+ let depth = 1;
361
+ let endIndex = startIndex;
362
+
363
+ for (let i = startIndex; i < combinedContent.length; i++) {
364
+ if (combinedContent[i] === '{') {
365
+ depth++;
366
+ } else if (combinedContent[i] === '}') {
367
+ depth--;
368
+ if (depth === 0) {
369
+ endIndex = i;
370
+ break;
371
+ }
372
+ }
373
+ }
374
+
375
+ if (depth !== 0) {
376
+ return { hasDateRange: false };
377
+ }
378
+
379
+ const interfaceBody = combinedContent.slice(startIndex, endIndex);
380
+
381
+ // bgnDtm/endDtm 패턴 확인
382
+ const hasBgnDtm = /bgnDtm\s*\??:\s*string/.test(interfaceBody);
383
+ const hasEndDtm = /endDtm\s*\??:\s*string/.test(interfaceBody);
384
+
385
+ if (hasBgnDtm && hasEndDtm) {
386
+ return {
387
+ hasDateRange: true,
388
+ startField: 'bgnDtm',
389
+ endField: 'endDtm',
390
+ };
391
+ }
392
+
393
+ // startDateTime/endDateTime 패턴 확인
394
+ const hasStartDateTime = /startDateTime\s*\??:\s*string/.test(interfaceBody);
395
+ const hasEndDateTime = /endDateTime\s*\??:\s*string/.test(interfaceBody);
396
+
397
+ if (hasStartDateTime && hasEndDateTime) {
398
+ return {
399
+ hasDateRange: true,
400
+ startField: 'startDateTime',
401
+ endField: 'endDateTime',
402
+ };
403
+ }
404
+
405
+ return { hasDateRange: false };
406
+ }
407
+
408
+ // ===== 훅 코드 생성 =====
409
+
410
+ interface HookMethod {
411
+ // 공통
412
+ name: string;
413
+ functionName: string;
414
+ requestType: string;
415
+ responseType: string;
416
+ dtoType: string;
417
+
418
+ // 목록 전용
419
+ isListMethod: boolean;
420
+ listName?: string;
421
+ hasPage: boolean;
422
+
423
+ // 단건 전용
424
+ detailName?: string;
425
+
426
+ // dateRange
427
+ hasDateRange: boolean;
428
+ dateRangeStartField?: string;
429
+ dateRangeEndField?: string;
430
+ }
431
+
432
+ /**
433
+ * Response 인터페이스에서 ds 필드 타입 추출
434
+ * PostBannerListBannerResponse → Banner (ds: Banner[])
435
+ */
436
+ function extractDsFieldType(combinedContent: string, responseType: string): string {
437
+ if (!responseType || responseType === 'void' || responseType === 'any') {
438
+ return 'unknown';
439
+ }
440
+
441
+ // Response 인터페이스 찾기
442
+ const searchStr = `export interface ${responseType}`;
443
+ const searchIdx = combinedContent.indexOf(searchStr);
444
+
445
+ if (searchIdx === -1) {
446
+ return 'unknown';
447
+ }
448
+
449
+ const afterIdx = searchIdx + searchStr.length;
450
+ const remaining = combinedContent.slice(afterIdx);
451
+
452
+ const match = remaining.match(/^\s*(?:extends\s+[\w<>]+\s*)?\{/);
453
+ if (!match) {
454
+ return 'unknown';
455
+ }
456
+
457
+ const startIndex = afterIdx + match[0].length;
458
+ let depth = 1;
459
+ let endIndex = startIndex;
460
+
461
+ for (let i = startIndex; i < combinedContent.length; i++) {
462
+ if (combinedContent[i] === '{') {
463
+ depth++;
464
+ } else if (combinedContent[i] === '}') {
465
+ depth--;
466
+ if (depth === 0) {
467
+ endIndex = i;
468
+ break;
469
+ }
470
+ }
471
+ }
472
+
473
+ if (depth !== 0) {
474
+ return 'unknown';
475
+ }
476
+
477
+ const interfaceBody = combinedContent.slice(startIndex, endIndex);
478
+
479
+ // ds 필드 찾기
480
+ const dsMatch = interfaceBody.match(/ds\s*:\s*([\w<>[\]\s]+)(?:\[\])?/);
481
+ if (dsMatch) {
482
+ let dsType = dsMatch[1].trim();
483
+ // 배열 제거 (Banner[] → Banner)
484
+ dsType = dsType.replace(/\[\]$/, '').trim();
485
+ // 제네릭 제거
486
+ dsType = dsType.replace(/<[^>]+>/, '').trim();
487
+ return dsType;
488
+ }
489
+
490
+ // rs 필드 찾기 (단건)
491
+ const rsMatch = interfaceBody.match(/rs\s*:\s*([\w<>[\]\s]+)/);
492
+ if (rsMatch) {
493
+ let rsType = rsMatch[1].trim();
494
+ rsType = rsType.replace(/<[^>]+>/, '').trim();
495
+ return rsType;
496
+ }
497
+
498
+ return 'unknown';
499
+ }
500
+
501
+ /**
502
+ * 훅 코드 생성
503
+ */
504
+ async function generateHookCode(analysis: InterfaceAnalysis, interfacePath: string): Promise<string> {
505
+ const entityName = analysis.entityName || analysis.className?.replace('Repository', '') || 'Generated';
506
+ const serviceName = analysis.serviceName || 'GeneratedService';
507
+ const hookName = `use${entityName}Service`;
508
+
509
+ // Repository 파일 읽기 (Response 인터페이스 파싱용)
510
+ let combinedContent = '';
511
+ try {
512
+ const repoContent = await fs.readFile(interfacePath, 'utf-8');
513
+
514
+ // Interface 파일 경로 찾기
515
+ const interfaceDir = path.dirname(interfacePath);
516
+ const interfaceFilePath = path.join(interfaceDir, '../interface', `${entityName}Interface.ts`);
517
+
518
+ let interfaceContent = '';
519
+ try {
520
+ interfaceContent = await fs.readFile(interfaceFilePath, 'utf-8');
521
+ } catch {
522
+ // Interface 파일이 없으면 skip
523
+ }
524
+
525
+ combinedContent = repoContent + '\n' + interfaceContent;
526
+ } catch {
527
+ // 파일 읽기 실패 시 빈 내용
528
+ }
529
+
530
+ // 1. 필터링: 포함/제외 키워드로 메서드 필터링
531
+ const filteredMethods = analysis.methods.filter(shouldIncludeMethod);
532
+
533
+ if (filteredMethods.length === 0) {
534
+ return `// 생성할 훅 메서드가 없습니다.\n// 포함 키워드: ${INCLUDE_KEYWORDS.join(', ')}\n// 제외 키워드: ${EXCLUDE_KEYWORDS.join(', ')}`;
535
+ }
536
+
537
+ // 2. 메서드 분류 및 네이밍
538
+ const processedMethods: ProcessedMethod[] = [];
539
+
540
+ for (const method of filteredMethods) {
541
+ // Response 인터페이스에서 ds 필드 타입 추출
542
+ const dtoType = combinedContent
543
+ ? extractDsFieldType(combinedContent, method.responseType || '')
544
+ : extractDtoType(method.responseType || '');
545
+
546
+ // Request 타입에서 dateRange 패턴 추출을 위해 combinedContent 필요
547
+ // 분석 단계에서 이미 dateRangeFields가 있으면 사용
548
+ const hasDateRange = analysis.hasDateRange || false;
549
+ const dateRangeFields = analysis.dateRangeFields;
550
+
551
+ if (method.responseListField) {
552
+ // 목록 메서드
553
+ const listName = extractListName(method.name, entityName);
554
+ const functionName = extractListFunctionName(method.name, entityName);
555
+
556
+ processedMethods.push({
557
+ method,
558
+ listName,
559
+ functionName,
560
+ });
561
+ } else if (method.responseDetailField) {
562
+ // 단건 메서드
563
+ const detailName = extractDetailName(method.name, entityName);
564
+ const functionName = extractDetailFunctionName(method.name, entityName);
565
+
566
+ processedMethods.push({
567
+ method,
568
+ detailName,
569
+ functionName,
570
+ });
571
+ }
572
+ }
573
+
574
+ // 3. 중복 처리 (옵션 C)
575
+ const resolvedMethods = resolveDuplicateNames(processedMethods, entityName);
576
+
577
+ // 4. HookMethod 배열 생성 (DTO 타입 재추출)
578
+ const hookMethods: HookMethod[] = resolvedMethods.map(pm => {
579
+ // Response 인터페이스에서 ds 필드 타입 추출
580
+ const dtoType = combinedContent
581
+ ? extractDsFieldType(combinedContent, pm.method.responseType || '')
582
+ : extractDtoType(pm.method.responseType || '');
583
+ const hasPage = pm.method.hasPage || false;
584
+
585
+ // dateRange 정보
586
+ const hasDateRange = analysis.hasDateRange || false;
587
+ const dateRangeFields = analysis.dateRangeFields;
588
+
589
+ return {
590
+ name: pm.method.name,
591
+ functionName: pm.functionName,
592
+ requestType: pm.method.requestType || 'any',
593
+ responseType: pm.method.responseType || 'any',
594
+ dtoType,
595
+ isListMethod: !!pm.listName,
596
+ listName: pm.listName,
597
+ hasPage,
598
+ detailName: pm.detailName,
599
+ hasDateRange,
600
+ dateRangeStartField: dateRangeFields?.startField,
601
+ dateRangeEndField: dateRangeFields?.endField,
602
+ };
603
+ });
604
+
605
+ // 5. 코드 생성
606
+ const lines: string[] = [];
607
+
608
+ // Import 수집
609
+ const requestTypes = new Set<string>();
610
+ const dtoTypes = new Set<string>();
611
+
612
+ for (const hm of hookMethods) {
613
+ if (hm.requestType && hm.requestType !== 'any' && hm.requestType !== 'void') {
614
+ requestTypes.add(hm.requestType);
615
+ }
616
+ if (hm.dtoType && hm.dtoType !== 'unknown') {
617
+ dtoTypes.add(hm.dtoType);
618
+ }
619
+ }
620
+
621
+ // Imports
622
+ lines.push('import { AXDGPage } from "@axboot/datagrid";');
623
+ lines.push('import { useCallback, useState } from "react";');
624
+ lines.push(`import { ${serviceName} } from "services";`);
625
+ lines.push('import { errorHandling } from "utils";');
626
+ lines.push('import { deleteEmptyValue } from "@core/utils/object";');
627
+
628
+ // Request 타입 import
629
+ if (requestTypes.size > 0) {
630
+ const sortedRequestTypes = Array.from(requestTypes).sort();
631
+ lines.push(`import { ${sortedRequestTypes.join(', ')} } from "services/@interface/interface";`);
632
+ }
633
+
634
+ // DTO 타입 import
635
+ if (dtoTypes.size > 0) {
636
+ const sortedDtoTypes = Array.from(dtoTypes).sort();
637
+ lines.push(`import { ${sortedDtoTypes.join(', ')} } from "services/@interface/dto";`);
638
+ }
639
+
640
+ // dateRange가 있으면 dateRangeToDts import
641
+ const anyHasDateRange = hookMethods.some(m => m.hasDateRange);
642
+ if (anyHasDateRange) {
643
+ lines.push('import { dateRangeToDts } from "utils/dateRangeToDts";');
644
+ lines.push('import { DT_FORMAT } from "@types";');
645
+ }
646
+
647
+ lines.push('');
648
+
649
+ // Hook 함수 선언
650
+ lines.push(`export function ${hookName}(params?: any) {`);
651
+
652
+ // ===== 상태 선언 =====
653
+ lines.push(' // ===== States =====');
654
+
655
+ for (const hm of hookMethods) {
656
+ if (hm.isListMethod) {
657
+ // 목록 상태
658
+ const listName = hm.listName || 'list';
659
+ lines.push(` const [${listName}, set${capitalize(listName)}] = useState<${hm.dtoType}[]>([]);`);
660
+ lines.push(` const [${listName}Spinning, set${capitalize(listName)}Spinning] = useState(false);`);
661
+
662
+ // 페이징 상태 (hasPage: true인 경우만)
663
+ if (hm.hasPage) {
664
+ const pageName = listName === 'list' ? 'page' : `${listName}Page`;
665
+ const setPageName = listName === 'list' ? 'setPage' : `set${capitalize(listName)}Page`;
666
+ lines.push(` const [${pageName}, ${setPageName}] = useState<AXDGPage>({`);
667
+ lines.push(` totalPages: 0,`);
668
+ lines.push(` totalElements: 0,`);
669
+ lines.push(` currentPage: 0,`);
670
+ lines.push(` pageSize: 0,`);
671
+ lines.push(` });`);
672
+ }
673
+ } else {
674
+ // 단건 상태
675
+ const detailName = hm.detailName || 'detail';
676
+ lines.push(` const [${detailName}, set${capitalize(detailName)}] = useState<${hm.dtoType}>();`);
677
+ lines.push(` const [${detailName}Spinning, set${capitalize(detailName)}Spinning] = useState(false);`);
678
+ }
679
+ }
680
+
681
+ lines.push('');
682
+
683
+ // ===== 함수 선언 =====
684
+ lines.push(' // ===== Functions =====');
685
+
686
+ for (const hm of hookMethods) {
687
+ // Request 타입 결정 (void인 경우 Partial 제거)
688
+ let baseParamType: string;
689
+ if (hm.requestType && hm.requestType !== 'any' && hm.requestType !== 'void') {
690
+ baseParamType = `Partial<${hm.requestType}>`;
691
+ } else if (hm.requestType === 'void') {
692
+ baseParamType = 'void';
693
+ } else {
694
+ baseParamType = 'any';
695
+ }
696
+
697
+ // dateRange가 있으면 intersection type으로 추가 (void가 아닐 때만)
698
+ const fullParamType = hm.hasDateRange && baseParamType !== 'void'
699
+ ? `(${baseParamType} & { dateRange?: [string, string] })`
700
+ : baseParamType;
701
+
702
+ // void인 경우 파라미터 없음
703
+ const paramDef = baseParamType === 'void'
704
+ ? ''
705
+ : `${hm.isListMethod ? 'listParams' : 'detail'}: ${fullParamType}`;
706
+
707
+ lines.push(` const ${hm.functionName} = useCallback(async (${paramDef}) => {`);
708
+
709
+ if (hm.isListMethod) {
710
+ const listName = hm.listName || 'list';
711
+ lines.push(` set${capitalize(listName)}Spinning(true);`);
712
+ } else {
713
+ const detailName = hm.detailName || 'detail';
714
+ lines.push(` set${capitalize(detailName)}Spinning(true);`);
715
+ }
716
+
717
+ lines.push(' try {');
718
+
719
+ // void 파라미터 처리
720
+ const isVoidParam = hm.requestType === 'void';
721
+ const paramVar = hm.isListMethod ? 'listParams' : 'detail';
722
+
723
+ // dateRange 변환 처리
724
+ if (hm.hasDateRange && hm.dateRangeStartField && hm.dateRangeEndField) {
725
+ const requestType = hm.requestType && hm.requestType !== 'any' && hm.requestType !== 'void'
726
+ ? ` as ${hm.requestType}`
727
+ : '';
728
+ lines.push(` const data = await ${serviceName}.${hm.name}(deleteEmptyValue({`);
729
+ lines.push(` ...${paramVar},`);
730
+ lines.push(` ...dateRangeToDts(${paramVar}.dateRange, {`);
731
+ lines.push(` ${hm.dateRangeStartField}: DT_FORMAT.DATETIME_HHMMSS,`);
732
+ lines.push(` ${hm.dateRangeEndField}: DT_FORMAT.DATETIME_HHMMSS,`);
733
+ lines.push(` }),`);
734
+ lines.push(` })${requestType});`);
735
+ } else {
736
+ if (isVoidParam) {
737
+ lines.push(` const data = await ${serviceName}.${hm.name}();`);
738
+ } else {
739
+ const requestType = hm.requestType && hm.requestType !== 'any' && hm.requestType !== 'void'
740
+ ? ` as ${hm.requestType}`
741
+ : '';
742
+ lines.push(` const data = await ${serviceName}.${hm.name}(deleteEmptyValue(${paramVar})${requestType});`);
743
+ }
744
+ }
745
+
746
+ if (hm.isListMethod) {
747
+ // 목록: data.ds 사용
748
+ const listName = hm.listName || 'list';
749
+ const responseField = listName === 'list' ? 'ds' : 'ds'; // 항상 ds 사용
750
+ lines.push(` set${capitalize(listName)}(data.${responseField});`);
751
+
752
+ // 페이징 처리 (hasPage: true인 경우만)
753
+ if (hm.hasPage) {
754
+ const pageName = listName === 'list' ? 'page' : `${listName}Page`;
755
+ const setPageName = listName === 'list' ? 'setPage' : `set${capitalize(listName)}Page`;
756
+ lines.push(` ${setPageName}({`);
757
+ lines.push(` totalPages: data.page.pageCount,`);
758
+ lines.push(` totalElements: data.page.totalCount,`);
759
+ lines.push(` currentPage: data.page.pageNumber,`);
760
+ lines.push(` pageSize: data.page.pageSize,`);
761
+ lines.push(` });`);
762
+ }
763
+ } else {
764
+ // 단건: data.rs 사용
765
+ const detailName = hm.detailName || 'detail';
766
+ lines.push(` set${capitalize(detailName)}(data.rs);`);
767
+ }
768
+
769
+ lines.push(' } catch (err) {');
770
+ lines.push(' await errorHandling(err);');
771
+ lines.push(' } finally {');
772
+
773
+ if (hm.isListMethod) {
774
+ const listName = hm.listName || 'list';
775
+ lines.push(` set${capitalize(listName)}Spinning(false);`);
776
+ } else {
777
+ const detailName = hm.detailName || 'detail';
778
+ lines.push(` set${capitalize(detailName)}Spinning(false);`);
779
+ }
780
+
781
+ lines.push(' }');
782
+ lines.push(' }, []);');
783
+ lines.push('');
784
+ }
785
+
786
+ // ===== 옵션: 자동 초기화 (주석 처리) =====
787
+ lines.push(' // ===== 옵션: 자동 초기화 (필요시 주석 해제) =====');
788
+ lines.push(' // React.useEffect(() => {');
789
+ lines.push(' // (async () => {');
790
+ if (hookMethods.length > 0 && hookMethods[0].isListMethod) {
791
+ lines.push(` // await ${hookMethods[0].functionName}(params);`);
792
+ }
793
+ lines.push(' // })();');
794
+ lines.push(' // }, [params]);');
795
+ lines.push(' // ===============================================');
796
+ lines.push('');
797
+
798
+ // ===== return =====
799
+ lines.push(' return {');
800
+
801
+ for (const hm of hookMethods) {
802
+ if (hm.isListMethod) {
803
+ lines.push(` ${hm.listName},`);
804
+ // 페이징 (hasPage: true인 경우만)
805
+ if (hm.hasPage) {
806
+ const pageName = hm.listName === 'list' ? 'page' : `${hm.listName}Page`;
807
+ lines.push(` ${pageName},`);
808
+ }
809
+ lines.push(` ${hm.listName}Spinning,`);
810
+ lines.push(` ${hm.functionName},`);
811
+ } else {
812
+ lines.push(` ${hm.detailName},`);
813
+ lines.push(` ${hm.detailName}Spinning,`);
814
+ lines.push(` ${hm.functionName},`);
815
+ }
816
+ }
817
+
818
+ lines.push(' };');
819
+ lines.push('}');
820
+
821
+ return lines.join('\n');
822
+ }
823
+
824
+ /**
825
+ * 첫 글자 대문자화
826
+ */
827
+ function capitalize(str: string): string {
828
+ if (!str) return str;
829
+ return str.charAt(0).toUpperCase() + str.slice(1);
830
+ }
831
+
832
+ /**
833
+ * 훅 생성 (타입 체크 포함)
834
+ */
835
+ export async function generateHook(args: {
836
+ interfacePath: string;
837
+ outputPath?: string;
838
+ }) {
839
+ const { interfacePath, outputPath } = args;
840
+
841
+ try {
842
+ // 1. 인터페이스 분석
843
+ const analyzeResult = await analyzeInterface({ interfacePath });
844
+ const analyzeData = JSON.parse(analyzeResult.content[0].text);
845
+
846
+ if (!analyzeData.success) {
847
+ return {
848
+ content: [{
849
+ type: 'text',
850
+ text: JSON.stringify({
851
+ success: false,
852
+ error: analyzeData.error || '인터페이스 분석 실패',
853
+ }),
854
+ }],
855
+ };
856
+ }
857
+
858
+ const analysis: InterfaceAnalysis = analyzeData;
859
+
860
+ // 2. 기본 경로 설정
861
+ const entityName = analysis.entityName || analysis.className?.replace('Repository', '') || 'Generated';
862
+ const finalPath = outputPath || path.join('src', 'hooks', `use${entityName}Service.ts`);
863
+
864
+ // 3. 훅 코드 생성
865
+ const hookCode = await generateHookCode(analysis, interfacePath);
866
+ await fs.writeFile(finalPath, hookCode, 'utf-8');
867
+
868
+ // 4. 타입 체크 (최대 3회 시도)
869
+ const maxRetries = 3;
870
+ let typeErrors: string[] = [];
871
+ let aiFixed = false;
872
+
873
+ for (let i = 0; i < maxRetries; i++) {
874
+ const result = await typeCheck(finalPath);
875
+
876
+ if (result.success) {
877
+ break;
878
+ }
879
+
880
+ typeErrors = result.errors || [];
881
+
882
+ if (i === maxRetries - 1) {
883
+ break;
884
+ }
885
+
886
+ // 1차 시도: AI 자동 수정
887
+ if (i === 0) {
888
+ console.log(`[AI 자동 수정] 타입 오류 ${typeErrors.length}건`);
889
+ // TODO: AI 기반 수정 로직
890
+ aiFixed = true;
891
+ }
892
+ }
893
+
894
+ // 5. Lint 및 Prettier
895
+ await runLintAndPrettier(finalPath);
896
+
897
+ // 6. index.ts 자동 생성 및 export 추가
898
+ const outputDir = path.dirname(finalPath);
899
+ const indexPath = path.join(outputDir, 'index.ts');
900
+
901
+ try {
902
+ let indexContent = '';
903
+ try {
904
+ indexContent = await fs.readFile(indexPath, 'utf-8');
905
+ } catch {
906
+ // 파일이 없으면 빈 내용으로 생성
907
+ }
908
+
909
+ const fileName = path.basename(finalPath, '.ts');
910
+ const exportLine = `export * from './${fileName}';`;
911
+
912
+ // 이미 export가 있는지 확인
913
+ if (!indexContent.includes(exportLine)) {
914
+ if (indexContent.trim() && !indexContent.trim().endsWith('\n')) {
915
+ indexContent += '\n';
916
+ }
917
+ indexContent += exportLine + '\n';
918
+ await fs.writeFile(indexPath, indexContent, 'utf-8');
919
+ }
920
+ } catch {
921
+ // index.ts 생성 실패는 무시
922
+ }
923
+
924
+ // 7. 결과 반환
925
+ return {
926
+ content: [{
927
+ type: 'text',
928
+ text: JSON.stringify({
929
+ success: true,
930
+ message: typeErrors.length > 0
931
+ ? `Hook 파일 생성 완료 (타입 오류 ${typeErrors.length}건${aiFixed ? ', AI 수정 후 실패' : ''})`
932
+ : 'Hook 파일 생성 완료 (타입 체크 통과)',
933
+ path: finalPath,
934
+ typeErrors: typeErrors.length > 0 ? typeErrors : undefined,
935
+ aiFixed: aiFixed,
936
+ }),
937
+ }],
938
+ };
939
+ } catch (error) {
940
+ return {
941
+ content: [{
942
+ type: 'text',
943
+ text: JSON.stringify({
944
+ success: false,
945
+ error: error instanceof Error ? error.message : String(error),
946
+ }),
947
+ }],
948
+ };
949
+ }
950
+ }