@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,840 @@
1
+ import { promises as fs } from 'fs';
2
+ import * as path from 'path';
3
+ import { glob } from 'glob';
4
+ import { analyzeInterface, InterfaceAnalysis } from './analyze.js';
5
+ import { generateStore } from './generate.js';
6
+ import { generateHook } from './generate-hook.js';
7
+
8
+ /**
9
+ * Repository 접미사 키워드 (입력값에서 Entity명 추출용)
10
+ */
11
+ const REPOSITORY_PATTERNS = [
12
+ /([A-Z][a-zA-Z]+)Repository/, // MemberRepository → Member
13
+ /([a-z][a-zA-Z]*)(?:Repository|Repo)/, // memberRepository → member
14
+ /(\w+?)리포/, // 회원리포 → 회원
15
+ /(\w+?)저장소/, // 회원저장소 → 회원
16
+ ];
17
+
18
+ /**
19
+ * 입력값에서 검색 패턴 생성
20
+ * 항상 *Repository.ts 패턴으로 검색한 후, 입력값에 맞는 구체적 패턴 추가
21
+ */
22
+ export function buildSearchPatternsFromInput(input: string): string[] {
23
+ const patterns: string[] = [];
24
+
25
+ // 1. 기본 패턴: 모든 Repository 파일 검색
26
+ patterns.push('**/*Repository.ts');
27
+
28
+ // 2. 입력값에서 가능한 키워드 추출 (영문, 한글 모두 지원)
29
+ const keywords = extractKeywordsFromInput(input);
30
+
31
+ // 3. 키워드를 포함하는 Repository 파일 검색
32
+ for (const keyword of keywords) {
33
+ // PascalCase (예: Coupon)
34
+ const pascalCase = keyword.charAt(0).toUpperCase() + keyword.slice(1).toLowerCase();
35
+ patterns.push(`**/*${pascalCase}*Repository.ts`);
36
+
37
+ // camelCase (예: coupon)
38
+ const camelCase = keyword.toLowerCase();
39
+ patterns.push(`**/*${camelCase}*Repository.ts`);
40
+
41
+ // 대문자만 (예: COUPON)
42
+ const upperCase = keyword.toUpperCase();
43
+ patterns.push(`**/*${upperCase}*Repository.ts`);
44
+ }
45
+
46
+ // 중복 제거
47
+ return [...new Set(patterns)];
48
+ }
49
+
50
+ /**
51
+ * 입력값에서 키워드 추출 (영문, 한글, 특수문자 처리)
52
+ */
53
+ function extractKeywordsFromInput(input: string): string[] {
54
+ const keywords: string[] = [];
55
+
56
+ // 1. Repository 접미사 제거
57
+ const withoutRepo = input.replace(/Repository|리포|저장소/gi, '');
58
+
59
+ // 2. 영문 키워드 추출
60
+ const englishMatches = withoutRepo.match(/[a-zA-Z]+/g);
61
+ if (englishMatches) {
62
+ keywords.push(...englishMatches);
63
+ }
64
+
65
+ // 3. 한글 키워드 추출
66
+ const koreanMatches = withoutRepo.match(/[가-힣]+/g);
67
+ if (koreanMatches) {
68
+ keywords.push(...koreanMatches);
69
+ }
70
+
71
+ // 4. 입력값 전체도 추가 (예: "쿠폰")
72
+ if (withoutRepo.trim().length > 0) {
73
+ keywords.push(withoutRepo.trim());
74
+ }
75
+
76
+ return [...new Set(keywords)];
77
+ }
78
+
79
+ /**
80
+ * 입력값에서 가능한 Repository 이름들 추출 (AI 추론 보조용)
81
+ */
82
+ export function extractPossibleRepositoryNames(input: string): {
83
+ originalInput: string;
84
+ possibleNames: string[];
85
+ searchPatterns: string[];
86
+ keywords: string[];
87
+ } {
88
+ const patterns = buildSearchPatternsFromInput(input);
89
+ const keywords = extractKeywordsFromInput(input);
90
+
91
+ // 패턴에서 가능한 Entity명 추출
92
+ const entityNames: string[] = [];
93
+
94
+ for (const pattern of patterns) {
95
+ // **/MemberRepository.ts → Member
96
+ const match = pattern.match(/\/\*?\*?\/?([A-Z][a-zA-Z]+)Repository/);
97
+ if (match) {
98
+ entityNames.push(match[1]);
99
+ }
100
+ // **/*member*.ts → member
101
+ const wildcardMatch = pattern.match(/\*?\/?\*([a-zA-Z]+)\*/);
102
+ if (wildcardMatch) {
103
+ entityNames.push(wildcardMatch[1]);
104
+ }
105
+ }
106
+
107
+ return {
108
+ originalInput: input,
109
+ possibleNames: [...new Set(entityNames)],
110
+ searchPatterns: patterns,
111
+ keywords: [...new Set(keywords)]
112
+ };
113
+ }
114
+
115
+ /**
116
+ * 사용자 선택 타입
117
+ */
118
+ export interface UserSelections {
119
+ /** 선택한 파일 인덱스 (1부터 시작) */
120
+ fileIndex?: number;
121
+ /** Store 경로 (null이면 제안 경로 사용) */
122
+ storePath?: string | null;
123
+ /** Hook 경로 (null이면 제안 경로 사용) */
124
+ hookPath?: string | null;
125
+ /** 경로 확인 완료 플래그 (true면 생성 진행) */
126
+ confirmed?: boolean;
127
+ }
128
+
129
+ /**
130
+ * 대화형 응답
131
+ */
132
+ export interface InteractiveResponse {
133
+ success: boolean;
134
+ status: 'complete' | 'need_file_selection' | 'need_path_confirmation' | 'error';
135
+
136
+ // 오류 시
137
+ error?: string;
138
+
139
+ // 자연어 분석 정보
140
+ naturalLanguageInfo?: {
141
+ originalInput: string;
142
+ detectedKeywords: string[];
143
+ matchedCandidates: string[];
144
+ };
145
+
146
+ // 파일 선택 필요 시
147
+ fileCandidates?: FileCandidate[];
148
+
149
+ // 경로 확인 필요 시
150
+ pathSuggestions?: PathSuggestions;
151
+ selectedFileInfo?: SelectedFileInfo;
152
+
153
+ // 완료 시
154
+ result?: GenerationResult;
155
+ }
156
+
157
+ export interface FileCandidate {
158
+ index: number;
159
+ path: string;
160
+ relativePath: string;
161
+ summary: string;
162
+ methodCount: number;
163
+ entityName: string;
164
+ commentMatch?: {
165
+ hasMatch: boolean;
166
+ matchedText?: string;
167
+ matchReason?: string;
168
+ };
169
+ }
170
+
171
+ export interface PathSuggestions {
172
+ storePath: string;
173
+ hookPath: string;
174
+ repositoryPath: string;
175
+ }
176
+
177
+ export interface SelectedFileInfo {
178
+ fileName: string;
179
+ entityName: string;
180
+ methodCount: number;
181
+ }
182
+
183
+ export interface GenerationResult {
184
+ storePath: string;
185
+ hookPath: string;
186
+ storeStatus: string;
187
+ hookStatus: string;
188
+ storeErrors?: string[];
189
+ hookErrors?: string[];
190
+ }
191
+
192
+ /**
193
+ * 대화형 Store와 Hook 생성
194
+ *
195
+ * 단계:
196
+ * 1. 입력값 분석 → 검색 패턴 생성 (AI 추론 지원)
197
+ * 2. 파일 검색 → 파일 후보 목록 반환
198
+ * 3. 파일 선택 → 경로 제안 반환
199
+ * 4. 경로 확인 → 생성 완료
200
+ *
201
+ * AI가 입력값을 해석하고 적절한 Repository 이름을 추론하여
202
+ * fileName으로 전달하면, MCP는 다양한 패턴으로 검색을 시도합니다.
203
+ */
204
+ export async function generateStoreAndHookInteractive(args: {
205
+ /** 파일명 또는 자연어 (예: MemberRepository, member, 회원) */
206
+ fileName: string;
207
+ /** 사용자 선택 (선택사항) */
208
+ userSelections?: UserSelections;
209
+ /** 프로젝트 루트 경로 (선택사항, 기본: 현재 작업 디렉토리) */
210
+ projectRoot?: string;
211
+ }): Promise<{ content: Array<{ type: string; text: string }> }> {
212
+ const { fileName, userSelections, projectRoot = process.cwd() } = args;
213
+
214
+ try {
215
+ // 1단계: 입력값 분석 및 검색 패턴 생성
216
+ const extracted = extractPossibleRepositoryNames(fileName);
217
+
218
+ console.log(`[입력 분석] "${extracted.originalInput}"`);
219
+ console.log(`[검색 패턴] ${extracted.searchPatterns.slice(0, 3).join(', ')}${extracted.searchPatterns.length > 3 ? '...' : ''}`);
220
+
221
+ // 2단계: 파일 검색
222
+ const searchResult = await findRepositoryFiles(extracted.searchPatterns, projectRoot, extracted);
223
+
224
+ if (searchResult.status === 'not_found') {
225
+ return createResponse({
226
+ success: false,
227
+ status: 'error',
228
+ error: `"${fileName}"에 해당하는 Repository 파일을 찾을 수 없습니다.
229
+
230
+ ${searchResult.suggestions || '파일명을 확인하거나 전체 경로를 입력해주세요.'}`,
231
+ naturalLanguageInfo: {
232
+ originalInput: extracted.originalInput,
233
+ detectedKeywords: [],
234
+ matchedCandidates: extracted.possibleNames
235
+ }
236
+ });
237
+ }
238
+
239
+ // 여러 파일 발견된 경우 (단일 파일도 포함 - 항상 사용자 확인 필요)
240
+ if (searchResult.status === 'multiple_candidates' || searchResult.status === 'found') {
241
+ const candidates = searchResult.status === 'found'
242
+ ? await analyzeCandidates([searchResult.path!], extracted.keywords || [])
243
+ : searchResult.candidates!;
244
+
245
+ // 사용자가 아직 선택하지 않음
246
+ if (!userSelections?.fileIndex) {
247
+ return createResponse({
248
+ success: true,
249
+ status: 'need_file_selection',
250
+ naturalLanguageInfo: {
251
+ originalInput: extracted.originalInput,
252
+ detectedKeywords: [],
253
+ matchedCandidates: extracted.possibleNames
254
+ },
255
+ fileCandidates: candidates.map((c, i) => ({
256
+ index: i + 1,
257
+ path: c.path,
258
+ relativePath: path.relative(projectRoot, c.path),
259
+ summary: formatFileSummary(c),
260
+ methodCount: c.methodCount || 0,
261
+ entityName: c.entityName || 'Unknown'
262
+ }))
263
+ });
264
+ }
265
+
266
+ // 사용자 선택 확인
267
+ const selected = candidates[userSelections.fileIndex - 1];
268
+ if (!selected) {
269
+ return createResponse({
270
+ success: false,
271
+ status: 'error',
272
+ error: `잘못된 선택입니다. 1부터 ${candidates.length} 사이의 번호를 입력해주세요.`
273
+ });
274
+ }
275
+
276
+ return handleSingleFile(selected.path, projectRoot, userSelections);
277
+ }
278
+
279
+ return createResponse({
280
+ success: false,
281
+ status: 'error',
282
+ error: '알 수 없는 오류가 발생했습니다.'
283
+ });
284
+
285
+ } catch (error) {
286
+ return createResponse({
287
+ success: false,
288
+ status: 'error',
289
+ error: error instanceof Error ? error.message : String(error)
290
+ });
291
+ }
292
+ }
293
+
294
+ /**
295
+ * 단일 파일 처리
296
+ */
297
+ async function handleSingleFile(
298
+ filePath: string,
299
+ projectRoot: string,
300
+ userSelections?: UserSelections,
301
+ extracted?: { originalInput: string; possibleNames: string[]; searchPatterns: string[] }
302
+ ): Promise<{ content: Array<{ type: string; text: string }> }> {
303
+
304
+ // 1. 인터페이스 분석
305
+ const analyzeResult = await analyzeInterface({ interfacePath: filePath });
306
+ const analyzeData = JSON.parse(analyzeResult.content[0].text);
307
+
308
+ if (!analyzeData.success) {
309
+ return createResponse({
310
+ success: false,
311
+ status: 'error',
312
+ error: `인터페이스 분석 실패: ${analyzeData.error || '알 수 없는 오류'}`
313
+ });
314
+ }
315
+
316
+ const analysis: InterfaceAnalysis = analyzeData;
317
+ const entityName = analysis.entityName || analysis.className?.replace('Repository', '') || 'Generated';
318
+ const repositoryPath = path.relative(projectRoot, filePath);
319
+
320
+ // 2. 경로 제안 (프로젝트 스캔 기반)
321
+ const suggestions = await suggestPaths(filePath, projectRoot, entityName);
322
+
323
+ // 사용자가 명시적으로 경로를 지정했거나 확인했는지 확인
324
+ const hasExplicitPaths = userSelections?.storePath !== undefined || userSelections?.hookPath !== undefined;
325
+ const isConfirmed = userSelections?.confirmed === true;
326
+
327
+ // 경로 제안이 필요한 경우: confirmed도 false고 명시적 경로도 없는 경우에만 제안 표시
328
+ // 사용자가 storePath/hookPath를 지정하면 confirmed가 없어도 즉시 생성
329
+ if (!isConfirmed && !hasExplicitPaths) {
330
+ return createResponse({
331
+ success: true,
332
+ status: 'need_path_confirmation',
333
+ pathSuggestions: suggestions,
334
+ selectedFileInfo: {
335
+ fileName: path.basename(filePath),
336
+ entityName,
337
+ methodCount: analysis.methods?.length || 0
338
+ },
339
+ naturalLanguageInfo: extracted ? {
340
+ originalInput: extracted.originalInput,
341
+ detectedKeywords: [],
342
+ matchedCandidates: extracted.possibleNames
343
+ } : undefined
344
+ });
345
+ }
346
+
347
+ // 3. 경로 결정
348
+ const finalStorePath = userSelections.storePath === null
349
+ ? suggestions.storePath
350
+ : (userSelections.storePath || suggestions.storePath);
351
+
352
+ const finalHookPath = userSelections.hookPath === null
353
+ ? suggestions.hookPath
354
+ : (userSelections.hookPath || suggestions.hookPath);
355
+
356
+ // 4. Store 생성
357
+ const storeResult = await generateStore({
358
+ interfacePath: filePath,
359
+ outputPath: finalStorePath,
360
+ storeType: 1,
361
+ storeName: `use${entityName}ListStore`
362
+ });
363
+
364
+ const storeData = JSON.parse(storeResult.content[0].text);
365
+
366
+ // 5. Hook 생성
367
+ const hookResult = await generateHook({
368
+ interfacePath: filePath,
369
+ outputPath: finalHookPath
370
+ });
371
+
372
+ const hookData = JSON.parse(hookResult.content[0].text);
373
+
374
+ // 6. 결과
375
+ if (!storeData.success || !hookData.success) {
376
+ return createResponse({
377
+ success: false,
378
+ status: 'error',
379
+ error: `Store 또는 Hook 생성 실패:\n${storeData.error || ''}\n${hookData.error || ''}`
380
+ });
381
+ }
382
+
383
+ return createResponse({
384
+ success: true,
385
+ status: 'complete',
386
+ result: {
387
+ storePath: finalStorePath,
388
+ hookPath: finalHookPath,
389
+ storeStatus: storeData.typeErrors
390
+ ? `Store: 타입 오류 ${storeData.typeErrors.length}건`
391
+ : 'Store: 타입 체크 통과',
392
+ hookStatus: hookData.typeErrors
393
+ ? `Hook: 타입 오류 ${hookData.typeErrors.length}건`
394
+ : 'Hook: 타입 체크 통과',
395
+ storeErrors: storeData.typeErrors,
396
+ hookErrors: hookData.typeErrors
397
+ }
398
+ });
399
+ }
400
+
401
+ /**
402
+ * Repository 파일 검색
403
+ */
404
+ async function findRepositoryFiles(
405
+ searchPatterns: string[],
406
+ projectRoot: string,
407
+ extracted?: { originalInput: string; possibleNames: string[]; searchPatterns: string[]; keywords: string[] }
408
+ ) {
409
+ const allFiles: string[] = [];
410
+
411
+ for (const pattern of searchPatterns) {
412
+ const files = await glob(pattern, {
413
+ cwd: projectRoot,
414
+ absolute: true,
415
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**']
416
+ });
417
+ allFiles.push(...files);
418
+ }
419
+
420
+ // 중복 제거
421
+ const uniqueFiles = [...new Set(allFiles)];
422
+
423
+ if (uniqueFiles.length === 0) {
424
+ // 분석 결과가 있으면 도움말 제공
425
+ let suggestions = '';
426
+ if (extracted && extracted.possibleNames.length > 0) {
427
+ suggestions = `입력값: "${extracted.originalInput}"\n`;
428
+ suggestions += `가능한 Repository: ${extracted.possibleNames.join(', ')}\n`;
429
+ if (extracted.keywords && extracted.keywords.length > 0) {
430
+ suggestions += `검색 키워드: ${extracted.keywords.join(', ')}\n`;
431
+ }
432
+ suggestions += `\n검색한 패턴 (처음 3개):\n`;
433
+ suggestions += extracted.searchPatterns.slice(0, 3).map(p => `- ${p}`).join('\n');
434
+ suggestions += `\n\n파일을 찾을 수 없습니다. 파일명을 확인하거나 전체 경로를 입력해주세요.`;
435
+ } else {
436
+ suggestions = `검색 패턴:\n`;
437
+ suggestions += searchPatterns.slice(0, 5).map(p => `- ${p}`).join('\n');
438
+ suggestions += `\n\n전체 경로를 입력하거나 파일명을 확인해주세요.`;
439
+ }
440
+
441
+ return {
442
+ status: 'not_found',
443
+ suggestions
444
+ };
445
+ }
446
+
447
+ if (uniqueFiles.length === 1) {
448
+ return {
449
+ status: 'found',
450
+ path: uniqueFiles[0]
451
+ };
452
+ }
453
+
454
+ // 여러 파일 발견: 추가 분석 (키워드 전달)
455
+ const keywords = extracted?.keywords || [];
456
+ const candidates = await analyzeCandidates(uniqueFiles, keywords);
457
+
458
+ return {
459
+ status: 'multiple_candidates',
460
+ candidates
461
+ };
462
+ }
463
+
464
+ /**
465
+ * 파일에서 주석 추출 및 키워드 일치 검색
466
+ */
467
+ function searchCommentsInFile(content: string, keywords: string[]): {
468
+ hasMatch: boolean;
469
+ matchedText?: string;
470
+ matchReason?: string;
471
+ } {
472
+ // 주석 추출 (한 줄 주석과 여러 줄 주석 모두)
473
+ const commentPatterns = [
474
+ /\/\/.*$/gm, // 한 줄 주석
475
+ /\/\*[\s\S]*?\*\//g, // 여러 줄 주석
476
+ ];
477
+
478
+ let allComments = '';
479
+ for (const pattern of commentPatterns) {
480
+ const matches = content.match(pattern);
481
+ if (matches) {
482
+ allComments += ' ' + matches.join(' ');
483
+ }
484
+ }
485
+
486
+ // 키워드 일치 검색 (대소문자 구분 없이)
487
+ const lowerComments = allComments.toLowerCase();
488
+ const lowerContent = content.toLowerCase();
489
+
490
+ for (const keyword of keywords) {
491
+ const lowerKeyword = keyword.toLowerCase();
492
+
493
+ // 주석에서 키워드 검색
494
+ if (lowerComments.includes(lowerKeyword)) {
495
+ // 일치하는 텍스트 추출
496
+ const regex = new RegExp(`.{0,30}${keyword}.{0,30}`, 'i');
497
+ const match = allComments.match(regex);
498
+ const matchedText = match ? match[0].trim() : keyword;
499
+
500
+ return {
501
+ hasMatch: true,
502
+ matchedText,
503
+ matchReason: '주석에서 키워드 발견'
504
+ };
505
+ }
506
+
507
+ // 전체 내용에서 키워드 검색 (주석뿐만 아니라 클래스명, 메서드명 등도 포함)
508
+ if (lowerContent.includes(lowerKeyword)) {
509
+ return {
510
+ hasMatch: true,
511
+ matchedText: keyword,
512
+ matchReason: '파일 내용에서 키워드 발견'
513
+ };
514
+ }
515
+ }
516
+
517
+ return { hasMatch: false };
518
+ }
519
+
520
+ /**
521
+ * 후보 파일 분석 (주석 검색 포함)
522
+ */
523
+ async function analyzeCandidates(filePaths: string[], inputKeywords: string[]) {
524
+ const candidates = await Promise.all(
525
+ filePaths.map(async (filePath) => {
526
+ try {
527
+ const content = await fs.readFile(filePath, 'utf-8');
528
+
529
+ // 간단한 분석: 메서드 수 추정
530
+ const methodMatches = content.match(/async (post|get|put|delete)\w*\(/g);
531
+ const methodCount = methodMatches?.length || 0;
532
+
533
+ // entityName 추정
534
+ const entityMatch = path.basename(filePath).match(/(\w+)Repository/);
535
+ const entityName = entityMatch ? entityMatch[1] : 'Unknown';
536
+
537
+ // 주석 검색
538
+ const commentMatch = searchCommentsInFile(content, inputKeywords);
539
+
540
+ return {
541
+ path: filePath,
542
+ methodCount,
543
+ entityName,
544
+ commentMatch
545
+ };
546
+ } catch {
547
+ return {
548
+ path: filePath,
549
+ methodCount: 0,
550
+ entityName: 'Unknown',
551
+ commentMatch: { hasMatch: false }
552
+ };
553
+ }
554
+ })
555
+ );
556
+
557
+ // 정렬 우선순위:
558
+ // 1. 주석 일치 (hasMatch: true)
559
+ // 2. 파일명 일치 (entityName이 키워드와 유사한지)
560
+ // 3. 메서드 수
561
+ return candidates.sort((a, b) => {
562
+ // 주석 일치 우선
563
+ if (a.commentMatch?.hasMatch && !b.commentMatch?.hasMatch) {
564
+ return -1;
565
+ }
566
+ if (!a.commentMatch?.hasMatch && b.commentMatch?.hasMatch) {
567
+ return 1;
568
+ }
569
+
570
+ // 둘 다 일치하거나 둘 다 불일치하면 파일명 유사도 확인
571
+ const aSimilarity = calculateSimilarity(a.entityName.toLowerCase(), inputKeywords.join(' ').toLowerCase());
572
+ const bSimilarity = calculateSimilarity(b.entityName.toLowerCase(), inputKeywords.join(' ').toLowerCase());
573
+
574
+ if (aSimilarity !== bSimilarity) {
575
+ return bSimilarity - aSimilarity; // 유사도 높은 것 우선
576
+ }
577
+
578
+ // 그 다음 메서드 수
579
+ if (a.entityName === b.entityName) {
580
+ return b.methodCount - a.methodCount;
581
+ }
582
+
583
+ return a.entityName.localeCompare(b.entityName);
584
+ });
585
+ }
586
+
587
+ /**
588
+ * 문자열 유사도 계산 (간단한 Levenshtein distance 기반)
589
+ */
590
+ function calculateSimilarity(str1: string, str2: string): number {
591
+ const longer = str1.length > str2.length ? str1 : str2;
592
+ const shorter = str1.length > str2.length ? str2 : str1;
593
+
594
+ if (longer.length === 0) {
595
+ return 1.0;
596
+ }
597
+
598
+ const editDistance = levenshteinDistance(longer, shorter);
599
+ return (longer.length - editDistance) / longer.length;
600
+ }
601
+
602
+ /**
603
+ * Levenshtein distance 계산
604
+ */
605
+ function levenshteinDistance(str1: string, str2: string): number {
606
+ const matrix = [];
607
+
608
+ for (let i = 0; i <= str2.length; i++) {
609
+ matrix[i] = [i];
610
+ }
611
+
612
+ for (let j = 0; j <= str1.length; j++) {
613
+ matrix[0][j] = j;
614
+ }
615
+
616
+ for (let i = 1; i <= str2.length; i++) {
617
+ for (let j = 1; j <= str1.length; j++) {
618
+ if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
619
+ matrix[i][j] = matrix[i - 1][j - 1];
620
+ } else {
621
+ matrix[i][j] = Math.min(
622
+ matrix[i - 1][j - 1] + 1,
623
+ matrix[i][j - 1] + 1,
624
+ matrix[i - 1][j] + 1
625
+ );
626
+ }
627
+ }
628
+ }
629
+
630
+ return matrix[str2.length][str1.length];
631
+ }
632
+
633
+ /**
634
+ * 프로젝트에서 기존 Store/Hook 파일이 있는 디렉토리 찾기
635
+ * @param projectRoot 프로젝트 루트 경로
636
+ * @param fileType 'store' 또는 'hook'
637
+ * @returns 찾은 디렉토리 경로 (절대 경로), 없으면 null
638
+ */
639
+ async function findExistingDirectories(
640
+ projectRoot: string,
641
+ fileType: 'store' | 'hook'
642
+ ): Promise<string | null> {
643
+ try {
644
+ // 파일 패턴 정의
645
+ const filePatterns = fileType === 'store'
646
+ ? ['**/*Store.ts'] // Store만 찾기 (use*.ts 제거 - Hook과 혼동 방지)
647
+ : ['**/use*Service.ts', '**/use*.ts'];
648
+
649
+ // 디렉토리 후보 수집
650
+ const dirCandidates = new Map<string, number>(); // 경로 -> 파일 수
651
+
652
+ for (const pattern of filePatterns) {
653
+ const files = await glob(pattern, {
654
+ cwd: projectRoot,
655
+ absolute: true,
656
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**', '**/test/**'],
657
+ });
658
+
659
+ // 파일이 있는 디렉토리 수집
660
+ for (const file of files) {
661
+ const dir = path.dirname(file);
662
+ const relativePath = path.relative(projectRoot, dir);
663
+ const depth = relativePath.split(path.sep).length;
664
+
665
+ // depth가 2-5 사이인 것만 (너무 깊거나 얕은 것 제외)
666
+ if (depth >= 1 && depth <= 5) {
667
+ dirCandidates.set(dir, (dirCandidates.get(dir) || 0) + 1);
668
+ }
669
+ }
670
+ }
671
+
672
+ if (dirCandidates.size === 0) {
673
+ return null;
674
+ }
675
+
676
+ // 파일 수가 가장 많은 디렉토리 반환
677
+ const sortedDirs = Array.from(dirCandidates.entries())
678
+ .sort((a, b) => b[1] - a[1]); // 파일 수 내림차순
679
+
680
+ return sortedDirs[0][0];
681
+ } catch {
682
+ // 실패 시 null 반환
683
+ }
684
+ return null;
685
+ }
686
+
687
+ /**
688
+ * 경로 제안 (프로젝트 스캔 기반)
689
+ */
690
+ async function suggestPaths(
691
+ repositoryPath: string,
692
+ projectRoot: string,
693
+ entityName: string
694
+ ): Promise<PathSuggestions> {
695
+ // 1. Repository 경로 분석
696
+ const repoDir = path.dirname(repositoryPath);
697
+
698
+ // 2. 기존 stores 폴더 찾기
699
+ const existingStoresDir = await findExistingDirectories(projectRoot, 'store');
700
+
701
+ // 3. 기존 hooks 폴더 찾기
702
+ const existingHooksDir = await findExistingDirectories(projectRoot, 'hook');
703
+
704
+ // 4. Store 경로 결정
705
+ let storePath: string;
706
+ if (existingStoresDir) {
707
+ storePath = path.join(existingStoresDir, `use${entityName}ListStore.ts`);
708
+ } else {
709
+ // 기본 경로: Repository의 상위 폴더 기준
710
+ // src/services/repository/member/MemberRepository.ts → src/stores/
711
+ const srcDir = repoDir.split(path.sep).includes('src')
712
+ ? repoDir.split(path.sep).slice(0, repoDir.split(path.sep).indexOf('src') + 1).join(path.sep)
713
+ : path.dirname(repoDir);
714
+ storePath = path.join(srcDir, 'stores', `use${entityName}ListStore.ts`);
715
+ }
716
+
717
+ // 5. Hook 경로 결정
718
+ let hookPath: string;
719
+ if (existingHooksDir) {
720
+ hookPath = path.join(existingHooksDir, `use${entityName}Service.ts`);
721
+ } else {
722
+ // 기본 경로
723
+ const srcDir = repoDir.split(path.sep).includes('src')
724
+ ? repoDir.split(path.sep).slice(0, repoDir.split(path.sep).indexOf('src') + 1).join(path.sep)
725
+ : path.dirname(repoDir);
726
+ hookPath = path.join(srcDir, 'hooks', `use${entityName}Service.ts`);
727
+ }
728
+
729
+ return {
730
+ storePath: path.relative(projectRoot, storePath),
731
+ hookPath: path.relative(projectRoot, hookPath),
732
+ repositoryPath: path.relative(projectRoot, repositoryPath)
733
+ };
734
+ }
735
+
736
+ /**
737
+ * 파일 요약 포맷 (주석 일치 정보 포함)
738
+ */
739
+ function formatFileSummary(candidate: {
740
+ path: string;
741
+ methodCount?: number;
742
+ entityName?: string;
743
+ commentMatch?: { hasMatch: boolean; matchedText?: string; matchReason?: string };
744
+ }): string {
745
+ const fileName = path.basename(candidate.path);
746
+ const methodCount = candidate.methodCount || 0;
747
+ const entityName = candidate.entityName || 'Unknown';
748
+ const commentMatch = candidate.commentMatch;
749
+
750
+ let summary = `${fileName} (${entityName}, ${methodCount}개 API)`;
751
+
752
+ // 주석 일치 정보 추가
753
+ if (commentMatch?.hasMatch) {
754
+ const icon = commentMatch.matchReason === '주석에서 키워드 발견' ? '💡' : '🔍';
755
+ summary += ` ${icon}`;
756
+ if (commentMatch.matchedText) {
757
+ summary += ` - "${commentMatch.matchedText}"`;
758
+ }
759
+ }
760
+
761
+ return summary;
762
+ }
763
+
764
+ /**
765
+ * 응답 생성
766
+ */
767
+ function createResponse(response: InteractiveResponse): { content: Array<{ type: string; text: string }> } {
768
+ return {
769
+ content: [{
770
+ type: 'text',
771
+ text: formatInteractiveResponse(response)
772
+ }]
773
+ };
774
+ }
775
+
776
+ /**
777
+ * 대화형 응답 포맷
778
+ */
779
+ function formatInteractiveResponse(response: InteractiveResponse): string {
780
+ const lines: string[] = [];
781
+
782
+ // 자연어 분석 헤더
783
+ if (response.naturalLanguageInfo && response.naturalLanguageInfo.detectedKeywords.length > 0) {
784
+ const info = response.naturalLanguageInfo;
785
+ lines.push('🔍 자연어 분석:');
786
+ lines.push(` "${info.originalInput}" → ${info.matchedCandidates.join(' 또는 ')}\n`);
787
+ }
788
+
789
+ if (response.status === 'complete') {
790
+ const result = response.result!;
791
+ lines.push('✅ Store 및 Hook 생성 완료\n');
792
+ lines.push(`Store: ${result.storePath}`);
793
+ lines.push(` → ${result.storeStatus}`);
794
+ lines.push(`Hook: ${result.hookPath}`);
795
+ lines.push(` → ${result.hookStatus}`);
796
+
797
+ if (result.storeErrors && result.storeErrors.length > 0) {
798
+ lines.push('\nStore 타입 오류:');
799
+ result.storeErrors.forEach(err => lines.push(` - ${err}`));
800
+ }
801
+
802
+ if (result.hookErrors && result.hookErrors.length > 0) {
803
+ lines.push('\nHook 타입 오류:');
804
+ result.hookErrors.forEach(err => lines.push(` - ${err}`));
805
+ }
806
+ }
807
+ else if (response.status === 'need_file_selection') {
808
+ lines.push('📁 발견된 Repository 파일:\n');
809
+
810
+ response.fileCandidates!.forEach(candidate => {
811
+ lines.push(` ${candidate.index}. ${candidate.summary}`);
812
+ lines.push(` 경로: ${candidate.relativePath}`);
813
+ });
814
+
815
+ lines.push('\n[중요] 위 목록에서 사용할 파일의 번호를 입력받으세요.');
816
+ lines.push('[중요] 사용자가 번호를 입력하기 전까지는 fileIndex를 설정하거나 파일을 선택하지 마세요.');
817
+ }
818
+ else if (response.status === 'need_path_confirmation') {
819
+ const info = response.selectedFileInfo!;
820
+ const paths = response.pathSuggestions!;
821
+
822
+ lines.push(`📁 선택된 파일: ${info.fileName} (${info.entityName}, ${info.methodCount}개 API)\n`);
823
+ lines.push('📂 생성 경로 제안:\n');
824
+ lines.push(` Store: ${paths.storePath}`);
825
+ lines.push(` Hook: ${paths.hookPath}\n`);
826
+ lines.push('[선택사항]\n');
827
+ lines.push('1. 제안된 경로로 생성 → "확인" 또는 "진행" 입력');
828
+ lines.push('2. 경로 변경 후 생성 → 변경할 경로 입력:');
829
+ lines.push(` - Store만: "store는 src/stores/useBannerStore.ts로"`);
830
+ lines.push(` - Hook만: "hook은 src/hooks/useBannerService.ts로"`);
831
+ lines.push(` - 둘 다: "store는 src/stores/useBannerStore.ts, hook은 src/hooks/useBannerService.ts로"`);
832
+ lines.push('\n[중요] 사용자가 "확인", "진행" 또는 경로를 입력하기 전까지는 파일을 생성하지 마세요.');
833
+ }
834
+ else if (response.status === 'error') {
835
+ lines.push('❌ 오류\n');
836
+ lines.push(response.error || '알 수 없는 오류가 발생했습니다.');
837
+ }
838
+
839
+ return lines.join('\n');
840
+ }