@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,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
|
+
}
|