@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,1373 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
export interface MethodInfo {
|
|
5
|
+
name: string;
|
|
6
|
+
requestType?: string;
|
|
7
|
+
responseType?: string;
|
|
8
|
+
isListMethod: boolean;
|
|
9
|
+
isRequestArray?: boolean; // Request 타입이 배열인지 여부
|
|
10
|
+
hasPage?: boolean; // 페이징 여부
|
|
11
|
+
comment?: string; // 메서드 주석 (/* 주석 */ 형태)
|
|
12
|
+
firstParamName?: string; // 첫 번째 파라미터 이름 (예: id, bbscttNo)
|
|
13
|
+
responseListField?: string | null; // 리스트 응답 필드명 (ds, list, items 등)
|
|
14
|
+
responseDetailField?: string | null; // 상세 응답 필드명 (rs, detail 등)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DateRangeField {
|
|
18
|
+
startField: string; // bgnDtm, startDateTime 등
|
|
19
|
+
endField: string; // endDtm, endDateTime 등
|
|
20
|
+
searchType: string; // 'bgnDtm' | 'startDateTime' | 'unknown'
|
|
21
|
+
hasDayJs: boolean; // dayjs import 필요 여부 (Dt/Dtm 필드가 1개 이상 있는 경우)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 요청 필드 분류
|
|
25
|
+
export type RequestFieldType =
|
|
26
|
+
| 'page' // pageNumber, pageSize
|
|
27
|
+
| 'dateRange' // bgnDtm, endDtm 등
|
|
28
|
+
| 'search' // searchText, searchKeyword
|
|
29
|
+
| 'searchType' // searchType, searchDiv
|
|
30
|
+
| 'code' // statusCd, useYn 등 (Code 타입)
|
|
31
|
+
| 'yn' // useYn, delYn 등 (Y|N)
|
|
32
|
+
| 'number' // 숫자 필드
|
|
33
|
+
| 'string' // 문자열 필드
|
|
34
|
+
| 'unknown'; // 기타
|
|
35
|
+
|
|
36
|
+
export interface RequestFieldInfo {
|
|
37
|
+
name: string; // 필드명
|
|
38
|
+
type: string; // 타입 (string, number, Code 등)
|
|
39
|
+
fieldType: RequestFieldType; // 필드 분류
|
|
40
|
+
isOptional: boolean; // optional 여부 (?)
|
|
41
|
+
comment?: string; // 주석
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface InterfaceAnalysis {
|
|
45
|
+
success: boolean;
|
|
46
|
+
filePath: string;
|
|
47
|
+
className: string;
|
|
48
|
+
serviceName: string;
|
|
49
|
+
methods: MethodInfo[];
|
|
50
|
+
imports: string[]; // import 경로
|
|
51
|
+
types: string[]; // 타입 이름들
|
|
52
|
+
dtoTypes: string[];
|
|
53
|
+
hasPage?: boolean; // 전체 페이징 여부 (Response에 page 필드가 있는지)
|
|
54
|
+
hasPageInRequest?: boolean; // Request에 pageNumber/pageSize 필드가 있는지 (listRequestValue 초기값 결정용)
|
|
55
|
+
hasDateRange: boolean; // dateRange 패턴 존재 여부 (쌍이 모두 있는 경우)
|
|
56
|
+
hasDayJs: boolean; // dayjs import 필요 여부 (Dt/Dtm 필드가 1개 이상 있는 경우)
|
|
57
|
+
dateRangeFields?: DateRangeField; // dateRange 관련 필드 정보
|
|
58
|
+
idField?: string; // __status__ 패턴용 ID 필드 (삭제 메서드 Request의 첫 번째 필드)
|
|
59
|
+
idFieldType?: string; // ID 필드의 실제 타입 (예: 'string', 'number', 'string | undefined')
|
|
60
|
+
idFieldIsOptional?: boolean; // ID 필드가 optional인지 여부
|
|
61
|
+
requiredFields?: string[]; // Request 타입의 필수 필드들 (pageNumber, pageSize 제외)
|
|
62
|
+
hasExcel?: boolean; // 엑셀 다운로드 API 존재 여부
|
|
63
|
+
hasArrayDeleteApi?: boolean; // 배열 삭제 API 존재 여부 (callMultiDeleteApi 생성 결정용)
|
|
64
|
+
excelMethodName?: string | null; // 엑셀 메서드 이름
|
|
65
|
+
excelRequestType?: string; // 엑셀 메서드 Request 타입
|
|
66
|
+
isExcelRequestArray?: boolean; // 엑셀 메서드 Request가 배열인지 여부
|
|
67
|
+
// 주석 기반 기능 판단 (더 정확한 판단)
|
|
68
|
+
hasDeleteFromComment?: boolean; // 주석에서 삭제 판단
|
|
69
|
+
hasSaveFromComment?: boolean; // 주석에서 저장 판단
|
|
70
|
+
hasDetailFromComment?: boolean; // 주석에서 상세 판단
|
|
71
|
+
// 요청 필드 분석
|
|
72
|
+
requestFields?: RequestFieldInfo[]; // List Request 인터페이스의 필드 정보
|
|
73
|
+
entityName?: string; // 엔티티명 (예: Banner)
|
|
74
|
+
isServiceExported?: boolean; // services/index.ts에서 실제 export되었는지 여부
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 인터페이스 Repository 파일 분석
|
|
79
|
+
*/
|
|
80
|
+
export async function analyzeInterface(args: { interfacePath: string }) {
|
|
81
|
+
const { interfacePath } = args;
|
|
82
|
+
|
|
83
|
+
// 파일 존재 확인
|
|
84
|
+
try {
|
|
85
|
+
await fs.access(interfacePath);
|
|
86
|
+
} catch {
|
|
87
|
+
return {
|
|
88
|
+
content: [
|
|
89
|
+
{
|
|
90
|
+
type: 'text',
|
|
91
|
+
text: JSON.stringify({
|
|
92
|
+
success: false,
|
|
93
|
+
error: `파일을 찾을 수 없습니다: ${interfacePath}`,
|
|
94
|
+
}),
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Repository 파일 읽기
|
|
101
|
+
let repositoryContent = await fs.readFile(interfacePath, 'utf-8');
|
|
102
|
+
|
|
103
|
+
// 파일 타입 감지 (Interface 파일 vs Repository 파일)
|
|
104
|
+
const isDirectInterfaceFile = /export\s+abstract\s+class\s+\w+Interface/.test(repositoryContent);
|
|
105
|
+
|
|
106
|
+
// Interface 파일 경로 찾기 (import 문에서 추출)
|
|
107
|
+
let interfaceContent = '';
|
|
108
|
+
let dtoContent = '';
|
|
109
|
+
|
|
110
|
+
// 파일 타입에 따라 다르게 처리
|
|
111
|
+
let interfaceDir: string;
|
|
112
|
+
let dtoDir: string;
|
|
113
|
+
|
|
114
|
+
if (isDirectInterfaceFile) {
|
|
115
|
+
// Interface 파일을 직접 전달받은 경우
|
|
116
|
+
// interfacePath: /path/to/services/@interface/interface/BannerInterface.ts
|
|
117
|
+
// interfaceDir: /path/to/services/@interface/interface (같은 폴더)
|
|
118
|
+
// dtoDir: /path/to/services/@interface/dto
|
|
119
|
+
interfaceDir = path.dirname(interfacePath);
|
|
120
|
+
dtoDir = path.join(path.dirname(interfaceDir), 'dto');
|
|
121
|
+
interfaceContent = repositoryContent; // 이미 interface 파일 내용
|
|
122
|
+
} else {
|
|
123
|
+
// Repository 파일을 전달받은 경우
|
|
124
|
+
const interfaceImportMatch = repositoryContent.match(/import\s+{[^}]*}\s+from\s+["']\.\.\/?interface["']/);
|
|
125
|
+
const repoDir = path.dirname(interfacePath);
|
|
126
|
+
|
|
127
|
+
if (interfaceImportMatch) {
|
|
128
|
+
interfaceDir = path.join(repoDir, '..', 'interface');
|
|
129
|
+
dtoDir = path.join(repoDir, '..', 'dto');
|
|
130
|
+
|
|
131
|
+
const classMatch = repositoryContent.match(/export\s+class\s+(\w+)Repository/);
|
|
132
|
+
if (classMatch) {
|
|
133
|
+
const entityName = classMatch[1];
|
|
134
|
+
const interfaceFilePath = path.join(interfaceDir, `${entityName}Interface.ts`);
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
await fs.access(interfaceFilePath);
|
|
138
|
+
interfaceContent = await fs.readFile(interfaceFilePath, 'utf-8');
|
|
139
|
+
} catch {
|
|
140
|
+
// Interface 파일이 없으면 기존 방식대로 진행
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
// interface import가 없는 경우 기본 경로 설정
|
|
145
|
+
interfaceDir = repoDir;
|
|
146
|
+
dtoDir = path.join(repoDir, 'dto');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// dto 폴더의 모든 파일 읽기 (Search DTO 등이 있을 수 있음)
|
|
151
|
+
try {
|
|
152
|
+
const dtoFiles = await fs.readdir(dtoDir);
|
|
153
|
+
for (const file of dtoFiles) {
|
|
154
|
+
if (file.endsWith('.ts')) {
|
|
155
|
+
const dtoFilePath = path.join(dtoDir, file);
|
|
156
|
+
try {
|
|
157
|
+
const content = await fs.readFile(dtoFilePath, 'utf-8');
|
|
158
|
+
dtoContent += '\n' + content;
|
|
159
|
+
} catch {
|
|
160
|
+
// 파일 읽기 실패 시 무시
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
// dto 폴더가 없으면 무시
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 세 파일 내용을 합쳐서 분석 (Interface + DTO 파일 내용 포함)
|
|
169
|
+
const combinedContent = repositoryContent + '\n' + interfaceContent + '\n' + dtoContent;
|
|
170
|
+
|
|
171
|
+
// services/index.ts 읽기 (실제 export된 Service 이름을 찾기 위해)
|
|
172
|
+
let servicesIndexContent = '';
|
|
173
|
+
// services/index.ts 경로 찾기 - 현재 경로에서 상위 폴더로 올라가며 찾기
|
|
174
|
+
let currentDir = path.dirname(interfacePath);
|
|
175
|
+
let servicesIndexFound = false;
|
|
176
|
+
|
|
177
|
+
// 최대 5번 상위 폴더로 올라가며 services/index.ts 찾기
|
|
178
|
+
for (let i = 0; i < 5; i++) {
|
|
179
|
+
const indexPath = path.join(currentDir, 'index.ts');
|
|
180
|
+
const servicesPath = path.join(currentDir, 'services', 'index.ts');
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
// 먼저 현재 폴더에 index.ts가 있는지 확인
|
|
184
|
+
await fs.access(indexPath);
|
|
185
|
+
const content = await fs.readFile(indexPath, 'utf-8');
|
|
186
|
+
// Service export 패턴이 있는지 확인
|
|
187
|
+
if (/export\s+const\s+\w+\s*=\s+new\s+\w+Repository\s*\(/.test(content) ||
|
|
188
|
+
/export\s+const\s+\w+\s*=\s+new\s+\w+Interface\s*\(/.test(content)) {
|
|
189
|
+
servicesIndexContent = content;
|
|
190
|
+
servicesIndexFound = true;
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
// 현재 폴더에 index.ts가 없거나 Service export 패턴이 없음
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
// services 폴더 안에 index.ts가 있는지 확인
|
|
199
|
+
await fs.access(servicesPath);
|
|
200
|
+
const content = await fs.readFile(servicesPath, 'utf-8');
|
|
201
|
+
if (/export\s+const\s+\w+\s*=\s+new\s+\w+Repository\s*\(/.test(content) ||
|
|
202
|
+
/export\s+const\s+\w+\s*=\s+new\s+\w+Interface\s*\(/.test(content)) {
|
|
203
|
+
servicesIndexContent = content;
|
|
204
|
+
servicesIndexFound = true;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
// services/index.ts가 없음
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 상위 폴더로 이동
|
|
212
|
+
const parentDir = path.dirname(currentDir);
|
|
213
|
+
if (parentDir === currentDir) {
|
|
214
|
+
// 더 이상 상위 폴더가 없음
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
currentDir = parentDir;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 분석 결과 - combinedContent를 전달하여 Response 인터페이스를 찾을 수 있게 함
|
|
221
|
+
const analysis = parseInterfaceFile(interfacePath, repositoryContent, combinedContent, servicesIndexContent);
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
content: [
|
|
225
|
+
{
|
|
226
|
+
type: 'text',
|
|
227
|
+
text: JSON.stringify(analysis, null, 2),
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 인터페이스 파일 파싱
|
|
235
|
+
* @param filePath 파일 경로
|
|
236
|
+
* @param repoContent Repository 파일 내용 (메서드 추출용)
|
|
237
|
+
* @param combinedContent Repository + Interface 파일 내용 (Response 인터페이스 추출용)
|
|
238
|
+
* @param servicesIndexContent services/index.ts 내용 (실제 export된 Service 이름 추출용)
|
|
239
|
+
*/
|
|
240
|
+
function parseInterfaceFile(filePath: string, repoContent: string, combinedContent: string, servicesIndexContent: string): InterfaceAnalysis {
|
|
241
|
+
const analysis: InterfaceAnalysis = {
|
|
242
|
+
success: true,
|
|
243
|
+
filePath,
|
|
244
|
+
className: '',
|
|
245
|
+
serviceName: '',
|
|
246
|
+
methods: [],
|
|
247
|
+
imports: [],
|
|
248
|
+
types: [],
|
|
249
|
+
dtoTypes: [],
|
|
250
|
+
hasPage: false,
|
|
251
|
+
hasDateRange: false,
|
|
252
|
+
hasDayJs: false,
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// 클래스 이름 추출 (예: MemberRepository 또는 BannerInterface)
|
|
256
|
+
let classMatch = repoContent.match(/export\s+class\s+(\w+)Repository/);
|
|
257
|
+
let isInterfaceFile = false;
|
|
258
|
+
|
|
259
|
+
if (!classMatch) {
|
|
260
|
+
// Interface 파일도 지원 (추상 클래스)
|
|
261
|
+
classMatch = repoContent.match(/export\s+abstract\s+class\s+(\w+)Interface/);
|
|
262
|
+
if (classMatch) {
|
|
263
|
+
isInterfaceFile = true;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (classMatch) {
|
|
268
|
+
const entityName = classMatch[1];
|
|
269
|
+
analysis.className = isInterfaceFile ? `${entityName}Interface` : `${entityName}Repository`;
|
|
270
|
+
analysis.entityName = entityName;
|
|
271
|
+
|
|
272
|
+
// services/index.ts에서 실제 export된 Service 이름 찾기
|
|
273
|
+
// 패턴: export const ServiceName = new EntityRepository(...)
|
|
274
|
+
if (servicesIndexContent) {
|
|
275
|
+
// 대소문자 구분 없이 매칭하지만, 실제 export된 이름을 그대로 사용
|
|
276
|
+
// BannerRepository -> BannerService, BannerSerevice, bannerService 등 모두 매칭
|
|
277
|
+
const serviceExportPatterns = [
|
|
278
|
+
new RegExp(`export\\s+const\\s+(\\w+)\\s*=\\s+new\\s+${entityName}Repository\\s*\\(`, 'i'),
|
|
279
|
+
new RegExp(`export\\s+const\\s+(\\w+)\\s*=\\s+new\\s+${entityName}Interface\\s*\\(`, 'i'),
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
for (const pattern of serviceExportPatterns) {
|
|
283
|
+
const match = servicesIndexContent.match(pattern);
|
|
284
|
+
if (match) {
|
|
285
|
+
analysis.serviceName = match[1]; // 실제 export된 이름 (대소문자 포함)
|
|
286
|
+
analysis.isServiceExported = true; // 실제로 export됨을 표시
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 못 찾으면 기존 방식대로 추론
|
|
292
|
+
if (!analysis.serviceName) {
|
|
293
|
+
analysis.serviceName = `${entityName}Service`;
|
|
294
|
+
analysis.isServiceExported = false; // export되지 않음
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
analysis.serviceName = `${entityName}Service`;
|
|
298
|
+
analysis.isServiceExported = false; // export되지 않음
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Import 경로 추출
|
|
303
|
+
const importPathSet = new Set<string>();
|
|
304
|
+
const importMatches = repoContent.matchAll(/import\s+\{[^}]+\}\s+from\s+["']([^"']+)["']/g);
|
|
305
|
+
for (const match of importMatches) {
|
|
306
|
+
if (match[1]) {
|
|
307
|
+
importPathSet.add(match[1]);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
analysis.imports = Array.from(importPathSet);
|
|
311
|
+
|
|
312
|
+
// Import 문에서 타입들 추출 (interface import에서)
|
|
313
|
+
const interfaceImportMatch = repoContent.match(/import\s+\{([^}]+)\}\s+from\s+["'][^"']*interface["']/);
|
|
314
|
+
const typeSet = new Set<string>();
|
|
315
|
+
|
|
316
|
+
if (interfaceImportMatch) {
|
|
317
|
+
const importTypes = interfaceImportMatch[1].split(',');
|
|
318
|
+
for (const type of importTypes) {
|
|
319
|
+
const trimmed = type.trim();
|
|
320
|
+
// Interface 타입과 배열 타입 제외
|
|
321
|
+
if (trimmed && !trimmed.endsWith('Interface') && !trimmed.endsWith('[]')) {
|
|
322
|
+
typeSet.add(trimmed);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// 메서드에서 사용되는 Response 타입도 추가 (Interface 제외)
|
|
328
|
+
for (const methodMatch of repoContent.matchAll(/_apiWrapper<([^>]+)>/g)) {
|
|
329
|
+
const returnType = methodMatch[1];
|
|
330
|
+
if (returnType && returnType !== 'void' && !returnType.endsWith('Interface')) {
|
|
331
|
+
// 배열 타입인 경우 비배열 버전 추가
|
|
332
|
+
if (returnType.endsWith('[]')) {
|
|
333
|
+
typeSet.add(returnType.slice(0, -2));
|
|
334
|
+
} else {
|
|
335
|
+
typeSet.add(returnType);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
analysis.types = Array.from(typeSet);
|
|
341
|
+
|
|
342
|
+
// 메서드 추출 (Repository async 메서드 또는 Interface abstract 메서드)
|
|
343
|
+
// 1단계: Repository 패턴 (async method with body): async methodName(...) { ... }
|
|
344
|
+
const repoMethodPattern = /async\s+(\w+)\s*\(([^)]*)\)(?:\s*:\s*Promise<([^>]+)>)?\s*\{/g;
|
|
345
|
+
// 2단계: Interface 패턴 (abstract method with semicolon): abstract methodName(...): Promise<...>;
|
|
346
|
+
const interfaceMethodPattern = /abstract\s+(\w+)\s*\(([^)]*)\)(?:\s*:\s*Promise<([^>]+)>)?\s*;/g;
|
|
347
|
+
|
|
348
|
+
// Repository 패턴 추출
|
|
349
|
+
let repoMethodMatch;
|
|
350
|
+
while ((repoMethodMatch = repoMethodPattern.exec(repoContent)) !== null) {
|
|
351
|
+
const methodName = repoMethodMatch[1];
|
|
352
|
+
const params = repoMethodMatch[2];
|
|
353
|
+
const explicitReturnType = repoMethodMatch[3];
|
|
354
|
+
|
|
355
|
+
const methodComment = extractMethodComment(repoContent, methodName, repoMethodMatch.index);
|
|
356
|
+
analysis.methods.push(extractMethodInfo(methodName, params, explicitReturnType, methodComment, repoContent, combinedContent, analysis));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Interface 패턴 추출 (추상 클래스)
|
|
360
|
+
let interfaceMethodMatch;
|
|
361
|
+
while ((interfaceMethodMatch = interfaceMethodPattern.exec(repoContent)) !== null) {
|
|
362
|
+
const methodName = interfaceMethodMatch[1];
|
|
363
|
+
const params = interfaceMethodMatch[2];
|
|
364
|
+
const explicitReturnType = interfaceMethodMatch[3];
|
|
365
|
+
|
|
366
|
+
const methodComment = extractMethodComment(repoContent, methodName, interfaceMethodMatch.index);
|
|
367
|
+
analysis.methods.push(extractMethodInfo(methodName, params, explicitReturnType, methodComment, repoContent, combinedContent, analysis));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// DTO 타입 추출 (Response에서 ds 타입 찾기) - combinedContent 사용
|
|
371
|
+
for (const method of analysis.methods) {
|
|
372
|
+
if (method.responseType) {
|
|
373
|
+
const dtoType = extractDtoType(combinedContent, method.responseType);
|
|
374
|
+
// 빈 문자열, unknown, Interface로 끝나는 타입은 제외
|
|
375
|
+
if (dtoType && dtoType !== 'unknown' && !dtoType.endsWith('Interface') && !analysis.dtoTypes.includes(dtoType)) {
|
|
376
|
+
analysis.dtoTypes.push(dtoType);
|
|
377
|
+
// DTO 타입도 import에 추가
|
|
378
|
+
typeSet.add(dtoType);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
analysis.types = Array.from(typeSet);
|
|
384
|
+
|
|
385
|
+
// dateRange/dayjs 패턴 감지 (Request 타입에서 Dt/Dtm 필드 찾기)
|
|
386
|
+
const selectedMethod = analysis.methods.find(m => m.isListMethod);
|
|
387
|
+
if (selectedMethod && selectedMethod.requestType) {
|
|
388
|
+
const dateRangeInfo = detectDateRangePattern(combinedContent, selectedMethod.requestType);
|
|
389
|
+
if (dateRangeInfo) {
|
|
390
|
+
analysis.hasDayJs = dateRangeInfo.hasDayJs;
|
|
391
|
+
analysis.dateRangeFields = dateRangeInfo;
|
|
392
|
+
// 쌍이 모두 있는 경우에만 hasDateRange를 true로 설정
|
|
393
|
+
if (dateRangeInfo.startField && dateRangeInfo.endField) {
|
|
394
|
+
analysis.hasDateRange = true;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// __status__ 패턴용 ID 필드 추출 (삭제 메서드 Request의 첫 번째 필드)
|
|
400
|
+
const deleteMethod = analysis.methods.find(m =>
|
|
401
|
+
m.name.toLowerCase().includes('delete') || m.name.toLowerCase().includes('remove')
|
|
402
|
+
);
|
|
403
|
+
if (deleteMethod && deleteMethod.requestType) {
|
|
404
|
+
const idFieldInfo = extractIdFieldFromRequest(combinedContent, deleteMethod.requestType);
|
|
405
|
+
analysis.idField = idFieldInfo.fieldName;
|
|
406
|
+
analysis.idFieldType = idFieldInfo.fieldType;
|
|
407
|
+
analysis.idFieldIsOptional = idFieldInfo.isOptional;
|
|
408
|
+
} else if (selectedMethod && selectedMethod.requestType) {
|
|
409
|
+
// 삭제 메서드가 없으면 리스트 메서드의 Request 타입에서 ID 필드 추출
|
|
410
|
+
const idFieldInfo = extractIdFieldFromRequest(combinedContent, selectedMethod.requestType);
|
|
411
|
+
analysis.idField = idFieldInfo.fieldName;
|
|
412
|
+
analysis.idFieldType = idFieldInfo.fieldType;
|
|
413
|
+
analysis.idFieldIsOptional = idFieldInfo.isOptional;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Request 타입의 필수 필드 추출 (List 메서드의 Request 타입에서)
|
|
417
|
+
if (selectedMethod && selectedMethod.requestType) {
|
|
418
|
+
analysis.requiredFields = extractRequiredFieldsFromRequest(combinedContent, selectedMethod.requestType);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 엔티티명 추출 (예: BannerRepository → Banner)
|
|
422
|
+
const entityName = analysis.className.replace('Repository', '');
|
|
423
|
+
analysis.entityName = entityName;
|
|
424
|
+
|
|
425
|
+
// 주석 기반 기능 판단 (더 정확한 판단)
|
|
426
|
+
// 주석에서 "/* 엔티티명 삭제 */", "/* 엔티티명 저장 */" 패턴 찾기
|
|
427
|
+
for (const method of analysis.methods) {
|
|
428
|
+
if (method.comment) {
|
|
429
|
+
const commentLower = method.comment.toLowerCase().replace(/\s+/g, '');
|
|
430
|
+
// 예: "/* 배너 삭제 */" → "배너삭제"
|
|
431
|
+
// 삭제/저장/상세 키워드만 확인 (엔티티명 매칭은 복잡하므로 키워드만 사용)
|
|
432
|
+
if (commentLower.includes('삭제') || commentLower.includes('delete')) {
|
|
433
|
+
analysis.hasDeleteFromComment = true;
|
|
434
|
+
}
|
|
435
|
+
if (commentLower.includes('저장') || commentLower.includes('save') || commentLower.includes('insert') || commentLower.includes('create')) {
|
|
436
|
+
analysis.hasSaveFromComment = true;
|
|
437
|
+
}
|
|
438
|
+
if (commentLower.includes('상세') || commentLower.includes('detail') || commentLower.includes('정보') || commentLower.includes('info') || commentLower.includes('get')) {
|
|
439
|
+
analysis.hasDetailFromComment = true;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// 엑셀 다운로드 API 존재 여부 체크
|
|
445
|
+
// 1. 메서드명에 excel 키워드가 있거나 주석에 엑셀 키워드가 있는 메서드 찾기
|
|
446
|
+
// 2. 첫 번째 엑셀 API를 사용
|
|
447
|
+
const excelMethods = analysis.methods.filter(m => {
|
|
448
|
+
const nameLower = m.name.toLowerCase();
|
|
449
|
+
const commentLower = (m.comment || '').toLowerCase();
|
|
450
|
+
// 메서드명에 excel 또는 주석에 엑셀
|
|
451
|
+
return nameLower.includes('excel') || commentLower.includes('엑셀');
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
if (excelMethods.length > 0) {
|
|
455
|
+
analysis.hasExcel = true;
|
|
456
|
+
// 첫 번째 엑셀 메서드 사용
|
|
457
|
+
analysis.excelMethodName = toCamelCase(excelMethods[0].name);
|
|
458
|
+
analysis.excelRequestType = excelMethods[0].requestType;
|
|
459
|
+
analysis.isExcelRequestArray = excelMethods[0].isRequestArray || false;
|
|
460
|
+
} else {
|
|
461
|
+
analysis.hasExcel = false;
|
|
462
|
+
analysis.excelMethodName = null;
|
|
463
|
+
analysis.excelRequestType = undefined;
|
|
464
|
+
analysis.isExcelRequestArray = false;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// 배열 삭제 API 존재 여부 확인 (callMultiDeleteApi 생성 결정용)
|
|
468
|
+
// 삭제 메서드가 2개 이상이고, 선택되지 않은 메서드 중 배열이 있으면 true
|
|
469
|
+
const deleteMethods = analysis.methods.filter(m => {
|
|
470
|
+
const nameLower = m.name.toLowerCase();
|
|
471
|
+
const commentLower = (m.comment || '').toLowerCase();
|
|
472
|
+
return nameLower.includes('delete') || nameLower.includes('remove') || commentLower.includes('삭제');
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// 삭제 메서드가 2개 이상이고, 그중 배열이 있는지 확인
|
|
476
|
+
const hasArrayDeleteInOtherMethods = deleteMethods.length >= 2 && deleteMethods.some(m => m.isRequestArray === true);
|
|
477
|
+
analysis.hasArrayDeleteApi = hasArrayDeleteInOtherMethods;
|
|
478
|
+
|
|
479
|
+
// Request에 pageNumber/pageSize 필드가 있는지 확인 (listRequestValue 초기값 결정용)
|
|
480
|
+
// 선택된 List 메서드의 Request 타입 확인
|
|
481
|
+
const listMethods = analysis.methods.filter((m) => m.isListMethod);
|
|
482
|
+
if (listMethods.length > 0 && listMethods[0].requestType) {
|
|
483
|
+
analysis.hasPageInRequest = hasPageInRequest(combinedContent, listMethods[0].requestType);
|
|
484
|
+
|
|
485
|
+
// 요청 필드 분석
|
|
486
|
+
analysis.requestFields = extractRequestFields(combinedContent, listMethods[0].requestType);
|
|
487
|
+
} else {
|
|
488
|
+
analysis.hasPageInRequest = false;
|
|
489
|
+
analysis.requestFields = [];
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return analysis;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* List 메서드인지 확인
|
|
497
|
+
* 우선순위:
|
|
498
|
+
* 1. 주석에 '목록' 키워드 포함 (예: 메뉴 목록, 배너 목록)
|
|
499
|
+
* 2. '목록'으로 끝나는 메서드 (예: getMember목록)
|
|
500
|
+
* 3. 'list' 또는 'List'를 포함하는 메서드
|
|
501
|
+
* 4. 그 외는 List 메서드로 분류하지 않음
|
|
502
|
+
*/
|
|
503
|
+
function isListMethod(methodName: string, methodComment?: string): boolean {
|
|
504
|
+
// 주석에 '목록' 키워드가 있는지 확인 (우선순위 1순위)
|
|
505
|
+
// 예: '메뉴 목록', '배너 목록'
|
|
506
|
+
if (methodComment) {
|
|
507
|
+
const cleaned = methodComment.replace(/\/\*\*?|\*\//g, '').trim();
|
|
508
|
+
// '모듈명 목록' 또는 '목록' 패턴 확인
|
|
509
|
+
if (cleaned.includes('목록')) {
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
// '목록'으로 끝나는 경우 우선 (한국어 프로젝트 관례)
|
|
514
|
+
if (methodName.endsWith('목록')) {
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
// 'list' 또는 'List'를 포함하는 경우
|
|
518
|
+
if (methodName.includes('list') || methodName.includes('List')) {
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
521
|
+
// 그 외는 List 메서드로 분류하지 않음 (get으로 시작하는 것 제거)
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Response 인터페이스에 페이징 정보가 있는지 확인
|
|
527
|
+
* 1. page: 필드 직접 확인
|
|
528
|
+
* 2. DataGridPageResponse 또는 ApiPageResponse 상속 확인
|
|
529
|
+
*/
|
|
530
|
+
function hasPageInResponse(content: string, responseType: string): boolean {
|
|
531
|
+
// Response 인터페이스 찾기 - 중괄호 균형 맞춤
|
|
532
|
+
const interfaceMatch = findInterface(content, responseType);
|
|
533
|
+
|
|
534
|
+
if (interfaceMatch) {
|
|
535
|
+
const body = interfaceMatch;
|
|
536
|
+
|
|
537
|
+
// 방법 1: page: 필드 직접 확인
|
|
538
|
+
if (/page:/.test(body)) {
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// 방법 2: DataGridPageResponse 또는 ApiPageResponse 상속 확인
|
|
543
|
+
const extendsMatch = body.match(/extends\s+(DataGridPageResponse|ApiPageResponse)\b/);
|
|
544
|
+
if (extendsMatch) {
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Request 인터페이스에 pageNumber 또는 pageSize 필드가 있는지 확인
|
|
554
|
+
* listRequestValue 초기값 결정용
|
|
555
|
+
*/
|
|
556
|
+
export function hasPageInRequest(content: string, requestType: string): boolean {
|
|
557
|
+
// 모든 관련 인터페이스 내용 수집 (현재 + extends 체인)
|
|
558
|
+
let combinedBody = '';
|
|
559
|
+
const visited = new Set<string>();
|
|
560
|
+
let currentType = requestType;
|
|
561
|
+
|
|
562
|
+
while (currentType && !visited.has(currentType)) {
|
|
563
|
+
visited.add(currentType);
|
|
564
|
+
|
|
565
|
+
const searchStr = `export interface ${currentType}`;
|
|
566
|
+
const searchIdx = content.indexOf(searchStr);
|
|
567
|
+
|
|
568
|
+
if (searchIdx === -1) break;
|
|
569
|
+
|
|
570
|
+
const afterIdx = searchIdx + searchStr.length;
|
|
571
|
+
const remaining = content.slice(afterIdx);
|
|
572
|
+
|
|
573
|
+
const extendsMatch = remaining.match(/^\s*(?:extends\s+([\w<>]+)\s*)?\{/);
|
|
574
|
+
if (!extendsMatch) break;
|
|
575
|
+
|
|
576
|
+
const startIndex = afterIdx + extendsMatch[0].length;
|
|
577
|
+
let depth = 1;
|
|
578
|
+
let endIndex = startIndex;
|
|
579
|
+
|
|
580
|
+
for (let i = startIndex; i < content.length; i++) {
|
|
581
|
+
if (content[i] === '{') {
|
|
582
|
+
depth++;
|
|
583
|
+
} else if (content[i] === '}') {
|
|
584
|
+
depth--;
|
|
585
|
+
if (depth === 0) {
|
|
586
|
+
endIndex = i;
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const interfaceBody = content.slice(startIndex, endIndex);
|
|
593
|
+
combinedBody += interfaceBody + '\n';
|
|
594
|
+
|
|
595
|
+
if (extendsMatch[1]) {
|
|
596
|
+
currentType = extendsMatch[1];
|
|
597
|
+
} else {
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (combinedBody) {
|
|
603
|
+
// pageNumber 또는 pageSize 필드 확인 (옵셔널 타입 `?:` 지원)
|
|
604
|
+
return /pageNumber\s*\?:|pageSize\s*\?:/.test(combinedBody);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Response 인터페이스에서 DTO 타입 추출
|
|
612
|
+
*/
|
|
613
|
+
function extractDtoType(content: string, responseType: string): string {
|
|
614
|
+
// Response 인터페이스 찾기 - 중괄호 균형 맞춤
|
|
615
|
+
const interfaceBody = findInterface(content, responseType);
|
|
616
|
+
|
|
617
|
+
if (interfaceBody) {
|
|
618
|
+
// ds: Type[] 형태 찾기 (단, 제너릭 타입 제외)
|
|
619
|
+
const dsMatch = interfaceBody.match(/ds:\s*(\w+)(\[|<)/);
|
|
620
|
+
if (dsMatch && dsMatch[2] === '[') {
|
|
621
|
+
const typeName = dsMatch[1];
|
|
622
|
+
// 제너릭 타입 키워드 제외
|
|
623
|
+
if (!['Record', 'Array', 'Map', 'Set', 'Promise', 'Partial', 'Required', 'Readonly'].includes(typeName)) {
|
|
624
|
+
return typeName;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// rs: Type 형태 찾기 (단, 제너릭 타입 제외)
|
|
629
|
+
const rsMatch = interfaceBody.match(/rs:\s*(\w+)(<|;|\n)/);
|
|
630
|
+
if (rsMatch && rsMatch[2] === '<') {
|
|
631
|
+
// 제너릭 타입이면 스킵
|
|
632
|
+
return '';
|
|
633
|
+
}
|
|
634
|
+
if (rsMatch) {
|
|
635
|
+
const typeName = rsMatch[1];
|
|
636
|
+
// 제너릭 타입 키워드 제외
|
|
637
|
+
if (!['Record', 'Array', 'Map', 'Set', 'Promise', 'Partial', 'Required', 'Readonly', 'undefined'].includes(typeName)) {
|
|
638
|
+
return typeName;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// 인터페이스에서 ds/rs를 찾지 못하면 unknown 반환
|
|
644
|
+
// (generate.ts에서 extractDtoFromResponse fallback 사용)
|
|
645
|
+
return 'unknown';
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Response 인터페이스에서 응답 필드 구조 분석
|
|
650
|
+
* @returns { listField: string | null, detailField: string | null, isDirectObject: boolean }
|
|
651
|
+
*/
|
|
652
|
+
export function extractResponseFieldInfo(content: string, responseType: string): {
|
|
653
|
+
listField: string | null;
|
|
654
|
+
detailField: string | null;
|
|
655
|
+
isDirectObject: boolean;
|
|
656
|
+
} {
|
|
657
|
+
const result = {
|
|
658
|
+
listField: null as string | null,
|
|
659
|
+
detailField: null as string | null,
|
|
660
|
+
isDirectObject: false,
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
const interfaceBody = findInterface(content, responseType);
|
|
664
|
+
if (!interfaceBody) {
|
|
665
|
+
console.error('[DEBUG] extractResponseFieldInfo - interface not found:', responseType);
|
|
666
|
+
return result;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
console.error('[DEBUG] extractResponseFieldInfo - responseType:', responseType);
|
|
670
|
+
console.error('[DEBUG] extractResponseFieldInfo - interfaceBody:', interfaceBody.trim());
|
|
671
|
+
|
|
672
|
+
// 빈 인터페이스인 경우 (extends만 있고 본문이 없는 경우) -> 직접 객체
|
|
673
|
+
const trimmedBody = interfaceBody.trim();
|
|
674
|
+
if (trimmedBody === '') {
|
|
675
|
+
result.isDirectObject = true;
|
|
676
|
+
return result;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// 리스트 응답 필드 패턴 (우선순위 순)
|
|
680
|
+
const listPatterns = [
|
|
681
|
+
{ regex: /ds:\s*(\w+)\[\]\s*;?/, field: 'ds' },
|
|
682
|
+
{ regex: /list:\s*(\w+)\[\]\s*;?/, field: 'list' },
|
|
683
|
+
{ regex: /items:\s*(\w+)\[\]\s*;?/, field: 'items' },
|
|
684
|
+
{ regex: /data:\s*(\w+)\[\]\s*;?/, field: 'data' },
|
|
685
|
+
];
|
|
686
|
+
|
|
687
|
+
for (const pattern of listPatterns) {
|
|
688
|
+
const match = interfaceBody.match(pattern.regex);
|
|
689
|
+
if (match) {
|
|
690
|
+
result.listField = pattern.field;
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// 상세 응답 필드 패턴 (우선순위 순)
|
|
696
|
+
const detailPatterns = [
|
|
697
|
+
{ regex: /rs:\s*(\w+)(?![])\s*;?/, field: 'rs' },
|
|
698
|
+
{ regex: /detail:\s*(\w+)(?![])\s*;?/, field: 'detail' },
|
|
699
|
+
{ regex: /data:\s*(\w+)(?![])\s*;?/, field: 'data' },
|
|
700
|
+
];
|
|
701
|
+
|
|
702
|
+
for (const pattern of detailPatterns) {
|
|
703
|
+
const match = interfaceBody.match(pattern.regex);
|
|
704
|
+
if (match) {
|
|
705
|
+
const typeName = match[1];
|
|
706
|
+
// 제너릭 타입 키워드 제외
|
|
707
|
+
if (!['Record', 'Array', 'Map', 'Set', 'Promise', 'Partial', 'Required', 'Readonly', 'undefined'].includes(typeName)) {
|
|
708
|
+
result.detailField = pattern.field;
|
|
709
|
+
break;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// 필드가 하나도 없고, 인터페이스에 필드가 하나만 있는 경우 직접 객체일 수 있음
|
|
715
|
+
const lines = interfaceBody.split('\n').filter(line => line.trim() && !line.trim().startsWith('//'));
|
|
716
|
+
if (lines.length === 0 && !result.listField && !result.detailField) {
|
|
717
|
+
// 빈 인터페이스 (extends만 있음) -> 직접 객체
|
|
718
|
+
result.isDirectObject = true;
|
|
719
|
+
} else if (lines.length === 1 && !result.listField && !result.detailField) {
|
|
720
|
+
const fieldMatch = lines[0].match(/^\s*(\w+)\s*:\s*(\w+);/);
|
|
721
|
+
if (fieldMatch) {
|
|
722
|
+
result.isDirectObject = true;
|
|
723
|
+
result.detailField = fieldMatch[1]; // 필드명 자체를 반환
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return result;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Request 인터페이스에서 첫 번째 필드 이름 추출
|
|
732
|
+
* __status__ 패턴용 ID 필드 추출에 사용
|
|
733
|
+
* 우선순위: No/Number/Id/ID로 끝나는 필드 -> 첫 번째 필드
|
|
734
|
+
* extends 체인을 따라감
|
|
735
|
+
*/
|
|
736
|
+
export function extractIdFieldFromRequest(content: string, requestType: string): { fieldName: string; fieldType: string; isOptional: boolean } {
|
|
737
|
+
// 모든 관련 인터페이스 내용 수집 (현재 + extends 체인)
|
|
738
|
+
let combinedBody = '';
|
|
739
|
+
|
|
740
|
+
// extends 체인 추적
|
|
741
|
+
const visited = new Set<string>();
|
|
742
|
+
let currentType = requestType;
|
|
743
|
+
|
|
744
|
+
while (currentType && !visited.has(currentType)) {
|
|
745
|
+
visited.add(currentType);
|
|
746
|
+
|
|
747
|
+
// 인터페이스 선언 찾기 (본문 포함)
|
|
748
|
+
const searchStr = `export interface ${currentType}`;
|
|
749
|
+
const searchIdx = content.indexOf(searchStr);
|
|
750
|
+
|
|
751
|
+
if (searchIdx === -1) break;
|
|
752
|
+
|
|
753
|
+
const afterIdx = searchIdx + searchStr.length;
|
|
754
|
+
const remaining = content.slice(afterIdx);
|
|
755
|
+
|
|
756
|
+
// extends 추출 및 본문 찾기
|
|
757
|
+
const extendsMatch = remaining.match(/^\s*(?:extends\s+(\w+)\s*)?\{/);
|
|
758
|
+
if (!extendsMatch) break;
|
|
759
|
+
|
|
760
|
+
const startIndex = afterIdx + extendsMatch[0].length;
|
|
761
|
+
let depth = 1;
|
|
762
|
+
let endIndex = startIndex;
|
|
763
|
+
|
|
764
|
+
for (let i = startIndex; i < content.length; i++) {
|
|
765
|
+
if (content[i] === '{') {
|
|
766
|
+
depth++;
|
|
767
|
+
} else if (content[i] === '}') {
|
|
768
|
+
depth--;
|
|
769
|
+
if (depth === 0) {
|
|
770
|
+
endIndex = i;
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const interfaceBody = content.slice(startIndex, endIndex);
|
|
777
|
+
combinedBody += interfaceBody + '\n';
|
|
778
|
+
|
|
779
|
+
// extends 타입 추출
|
|
780
|
+
if (extendsMatch[1]) {
|
|
781
|
+
currentType = extendsMatch[1];
|
|
782
|
+
} else {
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (combinedBody) {
|
|
788
|
+
const lines = combinedBody.split('\n').filter(line => line.trim() && !line.trim().startsWith('//'));
|
|
789
|
+
|
|
790
|
+
// DEBUG: ID 필드 추출 로깅
|
|
791
|
+
console.error('[DEBUG] extractIdFieldFromRequest - requestType:', requestType);
|
|
792
|
+
console.error('[DEBUG] combinedBody lines (first 10):', lines.slice(0, 10).map(l => l.trim()));
|
|
793
|
+
|
|
794
|
+
// 우선순위 1: No 또는 Number로 끝나는 필드 찾기 (게시판번호, 회원번호 등)
|
|
795
|
+
for (const line of lines) {
|
|
796
|
+
const fieldMatch = line.match(/^\s*(\w+)\s*(\?)?:\s*(.+?);?$/);
|
|
797
|
+
if (fieldMatch) {
|
|
798
|
+
const fieldName = fieldMatch[1];
|
|
799
|
+
const isOptional = fieldMatch[2] !== undefined; // ?가 있으면 optional
|
|
800
|
+
const fieldType = fieldMatch[3].trim();
|
|
801
|
+
// __status__, 페이징 필드는 제외
|
|
802
|
+
if (fieldName !== '__status__' && !/(pageNumber|pageSize|page|limit|offset)$/.test(fieldName) && /(No|Number)$/.test(fieldName)) {
|
|
803
|
+
return { fieldName, fieldType: normalizeFieldType(fieldType, isOptional), isOptional };
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// 우선순위 2: Id 또는 ID로 끝나는 필드 찾기 (단, uploadId, fileId 등은 제외)
|
|
809
|
+
for (const line of lines) {
|
|
810
|
+
const fieldMatch = line.match(/^\s*(\w+)\s*(\?)?:\s*(.+?);?$/);
|
|
811
|
+
if (fieldMatch) {
|
|
812
|
+
const fieldName = fieldMatch[1];
|
|
813
|
+
const isOptional = fieldMatch[2] !== undefined;
|
|
814
|
+
const fieldType = fieldMatch[3].trim();
|
|
815
|
+
// __status__, 페이징 필드는 제외
|
|
816
|
+
if (fieldName !== '__status__' && !/(pageNumber|pageSize|page|limit|offset)$/.test(fieldName) && /(Id|ID)$/.test(fieldName) && !/(Upload|File|Image|Thumb)/i.test(fieldName)) {
|
|
817
|
+
return { fieldName, fieldType: normalizeFieldType(fieldType, isOptional), isOptional };
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// 우선순위 3: 첫 번째 필드 반환 (단, __status__, 페이징 필드 제외)
|
|
823
|
+
for (const line of lines) {
|
|
824
|
+
const fieldMatch = line.match(/^\s*(\w+)\s*(\??):\s*(.+?);?$/);
|
|
825
|
+
if (fieldMatch) {
|
|
826
|
+
const fieldName = fieldMatch[1];
|
|
827
|
+
const isOptional = fieldMatch[2] !== undefined;
|
|
828
|
+
const fieldType = fieldMatch[3].trim();
|
|
829
|
+
// __status__, rowId, crtrId, crtrNm, creatDtm, upusrId, upusrNm, upddeDtm, 페이징 필드는 ID로 사용하지 않음
|
|
830
|
+
if (!/^(__status__|rowId|crtrId|crtrNm|creatDtm|upusrId|upusrNm|upddeDtm|pageNumber|pageSize|page|limit|offset)$/.test(fieldName)) {
|
|
831
|
+
return { fieldName, fieldType: normalizeFieldType(fieldType, isOptional), isOptional };
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// 우선순위 4: Request 타입이 *Req로 끝나면 그 첫 번째 필드 사용 (DefaultDto 제외)
|
|
837
|
+
// combinedBody는 extends 체인의 모든 필드를 포함하므로, 방문 순서대로 첫 번째 *Req 인터페이스의 필드만 사용
|
|
838
|
+
if (requestType.endsWith('Req')) {
|
|
839
|
+
// 원본 content에서 *Req 인터페이스 개별 추출
|
|
840
|
+
const reqInterfaceRegex = new RegExp(`export interface (\\w+Req) \\{([\\s\\S]*?)\\n\\}`, 'g');
|
|
841
|
+
let reqMatch;
|
|
842
|
+
const firstReqFields: string[] = [];
|
|
843
|
+
|
|
844
|
+
// 첫 번째 *Req 인터페이스 찾기
|
|
845
|
+
while ((reqMatch = reqInterfaceRegex.exec(content)) !== null) {
|
|
846
|
+
const reqName = reqMatch[1];
|
|
847
|
+
const reqBody = reqMatch[2];
|
|
848
|
+
const reqLines = reqBody.split('\n').filter(line => line.trim() && !line.trim().startsWith('//'));
|
|
849
|
+
|
|
850
|
+
for (const line of reqLines) {
|
|
851
|
+
const fieldMatch = line.match(/^\s*(\w+)\s*(\??):\s*(.+?);?$/);
|
|
852
|
+
if (fieldMatch) {
|
|
853
|
+
const fieldName = fieldMatch[1];
|
|
854
|
+
// 페이징 필드와 메타데이터 필드 제외
|
|
855
|
+
if (!/^(__status__|rowId|crtrId|crtrNm|creatDtm|upusrId|upusrNm|upddeDtm|pageNumber|pageSize|page|limit|offset)$/.test(fieldName)) {
|
|
856
|
+
return {
|
|
857
|
+
fieldName,
|
|
858
|
+
fieldType: normalizeFieldType(fieldMatch[3].trim(), fieldMatch[2] !== undefined),
|
|
859
|
+
isOptional: fieldMatch[2] !== undefined
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
break; // 첫 번째 *Req 인터페이스만 처리
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
return { fieldName: 'id', fieldType: 'string | number', isOptional: false }; // 기본값
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* 필드 타입을 정규화하여 'string' | 'number' | 'string | number' | 'string | undefined' 등으로 반환
|
|
874
|
+
*/
|
|
875
|
+
function normalizeFieldType(fieldType: string, isOptional: boolean): string {
|
|
876
|
+
const type = fieldType.replace(/\|/g, ' | ').toLowerCase().trim();
|
|
877
|
+
let baseType = '';
|
|
878
|
+
|
|
879
|
+
// string | number 또는 number | string
|
|
880
|
+
if ((type.includes('string') && type.includes('number')) || (type.includes('number') && type.includes('string'))) {
|
|
881
|
+
baseType = 'string | number';
|
|
882
|
+
}
|
|
883
|
+
// string
|
|
884
|
+
else if (type.includes('string') && !type.includes('number')) {
|
|
885
|
+
baseType = 'string';
|
|
886
|
+
}
|
|
887
|
+
// number
|
|
888
|
+
else if (type.includes('number') && !type.includes('string')) {
|
|
889
|
+
baseType = 'number';
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
baseType = 'string | number'; // 기본값
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// optional이면 undefined 추가
|
|
896
|
+
return isOptional ? `${baseType} | undefined` : baseType;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* 중괄호 균형을 맞춰 인터페이스 내용 추출
|
|
901
|
+
*/
|
|
902
|
+
function findInterface(content: string, interfaceName: string): string | undefined {
|
|
903
|
+
// 인터페이스 시작 위치 찾기
|
|
904
|
+
// 단순 문자열 검색 후 정규식으로 정확성 확인
|
|
905
|
+
const searchStr = `export interface ${interfaceName}`;
|
|
906
|
+
const searchIdx = content.indexOf(searchStr);
|
|
907
|
+
|
|
908
|
+
if (searchIdx === -1) {
|
|
909
|
+
return undefined;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// 검색된 위치 뒤에 extends나 {가 오는지 확인
|
|
913
|
+
const afterIdx = searchIdx + searchStr.length;
|
|
914
|
+
const remaining = content.slice(afterIdx);
|
|
915
|
+
// extends SomeType { 또는 바로 { 둘 다 허용
|
|
916
|
+
// 제네릭 타입도 처리하기 위해 <> 허용
|
|
917
|
+
const match = remaining.match(/^\s*(?:extends\s+[\w<>]+\s*)?\{/);
|
|
918
|
+
|
|
919
|
+
if (!match) {
|
|
920
|
+
return undefined;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const startIndex = afterIdx + match[0].length;
|
|
924
|
+
let depth = 1;
|
|
925
|
+
let endIndex = startIndex;
|
|
926
|
+
|
|
927
|
+
// 중괄호 균형 맞추기
|
|
928
|
+
for (let i = startIndex; i < content.length; i++) {
|
|
929
|
+
if (content[i] === '{') {
|
|
930
|
+
depth++;
|
|
931
|
+
} else if (content[i] === '}') {
|
|
932
|
+
depth--;
|
|
933
|
+
if (depth === 0) {
|
|
934
|
+
endIndex = i;
|
|
935
|
+
break;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
if (depth !== 0) {
|
|
941
|
+
return undefined;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
return content.slice(startIndex, endIndex);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* 메서드 주석 추출
|
|
949
|
+
* @param content Repository 파일 전체 내용
|
|
950
|
+
* @param methodName 메서드 이름
|
|
951
|
+
* @param methodIndex 메서드 시작 위치
|
|
952
|
+
* @returns 주석 문자열 또는 undefined
|
|
953
|
+
*/
|
|
954
|
+
function extractMethodComment(content: string, methodName: string, methodIndex: number): string | undefined {
|
|
955
|
+
// 정확한 메서드 이름 매칭을 위한 regex (특수문자 이스케이프)
|
|
956
|
+
const escapedMethodName = methodName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
957
|
+
const methodRegex = new RegExp(`async\\s+(${escapedMethodName})\\s*\\(`);
|
|
958
|
+
const match = content.match(methodRegex);
|
|
959
|
+
|
|
960
|
+
if (!match) return undefined;
|
|
961
|
+
|
|
962
|
+
const exactMethodIndex = match.index;
|
|
963
|
+
|
|
964
|
+
// 메서드 바로 앞의 내용 추출
|
|
965
|
+
const contentBeforeMethod = content.substring(0, exactMethodIndex).trim();
|
|
966
|
+
|
|
967
|
+
// 마지막 주석 찾기
|
|
968
|
+
const lastCommentStart = contentBeforeMethod.lastIndexOf('/*');
|
|
969
|
+
if (lastCommentStart === -1) return undefined;
|
|
970
|
+
|
|
971
|
+
const lastCommentEnd = contentBeforeMethod.indexOf('*/', lastCommentStart);
|
|
972
|
+
if (lastCommentEnd === -1) return undefined;
|
|
973
|
+
|
|
974
|
+
const commentCandidate = contentBeforeMethod.substring(lastCommentStart, lastCommentEnd + 2).trim();
|
|
975
|
+
|
|
976
|
+
// 주석과 메서드 사이의 내용 확인 (공백/줄바꿈만 허용)
|
|
977
|
+
const betweenCommentAndMethod = contentBeforeMethod.substring(lastCommentEnd + 2).trim();
|
|
978
|
+
|
|
979
|
+
// 주석과 메서드 사이에 다른 async 키워드가 있으면 안 됨
|
|
980
|
+
const nextAsyncIndex = betweenCommentAndMethod.lastIndexOf('async ');
|
|
981
|
+
if (nextAsyncIndex !== -1) return undefined;
|
|
982
|
+
|
|
983
|
+
return commentCandidate;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* Request 인터페이스에서 dateRange/dayjs 패턴 감지
|
|
988
|
+
* bgnDtm/endDtm 또는 startDateTime/endDateTime 쌍을 찾음
|
|
989
|
+
* Dt/Dtm으로 끝나는 필드가 1개 이상 있으면 dayjs import 필요
|
|
990
|
+
* extends로 확장된 타입도 체크
|
|
991
|
+
*/
|
|
992
|
+
function detectDateRangePattern(content: string, requestType: string): DateRangeField | undefined {
|
|
993
|
+
// Request 인터페이스의 전체 선언을 찾기 (extends 정보 포함)
|
|
994
|
+
const searchStr = `export interface ${requestType}`;
|
|
995
|
+
const searchIdx = content.indexOf(searchStr);
|
|
996
|
+
|
|
997
|
+
if (searchIdx === -1) {
|
|
998
|
+
return undefined;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// interface 선언 시작부터 중괄호 끝까지 전체 추출
|
|
1002
|
+
const afterIdx = searchIdx + searchStr.length;
|
|
1003
|
+
// extends SomeType { 또는 바로 { 둘 다 허용
|
|
1004
|
+
// 제네릭 타입도 처리하기 위해 <> 허용
|
|
1005
|
+
// 타입 이름도 캡처 (재귀 호출용)
|
|
1006
|
+
const match = content.slice(afterIdx).match(/^\s*(?:extends\s+([\w<>]+)\s*)?\{/);
|
|
1007
|
+
|
|
1008
|
+
if (!match) {
|
|
1009
|
+
return undefined;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const startIndex = afterIdx + match[0].length;
|
|
1013
|
+
let depth = 1;
|
|
1014
|
+
let endIndex = startIndex;
|
|
1015
|
+
|
|
1016
|
+
for (let i = startIndex; i < content.length; i++) {
|
|
1017
|
+
if (content[i] === '{') {
|
|
1018
|
+
depth++;
|
|
1019
|
+
} else if (content[i] === '}') {
|
|
1020
|
+
depth--;
|
|
1021
|
+
if (depth === 0) {
|
|
1022
|
+
endIndex = i;
|
|
1023
|
+
break;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
if (depth !== 0) {
|
|
1029
|
+
return undefined;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// interface 본문
|
|
1033
|
+
const interfaceBody = content.slice(startIndex, endIndex);
|
|
1034
|
+
|
|
1035
|
+
// Dt/Dtm으로 끝나는 필드 개수 카운트 (dayjs import 필요 여부 판단)
|
|
1036
|
+
const dtFieldsMatch = interfaceBody.matchAll(/(\w+(?:Dt|Dtm))\s*\??:\s*string/g);
|
|
1037
|
+
const dtFields = Array.from(dtFieldsMatch).map(m => m[1]);
|
|
1038
|
+
const hasDtField = dtFields.length > 0;
|
|
1039
|
+
|
|
1040
|
+
// 패턴 1: bgnDtm/endDtm (한국어 프로젝트 관례)
|
|
1041
|
+
const hasBgnDtm = /bgnDtm\s*\??:\s*string/.test(interfaceBody);
|
|
1042
|
+
const hasEndDtm = /endDtm\s*\??:\s*string/.test(interfaceBody);
|
|
1043
|
+
|
|
1044
|
+
if (hasBgnDtm && hasEndDtm) {
|
|
1045
|
+
return {
|
|
1046
|
+
startField: 'bgnDtm',
|
|
1047
|
+
endField: 'endDtm',
|
|
1048
|
+
searchType: 'bgnDtm',
|
|
1049
|
+
hasDayJs: hasDtField,
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// 패턴 2: startDateTime/endDateTime (영어 관례)
|
|
1054
|
+
const hasStartDateTime = /startDateTime\s*\??:\s*string/.test(interfaceBody);
|
|
1055
|
+
const hasEndDateTime = /endDateTime\s*\??:\s*string/.test(interfaceBody);
|
|
1056
|
+
|
|
1057
|
+
if (hasStartDateTime && hasEndDateTime) {
|
|
1058
|
+
return {
|
|
1059
|
+
startField: 'startDateTime',
|
|
1060
|
+
endField: 'endDateTime',
|
|
1061
|
+
searchType: 'startDateTime',
|
|
1062
|
+
hasDayJs: hasDtField,
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// Dt/Dtm 필드가 1개 이상 있는 경우 (쌍은 아니지만 dayjs는 필요)
|
|
1067
|
+
if (hasDtField) {
|
|
1068
|
+
return {
|
|
1069
|
+
startField: '',
|
|
1070
|
+
endField: '',
|
|
1071
|
+
searchType: 'unknown',
|
|
1072
|
+
hasDayJs: true,
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// extends로 확장된 타입 체크 (match[1]은 extends 뒤의 타입 이름)
|
|
1077
|
+
if (match[1]) {
|
|
1078
|
+
const baseType = match[1];
|
|
1079
|
+
// 기본 타입도 재귀적으로 체크
|
|
1080
|
+
return detectDateRangePattern(content, baseType);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
return undefined;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Request 인터페이스에서 필수 필드 추출
|
|
1088
|
+
* 옵셔널(?)이 아닌 필드들 중에서 pageNumber, pageSize는 제외
|
|
1089
|
+
*/
|
|
1090
|
+
export function extractRequiredFieldsFromRequest(content: string, requestType: string): string[] {
|
|
1091
|
+
const requiredFields: string[] = [];
|
|
1092
|
+
const excludedFields = new Set(['pageNumber', 'pageSize', 'dateRange']);
|
|
1093
|
+
|
|
1094
|
+
// 모든 관련 인터페이스 내용 수집 (현재 + extends 체인)
|
|
1095
|
+
let combinedBody = '';
|
|
1096
|
+
const visited = new Set<string>();
|
|
1097
|
+
let currentType = requestType;
|
|
1098
|
+
|
|
1099
|
+
while (currentType && !visited.has(currentType)) {
|
|
1100
|
+
visited.add(currentType);
|
|
1101
|
+
|
|
1102
|
+
const searchStr = `export interface ${currentType}`;
|
|
1103
|
+
const searchIdx = content.indexOf(searchStr);
|
|
1104
|
+
|
|
1105
|
+
if (searchIdx === -1) break;
|
|
1106
|
+
|
|
1107
|
+
const afterIdx = searchIdx + searchStr.length;
|
|
1108
|
+
const remaining = content.slice(afterIdx);
|
|
1109
|
+
|
|
1110
|
+
const extendsMatch = remaining.match(/^\s*(?:extends\s+([\w<>]+)\s*)?\{/);
|
|
1111
|
+
if (!extendsMatch) break;
|
|
1112
|
+
|
|
1113
|
+
const startIndex = afterIdx + extendsMatch[0].length;
|
|
1114
|
+
let depth = 1;
|
|
1115
|
+
let endIndex = startIndex;
|
|
1116
|
+
|
|
1117
|
+
for (let i = startIndex; i < content.length; i++) {
|
|
1118
|
+
if (content[i] === '{') {
|
|
1119
|
+
depth++;
|
|
1120
|
+
} else if (content[i] === '}') {
|
|
1121
|
+
depth--;
|
|
1122
|
+
if (depth === 0) {
|
|
1123
|
+
endIndex = i;
|
|
1124
|
+
break;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
const interfaceBody = content.slice(startIndex, endIndex);
|
|
1130
|
+
combinedBody += interfaceBody + '\n';
|
|
1131
|
+
|
|
1132
|
+
if (extendsMatch[1]) {
|
|
1133
|
+
currentType = extendsMatch[1];
|
|
1134
|
+
} else {
|
|
1135
|
+
break;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
if (combinedBody) {
|
|
1140
|
+
const lines = combinedBody.split('\n').filter(line => line.trim() && !line.trim().startsWith('//'));
|
|
1141
|
+
|
|
1142
|
+
for (const line of lines) {
|
|
1143
|
+
// 필수 필드 (옵셔널 ?이 없는 필드)
|
|
1144
|
+
const requiredMatch = line.match(/^\s*(\w+)\s*:/);
|
|
1145
|
+
if (requiredMatch && !excludedFields.has(requiredMatch[1])) {
|
|
1146
|
+
requiredFields.push(requiredMatch[1]);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
return requiredFields;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* 메서드 정보 추출 헬퍼 함수
|
|
1156
|
+
*/
|
|
1157
|
+
function extractMethodInfo(
|
|
1158
|
+
methodName: string,
|
|
1159
|
+
params: string,
|
|
1160
|
+
explicitReturnType: string | undefined,
|
|
1161
|
+
methodComment: string | undefined,
|
|
1162
|
+
repoContent: string,
|
|
1163
|
+
combinedContent: string,
|
|
1164
|
+
analysis: InterfaceAnalysis
|
|
1165
|
+
): MethodInfo {
|
|
1166
|
+
const methodInfo: MethodInfo = {
|
|
1167
|
+
name: methodName,
|
|
1168
|
+
isListMethod: isListMethod(methodName, methodComment),
|
|
1169
|
+
comment: methodComment,
|
|
1170
|
+
};
|
|
1171
|
+
|
|
1172
|
+
// 첫 번째 파라미터 이름 추출 (예: id, bbscttNo)
|
|
1173
|
+
if (params.trim()) {
|
|
1174
|
+
const firstParam = params.split(',').map(p => p.trim()).filter(p => p)[0];
|
|
1175
|
+
if (firstParam) {
|
|
1176
|
+
const paramNameMatch = firstParam.match(/^(\w+)/);
|
|
1177
|
+
if (paramNameMatch) {
|
|
1178
|
+
methodInfo.firstParamName = paramNameMatch[1];
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// Request 타입 추출 (params: Type 또는 params: Type[])
|
|
1184
|
+
// 복잡한 타입 이름도 추출 가능하도록 개선 (예: PostAppBannerMgmtDeleteRequest)
|
|
1185
|
+
// 첫 번째 파라미터만 추출 (쉼표나 닫는 괄호까지)
|
|
1186
|
+
const firstParamOnly = params.split(',')[0].trim();
|
|
1187
|
+
const paramMatch = firstParamOnly.match(/params:\s*([\w]+(?:\[\])?)/);
|
|
1188
|
+
if (paramMatch) {
|
|
1189
|
+
methodInfo.requestType = paramMatch[1];
|
|
1190
|
+
// 배열 타입인지 확인
|
|
1191
|
+
methodInfo.isRequestArray = paramMatch[1].endsWith('[]');
|
|
1192
|
+
if (methodInfo.isRequestArray) {
|
|
1193
|
+
methodInfo.requestType = paramMatch[1].slice(0, -2); // [] 제거
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// Response 타입 추출
|
|
1198
|
+
let responseType = explicitReturnType;
|
|
1199
|
+
|
|
1200
|
+
// 명시적 반환 타입이 없으면 메서드 본문에서 apiWrapper 타입 추출 (Repository인 경우)
|
|
1201
|
+
if (!responseType) {
|
|
1202
|
+
const methodIndex = repoContent.indexOf(`async ${methodName}(`);
|
|
1203
|
+
if (methodIndex !== -1) {
|
|
1204
|
+
const nextMethodIdx = repoContent.indexOf('\n async', methodIndex + 1);
|
|
1205
|
+
const classEndIdx = repoContent.indexOf('\n}\n', methodIndex);
|
|
1206
|
+
const methodEnd = nextMethodIdx > 0 ? nextMethodIdx : (classEndIdx > 0 ? classEndIdx : repoContent.length);
|
|
1207
|
+
const methodBody = repoContent.slice(methodIndex, methodEnd);
|
|
1208
|
+
|
|
1209
|
+
const wrapperMatch = methodBody.match(/(?:this\.)?_?apiWrapper<([^>]+)>/);
|
|
1210
|
+
if (wrapperMatch) {
|
|
1211
|
+
responseType = wrapperMatch[1];
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
if (responseType && responseType !== 'void') {
|
|
1217
|
+
const responseMatch = responseType.match(/Post(\w+Response)|Get(\w+Response)/);
|
|
1218
|
+
if (responseMatch) {
|
|
1219
|
+
methodInfo.responseType = responseMatch[0] || responseType;
|
|
1220
|
+
} else {
|
|
1221
|
+
methodInfo.responseType = responseType;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// 페이징 여부 확인
|
|
1225
|
+
methodInfo.hasPage = hasPageInResponse(combinedContent, methodInfo.responseType);
|
|
1226
|
+
if (methodInfo.hasPage) {
|
|
1227
|
+
analysis.hasPage = true;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// 응답 필드 구조 분석
|
|
1231
|
+
const responseFieldInfo = extractResponseFieldInfo(combinedContent, methodInfo.responseType);
|
|
1232
|
+
methodInfo.responseListField = responseFieldInfo.listField;
|
|
1233
|
+
methodInfo.responseDetailField = responseFieldInfo.detailField;
|
|
1234
|
+
|
|
1235
|
+
// 응답 타입 기반 isListMethod 재설정 (주석보다 응답 구조 우선)
|
|
1236
|
+
// 응답에 배열 필드(ds[], list[], items[] 등)가 있으면 리스트 메서드
|
|
1237
|
+
if (responseFieldInfo.listField) {
|
|
1238
|
+
methodInfo.isListMethod = true;
|
|
1239
|
+
} else if (responseFieldInfo.detailField || responseFieldInfo.isDirectObject) {
|
|
1240
|
+
// 응답이 단일 객체(detail, rs 등)이면 리스트 메서드가 아님
|
|
1241
|
+
methodInfo.isListMethod = false;
|
|
1242
|
+
}
|
|
1243
|
+
// 응답 구조를 확인할 수 없으면 주석 기반 판단 유지
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
return methodInfo;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/**
|
|
1250
|
+
* camelCase 변환
|
|
1251
|
+
*/
|
|
1252
|
+
function toCamelCase(str: string): string {
|
|
1253
|
+
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
/**
|
|
1257
|
+
* Request 인터페이스에서 필드 정보 추출
|
|
1258
|
+
* SearchParams 생성 등에 활용
|
|
1259
|
+
*/
|
|
1260
|
+
export function extractRequestFields(content: string, requestType: string): RequestFieldInfo[] {
|
|
1261
|
+
const fields: RequestFieldInfo[] = [];
|
|
1262
|
+
|
|
1263
|
+
// 빈 인터페이스이거나 void인 경우
|
|
1264
|
+
if (!requestType || requestType === 'void' || requestType === 'any') {
|
|
1265
|
+
return fields;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// 인터페이스 본문 추출 (extends 체인 포함)
|
|
1269
|
+
let interfaceBody = '';
|
|
1270
|
+
const visited = new Set<string>();
|
|
1271
|
+
let currentType = requestType;
|
|
1272
|
+
|
|
1273
|
+
while (currentType && !visited.has(currentType)) {
|
|
1274
|
+
visited.add(currentType);
|
|
1275
|
+
|
|
1276
|
+
const body = findInterface(content, currentType);
|
|
1277
|
+
if (body) {
|
|
1278
|
+
interfaceBody += body + '\n';
|
|
1279
|
+
|
|
1280
|
+
// extends 추출
|
|
1281
|
+
const extendsMatch = body.match(/extends\s+([\w\d]+)/);
|
|
1282
|
+
if (extendsMatch) {
|
|
1283
|
+
currentType = extendsMatch[1];
|
|
1284
|
+
} else {
|
|
1285
|
+
break;
|
|
1286
|
+
}
|
|
1287
|
+
} else {
|
|
1288
|
+
break;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
if (!interfaceBody.trim()) {
|
|
1293
|
+
return fields;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
// 필드 파싱 (주석 포함)
|
|
1297
|
+
const fieldRegex = /(?:\/\/\s*(.+?)\s*\n)?\s*(\w+)\??\s*:\s*(.+?);/g;
|
|
1298
|
+
let match;
|
|
1299
|
+
|
|
1300
|
+
while ((match = fieldRegex.exec(interfaceBody)) !== null) {
|
|
1301
|
+
const [, comment, name, type] = match;
|
|
1302
|
+
const isOptional = match[0].includes('?');
|
|
1303
|
+
|
|
1304
|
+
// 필드 타입 분류
|
|
1305
|
+
const fieldType = classifyFieldType(name, type);
|
|
1306
|
+
|
|
1307
|
+
fields.push({
|
|
1308
|
+
name,
|
|
1309
|
+
type: type.trim(),
|
|
1310
|
+
fieldType,
|
|
1311
|
+
isOptional,
|
|
1312
|
+
comment: comment?.trim(),
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
return fields;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* 필드 타입 분류
|
|
1321
|
+
*/
|
|
1322
|
+
function classifyFieldType(name: string, type: string): RequestFieldType {
|
|
1323
|
+
const lowerName = name.toLowerCase();
|
|
1324
|
+
|
|
1325
|
+
// 페이지네이션
|
|
1326
|
+
if (lowerName === 'pageNumber' || lowerName === 'pageSize' || lowerName === 'page' || lowerName === 'limit') {
|
|
1327
|
+
return 'page';
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// 날짜 범위 (bgnDtm, endDtm, startDateTime, endDateTime, bgnDt, endDt)
|
|
1331
|
+
if (lowerName.includes('bgn') || lowerName.includes('start') || lowerName.includes('end')) {
|
|
1332
|
+
return 'dateRange';
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// 검색어
|
|
1336
|
+
if (lowerName.includes('search') && (lowerName.includes('text') || lowerName.includes('keyword') || lowerName === 'searchtext' || lowerName === 'search')) {
|
|
1337
|
+
return 'search';
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// 검색 구분
|
|
1341
|
+
if (lowerName.includes('search') && (lowerName.includes('type') || lowerName.includes('div') || lowerName.includes('kind'))) {
|
|
1342
|
+
return 'searchType';
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// Y/N 필드
|
|
1346
|
+
if (lowerName.endsWith('yn') || lowerName.endsWith('Yn')) {
|
|
1347
|
+
return 'yn';
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// 코드 필드 (Cd로 끝남)
|
|
1351
|
+
if (lowerName.endsWith('cd') || lowerName.endsWith('Cd')) {
|
|
1352
|
+
return 'code';
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// 숫자 필드
|
|
1356
|
+
if (type === 'number' || type === 'bigint') {
|
|
1357
|
+
return 'number';
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// 문자열 필드
|
|
1361
|
+
if (type === 'string') {
|
|
1362
|
+
return 'string';
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
return 'unknown';
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
/**
|
|
1369
|
+
* 필드 정보에서 특정 타입의 필드만 추출
|
|
1370
|
+
*/
|
|
1371
|
+
export function filterFieldsByType(fields: RequestFieldInfo[], fieldType: RequestFieldType): RequestFieldInfo[] {
|
|
1372
|
+
return fields.filter(f => f.fieldType === fieldType);
|
|
1373
|
+
}
|