@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.
- package/CLAUDE.md +119 -0
- package/MCP_TOOL_PLAN.md +710 -0
- package/MCP_USAGE.md +914 -0
- package/README.md +168 -0
- package/REPOSITORY_CONVENTIONS.md +250 -0
- package/SEARCH_PARAMS_MCP_TOOL_COMPLETE_PLAN.md +646 -0
- package/SEARCH_PARAMS_PLAN.md +2570 -0
- package/STORE_PATTERNS.md +1178 -0
- package/debug-dto.js +72 -0
- package/generate-banner-store.js +62 -0
- package/generation-plan.json +2176 -0
- package/generation-results.json +1817 -0
- package/package.json +45 -0
- package/scripts/batch-generate-all.js +159 -0
- package/scripts/batch-generate-mcp.js +329 -0
- package/scripts/batch-generate-stores-v2.js +272 -0
- package/scripts/batch-generate-stores.js +179 -0
- package/scripts/batch-plan.json +3810 -0
- package/scripts/batch-process.py +90 -0
- package/scripts/batch-regenerate.js +356 -0
- package/scripts/direct-generate.js +227 -0
- package/scripts/execute-batches.js +1911 -0
- package/scripts/generate-all-stores.js +144 -0
- package/scripts/generate-stores-mcp.js +161 -0
- package/scripts/generate-stores-v2.js +450 -0
- package/scripts/generate-stores-v3.js +412 -0
- package/scripts/generate-stores-v4.js +521 -0
- package/scripts/generate-stores.js +382 -0
- package/scripts/repos-to-process.json +1899 -0
- package/src/config/nh-layout-patterns.ts +166 -0
- package/src/docs/HOOK_GENERATION_PLAN.md +2226 -0
- package/src/docs/NH_STORE_PATTERNS.md +297 -0
- package/src/docs/README.md +216 -0
- package/src/docs/index.ts +28 -0
- package/src/docs/loader.ts +568 -0
- package/src/docs/patterns.json +419 -0
- package/src/docs/practical-examples.md +732 -0
- package/src/docs/quick-start.md +257 -0
- package/src/docs/requirements-analysis-guide.md +364 -0
- package/src/docs/rules.json +321 -0
- package/src/docs/store-pattern-analysis.md +664 -0
- package/src/docs/store-patterns-rules.md +1168 -0
- package/src/docs/store-patterns-usage-guide.md +1835 -0
- package/src/docs/troubleshooting.md +544 -0
- package/src/docs/type-selection-guide.md +572 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/src/features/store-features.ts +232 -0
- package/src/handlers/analyze-requirements.ts +403 -0
- package/src/handlers/analyze.ts +1373 -0
- package/src/handlers/generate-from-requirements.ts +250 -0
- package/src/handlers/generate-hook.ts +950 -0
- package/src/handlers/generate-interactive.ts +840 -0
- package/src/handlers/generate-listdatagrid.ts +521 -0
- package/src/handlers/generate-multi-stores.ts +577 -0
- package/src/handlers/generate-requirements-from-layout.ts +160 -0
- package/src/handlers/generate-search-params.ts +717 -0
- package/src/handlers/generate.ts +911 -0
- package/src/handlers/list-templates.ts +104 -0
- package/src/handlers/scan-metadata.ts +485 -0
- package/src/handlers/suggest-layout.ts +326 -0
- package/src/index.ts +959 -0
- package/src/prompts/search-params.md +793 -0
- package/src/templates/index.ts +107 -0
- package/src/templates/unified.ts +462 -0
- package/store-generation-error-patterns.md +225 -0
- package/test/useAgentStore.ts +136 -0
- package/test-server.js +78 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { analyzeInterface, InterfaceAnalysis } from './analyze.js';
|
|
4
|
+
import { unifiedTemplate, TemplateVariables } from '../templates/index.js';
|
|
5
|
+
import { buildTemplateVariables } from './generate.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* PascalCase 변환
|
|
9
|
+
*/
|
|
10
|
+
export function toPascalCase(str: string): string {
|
|
11
|
+
return str.replace(/(^|-)(\w)/g, (_, __, char) => char.toUpperCase());
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* camelCase 변환
|
|
16
|
+
*/
|
|
17
|
+
export function toCamelCase(str: string): string {
|
|
18
|
+
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 리스트 그룹 정보
|
|
23
|
+
*/
|
|
24
|
+
export interface ListGroup {
|
|
25
|
+
name: string; // 변수명용 (예: "mainList")
|
|
26
|
+
description: string; // 설명 (예: "메인 목록")
|
|
27
|
+
features: string[]; // 필요한 피처
|
|
28
|
+
hasSearch?: boolean; // 검색 기능
|
|
29
|
+
hasExcel?: boolean; // 엑셀 다운로드
|
|
30
|
+
hasCheckbox?: boolean; // 체크박스
|
|
31
|
+
methodName?: string; // 매핑할 메서드 이름
|
|
32
|
+
requestType?: string; // Request 타입
|
|
33
|
+
responseType?: string; // Response 타입
|
|
34
|
+
dtoType?: string; // DTO 타입
|
|
35
|
+
dtoFields?: string[]; // DTO 필드 목록
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 다중 리스트 패턴 분석 결과
|
|
40
|
+
*/
|
|
41
|
+
export interface MultiListPattern {
|
|
42
|
+
isMultiList: boolean;
|
|
43
|
+
listGroups: ListGroup[];
|
|
44
|
+
baseStoreName: string;
|
|
45
|
+
// 추가 피처 감지
|
|
46
|
+
hasForm?: boolean;
|
|
47
|
+
hasTab?: boolean;
|
|
48
|
+
hasAutoLoad?: boolean; // 선택 시 자동 로드
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 다중 리스트 패턴 감지
|
|
53
|
+
*/
|
|
54
|
+
export function detectMultiListPattern(requirements: string): MultiListPattern | null {
|
|
55
|
+
const lowerReq = requirements.toLowerCase();
|
|
56
|
+
|
|
57
|
+
// 다중 리스트 키워드 감지
|
|
58
|
+
const detectedGroups: ListGroup[] = [];
|
|
59
|
+
|
|
60
|
+
// 위치 기반 그룹 감지
|
|
61
|
+
const locationPatterns = [
|
|
62
|
+
{ pattern: /왼쪽.*목록|메인.*목록|주.*목록|main.*list|master.*list|list.*list/gi, name: 'list', desc: '메인 목록' },
|
|
63
|
+
{ pattern: /상단.*목록|상위.*목록|top.*list/gi, name: 'topList', desc: '상단 목록' },
|
|
64
|
+
{ pattern: /하단.*목록|하위.*목록|bottom.*list/gi, name: 'bottomList', desc: '하단 목록' },
|
|
65
|
+
{ pattern: /오른쪽.*목록|right.*list|detail.*list|sub.*list/gi, name: 'subList', desc: '서브 목록' },
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
for (const { pattern, name, desc } of locationPatterns) {
|
|
69
|
+
if (pattern.test(lowerReq)) {
|
|
70
|
+
detectedGroups.push({
|
|
71
|
+
name,
|
|
72
|
+
description: desc,
|
|
73
|
+
features: ['LIST'],
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 2개 이상 감지되면 다중 리스트 패턴
|
|
79
|
+
if (detectedGroups.length >= 2) {
|
|
80
|
+
const pattern: MultiListPattern = {
|
|
81
|
+
isMultiList: true,
|
|
82
|
+
listGroups: detectedGroups,
|
|
83
|
+
baseStoreName: extractBaseStoreName(requirements),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// 추가 피처 감지
|
|
87
|
+
pattern.hasForm = /폼|form|저장|수정|등록|save|edit|create/gi.test(lowerReq);
|
|
88
|
+
pattern.hasTab = /탭|tab|tabbed/gi.test(lowerReq);
|
|
89
|
+
pattern.hasAutoLoad = /선택.*시.*로드|자동.*로드|auto.*load/gi.test(lowerReq);
|
|
90
|
+
|
|
91
|
+
return pattern;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 또는 "각 목록", "모든 목록" 같은 표현으로 다중 판단
|
|
95
|
+
if (/각\s*목록|모든\s*목록|각각의\s*목록/.test(lowerReq)) {
|
|
96
|
+
const pattern: MultiListPattern = {
|
|
97
|
+
isMultiList: true,
|
|
98
|
+
listGroups: [
|
|
99
|
+
{ name: 'list', description: '메인 목록', features: ['LIST'] },
|
|
100
|
+
{ name: 'subList', description: '서브 목록', features: ['LIST'] },
|
|
101
|
+
],
|
|
102
|
+
baseStoreName: extractBaseStoreName(requirements),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
pattern.hasForm = /폼|form|저장|수정|등록|save|edit|create/gi.test(lowerReq);
|
|
106
|
+
pattern.hasTab = /탭|tab|tabbed/gi.test(lowerReq);
|
|
107
|
+
pattern.hasAutoLoad = /선택.*시.*로드|자동.*로드|auto.*load/gi.test(lowerReq);
|
|
108
|
+
|
|
109
|
+
return pattern;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 각 그룹별 피처 추출
|
|
117
|
+
*/
|
|
118
|
+
function extractGroupFeatures(pattern: MultiListPattern, requirements: string): MultiListPattern {
|
|
119
|
+
const lowerReq = requirements.toLowerCase();
|
|
120
|
+
|
|
121
|
+
// 공통 기능
|
|
122
|
+
const hasSearch = lowerReq.includes('검색') || lowerReq.includes('찾기');
|
|
123
|
+
const hasExcel = lowerReq.includes('엑셀') || lowerReq.includes('excel');
|
|
124
|
+
const hasCheckbox = lowerReq.includes('체크') || lowerReq.includes('checkbox');
|
|
125
|
+
const hasPaging = lowerReq.includes('페이징') || lowerReq.includes('pagination');
|
|
126
|
+
|
|
127
|
+
for (const group of pattern.listGroups) {
|
|
128
|
+
group.features = ['LIST'];
|
|
129
|
+
|
|
130
|
+
if (hasSearch) {
|
|
131
|
+
group.features.push('SEARCH');
|
|
132
|
+
group.hasSearch = true;
|
|
133
|
+
}
|
|
134
|
+
if (hasExcel) {
|
|
135
|
+
group.features.push('EXCEL_DOWNLOAD');
|
|
136
|
+
group.hasExcel = true;
|
|
137
|
+
}
|
|
138
|
+
if (hasCheckbox) {
|
|
139
|
+
group.features.push('CHECKBOX');
|
|
140
|
+
group.hasCheckbox = true;
|
|
141
|
+
}
|
|
142
|
+
if (hasPaging) {
|
|
143
|
+
group.features.push('PAGING');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return pattern;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Store 이름 추출
|
|
152
|
+
*/
|
|
153
|
+
function extractBaseStoreName(requirements: string): string {
|
|
154
|
+
const match = requirements.match(/(\w+)Repository/i);
|
|
155
|
+
if (match) {
|
|
156
|
+
return match[1];
|
|
157
|
+
}
|
|
158
|
+
return 'MultiList';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 프로젝트 루트 경로 찾기
|
|
163
|
+
*/
|
|
164
|
+
async function findProjectRoot(startPath: string): Promise<string> {
|
|
165
|
+
let currentPath = startPath;
|
|
166
|
+
|
|
167
|
+
while (currentPath !== path.parse(currentPath).root) {
|
|
168
|
+
const packageJsonPath = path.join(currentPath, 'package.json');
|
|
169
|
+
try {
|
|
170
|
+
await fs.access(packageJsonPath);
|
|
171
|
+
return currentPath;
|
|
172
|
+
} catch {
|
|
173
|
+
currentPath = path.dirname(currentPath);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
throw new Error('프로젝트 루트를 찾을 수 없습니다');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* DTO 파일 경로 찾기
|
|
182
|
+
*/
|
|
183
|
+
async function findDtoFile(projectRoot: string, dtoName: string): Promise<string | null> {
|
|
184
|
+
const dtoPaths = [
|
|
185
|
+
path.join(projectRoot, 'src/services/@interface/dto', `${dtoName}.ts`),
|
|
186
|
+
path.join(projectRoot, 'src/services/@interface/dto', dtoName, 'index.ts'),
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
for (const dtoPath of dtoPaths) {
|
|
190
|
+
try {
|
|
191
|
+
await fs.access(dtoPath);
|
|
192
|
+
return dtoPath;
|
|
193
|
+
} catch {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* DTO 파일에서 필드 추출
|
|
203
|
+
*/
|
|
204
|
+
async function extractDtoFields(dtoFilePath: string): Promise<string[]> {
|
|
205
|
+
try {
|
|
206
|
+
const content = await fs.readFile(dtoFilePath, 'utf-8');
|
|
207
|
+
const fields: string[] = [];
|
|
208
|
+
|
|
209
|
+
// 인터페이스 정의 찾기
|
|
210
|
+
const interfaceMatch = content.match(/export\s+interface\s+\w+\s+extends\s+\w+\s*{([^}]+)}/s);
|
|
211
|
+
if (!interfaceMatch) {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const interfaceBody = interfaceMatch[1];
|
|
216
|
+
|
|
217
|
+
// 필드 추출 (optional 체이닝 ?, readonly, 주석 제외)
|
|
218
|
+
const fieldRegex = /(?:readonly\s+)?(\w+)\??:\s*(?:(?:{[^}]*})|(?:[^,{\n]+))/g;
|
|
219
|
+
let match;
|
|
220
|
+
|
|
221
|
+
while ((match = fieldRegex.exec(interfaceBody)) !== null) {
|
|
222
|
+
const fieldName = match[1];
|
|
223
|
+
// 메서드 제외
|
|
224
|
+
if (!fieldName.includes('(') && fieldName !== 'rowId' && fieldName !== '__status__') {
|
|
225
|
+
fields.push(fieldName);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return fields;
|
|
230
|
+
} catch (error) {
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* DTO의 기본값 생성 (실제 DTO 파일 파싱)
|
|
237
|
+
*/
|
|
238
|
+
async function getDtoFieldsWithDefaults(
|
|
239
|
+
projectRoot: string,
|
|
240
|
+
dtoType: string
|
|
241
|
+
): Promise<{ fields: string[]; defaults: string }> {
|
|
242
|
+
try {
|
|
243
|
+
const dtoFilePath = await findDtoFile(projectRoot, dtoType);
|
|
244
|
+
|
|
245
|
+
if (dtoFilePath) {
|
|
246
|
+
const fields = await extractDtoFields(dtoFilePath);
|
|
247
|
+
|
|
248
|
+
if (fields.length > 0) {
|
|
249
|
+
// 기본값 생성 (문자열 필드는 빈 문자열, 선택적 필드는 undefined)
|
|
250
|
+
const defaultEntries = fields
|
|
251
|
+
.filter(f => !f.endsWith('?')) // required 필드만
|
|
252
|
+
.map(f => `${f}: ""`)
|
|
253
|
+
.join(', ');
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
fields,
|
|
257
|
+
defaults: defaultEntries || '{ /* DTO fields */ }',
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
// 파싱 실패 시 기본값 사용
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 파싱 실패 또는 파일을 찾지 못한 경우
|
|
266
|
+
const fallbackFields = [`${dtoType.toLowerCase()}Cd: ''`, `${dtoType.toLowerCase()}Nm: ''`];
|
|
267
|
+
return {
|
|
268
|
+
fields: fallbackFields.map(f => f.split(':')[0]),
|
|
269
|
+
defaults: fallbackFields.join(', '),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 통합 다중 리스트 Store 생성
|
|
275
|
+
*/
|
|
276
|
+
export async function generateMultiStores(args: {
|
|
277
|
+
interfacePath: string;
|
|
278
|
+
outputPath: string;
|
|
279
|
+
requirements: string;
|
|
280
|
+
baseStoreName: string;
|
|
281
|
+
}): Promise<{
|
|
282
|
+
content: Array<{ type: string; text: string }>;
|
|
283
|
+
}> {
|
|
284
|
+
const { interfacePath, outputPath, requirements, baseStoreName } = args;
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
// 1. 프로젝트 루트 경로 찾기
|
|
288
|
+
const projectRoot = await findProjectRoot(path.dirname(interfacePath));
|
|
289
|
+
|
|
290
|
+
// 2. 인터페이스 분석
|
|
291
|
+
const analyzeResult = await analyzeInterface({ interfacePath });
|
|
292
|
+
const analyzeData = JSON.parse(analyzeResult.content[0].text);
|
|
293
|
+
|
|
294
|
+
if (!analyzeData.success) {
|
|
295
|
+
return {
|
|
296
|
+
content: [
|
|
297
|
+
{
|
|
298
|
+
type: 'text',
|
|
299
|
+
text: JSON.stringify({
|
|
300
|
+
success: false,
|
|
301
|
+
error: analyzeData.error || '인터페이스 분석 실패',
|
|
302
|
+
}),
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// 3. 다중 리스트 패턴 감지
|
|
309
|
+
let pattern = detectMultiListPattern(requirements);
|
|
310
|
+
if (!pattern || !pattern.isMultiList) {
|
|
311
|
+
return {
|
|
312
|
+
content: [
|
|
313
|
+
{
|
|
314
|
+
type: 'text',
|
|
315
|
+
text: JSON.stringify({
|
|
316
|
+
success: false,
|
|
317
|
+
error: '다중 리스트 패턴을 감지하지 못했습니다',
|
|
318
|
+
}),
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// 4. 피처 추출
|
|
325
|
+
pattern = extractGroupFeatures(pattern, requirements);
|
|
326
|
+
|
|
327
|
+
// 5. Store 이름 설정 (사용자가 제시한 파일명 사용)
|
|
328
|
+
const storeName = baseStoreName;
|
|
329
|
+
const storeInterface = baseStoreName
|
|
330
|
+
.replace(/^use/, '')
|
|
331
|
+
.replace(/Store$/, '')
|
|
332
|
+
.toLowerCase() + 'Store';
|
|
333
|
+
|
|
334
|
+
// 6. Service 정보 및 메서드 매핑
|
|
335
|
+
const serviceName = analyzeData.serviceName || 'UnknownService';
|
|
336
|
+
const methods = analyzeData.methods || [];
|
|
337
|
+
const listMethods = methods.filter((m: any) => m.isListMethod);
|
|
338
|
+
|
|
339
|
+
// 스마트 메서드 매핑 - 그룹과 메서드 이름의 키워드를 매칭
|
|
340
|
+
const usedMethodIndices = new Set<number>();
|
|
341
|
+
|
|
342
|
+
for (const group of pattern.listGroups) {
|
|
343
|
+
const groupName = group.name.toLowerCase();
|
|
344
|
+
const groupDesc = group.description.toLowerCase();
|
|
345
|
+
|
|
346
|
+
// 그룹 이름/설명과 메서드 이름의 키워드 매칭
|
|
347
|
+
let bestMatchIndex = -1;
|
|
348
|
+
let bestMatchScore = 0;
|
|
349
|
+
|
|
350
|
+
listMethods.forEach((method: any, index: number) => {
|
|
351
|
+
if (usedMethodIndices.has(index)) return;
|
|
352
|
+
|
|
353
|
+
const methodName = method.name.toLowerCase();
|
|
354
|
+
let score = 0;
|
|
355
|
+
|
|
356
|
+
if (groupName.includes('list') || groupDesc.includes('메인') || groupDesc.includes('왼쪽') || groupDesc.includes('left')) {
|
|
357
|
+
if (methodName.includes('group') || methodName.includes('master') || methodName.includes('main')) {
|
|
358
|
+
score = 10;
|
|
359
|
+
} else if (methodName.includes('list')) {
|
|
360
|
+
score = 5;
|
|
361
|
+
}
|
|
362
|
+
} else if (groupName.includes('sub') || groupDesc.includes('서브') || groupDesc.includes('오른쪽') || groupDesc.includes('right') || groupDesc.includes('detail')) {
|
|
363
|
+
if (methodName.includes('list') && !methodName.includes('group')) {
|
|
364
|
+
score = 10;
|
|
365
|
+
} else if (methodName.includes('detail') || methodName.includes('child')) {
|
|
366
|
+
score = 8;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (score > bestMatchScore) {
|
|
371
|
+
bestMatchScore = score;
|
|
372
|
+
bestMatchIndex = index;
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
let targetIndex = bestMatchIndex;
|
|
377
|
+
if (targetIndex === -1) {
|
|
378
|
+
for (let i = 0; i < listMethods.length; i++) {
|
|
379
|
+
if (!usedMethodIndices.has(i)) {
|
|
380
|
+
targetIndex = i;
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (targetIndex !== -1 && targetIndex < listMethods.length) {
|
|
387
|
+
const method = listMethods[targetIndex];
|
|
388
|
+
group.methodName = method.name;
|
|
389
|
+
group.requestType = method.requestType || 'any';
|
|
390
|
+
group.responseType = method.responseType || 'any';
|
|
391
|
+
|
|
392
|
+
// DTO 타입 추출
|
|
393
|
+
if (method.responseType) {
|
|
394
|
+
group.dtoType = extractDtoType(method.responseType);
|
|
395
|
+
|
|
396
|
+
// 실제 DTO 필드 파싱
|
|
397
|
+
const dtoInfo = await getDtoFieldsWithDefaults(projectRoot, group.dtoType);
|
|
398
|
+
group.dtoFields = dtoInfo.fields;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
usedMethodIndices.add(targetIndex);
|
|
402
|
+
} else {
|
|
403
|
+
if (listMethods.length > 0) {
|
|
404
|
+
const method = listMethods[0];
|
|
405
|
+
group.methodName = method.name;
|
|
406
|
+
group.requestType = method.requestType || 'any';
|
|
407
|
+
group.responseType = method.responseType || 'any';
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// 7. 템플릿 변수 빌드 (기본 변수)
|
|
413
|
+
const baseVariables = buildTemplateVariables(analyzeData, 1, storeName);
|
|
414
|
+
|
|
415
|
+
// 8. 다중 리스트용 변수 추가
|
|
416
|
+
const mainGroup = pattern.listGroups[0];
|
|
417
|
+
const subGroup = pattern.listGroups[1];
|
|
418
|
+
|
|
419
|
+
const multiListVariables: Partial<TemplateVariables> = {
|
|
420
|
+
HAS_MULTI_LIST: true,
|
|
421
|
+
SUB_LIST_DTO_ITEM: subGroup.dtoType || 'any',
|
|
422
|
+
SUB_LIST_METHOD_NAME: toCamelCase(subGroup.methodName || ''),
|
|
423
|
+
SUB_LIST_REQUEST_EXTENDS: subGroup.requestType && subGroup.requestType !== 'any'
|
|
424
|
+
? `extends ${subGroup.requestType}`
|
|
425
|
+
: '',
|
|
426
|
+
SUB_LIST_RESPONSE_FIELD: 'ds',
|
|
427
|
+
IS_SUB_LIST_PARENT_KEY: true,
|
|
428
|
+
PARENT_KEY_FIELD: getParentKeyField(mainGroup.dtoType || ''),
|
|
429
|
+
HAS_SUB_LIST_CHECKBOX: subGroup.hasCheckbox || false,
|
|
430
|
+
HAS_FORM: pattern.hasForm || false,
|
|
431
|
+
HAS_AUTO_LOAD: pattern.hasAutoLoad || false,
|
|
432
|
+
SUB_LIST_REQUIRED_FIELDS_DEFAULTS: generateRequiredFieldsDefaults(subGroup.dtoFields || []),
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
// 9. 변수 병합
|
|
436
|
+
const variables: TemplateVariables = {
|
|
437
|
+
...baseVariables,
|
|
438
|
+
...multiListVariables,
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// 10. 템플릿 적용
|
|
442
|
+
const storeCode = applyTemplate(unifiedTemplate, variables);
|
|
443
|
+
|
|
444
|
+
// 8. 파일 생성
|
|
445
|
+
const outputDir = path.dirname(outputPath);
|
|
446
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
447
|
+
await fs.writeFile(outputPath, storeCode, 'utf-8');
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
content: [
|
|
451
|
+
{
|
|
452
|
+
type: 'text',
|
|
453
|
+
text: JSON.stringify(
|
|
454
|
+
{
|
|
455
|
+
success: true,
|
|
456
|
+
message: `${pattern.listGroups.length}개의 리스트를 통합한 Store를 생성했습니다`,
|
|
457
|
+
outputPath,
|
|
458
|
+
requirements,
|
|
459
|
+
pattern: {
|
|
460
|
+
listGroups: pattern.listGroups.map((g) => ({
|
|
461
|
+
name: g.name,
|
|
462
|
+
description: g.description,
|
|
463
|
+
features: g.features,
|
|
464
|
+
dtoType: g.dtoType,
|
|
465
|
+
})),
|
|
466
|
+
hasForm: pattern.hasForm,
|
|
467
|
+
hasTab: pattern.hasTab,
|
|
468
|
+
hasAutoLoad: pattern.hasAutoLoad,
|
|
469
|
+
},
|
|
470
|
+
storeName,
|
|
471
|
+
analysis: {
|
|
472
|
+
serviceName,
|
|
473
|
+
methodCount: analyzeData.methods.length,
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
null,
|
|
477
|
+
2
|
|
478
|
+
),
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
};
|
|
482
|
+
} catch (error) {
|
|
483
|
+
return {
|
|
484
|
+
content: [
|
|
485
|
+
{
|
|
486
|
+
type: 'text',
|
|
487
|
+
text: JSON.stringify({
|
|
488
|
+
success: false,
|
|
489
|
+
error: error instanceof Error ? error.message : String(error),
|
|
490
|
+
}),
|
|
491
|
+
},
|
|
492
|
+
],
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* DTO 타입 추출
|
|
499
|
+
*/
|
|
500
|
+
function extractDtoType(responseType: string): string {
|
|
501
|
+
// GetCodeGroupListResponse → CodeGroup
|
|
502
|
+
// GetCodeListResponse → Code
|
|
503
|
+
if (responseType.endsWith('Response')) {
|
|
504
|
+
const baseName = responseType.slice(0, -8); // Remove 'Response'
|
|
505
|
+
// GetCodeGroupList → CodeGroup
|
|
506
|
+
if (baseName.startsWith('Get')) {
|
|
507
|
+
const withoutGet = baseName.slice(3); // CodeGroupList
|
|
508
|
+
if (withoutGet.endsWith('List')) {
|
|
509
|
+
return withoutGet.slice(0, -4); // CodeGroup
|
|
510
|
+
}
|
|
511
|
+
return withoutGet;
|
|
512
|
+
}
|
|
513
|
+
return baseName;
|
|
514
|
+
}
|
|
515
|
+
return responseType;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* 템플릿 적용 - Mustache.js 사용
|
|
520
|
+
*/
|
|
521
|
+
function applyTemplate(template: string, variables: Record<string, any>): string {
|
|
522
|
+
const Mustache = require('mustache');
|
|
523
|
+
|
|
524
|
+
// TypeScript 코드 생성이므로 HTML 이스케이프 비활성화
|
|
525
|
+
const originalEscape = Mustache.escape;
|
|
526
|
+
Mustache.escape = (text: any) => String(text);
|
|
527
|
+
|
|
528
|
+
try {
|
|
529
|
+
let result = Mustache.render(template, variables);
|
|
530
|
+
|
|
531
|
+
// 후처리: 빈 줄 제거만 수행 (정규식 제거로 인한 함수 시그니처 손실 방지)
|
|
532
|
+
result = result
|
|
533
|
+
// 빈 줄 제거 (연속된 2개 이상의 줄바꿈을 하나로 줄임)
|
|
534
|
+
.replace(/\n\s*\n\s*\n/g, '\n\n')
|
|
535
|
+
// 빈 줄 정리
|
|
536
|
+
.replace(/\n\s*\n/g, '\n');
|
|
537
|
+
|
|
538
|
+
return result;
|
|
539
|
+
} finally {
|
|
540
|
+
Mustache.escape = originalEscape;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* 상위 키 필드명 추출
|
|
546
|
+
*/
|
|
547
|
+
function getParentKeyField(dtoType: string): string {
|
|
548
|
+
const lower = dtoType.toLowerCase();
|
|
549
|
+
if (lower.includes('group')) return 'groupCd';
|
|
550
|
+
if (lower.includes('category')) return 'categoryCd';
|
|
551
|
+
if (lower.includes('code')) return 'code';
|
|
552
|
+
return 'id';
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Request 필수 필드 기본값 생성
|
|
557
|
+
*/
|
|
558
|
+
function generateRequiredFieldsDefaults(fields: string[]): string {
|
|
559
|
+
const lines: string[] = [];
|
|
560
|
+
|
|
561
|
+
for (const field of fields) {
|
|
562
|
+
const lowerField = field.toLowerCase();
|
|
563
|
+
if (/(Tpcd|tpCd|Code|cd|Type|type)$/.test(field)) {
|
|
564
|
+
lines.push(` ${field}: '',`);
|
|
565
|
+
} else if (/(Yn|yn)$/.test(field)) {
|
|
566
|
+
lines.push(` ${field}: '',`);
|
|
567
|
+
} else if (/(No|Number)$/.test(field)) {
|
|
568
|
+
lines.push(` ${field}: '',`);
|
|
569
|
+
} else if (/(Date|Dt|Dtm)$/.test(field)) {
|
|
570
|
+
lines.push(` ${field}: '',`);
|
|
571
|
+
} else {
|
|
572
|
+
lines.push(` ${field}: '',`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return lines.join('\n');
|
|
577
|
+
}
|