@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,911 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { spawn, execSync } from 'child_process';
|
|
4
|
+
import { analyzeInterface, InterfaceAnalysis, extractIdFieldFromRequest } from './analyze.js';
|
|
5
|
+
import { getTemplate, TemplateVariables } from '../templates/index.js';
|
|
6
|
+
import {
|
|
7
|
+
getInitialValues,
|
|
8
|
+
getStoreTypeFields,
|
|
9
|
+
getRequiredImports,
|
|
10
|
+
getNamingRules,
|
|
11
|
+
getImplementationRules,
|
|
12
|
+
getStoreTypeInfo,
|
|
13
|
+
getCachedDocs
|
|
14
|
+
} from '../docs/index.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 프로젝트 루트 찾기 (tsconfig.json이 있는 위치)
|
|
18
|
+
* @param startPath 시작 경로 (생성된 파일 경로)
|
|
19
|
+
* @returns 프로젝트 루트 경로
|
|
20
|
+
*/
|
|
21
|
+
async function findProjectRoot(startPath: string): Promise<string> {
|
|
22
|
+
let currentDir = path.dirname(startPath);
|
|
23
|
+
|
|
24
|
+
// 최대 10단계 상위 폴더로 탐색
|
|
25
|
+
for (let i = 0; i < 10; i++) {
|
|
26
|
+
const tsconfigPath = path.join(currentDir, 'tsconfig.json');
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
await fs.access(tsconfigPath);
|
|
30
|
+
return currentDir; // tsconfig.json을 찾으면 해당 경로가 프로젝트 루트
|
|
31
|
+
} catch {
|
|
32
|
+
// 파일이 없으면 상위 폴드로 이동
|
|
33
|
+
const parentDir = path.dirname(currentDir);
|
|
34
|
+
if (parentDir === currentDir) {
|
|
35
|
+
break; // 더 이상 상위 폴더가 없음
|
|
36
|
+
}
|
|
37
|
+
currentDir = parentDir;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// tsconfig.json을 찾지 못하면 기본값 (현재 작업 디렉토리)
|
|
42
|
+
return process.cwd();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* package.json 확인하여 type-check 스크립트 확인
|
|
47
|
+
* @param projectRoot 프로젝트 루트 경로
|
|
48
|
+
* @returns type-check 스크립트 존재 여부
|
|
49
|
+
*/
|
|
50
|
+
async function hasTypeCheckScript(projectRoot: string): Promise<boolean> {
|
|
51
|
+
try {
|
|
52
|
+
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
53
|
+
const content = await fs.readFile(packageJsonPath, 'utf-8');
|
|
54
|
+
const packageJson = JSON.parse(content);
|
|
55
|
+
|
|
56
|
+
return !!(packageJson.scripts && packageJson.scripts['type-check']);
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 타입 체크 실행 (프로젝트 구조 자동 분석)
|
|
64
|
+
* @param filePath 타입 체크할 파일 경로
|
|
65
|
+
* @returns 성공 여부 및 오류 목록
|
|
66
|
+
*/
|
|
67
|
+
async function typeCheck(filePath: string): Promise<{ success: boolean; errors?: string[] }> {
|
|
68
|
+
try {
|
|
69
|
+
// 1. 프로젝트 루트 찾기
|
|
70
|
+
const projectRoot = await findProjectRoot(filePath);
|
|
71
|
+
|
|
72
|
+
// 2. package.json에서 type-check 스크립트 확인
|
|
73
|
+
const hasTypeCheck = await hasTypeCheckScript(projectRoot);
|
|
74
|
+
|
|
75
|
+
let command: string;
|
|
76
|
+
let args: string[];
|
|
77
|
+
|
|
78
|
+
if (hasTypeCheck) {
|
|
79
|
+
// package.json에 type-check 스크립트가 있으면 그것 사용
|
|
80
|
+
command = 'npm';
|
|
81
|
+
args = ['run', 'type-check'];
|
|
82
|
+
} else {
|
|
83
|
+
// 없으면 tsc 직접 실행
|
|
84
|
+
command = 'npx';
|
|
85
|
+
args = ['tsc', '--noEmit', filePath];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 3. 명령 실행 (백그라운드)
|
|
89
|
+
execSync(
|
|
90
|
+
`${command} ${args.join(' ')}`,
|
|
91
|
+
{
|
|
92
|
+
encoding: 'utf-8',
|
|
93
|
+
cwd: projectRoot,
|
|
94
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
95
|
+
timeout: 30000 // 30초 타임아웃
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return { success: true };
|
|
100
|
+
} catch (error: any) {
|
|
101
|
+
const stderr = error.stderr || error.stdout || '';
|
|
102
|
+
const errors = stderr
|
|
103
|
+
.split('\n')
|
|
104
|
+
.filter((line: string) => line.includes('error TS'))
|
|
105
|
+
.map((line: string) => line.trim())
|
|
106
|
+
.filter((line: string) => line.length > 0);
|
|
107
|
+
return { success: false, errors };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function generateStore(args: {
|
|
112
|
+
interfacePath: string;
|
|
113
|
+
outputPath: string;
|
|
114
|
+
storeType?: number;
|
|
115
|
+
storeName?: string;
|
|
116
|
+
}) {
|
|
117
|
+
const { interfacePath, outputPath, storeType = 1, storeName = 'useGeneratedStore' } = args;
|
|
118
|
+
|
|
119
|
+
// 1. 인터페이스 분석
|
|
120
|
+
const analyzeResult = await analyzeInterface({ interfacePath });
|
|
121
|
+
const analyzeData = JSON.parse(analyzeResult.content[0].text);
|
|
122
|
+
|
|
123
|
+
if (!analyzeData.success) {
|
|
124
|
+
return {
|
|
125
|
+
content: [
|
|
126
|
+
{
|
|
127
|
+
type: 'text',
|
|
128
|
+
text: JSON.stringify({
|
|
129
|
+
success: false,
|
|
130
|
+
error: analyzeData.error || '인터페이스 분석 실패',
|
|
131
|
+
}),
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const analysis: InterfaceAnalysis = analyzeData;
|
|
138
|
+
|
|
139
|
+
// 유효성 검사: 메서드가 없는 경우 스토어 생성 불가
|
|
140
|
+
if (!analysis.methods || analysis.methods.length === 0) {
|
|
141
|
+
return {
|
|
142
|
+
content: [
|
|
143
|
+
{
|
|
144
|
+
type: 'text',
|
|
145
|
+
text: JSON.stringify({
|
|
146
|
+
success: false,
|
|
147
|
+
error: '인터페이스에 메서드가 없습니다. Repository 또는 Interface 파일에 메서드를 정의해주세요.',
|
|
148
|
+
}),
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 유효성 검사: services/index.ts에 export되지 않은 Service는 건너뜀
|
|
155
|
+
// isServiceExported가 false이면 실제로 export되지 않은 것으로 간주
|
|
156
|
+
if (analysis.isServiceExported === false) {
|
|
157
|
+
return {
|
|
158
|
+
content: [
|
|
159
|
+
{
|
|
160
|
+
type: 'text',
|
|
161
|
+
text: JSON.stringify({
|
|
162
|
+
success: false,
|
|
163
|
+
error: `services/index.ts에 export되지 않은 Repository입니다. (${analysis.className})`,
|
|
164
|
+
skip: true, // 스킵 대상임을 표시
|
|
165
|
+
}),
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 유효성 검사: 리스트 메서드가 없는 경우 스토어 생성 불가
|
|
172
|
+
const listMethods = analysis.methods.filter((m) => m.isListMethod);
|
|
173
|
+
if (listMethods.length === 0) {
|
|
174
|
+
return {
|
|
175
|
+
content: [
|
|
176
|
+
{
|
|
177
|
+
type: 'text',
|
|
178
|
+
text: JSON.stringify({
|
|
179
|
+
success: false,
|
|
180
|
+
error: `리스트 메서드를 찾을 수 없습니다. Repository에 목록 조회 메서드가 필요합니다. (${analysis.className})`,
|
|
181
|
+
}),
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 2. 템플릿 로드 (통합 템플릿 사용)
|
|
188
|
+
const { unifiedTemplate } = await import('../templates/index.js');
|
|
189
|
+
const template = unifiedTemplate;
|
|
190
|
+
|
|
191
|
+
// 3. 템플릿 변수 빌드
|
|
192
|
+
const variables = buildTemplateVariables(analysis, storeType, storeName);
|
|
193
|
+
|
|
194
|
+
// 4. 템플릿 적용
|
|
195
|
+
const storeCode = applyTemplate(template, variables);
|
|
196
|
+
|
|
197
|
+
// 5. 파일 생성
|
|
198
|
+
try {
|
|
199
|
+
const outputDir = path.dirname(outputPath);
|
|
200
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
201
|
+
await fs.writeFile(outputPath, storeCode, 'utf-8');
|
|
202
|
+
|
|
203
|
+
// 6. 타입 체크 (최대 3회 시도)
|
|
204
|
+
const maxRetries = 3;
|
|
205
|
+
let typeErrors: string[] = [];
|
|
206
|
+
let aiFixed = false;
|
|
207
|
+
|
|
208
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
209
|
+
const result = await typeCheck(outputPath);
|
|
210
|
+
|
|
211
|
+
if (result.success) {
|
|
212
|
+
break; // 타입 체크 통과
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 오류 수집
|
|
216
|
+
typeErrors = result.errors || [];
|
|
217
|
+
|
|
218
|
+
// 마지막 시도이면 종료 (사용자에게 위임)
|
|
219
|
+
if (i === maxRetries - 1) {
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 1차 시도: AI 자동 수정
|
|
224
|
+
if (i === 0) {
|
|
225
|
+
console.log(`[AI 자동 수정] 타입 오류 ${typeErrors.length}건`);
|
|
226
|
+
// TODO: AI 기반 자동 수정 로직 (fixTypeErrors 함수)
|
|
227
|
+
// storeCode = await fixTypeErrors(storeCode, typeErrors, interfacePath);
|
|
228
|
+
// await fs.writeFile(outputPath, storeCode, 'utf-8');
|
|
229
|
+
aiFixed = true;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 7. Lint 및 Prettier 실행 (비동기로 실행하고 실패해도 파일 생성은 성공으로 처리)
|
|
234
|
+
runLintAndPrettier(outputPath).catch((err) => {
|
|
235
|
+
console.warn(`Lint/Prettier 실행 중 경고: ${err}`);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
content: [
|
|
240
|
+
{
|
|
241
|
+
type: 'text',
|
|
242
|
+
text: JSON.stringify({
|
|
243
|
+
success: true,
|
|
244
|
+
message: typeErrors.length > 0
|
|
245
|
+
? `Store 파일 생성 완료 (타입 오류 ${typeErrors.length}건${aiFixed ? ', AI 수정 후 실패' : ''})`
|
|
246
|
+
: 'Store 파일 생성 완료 (타입 체크 통과)',
|
|
247
|
+
outputPath,
|
|
248
|
+
analysis: {
|
|
249
|
+
className: analysis.className,
|
|
250
|
+
serviceName: analysis.serviceName,
|
|
251
|
+
methodCount: analysis.methods.length,
|
|
252
|
+
hasPage: analysis.hasPage,
|
|
253
|
+
},
|
|
254
|
+
typeErrors: typeErrors.length > 0 ? typeErrors : undefined, // 타입 오류가 있으면 결과에 포함
|
|
255
|
+
aiFixed: aiFixed, // AI 시도 여부 표시
|
|
256
|
+
}),
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
};
|
|
260
|
+
} catch (error) {
|
|
261
|
+
return {
|
|
262
|
+
content: [
|
|
263
|
+
{
|
|
264
|
+
type: 'text',
|
|
265
|
+
text: JSON.stringify({
|
|
266
|
+
success: false,
|
|
267
|
+
error: error instanceof Error ? error.message : String(error),
|
|
268
|
+
}),
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 템플릿 변수 빌드
|
|
277
|
+
* docs 폴더의 패턴 정보를 활용하여 더 정확한 변수 생성
|
|
278
|
+
*/
|
|
279
|
+
export function buildTemplateVariables(
|
|
280
|
+
analysis: InterfaceAnalysis,
|
|
281
|
+
storeType: number,
|
|
282
|
+
storeName: string
|
|
283
|
+
): TemplateVariables {
|
|
284
|
+
// docs에서 패턴 정보 로드
|
|
285
|
+
const typeFields = getStoreTypeFields(storeType);
|
|
286
|
+
const initialValues = getInitialValues(storeType);
|
|
287
|
+
const typeInfo = getStoreTypeInfo(storeType);
|
|
288
|
+
const namingRules = getNamingRules();
|
|
289
|
+
const implementationRules = getImplementationRules();
|
|
290
|
+
|
|
291
|
+
// List 메서드 찾기 - 우선순위에 따라 선택
|
|
292
|
+
// 1. 엔티티명 + List (예: postProductListPrd)
|
|
293
|
+
// 2. list + 엔티티명 (예: postListProduct)
|
|
294
|
+
// 3. 그 외 list 포함 메서드
|
|
295
|
+
const className = analysis.className || '';
|
|
296
|
+
const entityName = className.replace('Repository', ''); // Product
|
|
297
|
+
|
|
298
|
+
const listMethods = analysis.methods.filter((m) => m.isListMethod);
|
|
299
|
+
|
|
300
|
+
// 우선순위별로 메서드 찾기
|
|
301
|
+
// 1순위: 페이징이 있는 메서드 우선 (hasPage === true)
|
|
302
|
+
let selectedMethod = listMethods.find((m) =>
|
|
303
|
+
m.name.toLowerCase().includes(`list${entityName.toLowerCase()}`) && m.hasPage === true
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
// 2순위: 엔티티명 포함 메서드
|
|
307
|
+
if (!selectedMethod) {
|
|
308
|
+
selectedMethod = listMethods.find((m) =>
|
|
309
|
+
m.name.toLowerCase().includes(`list${entityName.toLowerCase()}`)
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// 3순위: 페이징이 있는 메서드
|
|
314
|
+
if (!selectedMethod) {
|
|
315
|
+
selectedMethod = listMethods.find((m) => m.hasPage === true);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// 4순위: 가장 짧은 이름 (구체적일수록 이름이 짧음)
|
|
319
|
+
if (!selectedMethod && listMethods.length > 0) {
|
|
320
|
+
selectedMethod = listMethods.reduce((prev, curr) =>
|
|
321
|
+
curr.name.length < prev.name.length ? curr : prev
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// List 메서드가 없으면 빈 기본값 사용 (첫 번째 메서드를 강제로 사용하지 않음)
|
|
326
|
+
const defaultListMethod: any = { name: '', requestType: 'any', responseType: 'any', isListMethod: false, comment: '' };
|
|
327
|
+
selectedMethod = selectedMethod || defaultListMethod;
|
|
328
|
+
|
|
329
|
+
// List 메서드 주석 추출
|
|
330
|
+
const listMethodComment = selectedMethod?.comment || '';
|
|
331
|
+
|
|
332
|
+
// 삭제/저장/상세 메서드 찾기 - 우선 순위: 주석에 "엔티티명 + 저장/삭제" → 주석에 "저장/삭제" → 메서드명 키워드
|
|
333
|
+
const deleteMethod = findMethodByPriority(analysis.methods, entityName, 'delete');
|
|
334
|
+
const saveMethod = findMethodByPriority(analysis.methods, entityName, 'save');
|
|
335
|
+
// detail 메서드는 list 메서드와 중복되지 않아야 함
|
|
336
|
+
const detailMethodCandidate = findMethodByPriority(analysis.methods, entityName, 'detail');
|
|
337
|
+
const detailMethod = detailMethodCandidate?.name !== selectedMethod?.name ? detailMethodCandidate : null;
|
|
338
|
+
|
|
339
|
+
// 삭제/저장/상세/엑셀 메서드 주석 추출
|
|
340
|
+
const deleteMethodComment = deleteMethod?.comment || '';
|
|
341
|
+
const saveMethodComment = saveMethod?.comment || '';
|
|
342
|
+
const detailMethodComment = detailMethod?.comment || '';
|
|
343
|
+
|
|
344
|
+
// Store 이름 처리 (docs 네이밍 규칙 준수)
|
|
345
|
+
const storeNameCamel = storeName; // useMemberListStore
|
|
346
|
+
const storeNamePascal = toPascalCase(storeName.replace(/^use/, '')); // MemberListStore
|
|
347
|
+
const storeInterface = `${storeNamePascal.replace(/Store$/, '')}Store`; // memberListStore
|
|
348
|
+
|
|
349
|
+
// Service 이름
|
|
350
|
+
const serviceName = analysis.serviceName || 'UnknownService';
|
|
351
|
+
|
|
352
|
+
// TypeScript 기본 타입 키워드 목록 (import에서 제외할 타입)
|
|
353
|
+
const TS_KEYWORDS = new Set([
|
|
354
|
+
'void', 'any', 'unknown', 'never', 'null', 'undefined',
|
|
355
|
+
'boolean', 'number', 'string', 'object', 'symbol', 'bigint'
|
|
356
|
+
]);
|
|
357
|
+
|
|
358
|
+
// Request/Response 타입
|
|
359
|
+
const rawListRequest = selectedMethod!.requestType || 'any';
|
|
360
|
+
const listResponse = selectedMethod!.responseType || 'any';
|
|
361
|
+
|
|
362
|
+
// 삭제/저장 Request 타입
|
|
363
|
+
const rawDeleteRequest = deleteMethod?.requestType || 'any';
|
|
364
|
+
const rawSaveRequest = saveMethod?.requestType || 'any';
|
|
365
|
+
|
|
366
|
+
// 디버깅: deleteMethod 확인
|
|
367
|
+
if (deleteMethod) {
|
|
368
|
+
console.log('[DEBUG] deleteMethod:', { name: deleteMethod.name, requestType: deleteMethod.requestType, isRequestArray: deleteMethod.isRequestArray });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// TypeScript 키워드인 경우 빈 문자열로 처리 (extends 없이 빈 interface 사용)
|
|
372
|
+
const listRequest = TS_KEYWORDS.has(rawListRequest) ? '' : rawListRequest;
|
|
373
|
+
const deleteRequest = TS_KEYWORDS.has(rawDeleteRequest) ? '' : rawDeleteRequest;
|
|
374
|
+
const saveRequest = TS_KEYWORDS.has(rawSaveRequest) ? '' : rawSaveRequest;
|
|
375
|
+
|
|
376
|
+
// DTO 타입 추출
|
|
377
|
+
const dtoItem = analysis.dtoTypes[0] || extractDtoFromResponse(listResponse);
|
|
378
|
+
|
|
379
|
+
// 메서드 이름 (camelCase)
|
|
380
|
+
const listMethodName = toCamelCase(selectedMethod!.name);
|
|
381
|
+
const deleteMethodName = deleteMethod ? toCamelCase(deleteMethod.name) : null;
|
|
382
|
+
const saveMethodName = saveMethod ? toCamelCase(saveMethod.name) : null;
|
|
383
|
+
const detailMethodName = detailMethod ? toCamelCase(detailMethod.name) : null;
|
|
384
|
+
|
|
385
|
+
// DTO 소문자 변환
|
|
386
|
+
const dtoItemLower = toCamelCase(dtoItem);
|
|
387
|
+
|
|
388
|
+
// 페이징 여부
|
|
389
|
+
const hasPage = analysis.hasPage || false;
|
|
390
|
+
|
|
391
|
+
// API 여부 플래그 - 주석 기반 판단을 우선 적용
|
|
392
|
+
// 주석에서 "엔티티명 삭제", "엔티티명 저장" 패턴이 있으면 무조건 true
|
|
393
|
+
const hasSave = analysis.hasSaveFromComment || saveMethodName !== null;
|
|
394
|
+
const hasDelete = analysis.hasDeleteFromComment || deleteMethodName !== null;
|
|
395
|
+
const hasDetail = analysis.hasDetailFromComment || detailMethodName !== null;
|
|
396
|
+
const hasList = selectedMethod!.isListMethod;
|
|
397
|
+
|
|
398
|
+
// 파라미터 타입과 인터페이스 필드 타입 간의 호환성 판단
|
|
399
|
+
// 파라미터: id: number | string (고정)
|
|
400
|
+
// 인터페이스 필드: Repository에서 추출한 타입 (예: string | undefined)
|
|
401
|
+
const idFieldType = analysis.idFieldType || 'string | number';
|
|
402
|
+
// idFieldType이 number 또는 number|undefined인 경우 Number() 변환, string 또는 string|undefined인 경우 String() 변환
|
|
403
|
+
const isNumberOnly = idFieldType === 'number' || idFieldType === 'number | undefined';
|
|
404
|
+
const isStringOnly = idFieldType === 'string' || idFieldType === 'string | undefined';
|
|
405
|
+
const idValue = isNumberOnly ? 'Number(id)' :
|
|
406
|
+
isStringOnly ? 'String(id)' : 'id';
|
|
407
|
+
const keyValue = isNumberOnly ? 'Number(key)' :
|
|
408
|
+
isStringOnly ? 'String(key)' : 'key';
|
|
409
|
+
|
|
410
|
+
// Save Request 배열 여부 확인
|
|
411
|
+
const isSaveRequestArray = saveMethod?.isRequestArray || false;
|
|
412
|
+
// Delete Request 배열 여부 확인
|
|
413
|
+
const isDeleteRequestArray = deleteMethod?.isRequestArray || false;
|
|
414
|
+
// 실제 Repository Request 타입 사용 (rawDeleteRequest는 이미 line 214에서 선언됨)
|
|
415
|
+
const hasDeleteRequestType = rawDeleteRequest && rawDeleteRequest !== 'any';
|
|
416
|
+
const deleteRequestType = hasDeleteRequestType ? rawDeleteRequest : null;
|
|
417
|
+
const deleteParamType = deleteRequestType
|
|
418
|
+
? (isDeleteRequestArray ? rawDeleteRequest + '[]' : rawDeleteRequest)
|
|
419
|
+
: (isDeleteRequestArray ? 'React.Key[]' : 'number | string');
|
|
420
|
+
const deleteParamName = deleteRequestType ? 'request' : (isDeleteRequestArray ? 'ids' : 'id');
|
|
421
|
+
const callDeleteApiDeclaration = hasDelete ? ('callDeleteApi: (' + deleteParamName + ': ' + deleteParamType + ') => Promise<void>;') : '';
|
|
422
|
+
|
|
423
|
+
// API 호출 라인 생성 (메서드가 없으면 TODO 주석)
|
|
424
|
+
// 삭제 API는 Request 인터페이스의 필수 필드를 listRequestValue에서 가져와야 함
|
|
425
|
+
const deleteApiCall = deleteMethodName
|
|
426
|
+
? (deleteRequestType
|
|
427
|
+
? 'await ' + serviceName + '.' + deleteMethodName + '(' + deleteParamName + ');'
|
|
428
|
+
: (isDeleteRequestArray
|
|
429
|
+
? 'await ' + serviceName + '.' + deleteMethodName + '(' + deleteParamName + '.map(id => ({ ...get().listRequestValue, ' + (analysis.idField || 'id') + ': ' + idValue.replace(/\bid\b/g, 'id') + ' })));'
|
|
430
|
+
: 'await ' + serviceName + '.' + deleteMethodName + '({ ...get().listRequestValue, ' + (analysis.idField || 'id') + ': ' + idValue + ' });'))
|
|
431
|
+
: '// TODO: 삭제 API 호출 구현 필요\n // await ' + serviceName + '.remove' + dtoItem + '({ id });';
|
|
432
|
+
|
|
433
|
+
const saveApiCall = saveMethodName
|
|
434
|
+
? `${hasPage ? `request.__status__ = get().listSelectedRowKey ? ("U" as const) : ("C" as const);` : ``}
|
|
435
|
+
await ${serviceName}.${saveMethodName}(request);`
|
|
436
|
+
: `// TODO: 저장 API 호출 구현 필요\n // await ${serviceName}.save${dtoItem}(request);`;
|
|
437
|
+
|
|
438
|
+
// === dateRange 패턴 관련 ===
|
|
439
|
+
const hasDateRange = analysis.hasDateRange || false;
|
|
440
|
+
// dateRange가 있으면 dayjs import가 항상 필요함 (기본값에 dayjs() 사용)
|
|
441
|
+
const hasDayJs = hasDateRange || (analysis.hasDayJs || false);
|
|
442
|
+
const dateRangeFields = analysis.dateRangeFields;
|
|
443
|
+
const isBgnDtm = dateRangeFields?.searchType === 'bgnDtm';
|
|
444
|
+
|
|
445
|
+
// === docs 기반 추가 변수 ===
|
|
446
|
+
|
|
447
|
+
// MetaData 필드 (docs에서 가져온 순서대로)
|
|
448
|
+
const metadataRequired = typeFields.metadata.required || [];
|
|
449
|
+
const metadataOptional = typeFields.metadata.optional || [];
|
|
450
|
+
const allMetadataFields = [...metadataRequired, ...metadataOptional];
|
|
451
|
+
|
|
452
|
+
// States 필드
|
|
453
|
+
const statesRequired = typeFields.states.required || [];
|
|
454
|
+
const statesOptional = typeFields.states.optional || [];
|
|
455
|
+
|
|
456
|
+
// Actions
|
|
457
|
+
const actionsRequired = typeFields.actions.required || [];
|
|
458
|
+
const actionsOptional = typeFields.actions.optional || [];
|
|
459
|
+
|
|
460
|
+
// Subscribe selector
|
|
461
|
+
const subscribeSelector = typeFields.subscribeSelector || [];
|
|
462
|
+
|
|
463
|
+
// 초기값 생성 (docs의 initialValues 활용)
|
|
464
|
+
const createStateFields = generateCreateStateFields(initialValues, allMetadataFields, statesOptional);
|
|
465
|
+
|
|
466
|
+
// 구현 규칙 기반 callListApi 템플릿
|
|
467
|
+
const callListApiTemplate = implementationRules.callListApi?.template || '';
|
|
468
|
+
|
|
469
|
+
// 생성될 Store에 대한 문서 참조 주석
|
|
470
|
+
const docsReference = generateDocsReference(storeType, typeInfo);
|
|
471
|
+
|
|
472
|
+
// Request 필수 필드 기본값 생성 (requiredFields에서)
|
|
473
|
+
const requiredFields = analysis.requiredFields || [];
|
|
474
|
+
const requiredFieldsDefaults = generateRequiredFieldsDefaults(requiredFields);
|
|
475
|
+
|
|
476
|
+
// Imports - 실제로 사용하는 타입만 포함 (eslint no-unused-vars 방지)
|
|
477
|
+
// Response 타입은 템플릿에서 직접 사용하지 않으므로 import 제외
|
|
478
|
+
const requiredImports = new Set<string>();
|
|
479
|
+
requiredImports.add(serviceName); // Service 항상 추가
|
|
480
|
+
// ListRequest 타입 - TypeScript 키워드가 아니면 import
|
|
481
|
+
if (listRequest && !TS_KEYWORDS.has(rawListRequest)) {
|
|
482
|
+
requiredImports.add(listRequest);
|
|
483
|
+
}
|
|
484
|
+
// ListResponse는 API 호출 반환값으로 추론되므로 import 불필요
|
|
485
|
+
if (dtoItem && dtoItem !== 'Record<string, any>' && dtoItem !== listRequest && !TS_KEYWORDS.has(dtoItem)) {
|
|
486
|
+
requiredImports.add(dtoItem); // DTO 타입 (중복 제외)
|
|
487
|
+
}
|
|
488
|
+
// Delete/Save Request 타입 - 해당 API가 존재할 때만 import (TypeScript 키워드 제외)
|
|
489
|
+
if (hasDelete && deleteRequest && !TS_KEYWORDS.has(rawDeleteRequest) && deleteRequest !== listRequest && deleteRequest !== dtoItem) {
|
|
490
|
+
requiredImports.add(deleteRequest);
|
|
491
|
+
}
|
|
492
|
+
if (hasSave && saveRequest && !TS_KEYWORDS.has(rawSaveRequest) && saveRequest !== listRequest && saveRequest !== dtoItem) {
|
|
493
|
+
requiredImports.add(saveRequest);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// 정렬
|
|
497
|
+
const allImports = Array.from(requiredImports).sort();
|
|
498
|
+
|
|
499
|
+
// prettier 규칙: import가 길 경우 멀티 라인으로
|
|
500
|
+
// prettier는 import 문에 대해 더 관대하므로 매우 긴 경우에만 멀티 라인
|
|
501
|
+
let importsLine: string;
|
|
502
|
+
if (allImports.length > 0) {
|
|
503
|
+
const importsStr = allImports.join(', ');
|
|
504
|
+
// 전체 import 문 길이 (대략): importsStr + 30 characters
|
|
505
|
+
// prettier는 import가 120자 이상일 때만 멀티 라인 선호
|
|
506
|
+
const fullLength = importsStr.length + 30;
|
|
507
|
+
if (fullLength > 120) {
|
|
508
|
+
importsLine = `import {\n ${allImports.join(',\n ')},\n} from "services";`;
|
|
509
|
+
} else {
|
|
510
|
+
importsLine = `import { ${importsStr} } from "services";`;
|
|
511
|
+
}
|
|
512
|
+
} else {
|
|
513
|
+
importsLine = `import { ${serviceName} } from "services";`;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
// Store 이름
|
|
518
|
+
STORE_NAME: storeNameCamel,
|
|
519
|
+
STORE_NAME_PASCAL: storeNamePascal,
|
|
520
|
+
STORE_INTERFACE: storeInterface,
|
|
521
|
+
|
|
522
|
+
// Service
|
|
523
|
+
SERVICE_NAME: serviceName,
|
|
524
|
+
LIST_METHOD_NAME: listMethodName,
|
|
525
|
+
DELETE_METHOD_NAME: deleteMethodName,
|
|
526
|
+
SAVE_METHOD_NAME: saveMethodName,
|
|
527
|
+
DETAIL_METHOD_NAME: detailMethodName,
|
|
528
|
+
DELETE_API_CALL: deleteApiCall,
|
|
529
|
+
SAVE_API_CALL: saveApiCall,
|
|
530
|
+
|
|
531
|
+
// Request/Response
|
|
532
|
+
LIST_REQUEST: listRequest,
|
|
533
|
+
LIST_REQUEST_EXTENDS: listRequest ? `extends ${listRequest}` : '', // extends 또는 빈 문자열
|
|
534
|
+
LIST_RESPONSE: listResponse,
|
|
535
|
+
ACTIONS_EXTENDS: hasPage ? 'extends PageStoreActions<States> ' : 'extends Record<string, any> ', // Actions interface extends
|
|
536
|
+
DELETE_REQUEST: deleteRequest,
|
|
537
|
+
DELETE_REQUEST_EXTENDS: deleteRequest ? `extends ${deleteRequest}` : '',
|
|
538
|
+
SAVE_REQUEST: saveRequest,
|
|
539
|
+
SAVE_REQUEST_EXTENDS: saveRequest ? `extends ${saveRequest}` : '',
|
|
540
|
+
SAVE_PARAM_TYPE: saveRequest ? (isSaveRequestArray ? saveRequest + '[]' : saveRequest) : dtoItem, // Save 메서드 파라미터 타입 (SaveRequest 있으면 그것 사용, 없으면 DTO, 배열이면 [] 추가)
|
|
541
|
+
DTO_ITEM: dtoItem,
|
|
542
|
+
DTO_ITEM_LOWER: dtoItemLower,
|
|
543
|
+
|
|
544
|
+
// 페이징 여부
|
|
545
|
+
HAS_PAGE: hasPage,
|
|
546
|
+
HAS_SAVE: hasSave,
|
|
547
|
+
HAS_DELETE: hasDelete,
|
|
548
|
+
HAS_DETAIL: hasDetail,
|
|
549
|
+
HAS_LIST: hasList,
|
|
550
|
+
HAS_REORDER: storeType === 6, // Type 6 (재정렬 가능 리스트)만 재정렬 기능 활성화
|
|
551
|
+
HAS_LIST_REQUEST_PARAMS: !!listRequest, // ListRequest가 빈 문자열이 아니면 true (void가 아닌 경우)
|
|
552
|
+
HAS_LIST_RESPONSE: Boolean(listResponse && listResponse !== 'void' && listResponse !== 'any'), // 응답 타입이 void가 아니면 true (response.ds 접근용)
|
|
553
|
+
HAS_PAGE_IN_REQUEST: analysis.hasPageInRequest || false, // Request에 pageNumber/pageSize 필드가 있는지 (listRequestValue 초기값 결정용)
|
|
554
|
+
HAS_EXCEL: (analysis.hasExcel || false) && !!analysis.excelMethodName, // 엑셀 메서드가 있을 때만 true
|
|
555
|
+
EXCEL_METHOD_NAME: analysis.excelMethodName,
|
|
556
|
+
EXCEL_PARAM_TYPE: listRequest || 'ListRequest', // 엑셀 메서드 파라미터 타입 (ListRequest 또는 기본값)
|
|
557
|
+
IS_EXCEL_REQUEST_ARRAY: analysis.isExcelRequestArray || false, // 엑셀 Request 배열 여부
|
|
558
|
+
IS_DELETE_REQUEST_ARRAY: isDeleteRequestArray, // 삭제 Request 배열 여부
|
|
559
|
+
HAS_ARRAY_DELETE_API: analysis.hasArrayDeleteApi || false, // 다른 삭제 메서드 중 배열이 있는지 (callMultiDeleteApi 생성용)
|
|
560
|
+
|
|
561
|
+
// dateRange/dayjs 패턴
|
|
562
|
+
HAS_DATE_RANGE: hasDateRange,
|
|
563
|
+
HAS_DAY_JS: hasDayJs,
|
|
564
|
+
IS_BGNDTM: isBgnDtm,
|
|
565
|
+
|
|
566
|
+
// __status__ 패턴용 ID 필드
|
|
567
|
+
// idFieldType이 정확히 'number'인 경우 Number() 변환, 'string | number'인 경우 그대로 사용, string인 경우 String() 변환
|
|
568
|
+
ID_FIELD: analysis.idField || 'id',
|
|
569
|
+
CALL_DELETE_API_DECLARATION: callDeleteApiDeclaration, // callDeleteApi Actions interface 선언
|
|
570
|
+
DELETE_PARAM_NAME: deleteParamName,
|
|
571
|
+
DELETE_PARAM_TYPE: deleteParamType,
|
|
572
|
+
ID_FIELD_TYPE: analysis.idFieldType || 'string | number',
|
|
573
|
+
ID_FIELD_NEEDS_STRING_CONVERSION: !(analysis.idFieldType || 'string | number').includes('number'),
|
|
574
|
+
ID_FIELD_VALUE: idValue,
|
|
575
|
+
ID_FIELD_KEY_VALUE: keyValue,
|
|
576
|
+
|
|
577
|
+
// Request 필수 필드 기본값
|
|
578
|
+
REQUIRED_FIELDS_DEFAULTS: requiredFieldsDefaults,
|
|
579
|
+
|
|
580
|
+
// Imports
|
|
581
|
+
IMPORTS: importsLine,
|
|
582
|
+
LIST_METHOD_COMMENT: listMethodComment,
|
|
583
|
+
DELETE_METHOD_COMMENT: deleteMethodComment,
|
|
584
|
+
SAVE_METHOD_COMMENT: saveMethodComment,
|
|
585
|
+
DETAIL_METHOD_COMMENT: detailMethodComment,
|
|
586
|
+
|
|
587
|
+
// 응답 필드 정보
|
|
588
|
+
LIST_RESPONSE_FIELD: selectedMethod?.responseListField || 'ds',
|
|
589
|
+
DETAIL_RESPONSE_FIELD: detailMethod?.responseDetailField || 'rs',
|
|
590
|
+
IS_DIRECT_RESPONSE: detailMethod?.responseDetailField === null && detailMethod?.responseType !== undefined,
|
|
591
|
+
|
|
592
|
+
// 요청 필드 정보 (SearchParams 생성용)
|
|
593
|
+
REQUEST_FIELDS: analysis.requestFields ? JSON.stringify(analysis.requestFields) : '[]',
|
|
594
|
+
|
|
595
|
+
// === docs 기반 추가 변수 ===
|
|
596
|
+
METADATA_FIELDS: allMetadataFields.join(', '),
|
|
597
|
+
METADATA_REQUIRED: metadataRequired.join(', '),
|
|
598
|
+
METADATA_OPTIONAL: metadataOptional.join(', '),
|
|
599
|
+
STATES_REQUIRED: statesRequired.join(', '),
|
|
600
|
+
STATES_OPTIONAL: statesOptional.join(', '),
|
|
601
|
+
ACTIONS_REQUIRED: actionsRequired.join(', '),
|
|
602
|
+
ACTIONS_OPTIONAL: actionsOptional.join(', '),
|
|
603
|
+
SUBSCRIBE_SELECTOR: subscribeSelector.join(', '),
|
|
604
|
+
CREATE_STATE_FIELDS: createStateFields,
|
|
605
|
+
CALL_LIST_API_TEMPLATE: callListApiTemplate,
|
|
606
|
+
DOCS_REFERENCE: docsReference,
|
|
607
|
+
STORE_TYPE_NAME: typeInfo?.name || `Type ${storeType}`,
|
|
608
|
+
STORE_TYPE_DESCRIPTION: typeInfo?.description || '',
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Response 타입에서 DTO 추출
|
|
614
|
+
*/
|
|
615
|
+
function extractDtoFromResponse(responseType: string): string {
|
|
616
|
+
// PostMemberListMemberResponse -> MemberRes
|
|
617
|
+
const match = responseType.match(/Post\w+?(\w+)Response/);
|
|
618
|
+
if (match) {
|
|
619
|
+
const base = match[1];
|
|
620
|
+
// Member -> MemberRes, ListMember -> MemberRes
|
|
621
|
+
if (base.startsWith('List')) {
|
|
622
|
+
return base.replace('List', '') + 'Res';
|
|
623
|
+
}
|
|
624
|
+
return base + 'Res';
|
|
625
|
+
}
|
|
626
|
+
return 'unknown';
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* createState 필드 생성 (docs initialValues 활용)
|
|
631
|
+
*/
|
|
632
|
+
function generateCreateStateFields(
|
|
633
|
+
initialValues: any,
|
|
634
|
+
metadataFields: string[],
|
|
635
|
+
statesOptional: string[]
|
|
636
|
+
): string {
|
|
637
|
+
const lines: string[] = [];
|
|
638
|
+
|
|
639
|
+
// MetaData 필드 초기값
|
|
640
|
+
for (const field of metadataFields) {
|
|
641
|
+
if (initialValues[field] !== undefined) {
|
|
642
|
+
lines.push(` ${field}: ${JSON.stringify(initialValues[field])},`);
|
|
643
|
+
} else if (field === 'programFn') {
|
|
644
|
+
lines.push(` programFn: undefined,`);
|
|
645
|
+
} else if (field.includes('Value') || field.includes('Widths') || field.includes('Params') || field.includes('Keys')) {
|
|
646
|
+
lines.push(` ${field}: [],`);
|
|
647
|
+
} else if (field === 'flexGrow') {
|
|
648
|
+
lines.push(` flexGrow: 400,`);
|
|
649
|
+
} else if (field === 'formActive') {
|
|
650
|
+
lines.push(` formActive: false,`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// States 필드 초기값
|
|
655
|
+
const defaultStates: Record<string, string> = {
|
|
656
|
+
routePath: 'undefined',
|
|
657
|
+
listSpinning: 'false',
|
|
658
|
+
listData: '[]',
|
|
659
|
+
listPage: '{ currentPage: 0, totalPages: 0 }',
|
|
660
|
+
selectedItem: 'undefined',
|
|
661
|
+
detail: 'undefined',
|
|
662
|
+
saveRequestValue: '{}',
|
|
663
|
+
detailLoading: 'false',
|
|
664
|
+
checkedRowKeys: '[]',
|
|
665
|
+
listSelectedRowKey: '""',
|
|
666
|
+
modalOpen: 'false',
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
for (const [field, defaultValue] of Object.entries(defaultStates)) {
|
|
670
|
+
if (!metadataFields.includes(field)) {
|
|
671
|
+
lines.push(` ${field}: ${defaultValue},`);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// 추가 States 옵션 필드
|
|
676
|
+
for (const field of statesOptional) {
|
|
677
|
+
if (!metadataFields.includes(field) && !defaultStates[field]) {
|
|
678
|
+
if (field.includes('Spinning') || field.includes('Loading') || field === 'isDirty') {
|
|
679
|
+
lines.push(` ${field}: false,`);
|
|
680
|
+
} else {
|
|
681
|
+
lines.push(` ${field}: undefined,`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return lines.join('\n');
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* 문서 참조 주석 생성
|
|
691
|
+
*/
|
|
692
|
+
function generateDocsReference(storeType: number, typeInfo: any): string {
|
|
693
|
+
const typeName = typeInfo?.name || `Type ${storeType}`;
|
|
694
|
+
const description = typeInfo?.description || '';
|
|
695
|
+
const examples = typeInfo?.examples || [];
|
|
696
|
+
|
|
697
|
+
const lines: string[] = [];
|
|
698
|
+
lines.push(`/**`);
|
|
699
|
+
lines.push(` * Store Pattern: ${typeName}`);
|
|
700
|
+
if (description) {
|
|
701
|
+
lines.push(` * ${description}`);
|
|
702
|
+
}
|
|
703
|
+
if (examples.length > 0) {
|
|
704
|
+
lines.push(` *`);
|
|
705
|
+
lines.push(` * Examples:`);
|
|
706
|
+
for (const example of examples) {
|
|
707
|
+
lines.push(` * - ${example}`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
lines.push(` *`);
|
|
711
|
+
lines.push(` * Generated based on NH-FE-B Store patterns`);
|
|
712
|
+
lines.push(` * See: src/docs/store-patterns-usage-guide.md`);
|
|
713
|
+
lines.push(` */`);
|
|
714
|
+
|
|
715
|
+
return lines.join('\n');
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* 템플릿 적용 - Mustache.js 사용
|
|
720
|
+
*/
|
|
721
|
+
function applyTemplate(template: string, variables: TemplateVariables): string {
|
|
722
|
+
const Mustache = require('mustache');
|
|
723
|
+
|
|
724
|
+
// TypeScript 코드 생성이므로 HTML 이스케이프 비활성화
|
|
725
|
+
// 기본 escape 함수를 덮어써서 원본 그대로 반환
|
|
726
|
+
const originalEscape = Mustache.escape;
|
|
727
|
+
Mustache.escape = (text: any) => String(text);
|
|
728
|
+
|
|
729
|
+
try {
|
|
730
|
+
// Mustache.js를 사용하여 템플릿 렌더링
|
|
731
|
+
// {{#KEY}}...{{/KEY}}: 조건부 블록 (truthy일 때 내용 포함)
|
|
732
|
+
// {{^KEY}}...{{/KEY}}: 역조건부 블록 (falsy일 때 내용 포함)
|
|
733
|
+
// {{KEY}}: 변수 치환
|
|
734
|
+
let result = Mustache.render(template, variables);
|
|
735
|
+
|
|
736
|
+
// 후처리: 빈 줄 제거만 수행 (정규식 제거로 인한 함수 시그니처 손실 방지)
|
|
737
|
+
result = result
|
|
738
|
+
// 빈 줄 제거 (연속된 2개 이상의 줄바꿈을 하나로 줄임)
|
|
739
|
+
.replace(/\n\s*\n\s*\n/g, '\n\n')
|
|
740
|
+
// 빈 줄 정리
|
|
741
|
+
.replace(/\n\s*\n/g, '\n');
|
|
742
|
+
|
|
743
|
+
return result;
|
|
744
|
+
} finally {
|
|
745
|
+
// 원래 escape 함수 복원
|
|
746
|
+
Mustache.escape = originalEscape;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* PascalCase 변환
|
|
752
|
+
*/
|
|
753
|
+
function toPascalCase(str: string): string {
|
|
754
|
+
return str.replace(/(^|-)(\w)/g, (_, __, char) => char.toUpperCase());
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* camelCase 변환
|
|
759
|
+
*/
|
|
760
|
+
function toCamelCase(str: string): string {
|
|
761
|
+
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Request 필수 필드 기본값 생성
|
|
766
|
+
* 필드 이름을 기반으로 적절한 기본값 추론
|
|
767
|
+
*/
|
|
768
|
+
function generateRequiredFieldsDefaults(requiredFields: string[]): string {
|
|
769
|
+
const lines: string[] = [];
|
|
770
|
+
|
|
771
|
+
for (const field of requiredFields) {
|
|
772
|
+
// 필드 이름을 분석하여 기본값 결정
|
|
773
|
+
const lowerField = field.toLowerCase();
|
|
774
|
+
|
|
775
|
+
// Tpcd, tpCd, code, cd 등으로 끝나면 빈 문자열
|
|
776
|
+
if (/(Tpcd|tpCd|Code|cd|Type|type)$/.test(field)) {
|
|
777
|
+
lines.push(` ${field}: '',`);
|
|
778
|
+
}
|
|
779
|
+
// Yn으로 끝나면 빈 문자열
|
|
780
|
+
else if (/(Yn|yn)$/.test(field)) {
|
|
781
|
+
lines.push(` ${field}: '',`);
|
|
782
|
+
}
|
|
783
|
+
// No, Number로 끝나면 빈 문자열 또는 0
|
|
784
|
+
else if (/(No|Number)$/.test(field)) {
|
|
785
|
+
lines.push(` ${field}: '',`);
|
|
786
|
+
}
|
|
787
|
+
// Date, Dt, Dtm으로 끝나면 빈 문자열
|
|
788
|
+
else if (/(Date|Dt|Dtm)$/.test(field)) {
|
|
789
|
+
lines.push(` ${field}: '',`);
|
|
790
|
+
}
|
|
791
|
+
// 그 외에는 빈 문자열
|
|
792
|
+
else {
|
|
793
|
+
lines.push(` ${field}: '',`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return lines.join('\n');
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* 메서드를 우선 순위별로 찾기
|
|
802
|
+
* 1순위: 주석에 "첫단어 + 공백 + 저장/삭제/상세" 정확 매칭 (예: "배너 저장")
|
|
803
|
+
* "배너 순서저장 저장"은 첫단어+공백+키워드 패턴이 아니므로 제외
|
|
804
|
+
* 2순위: 주석에 "저장/삭제/상세"만 있는 경우
|
|
805
|
+
* 3순위: 메서드명에 키워드 포함
|
|
806
|
+
*/
|
|
807
|
+
function findMethodByPriority(
|
|
808
|
+
methods: any[],
|
|
809
|
+
entityName: string,
|
|
810
|
+
actionType: 'delete' | 'save' | 'detail'
|
|
811
|
+
): any | undefined {
|
|
812
|
+
const keywords = {
|
|
813
|
+
delete: ['delete', 'remove'],
|
|
814
|
+
save: ['save', 'insert', 'create'],
|
|
815
|
+
detail: ['detail', 'info', 'get'],
|
|
816
|
+
};
|
|
817
|
+
const koreanKeywords = {
|
|
818
|
+
delete: '삭제',
|
|
819
|
+
save: '저장',
|
|
820
|
+
detail: '상세',
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
const actionKeywords = keywords[actionType];
|
|
824
|
+
const koreanKeyword = koreanKeywords[actionType];
|
|
825
|
+
|
|
826
|
+
// 1순위: 주석에서 "첫단어 + 공백 + 한글 키워드" 정확 매칭
|
|
827
|
+
// 주석: "/* 배너 저장 */" → 첫단어 "배너" + 공백 + "저장" → 매칭!
|
|
828
|
+
// 주석: "/* 배너 순서저장 저장 */" → 첫단어 "배너" + 공백 + "순서저장" → 매칭 실패
|
|
829
|
+
// detail의 경우: "/* 상세 조회 */", "/* 정보 조회 */", "/* 내역 조회 */"도 매칭
|
|
830
|
+
for (const m of methods) {
|
|
831
|
+
if (m.comment) {
|
|
832
|
+
// 주석 정제: /*, */, 앞뒤 공백 제거
|
|
833
|
+
const cleaned = m.comment.replace(/\/\*\*?|\*\//g, '').trim();
|
|
834
|
+
// 주의: \b (word boundary)는 한글에서 제대로 작동하지 않으므로 사용하지 않음
|
|
835
|
+
// 대신 공백으로 분리하여 두 번째 단어가 정확히 키워드인지 확인
|
|
836
|
+
const parts = cleaned.split(/\s+/);
|
|
837
|
+
if (parts.length >= 2) {
|
|
838
|
+
// 일반 패턴: 첫단어 + 공백 + 키워드 (예: "배너 저장")
|
|
839
|
+
if (parts[1] === koreanKeyword) {
|
|
840
|
+
return m;
|
|
841
|
+
}
|
|
842
|
+
// detail 추가 패턴: "상세 조회", "정보 조회", "내역 조회"
|
|
843
|
+
if (actionType === 'detail' && parts[1] === '조회' && ['상세', '정보', '내역'].includes(parts[0])) {
|
|
844
|
+
return m;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// 2순위: 주석에 한글 키워드만 있는 경우
|
|
851
|
+
// detail의 경우 "조회" 키워드도 포함
|
|
852
|
+
const koreanMatch = methods.find((m) => {
|
|
853
|
+
if (!m.comment) return false;
|
|
854
|
+
const hasKeyword = m.comment.includes(koreanKeyword);
|
|
855
|
+
const hasInquiry = actionType === 'detail' && m.comment.includes('조회');
|
|
856
|
+
return hasKeyword || hasInquiry;
|
|
857
|
+
});
|
|
858
|
+
if (koreanMatch) return koreanMatch;
|
|
859
|
+
|
|
860
|
+
// 3순위: 메서드명에 키워드 포함
|
|
861
|
+
const nameMatch = methods.find((m) =>
|
|
862
|
+
actionKeywords.some((keyword) => m.name.toLowerCase().includes(keyword))
|
|
863
|
+
);
|
|
864
|
+
return nameMatch;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Lint 및 Prettier 실행 (프로젝트 루트 기준)
|
|
869
|
+
*/
|
|
870
|
+
async function runLintAndPrettier(filePath: string): Promise<void> {
|
|
871
|
+
// 프로젝트 루트 찾기
|
|
872
|
+
const projectRoot = await findProjectRoot(filePath);
|
|
873
|
+
const relativePath = path.relative(projectRoot, filePath);
|
|
874
|
+
|
|
875
|
+
// Prettier 실행
|
|
876
|
+
await runCommand('npx', ['prettier', '--write', relativePath], projectRoot);
|
|
877
|
+
|
|
878
|
+
// ESLint 실행
|
|
879
|
+
await runCommand('npx', ['eslint', '--fix', relativePath], projectRoot);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* 명령 실행 헬퍼
|
|
884
|
+
*/
|
|
885
|
+
function runCommand(command: string, args: string[], cwd: string): Promise<void> {
|
|
886
|
+
return new Promise((resolve, reject) => {
|
|
887
|
+
const proc = spawn(command, args, {
|
|
888
|
+
cwd,
|
|
889
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
890
|
+
shell: true,
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
let stderr = '';
|
|
894
|
+
|
|
895
|
+
proc.stderr?.on('data', (data) => {
|
|
896
|
+
stderr += data.toString();
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
proc.on('close', (code) => {
|
|
900
|
+
if (code === 0) {
|
|
901
|
+
resolve();
|
|
902
|
+
} else {
|
|
903
|
+
reject(new Error(`${command} ${args.join(' ')} failed with code ${code}: ${stderr}`));
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
proc.on('error', (err) => {
|
|
908
|
+
reject(err);
|
|
909
|
+
});
|
|
910
|
+
});
|
|
911
|
+
}
|