@axboot-mcp/mcp-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CLAUDE.md +119 -0
  2. package/MCP_TOOL_PLAN.md +710 -0
  3. package/MCP_USAGE.md +914 -0
  4. package/README.md +168 -0
  5. package/REPOSITORY_CONVENTIONS.md +250 -0
  6. package/SEARCH_PARAMS_MCP_TOOL_COMPLETE_PLAN.md +646 -0
  7. package/SEARCH_PARAMS_PLAN.md +2570 -0
  8. package/STORE_PATTERNS.md +1178 -0
  9. package/debug-dto.js +72 -0
  10. package/generate-banner-store.js +62 -0
  11. package/generation-plan.json +2176 -0
  12. package/generation-results.json +1817 -0
  13. package/package.json +45 -0
  14. package/scripts/batch-generate-all.js +159 -0
  15. package/scripts/batch-generate-mcp.js +329 -0
  16. package/scripts/batch-generate-stores-v2.js +272 -0
  17. package/scripts/batch-generate-stores.js +179 -0
  18. package/scripts/batch-plan.json +3810 -0
  19. package/scripts/batch-process.py +90 -0
  20. package/scripts/batch-regenerate.js +356 -0
  21. package/scripts/direct-generate.js +227 -0
  22. package/scripts/execute-batches.js +1911 -0
  23. package/scripts/generate-all-stores.js +144 -0
  24. package/scripts/generate-stores-mcp.js +161 -0
  25. package/scripts/generate-stores-v2.js +450 -0
  26. package/scripts/generate-stores-v3.js +412 -0
  27. package/scripts/generate-stores-v4.js +521 -0
  28. package/scripts/generate-stores.js +382 -0
  29. package/scripts/repos-to-process.json +1899 -0
  30. package/src/config/nh-layout-patterns.ts +166 -0
  31. package/src/docs/HOOK_GENERATION_PLAN.md +2226 -0
  32. package/src/docs/NH_STORE_PATTERNS.md +297 -0
  33. package/src/docs/README.md +216 -0
  34. package/src/docs/index.ts +28 -0
  35. package/src/docs/loader.ts +568 -0
  36. package/src/docs/patterns.json +419 -0
  37. package/src/docs/practical-examples.md +732 -0
  38. package/src/docs/quick-start.md +257 -0
  39. package/src/docs/requirements-analysis-guide.md +364 -0
  40. package/src/docs/rules.json +321 -0
  41. package/src/docs/store-pattern-analysis.md +664 -0
  42. package/src/docs/store-patterns-rules.md +1168 -0
  43. package/src/docs/store-patterns-usage-guide.md +1835 -0
  44. package/src/docs/troubleshooting.md +544 -0
  45. package/src/docs/type-selection-guide.md +572 -0
  46. package/src/docs//354/202/254/354/232/251/353/262/225/AntD-/354/273/264/355/217/254/353/204/214/355/212/270-/354/202/254/354/232/251/353/262/225.md +1515 -0
  47. package/src/docs//354/202/254/354/232/251/353/262/225/DataGrid-/354/202/254/354/232/251/353/262/225.md +866 -0
  48. package/src/docs//354/202/254/354/232/251/353/262/225/FormItem-/354/202/254/354/232/251/353/262/225.md +903 -0
  49. package/src/docs//354/202/254/354/232/251/353/262/225/FormModal-/354/202/254/354/232/251/353/262/225.md +1155 -0
  50. package/src/docs//354/202/254/354/232/251/353/262/225/MCP-/353/260/224/354/235/264/353/270/214/354/275/224/353/224/251-/352/260/200/354/235/264/353/223/234.md +1133 -0
  51. package/src/docs//354/202/254/354/232/251/353/262/225/MSW-Mock-/353/215/260/354/235/264/355/204/260-/354/202/254/354/232/251/353/262/225.md +579 -0
  52. package/src/docs//354/202/254/354/232/251/353/262/225/Search-/354/273/264/355/217/254/353/204/214/355/212/270-/354/202/254/354/232/251/353/262/225.md +738 -0
  53. package/src/docs//354/202/254/354/232/251/353/262/225/Store-/355/214/250/355/204/264-/354/202/254/354/232/251/353/262/225.md +1135 -0
  54. package/src/docs//354/202/254/354/232/251/353/262/225//355/231/224/353/251/264/352/265/254/354/204/261-/355/203/200/354/236/205/353/263/204-/352/260/234/353/260/234/354/210/234/354/204/234.md +1805 -0
  55. package/src/docs//354/202/254/354/232/251/353/262/225//355/231/224/353/251/264/355/203/200/354/236/205/353/263/204-/352/260/234/353/260/234-/355/224/204/353/241/254/355/224/204/355/212/270-/352/260/200/354/235/264/353/223/234.md +946 -0
  56. package/src/docs//354/202/254/354/232/251/353/262/225//355/231/225/354/236/245/355/231/224/353/251/264/355/203/200/354/236/205/353/263/204-/354/203/201/354/204/270-/355/224/204/353/241/254/355/224/204/355/212/270/352/260/200/354/235/264/353/223/234.md +2422 -0
  57. package/src/features/store-features.ts +232 -0
  58. package/src/handlers/analyze-requirements.ts +403 -0
  59. package/src/handlers/analyze.ts +1373 -0
  60. package/src/handlers/generate-from-requirements.ts +250 -0
  61. package/src/handlers/generate-hook.ts +950 -0
  62. package/src/handlers/generate-interactive.ts +840 -0
  63. package/src/handlers/generate-listdatagrid.ts +521 -0
  64. package/src/handlers/generate-multi-stores.ts +577 -0
  65. package/src/handlers/generate-requirements-from-layout.ts +160 -0
  66. package/src/handlers/generate-search-params.ts +717 -0
  67. package/src/handlers/generate.ts +911 -0
  68. package/src/handlers/list-templates.ts +104 -0
  69. package/src/handlers/scan-metadata.ts +485 -0
  70. package/src/handlers/suggest-layout.ts +326 -0
  71. package/src/index.ts +959 -0
  72. package/src/prompts/search-params.md +793 -0
  73. package/src/templates/index.ts +107 -0
  74. package/src/templates/unified.ts +462 -0
  75. package/store-generation-error-patterns.md +225 -0
  76. package/test/useAgentStore.ts +136 -0
  77. package/test-server.js +78 -0
  78. package/tsconfig.json +20 -0
@@ -0,0 +1,104 @@
1
+ /**
2
+ * 사용 가능한 Store 템플릿 목록 반환
3
+ */
4
+ export async function listTemplates(args: Record<string, unknown>) {
5
+ const templates = [
6
+ {
7
+ type: 1,
8
+ name: '기본 리스트 + 상세',
9
+ description: '선택된 아이템의 상세 정보 표시, 저장/수정 지원',
10
+ states: ['listSpinning', 'listData', 'listPage', 'selectedItem', 'detailLoading', 'saveRequestValue', 'checkedRowKeys'],
11
+ actions: ['callListApi', 'changeListPage', 'setSelectedItem', 'setCheckedRowKeys'],
12
+ useCases: ['회원 목록', '상품 목록', '쿠폰 목록'],
13
+ },
14
+ {
15
+ type: 2,
16
+ name: '마스터-디테일 + 모달 + 삭제',
17
+ description: '왼쪽 리스트, 오른쪽 상세, 모달, 삭제 기능 (B2B 패턴)',
18
+ states: ['listSpinning', 'listData', 'listPage', 'selectedItem', 'detail', 'detailSpinning', 'modalOpen', 'deleteSpinning', 'checkedRowKeys'],
19
+ actions: ['callListApi', 'changeListPage', 'setSelectedItem', 'setDetail', 'setModalOpen', 'callDeleteApi', 'setCheckedRowKeys'],
20
+ useCases: ['B2B 상품', '견적 상담', '구독 관리'],
21
+ },
22
+ {
23
+ type: 3,
24
+ name: '트리 기반',
25
+ description: '트리 구조 데이터, 펼치기/접기, 저장 지원',
26
+ states: ['treeSpinning', 'treeData', 'expandedKeys', 'saveRequestValues', 'isDirty', 'flexGrow'],
27
+ actions: ['callTreeApi', 'callTreeSaveApi', 'callFormSaveApi', 'setExpandedKeys'],
28
+ useCases: ['카테고리 관리', '메뉴 관리', '조직도'],
29
+ },
30
+ {
31
+ type: 4,
32
+ name: '대시보드',
33
+ description: '여러 개의 리스트와 요약 정보, 탭 전환',
34
+ states: ['summaryData', 'summarySpinning', 'list01Data', 'list02Data', 'activeTabKey'],
35
+ actions: ['callSummaryApi', 'callList01Api', 'callList02Api', 'setActiveTabKey'],
36
+ useCases: ['회원 대시보드', '주문 대시보드', '매출 대시보드'],
37
+ },
38
+ {
39
+ type: 5,
40
+ name: '단순 리스트',
41
+ description: '최소한의 기능만 있는 단순 리스트',
42
+ states: ['listSpinning', 'listData', 'listPage'],
43
+ actions: ['callListApi', 'changeListPage'],
44
+ useCases: ['참조 데이터 조회', '드롭다운 목록'],
45
+ },
46
+ {
47
+ type: 6,
48
+ name: '재정렬 가능 리스트',
49
+ description: '드래그 앤 드롭으로 순서 변경 가능',
50
+ states: ['listSpinning', 'listData', 'isReordered'],
51
+ actions: ['callListApi', 'setListData'],
52
+ useCases: ['팝업 메뉴', '배너 순서', '메뉴 순서'],
53
+ },
54
+ {
55
+ type: 7,
56
+ name: '마스터-디테일 + 모달 + 삭제 + 엑셀',
57
+ description: '왼쪽 리스트, 오른쪽 상세, 모달, 삭제, 엑셀 다운로드',
58
+ states: ['listSpinning', 'listData', 'listPage', 'selectedItem', 'detail', 'detailSpinning', 'modalOpen', 'deleteSpinning', 'saveSpinning', 'excelSpinning', 'checkedRowKeys'],
59
+ actions: ['callListApi', 'callExcelDownloadApi', 'callDeleteApi', 'callSaveApi', 'changeListPage', 'setSelectedItem', 'setDetail', 'setModalOpen', 'setCheckedRowKeys'],
60
+ useCases: ['B2B 견적 상담', '상담 관리', '주문 관리'],
61
+ },
62
+ {
63
+ type: 8,
64
+ name: '리스트 + 선택 + 엑셀 다운로드',
65
+ description: '선택 아이템, 체크박스, 엑셀 다운로드 (상세 없음)',
66
+ states: ['listSpinning', 'listData', 'listPage', 'selectedItem', 'checkedRowKeys', 'listSelectedRowKey', 'listCheckedIndexes', 'orderDetailId'],
67
+ actions: ['callListApi', 'changeListPage', 'setSelectedItem', 'setCheckedRowKeys', 'setListSelectedRowKey', 'setListCheckedIndexes', 'setOrderDetailId', 'downloadExcel'],
68
+ useCases: ['주문 전체 조회', '결제 조회', '배송 조회'],
69
+ },
70
+ {
71
+ type: 9,
72
+ name: '마스터-디테일 + 모달 + 상세조회 API',
73
+ description: '왼쪽 리스트, 오른쪽 상세, 모달, 상세 조회 API 호출',
74
+ states: ['listSpinning', 'listData', 'listPage', 'selectedItem', 'detail', 'modalOpen', 'detailSpinning'],
75
+ actions: ['callListApi', 'changeListPage', 'setSelectedItem', 'setModalOpen', 'setDetail', 'callDetailApi'],
76
+ useCases: ['직배송 구독', '상담 내역', '주문 상세'],
77
+ },
78
+ {
79
+ type: 10,
80
+ name: '마스터-디테일 + 엑셀 다운로드',
81
+ description: '선택된 행 표시, 엑셀 다운로드 (상세/모달 없음)',
82
+ states: ['listSpinning', 'excelSpinning', 'listData', 'listPage', 'listSelectedRowKey'],
83
+ actions: ['callListApi', 'callExcelDownloadApi', 'changeListPage', 'setListSelectedRowKey'],
84
+ useCases: ['상품 그룹 마스터', '분류 관리', '카테고리 마스터'],
85
+ },
86
+ ];
87
+
88
+ return {
89
+ content: [
90
+ {
91
+ type: 'text',
92
+ text: JSON.stringify(
93
+ {
94
+ success: true,
95
+ templates,
96
+ count: templates.length,
97
+ },
98
+ null,
99
+ 2
100
+ ),
101
+ },
102
+ ],
103
+ };
104
+ }
@@ -0,0 +1,485 @@
1
+ import { promises as fs } from 'fs';
2
+ import * as path from 'path';
3
+ import { glob } from 'glob';
4
+
5
+ /**
6
+ * 메타데이터 스캔 MCP 툴
7
+ * 프로젝트의 DTO, Repository, Code, Modal 등을 스캔하여 캐시 생성
8
+ */
9
+
10
+ // ========== 타입 정의 ==========
11
+
12
+ interface DtoField {
13
+ name: string;
14
+ type: string;
15
+ isOptional: boolean;
16
+ comment?: string;
17
+ source?: string; // 필드가 정의된 위치 (예: "MemberSearch", "DefaultDto")
18
+ }
19
+
20
+ interface DtoIndex {
21
+ name: string;
22
+ filePath: string;
23
+ isExport: boolean;
24
+ extends?: string[]; // 상속 체인
25
+ fields: DtoField[];
26
+ }
27
+
28
+ interface RepositoryMethod {
29
+ name: string;
30
+ requestType?: string;
31
+ responseType?: string;
32
+ }
33
+
34
+ interface RepositoryInfo {
35
+ name: string;
36
+ filePath: string;
37
+ methods: RepositoryMethod[];
38
+ }
39
+
40
+ interface CodeInfo {
41
+ name: string;
42
+ options: Array<{ label: string; value: string }>;
43
+ }
44
+
45
+ interface ModalFunction {
46
+ name: string; // 예: openCustomerModal
47
+ filePath: string;
48
+ returnType?: string;
49
+ }
50
+
51
+ interface MetadataCache {
52
+ version: string;
53
+ scannedAt: string;
54
+ projectRoot: string;
55
+ dtos: Record<string, DtoIndex>;
56
+ repositories: Record<string, RepositoryInfo>;
57
+ codes: Record<string, CodeInfo>;
58
+ modals: Record<string, ModalFunction>;
59
+ }
60
+
61
+ interface ScanMetadataParams {
62
+ projectRoot: string;
63
+ forceRescan?: boolean;
64
+ }
65
+
66
+ // ========== 스캔 경로 패턴 ==========
67
+
68
+ const DTO_PATTERNS = [
69
+ 'src/services/@interface/dto/**/*.ts',
70
+ 'src/services/**/dto/**/*.ts',
71
+ 'src/types/**/*Dto*.ts',
72
+ 'src/types/**/*.ts', // DefaultDto 등 포함
73
+ 'src/@types/**/*.ts', // DefaultDto 포함
74
+ 'src/services/@interface/types/**/*.ts', // DefaultDto 포함
75
+ 'src/interfaces/**/*Request*.ts',
76
+ 'src/interfaces/**/*Response*.ts',
77
+ ];
78
+
79
+ const REPOSITORY_PATTERNS = [
80
+ 'src/services/**/*Repository.ts',
81
+ 'src/services/**/repository/*.ts',
82
+ ];
83
+
84
+ const CODE_PATTERNS = [
85
+ 'src/stores/**/*.ts',
86
+ 'src/**/useCodeStore.ts',
87
+ ];
88
+
89
+ const MODAL_PATTERNS = [
90
+ 'src/modals/**/*Modal.ts',
91
+ 'src/components/**/*Modal.ts',
92
+ ];
93
+
94
+ // ========== 메인 함수 ==========
95
+
96
+ export async function scanMetadata(args: ScanMetadataParams): Promise<{
97
+ content: Array<{ type: string; text: string }>;
98
+ }> {
99
+ const { projectRoot, forceRescan = false } = args;
100
+
101
+ // 캐시 파일 경로 (.claude 폴더 사용)
102
+ const cachePath = path.join(projectRoot, '.claude', 'mcp-cache', 'search-params-meta.json');
103
+
104
+ // 캐시 확인
105
+ if (!forceRescan) {
106
+ try {
107
+ const cacheExists = await fs.access(cachePath).then(() => true).catch(() => false);
108
+ if (cacheExists) {
109
+ const cacheContent = await fs.readFile(cachePath, 'utf-8');
110
+ const cache: MetadataCache = JSON.parse(cacheContent);
111
+
112
+ // 캐시 유효성 확인 (1시간 이내)
113
+ const cacheAge = Date.now() - new Date(cache.scannedAt).getTime();
114
+ if (cacheAge < 60 * 60 * 1000) {
115
+ return {
116
+ content: [
117
+ { type: 'text', text: JSON.stringify({ success: true, cached: true, cache }, null, 2) },
118
+ ],
119
+ };
120
+ }
121
+ }
122
+ } catch {
123
+ // 캐시 없음 or 실패 → 계속 진행
124
+ }
125
+ }
126
+
127
+ // 메타데이터 스캔
128
+ console.error(`[MCP] 스캔 시작: ${projectRoot}`);
129
+
130
+ const cache: MetadataCache = {
131
+ version: '1.0.0',
132
+ scannedAt: new Date().toISOString(),
133
+ projectRoot,
134
+ dtos: {},
135
+ repositories: {},
136
+ codes: {},
137
+ modals: {},
138
+ };
139
+
140
+ // 1. DTO 스캔
141
+ await scanDtos(projectRoot, cache);
142
+
143
+ // 2. Repository 스캔
144
+ await scanRepositories(projectRoot, cache);
145
+
146
+ // 3. Code Store 스캔
147
+ await scanCodes(projectRoot, cache);
148
+
149
+ // 4. Modal 함수 스캔
150
+ await scanModals(projectRoot, cache);
151
+
152
+ // extends 체인 해결
153
+ await resolveExtendsChains(cache);
154
+
155
+ // 캐시 저장
156
+ const cacheDir = path.dirname(cachePath);
157
+ await fs.mkdir(cacheDir, { recursive: true });
158
+ await fs.writeFile(cachePath, JSON.stringify(cache, null, 2), 'utf-8');
159
+
160
+ // README 생성
161
+ const readmePath = path.join(cacheDir, 'README.md');
162
+ await fs.writeFile(
163
+ readmePath,
164
+ `# MCP Cache\n\n이 폴더는 MCP 툴이 생성한 메타데이터 캐시입니다.\n\n- search-params-meta.json: DTO, Repository, Code, Modal 메타데이터\n`,
165
+ 'utf-8'
166
+ );
167
+
168
+ console.error(`[MCP] 스캔 완료:`);
169
+ console.error(` - DTO: ${Object.keys(cache.dtos).length}개`);
170
+ console.error(` - Repository: ${Object.keys(cache.repositories).length}개`);
171
+ console.error(` - Code: ${Object.keys(cache.codes).length}개`);
172
+ console.error(` - Modal: ${Object.keys(cache.modals).length}개`);
173
+
174
+ return {
175
+ content: [
176
+ { type: 'text', text: JSON.stringify({ success: true, cached: false, cache }, null, 2) },
177
+ ],
178
+ };
179
+ }
180
+
181
+ // ========== DTO 스캔 ==========
182
+
183
+ async function scanDtos(projectRoot: string, cache: MetadataCache): Promise<void> {
184
+ for (const pattern of DTO_PATTERNS) {
185
+ const files = await glob(pattern, { cwd: projectRoot, absolute: true });
186
+
187
+ for (const filePath of files) {
188
+ try {
189
+ const content = await fs.readFile(filePath, 'utf-8');
190
+ const dtos = parseDtoFile(content, filePath);
191
+
192
+ for (const dto of dtos) {
193
+ cache.dtos[dto.name] = dto;
194
+ }
195
+ } catch (e) {
196
+ console.error(`[MCP] DTO 스캔 실패: ${filePath}`, e);
197
+ }
198
+ }
199
+ }
200
+ }
201
+
202
+ function parseDtoFile(content: string, filePath: string): DtoIndex[] {
203
+ const results: DtoIndex[] = [];
204
+
205
+ // 인터페이스 선언 추출 (export interface Name extends... { ... })
206
+ const interfaceRegex = /export\s+interface\s+(\w+)\s*(?:extends\s+([^{]+))?\s*\{([\s\S]*?)\n\}/g;
207
+
208
+ let match;
209
+ while ((match = interfaceRegex.exec(content)) !== null) {
210
+ const [, name, extendsClause, body] = match;
211
+
212
+ // extends 파싱
213
+ const extendsList: string[] = [];
214
+ if (extendsClause && extendsClause.trim()) {
215
+ // "A, B, C" → ["A", "B", "C"]
216
+ const types = extendsClause.split(',').map(e => e.trim()).filter(e => e);
217
+ // 제네릭 제거 (예: DataGridPageResponse<...> → DataGridPageResponse)
218
+ extendsList.push(...types.map(t => t.replace(/<[^>]+>/, '')));
219
+ }
220
+
221
+ // 필드 파싱
222
+ const fields = parseFields(body, name);
223
+
224
+ results.push({
225
+ name,
226
+ filePath,
227
+ isExport: true,
228
+ extends: extendsList.length > 0 ? extendsList : undefined,
229
+ fields,
230
+ });
231
+ }
232
+
233
+ return results;
234
+ }
235
+
236
+ function parseFields(body: string, interfaceName: string): DtoField[] {
237
+ const fields: DtoField[] = [];
238
+
239
+ // 필드 추출 (주석 포함)
240
+ const lines = body.split('\n');
241
+ let lastComment: string | undefined;
242
+
243
+ for (const line of lines) {
244
+ // 주석 추출
245
+ const commentMatch = line.match(/^\s*\/\/\s*(.+)$/);
246
+ if (commentMatch) {
247
+ lastComment = commentMatch[1].trim();
248
+ continue;
249
+ }
250
+
251
+ // 필드 추출
252
+ const fieldMatch = line.match(/^\s*(\w+)\s*(\?)?\s*:\s*([^;]+);/);
253
+ if (fieldMatch) {
254
+ const [, name, optional, type] = fieldMatch;
255
+
256
+ // 페이징 필드 제외 (pageNumber, pageSize는 SearchParams에서 자동 처리)
257
+ if (name === 'pageNumber' || name === 'pageSize') {
258
+ continue;
259
+ }
260
+
261
+ fields.push({
262
+ name,
263
+ type: type.trim(),
264
+ isOptional: optional === '?',
265
+ comment: lastComment,
266
+ source: interfaceName,
267
+ });
268
+
269
+ lastComment = undefined;
270
+ }
271
+ }
272
+
273
+ return fields;
274
+ }
275
+
276
+ // ========== Repository 스캔 ==========
277
+
278
+ async function scanRepositories(projectRoot: string, cache: MetadataCache): Promise<void> {
279
+ for (const pattern of REPOSITORY_PATTERNS) {
280
+ const files = await glob(pattern, { cwd: projectRoot, absolute: true });
281
+
282
+ for (const filePath of files) {
283
+ try {
284
+ const content = await fs.readFile(filePath, 'utf-8');
285
+ const repo = parseRepositoryFile(content, filePath);
286
+
287
+ if (repo) {
288
+ cache.repositories[repo.name] = repo;
289
+ }
290
+ } catch (e) {
291
+ console.error(`[MCP] Repository 스캔 실패: ${filePath}`, e);
292
+ }
293
+ }
294
+ }
295
+ }
296
+
297
+ function parseRepositoryFile(content: string, filePath: string): RepositoryInfo | null {
298
+ // 클래스 이름 추출
299
+ const classMatch = content.match(/export\s+class\s+(\w+)Repository/);
300
+ if (!classMatch) return null;
301
+
302
+ const name = classMatch[1] + 'Repository';
303
+ const methods: RepositoryMethod[] = [];
304
+
305
+ // 메서드 추출
306
+ // 예: async postMemberListMember(params: PostMemberListMemberRequest, config?: ApiRequestConfig)
307
+ const methodRegex = /async\s+(\w+)\s*\(\s*params:\s*(\w+Request)[^)]*\)/g;
308
+
309
+ let match;
310
+ while ((match = methodRegex.exec(content)) !== null) {
311
+ const [, methodName, requestType] = match;
312
+
313
+ // Response 타입 추출 (시도)
314
+ const responseMatch = content.match(
315
+ new RegExp(`${methodName}[^{]*{[^}]*_apiWrapper<(${methodName}Response)>`)
316
+ );
317
+
318
+ methods.push({
319
+ name: methodName,
320
+ requestType,
321
+ responseType: responseMatch?.[1],
322
+ });
323
+ }
324
+
325
+ return {
326
+ name,
327
+ filePath,
328
+ methods,
329
+ };
330
+ }
331
+
332
+ // ========== Code Store 스캔 ==========
333
+
334
+ async function scanCodes(projectRoot: string, cache: MetadataCache): Promise<void> {
335
+ for (const pattern of CODE_PATTERNS) {
336
+ const files = await glob(pattern, { cwd: projectRoot, absolute: true });
337
+
338
+ for (const filePath of files) {
339
+ try {
340
+ const content = await fs.readFile(filePath, 'utf-8');
341
+ const codes = parseCodeFile(content, filePath);
342
+
343
+ for (const code of codes) {
344
+ cache.codes[code.name] = code;
345
+ }
346
+ } catch (e) {
347
+ // 무시
348
+ }
349
+ }
350
+ }
351
+ }
352
+
353
+ function parseCodeFile(content: string, filePath: string): CodeInfo[] {
354
+ const results: CodeInfo[] = [];
355
+
356
+ // Code Store 패턴: STATUS_CD: { options: [...] }
357
+ const codeRegex = /(\w+):\s*\{[^}]*options:\s*\[([\s\S]*?)\]/g;
358
+
359
+ let match;
360
+ while ((match = codeRegex.exec(content)) !== null) {
361
+ const [, name, optionsStr] = match;
362
+
363
+ // options 배열 파싱
364
+ const options: Array<{ label: string; value: string }> = [];
365
+ const optionRegex = /\{\s*label:\s*["']([^"']+)["']\s*,\s*value:\s*["']([^"']+)["']\s*\}/g;
366
+
367
+ let optMatch;
368
+ while ((optMatch = optionRegex.exec(optionsStr)) !== null) {
369
+ options.push({
370
+ label: optMatch[1],
371
+ value: optMatch[2],
372
+ });
373
+ }
374
+
375
+ if (options.length > 0) {
376
+ results.push({ name, options });
377
+ }
378
+ }
379
+
380
+ return results;
381
+ }
382
+
383
+ // ========== Modal 함수 스캔 ==========
384
+
385
+ async function scanModals(projectRoot: string, cache: MetadataCache): Promise<void> {
386
+ for (const pattern of MODAL_PATTERNS) {
387
+ const files = await glob(pattern, { cwd: projectRoot, absolute: true });
388
+
389
+ for (const filePath of files) {
390
+ try {
391
+ const content = await fs.readFile(filePath, 'utf-8');
392
+ const modals = parseModalFile(content, filePath);
393
+
394
+ for (const modal of modals) {
395
+ cache.modals[modal.name] = modal;
396
+ }
397
+ } catch (e) {
398
+ // 무시
399
+ }
400
+ }
401
+ }
402
+ }
403
+
404
+ function parseModalFile(content: string, filePath: string): ModalFunction[] {
405
+ const results: ModalFunction[] = [];
406
+
407
+ // openXxxModal 함수 추출
408
+ // 예: export const openCustomerModal = async (): Promise<CustomerModalResult> =>
409
+ const modalRegex = /export\s+(const|async\s+function)\s+(open\w+Modal)\s*(?:=\s*async\s*\([^)]*\)\s*:\s*Promise\s*<\s*(\w+)\s*>|\([^)]*\)\s*\{)/g;
410
+
411
+ let match;
412
+ while ((match = modalRegex.exec(content)) !== null) {
413
+ const [, , funcName, returnType] = match;
414
+
415
+ results.push({
416
+ name: funcName,
417
+ filePath,
418
+ returnType,
419
+ });
420
+ }
421
+
422
+ return results;
423
+ }
424
+
425
+ // ========== extends 체인 해결 ==========
426
+
427
+ async function resolveExtendsChains(cache: MetadataCache): Promise<void> {
428
+ const visited = new Set<string>();
429
+
430
+ for (const dtoName of Object.keys(cache.dtos)) {
431
+ const resolved = await resolveExtendsChain(dtoName, cache, visited);
432
+ cache.dtos[dtoName] = resolved;
433
+ }
434
+ }
435
+
436
+ async function resolveExtendsChain(
437
+ interfaceName: string,
438
+ cache: MetadataCache,
439
+ visited: Set<string>
440
+ ): Promise<DtoIndex> {
441
+ // 순환 참조 방지
442
+ if (visited.has(interfaceName)) {
443
+ return { name: interfaceName, filePath: '', isExport: true, fields: [] };
444
+ }
445
+ visited.add(interfaceName);
446
+
447
+ const current = cache.dtos[interfaceName];
448
+ if (!current) {
449
+ return { name: interfaceName, filePath: '', isExport: true, fields: [] };
450
+ }
451
+
452
+ // extends가 없으면 그대로 반환
453
+ if (!current.extends || current.extends.length === 0) {
454
+ return current;
455
+ }
456
+
457
+ // 자체 필드 (직접 정의된 필드)
458
+ const ownFields = current.fields.filter(f => f.source === current.name);
459
+
460
+ // 부모 필드 수집
461
+ const allFields = [...ownFields];
462
+ const extendsChain: string[] = [];
463
+
464
+ for (const parentName of current.extends) {
465
+ extendsChain.push(parentName);
466
+
467
+ const parent = await resolveExtendsChain(parentName, cache, new Set(visited));
468
+ allFields.push(...parent.fields);
469
+
470
+ if (parent.extends) {
471
+ extendsChain.push(...parent.extends);
472
+ }
473
+ }
474
+
475
+ // 필드 중복 제거 (하위 클래스가 우선)
476
+ const uniqueFields = Array.from(
477
+ new Map(allFields.map(f => [f.name, f])).values()
478
+ );
479
+
480
+ return {
481
+ ...current,
482
+ extends: [...new Set(extendsChain)],
483
+ fields: uniqueFields,
484
+ };
485
+ }