@axboot-mcp/mcp-server 1.0.1 → 1.0.6

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.0.1",
6
+ "version": "1.0.6",
7
7
  "description": "Axboot MCP Server - Store 생성 및 다른 기능 확장 가능",
8
8
  "main": "./dist/index.js",
9
9
  "types": "./dist/index.d.ts",
@@ -27,6 +27,7 @@
27
27
  "author": "",
28
28
  "license": "MIT",
29
29
  "dependencies": {
30
+ "@axboot-mcp/mcp-server": "^1.0.1",
30
31
  "@modelcontextprotocol/sdk": "^1.0.4",
31
32
  "chokidar": "^5.0.0",
32
33
  "glob": "^13.0.0",
@@ -0,0 +1,570 @@
1
+ # MCP 명령어 사용 가이드
2
+
3
+ 이 문서는 NH-FE-B 프로젝트에서 MCP(Model Context Protocol)를 사용하여 SearchParams와 ListDataGrid를 자동 생성하는 방법을 안내합니다.
4
+
5
+ ---
6
+
7
+ ## 목차
8
+
9
+ 1. [케이스 1: SearchParams 생성 (검색 영역)](#케이스-1-searchparams-생성-검색-영역)
10
+ 2. [케이스 2: ListDataGrid 생성 (목록 테이블)](#케이스-2-listdatagrid-생성-목록-테이블)
11
+ 3. [케이스 3: 완전한 화면 생성 (검색 + 목록)](#케이스-3-완전한-화면-생성-검색-목록)
12
+
13
+ ---
14
+
15
+ ## 케이스 1: SearchParams 생성 (검색 영역)
16
+
17
+ ### 개요
18
+
19
+ 페이지 상단의 **검색 조건 영역**을 자동 생성합니다. App.tsx 파일을 수정하여 `<PageSearchBar>` 컴포넌트를 추가합니다.
20
+
21
+ ### 명령어
22
+
23
+ ```
24
+ axboot_generate_search_params
25
+ ```
26
+
27
+ ### 파라미터
28
+
29
+ | 파라미터 | 타입 | 필수 | 설명 |
30
+ |---------|------|:----:|------|
31
+ | `searchDtoPath` | string | ✓ | 검색용 DTO 파일 경로 (절대 경로) |
32
+ | `appPath` | string | ✗ | App.tsx 경로 (자동 감지됨) |
33
+ | `searchParams` | object | ✗ | 수동 설정 (DTO 사용 시 불필요) |
34
+
35
+ ---
36
+
37
+ ### 예시 1: 기본 검색 (날짜 범위)
38
+
39
+ **DTO:** `BannerSearch.ts`
40
+ ```typescript
41
+ export interface BannerSearch extends Request {
42
+ fromDate?: string; // 등록일 시작
43
+ toDate?: string; // 등록일 종료
44
+ }
45
+ ```
46
+
47
+ **요청:**
48
+ ```json
49
+ {
50
+ "searchDtoPath": "/Users/kyle/Desktop/nh-fe-bo/src/services/@interface/dto/BannerSearch.ts"
51
+ }
52
+ ```
53
+
54
+ **결과:** App.tsx에 날짜 범위 검색이 추가됨
55
+
56
+ ---
57
+
58
+ ### 예시 2: 코드 선택 포함
59
+
60
+ **DTO:** `ProductSearch.ts`
61
+ ```typescript
62
+ export interface ProductSearch extends Request {
63
+ categoryCd?: string; // 카테고리 코드
64
+ useYn?: string; // 사용여부
65
+ }
66
+ ```
67
+
68
+ **요청:**
69
+ ```json
70
+ {
71
+ "searchDtoPath": "/Users/kyle/Desktop/nh-fe-bo/src/services/@interface/dto/ProductSearch.ts"
72
+ }
73
+ ```
74
+
75
+ **결과:**
76
+ - 카테고리 셀렉트박스 추가
77
+ - 사용여부 라디오 버튼 추가
78
+ - CodeModel에 `CATEGORYCD` 자동 추가
79
+
80
+ ---
81
+
82
+ ### 예시 3: 복합 검색 조건
83
+
84
+ **DTO:** `OrderSearch.ts`
85
+ ```typescript
86
+ export interface OrderSearch extends Request {
87
+ orderDateFrom?: string; // 주문일 시작
88
+ orderDateTo?: string; // 주문일 종료
89
+ orderStatusCd?: string; // 주문상태 코드
90
+ paymentTypeCd?: string; // 결제수단 코드
91
+ useYn?: string; // 사용여부
92
+ searchText?: string; // 검색어
93
+ }
94
+ ```
95
+
96
+ **요청:**
97
+ ```json
98
+ {
99
+ "searchDtoPath": "/Users/kyle/Desktop/nh-fe-bo/src/services/@interface/dto/OrderSearch.ts"
100
+ }
101
+ ```
102
+
103
+ **결과:**
104
+ - 날짜 범위 (주문일)
105
+ - 코드 선택 (주문상태, 결제수단)
106
+ - Y/N 라디오 (사용여부)
107
+ - 검색어 입력
108
+
109
+ ---
110
+
111
+ ### 예시 4: 수동 설정
112
+
113
+ **요청:**
114
+ ```json
115
+ {
116
+ "appPath": "/Users/kyle/Desktop/nh-fe-bo/src/pages/resources/banner/App.tsx",
117
+ "searchParams": {
118
+ "dateRange": {
119
+ "label": "등록일",
120
+ "presetType": "all"
121
+ },
122
+ "codes": [
123
+ {
124
+ "name": "statusCd",
125
+ "label": "상태",
126
+ "codeVar": "STATUS_CODE",
127
+ "width": 150
128
+ }
129
+ ],
130
+ "search": {
131
+ "withSearchType": false,
132
+ "textName": "searchText"
133
+ }
134
+ }
135
+ }
136
+ ```
137
+
138
+ ---
139
+
140
+ ### 자동 감지 필드 규칙
141
+
142
+ | 필드 패턴 | 감지 결과 |
143
+ |-----------|----------|
144
+ | `fromDate`, `toDate`, `regDtFrom`, `regDtTo` | 날짜 범위 |
145
+ | `xxxTpcd`, `xxxClcd`, `xxxCd` (2글자 이상) | 코드 선택 |
146
+ | `xxxYn` | Y/N 라디오 |
147
+ | `searchText`, `keyword`, `xxxText` | 검색어 입력 |
148
+
149
+ ---
150
+
151
+ ## 케이스 2: ListDataGrid 생성 (목록 테이블)
152
+
153
+ ### 개요
154
+
155
+ 목록 **테이블 컬럼**을 자동 생성합니다. `ListDataGrid.tsx` 파일을 새로 생성합니다.
156
+
157
+ ### 명령어
158
+
159
+ ```
160
+ axboot_generate_listdatagrid
161
+ ```
162
+
163
+ ### 파라미터
164
+
165
+ | 파라미터 | 타입 | 필수 | 설명 |
166
+ |---------|------|:----:|------|
167
+ | `outputPath` | string | ✓ | 생성할 파일 경로 (절대 경로) |
168
+ | `responseDtoPath` | string | ✓ | 목록용 DTO 파일 경로 |
169
+ | `storeName` | string | ✓ | Zustand Store 이름 |
170
+ | `rowKey` | string | ✓ | 행 키 (예: uuid, id) |
171
+ | `config` | object | ✗ | 수동 설정 (DTO 사용 시 불필요) |
172
+
173
+ ---
174
+
175
+ ### 예시 1: 기본 목록
176
+
177
+ **DTO:** `BannerRes.ts`
178
+ ```typescript
179
+ export interface BannerRes extends Response {
180
+ uuid: string;
181
+ title: string;
182
+ statusCd: string;
183
+ useYn: string;
184
+ regDt: string;
185
+ regUser: string;
186
+ }
187
+ ```
188
+
189
+ **요청:**
190
+ ```json
191
+ {
192
+ "outputPath": "/Users/kyle/Desktop/nh-fe-bo/src/pages/resources/banner/ListDataGrid.tsx",
193
+ "responseDtoPath": "/Users/kyle/Desktop/nh-fe-bo/src/services/@interface/dto/BannerRes.ts",
194
+ "storeName": "useBannerStore",
195
+ "rowKey": "uuid"
196
+ }
197
+ ```
198
+
199
+ **결과:**
200
+ ```typescript
201
+ // 자동 생성된 컬럼
202
+ { key: "uuid", label: t("번호"), type: "rowNo" }
203
+ { key: "title", label: t("Title"), type: "title", align: "left", width: 200 }
204
+ { key: "statusCd", label: t("Status"), type: "custom", codeVar: "STATUSCD", width: 120 }
205
+ { key: "useYn", label: t("Use"), type: "selectable", align: "center", width: 80 }
206
+ { key: "regDt", label: t("Reg"), type: "date", align: "center", width: 100 }
207
+ { key: "regUser", label: t("RegUser"), type: "user", align: "left", width: 100 }
208
+ ```
209
+
210
+ ---
211
+
212
+ ### 예시 2: 금액/날짜 포함 목록
213
+
214
+ **DTO:** `OrderRes.ts`
215
+ ```typescript
216
+ export interface OrderRes extends Response {
217
+ orderNo: string;
218
+ orderDate: string;
219
+ customerName: string;
220
+ orderAmt: number;
221
+ paymentTypeCd: string;
222
+ orderStatusCd: string;
223
+ regDttm: string;
224
+ }
225
+ ```
226
+
227
+ **요청:**
228
+ ```json
229
+ {
230
+ "outputPath": "/Users/kyle/Desktop/nh-fe-bo/src/pages/resources/order/ListDataGrid.tsx",
231
+ "responseDtoPath": "/Users/kyle/Desktop/nh-fe-bo/src/services/@interface/dto/OrderRes.ts",
232
+ "storeName": "useOrderStore",
233
+ "rowKey": "orderNo"
234
+ }
235
+ ```
236
+
237
+ **결과:**
238
+ - `orderDate` → `date` 타입 (날짜)
239
+ - `orderAmt` → `money` 타입 (금액, 우측 정렬)
240
+ - `regDttm` → `dateTime` 타입 (날짜시간)
241
+ - `paymentTypeCd`, `orderStatusCd` → 코드 변환
242
+ - `customerName` → `user` 타입
243
+
244
+ ---
245
+
246
+ ### 예시 3: 이미지 포함 목록
247
+
248
+ **DTO:** `ProductRes.ts`
249
+ ```typescript
250
+ export interface ProductRes extends Response {
251
+ prdCd: string;
252
+ prdName: string;
253
+ thumbImage: string;
254
+ listImage: string;
255
+ price: number;
256
+ displayYn: string;
257
+ }
258
+ ```
259
+
260
+ **요청:**
261
+ ```json
262
+ {
263
+ "outputPath": "/Users/kyle/Desktop/nh-fe-bo/src/pages/resources/product/ListDataGrid.tsx",
264
+ "responseDtoPath": "/Users/kyle/Desktop/nh-fe-bo/src/services/@interface/dto/ProductRes.ts",
265
+ "storeName": "useProductStore",
266
+ "rowKey": "prdCd"
267
+ }
268
+ ```
269
+
270
+ **결과:**
271
+ - `thumbImage` → `ThumbPreview` (썸네일)
272
+ - `listImage` → `ImagePreview` (이미지 프리뷰)
273
+ - `price` → `money` 타입
274
+ - `displayYn` → `selectable` 타입
275
+
276
+ ---
277
+
278
+ ### 예시 4: 게시판 스타일
279
+
280
+ **DTO:** `BoardRes.ts`
281
+ ```typescript
282
+ export interface BoardRes extends Response {
283
+ bbsNo: string;
284
+ bbsTitle: string;
285
+ bbsTypeCd: string;
286
+ noticeYn: string;
287
+ fileCount: number;
288
+ readCount: number;
289
+ regDt: string;
290
+ regUser: string;
291
+ }
292
+ ```
293
+
294
+ **요청:**
295
+ ```json
296
+ {
297
+ "outputPath": "/Users/kyle/Desktop/nh-fe-bo/src/pages/resources/board/ListDataGrid.tsx",
298
+ "responseDtoPath": "/Users/kyle/Desktop/nh-fe-bo/src/services/@interface/dto/BoardRes.ts",
299
+ "storeName": "useBoardStore",
300
+ "rowKey": "bbsNo"
301
+ }
302
+ ```
303
+
304
+ **결과:**
305
+ - `bbsTitle` → `bbsTitle` 타입 (제목 + 파일 유형 표시)
306
+
307
+ ---
308
+
309
+ ### 예시 5: 옵션 추가 (엑셀, 삭제, 복사)
310
+
311
+ **요청:**
312
+ ```json
313
+ {
314
+ "outputPath": "/Users/kyle/Desktop/nh-fe-bo/src/pages/resources/banner/ListDataGrid.tsx",
315
+ "responseDtoPath": "/Users/kyle/Desktop/nh-fe-bo/src/services/@interface/dto/BannerRes.ts",
316
+ "storeName": "useBannerStore",
317
+ "rowKey": "uuid",
318
+ "config": {
319
+ "withFormHeader": true,
320
+ "withExcel": true,
321
+ "withDelete": true,
322
+ "withCopy": true,
323
+ "selectionMode": "single",
324
+ "onClickType": "modal"
325
+ }
326
+ }
327
+ ```
328
+
329
+ ---
330
+
331
+ ### 자동 감지 컬럼 타입
332
+
333
+ | 필드 패턴 | 컬럼 타입 | 정렬 | 너비 |
334
+ |-----------|----------|------|------|
335
+ | `xxxDt`, `xxxDate` | `date` | center | 100 |
336
+ | `xxxDttm`, `xxxDateTime` | `dateTime` | center | 140 |
337
+ | `xxxAmt`, `xxxPrice`, `xxxCost` | `money` | right | 120 |
338
+ | `xxxYn` | `selectable` | center | 80 |
339
+ | `xxxNm`, `xxxName`, `xxxUser` | `user` | left | 100 |
340
+ | `xxxTpcd`, `xxxClcd`, `xxxCd` | `custom` (코드) | - | 120 |
341
+ | `title`, `subject`, `bbsTitle` | `title`/`bbsTitle` | left | 200 |
342
+ | `xxxImg`, `xxxThumb` | `image` | center | 100 |
343
+ | `content`, `description`, `remark` | `custom` | left | 300 |
344
+
345
+ ---
346
+
347
+ ## 케이스 3: 완전한 화면 생성 (검색 + 목록)
348
+
349
+ ### 개요
350
+
351
+ 검색 영역과 목록 테이블을 모두 생성하여 **완전한 화면**을 만듭니다. 두 명령어를 순서대로 실행합니다.
352
+
353
+ ---
354
+
355
+ ### 예시 1: 배너 관리 화면
356
+
357
+ **Step 1: SearchParams 생성**
358
+ ```json
359
+ {
360
+ "searchDtoPath": "/Users/kyle/Desktop/nh-fe-bo/src/services/@interface/dto/BannerSearch.ts"
361
+ }
362
+ ```
363
+
364
+ **Step 2: ListDataGrid 생성**
365
+ ```json
366
+ {
367
+ "outputPath": "/Users/kyle/Desktop/nh-fe-bo/src/pages/resources/banner/ListDataGrid.tsx",
368
+ "responseDtoPath": "/Users/kyle/Desktop/nh-fe-bo/src/services/@interface/dto/BannerRes.ts",
369
+ "storeName": "useBannerStore",
370
+ "rowKey": "uuid"
371
+ }
372
+ ```
373
+
374
+ **Step 3: App.tsx에 import 추가**
375
+ ```typescript
376
+ import { ListDataGrid } from "./ListDataGrid";
377
+ ```
378
+
379
+ **결과:** 검색 + 목록 화면 완성
380
+
381
+ ---
382
+
383
+ ### 예시 2: 상품 관리 화면
384
+
385
+ **DTO:**
386
+ - `ProductSearch.ts` (검색용)
387
+ - `ProductRes.ts` (목록용)
388
+
389
+ **Step 1: SearchParams**
390
+ ```json
391
+ {
392
+ "searchDtoPath": "/Users/kyle/Desktop/nh-fe-bo/src/services/@interface/dto/ProductSearch.ts"
393
+ }
394
+ ```
395
+
396
+ **Step 2: ListDataGrid**
397
+ ```json
398
+ {
399
+ "outputPath": "/Users/kyle/Desktop/nh-fe-bo/src/pages/resources/product/ListDataGrid.tsx",
400
+ "responseDtoPath": "/Users/kyle/Desktop/nh-fe-bo/src/services/@interface/dto/ProductRes.ts",
401
+ "storeName": "useProductStore",
402
+ "rowKey": "prdCd",
403
+ "config": {
404
+ "withFormHeader": true,
405
+ "withExcel": true,
406
+ "selectionMode": "single",
407
+ "onClickType": "modal"
408
+ }
409
+ }
410
+ ```
411
+
412
+ ---
413
+
414
+ ### 예시 3: 주문 관리 화면
415
+
416
+ **DTO:**
417
+ - `OrderSearch.ts`
418
+ - `OrderRes.ts`
419
+
420
+ **Step 1: SearchParams**
421
+ ```json
422
+ {
423
+ "searchDtoPath": "/Users/kyle/Desktop/nh-fe-bo/src/services/@interface/dto/OrderSearch.ts"
424
+ }
425
+ ```
426
+
427
+ **Step 2: ListDataGrid**
428
+ ```json
429
+ {
430
+ "outputPath": "/Users/kyle/Desktop/nh-fe-bo/src/pages/resources/order/ListDataGrid.tsx",
431
+ "responseDtoPath": "/Users/kyle/Desktop/nh-fe-bo/src/services/@interface/dto/OrderRes.ts",
432
+ "storeName": "useOrderStore",
433
+ "rowKey": "orderNo",
434
+ "config": {
435
+ "selectionMode": "multi",
436
+ "onClickType": "link"
437
+ }
438
+ }
439
+ ```
440
+
441
+ ---
442
+
443
+ ### 예시 4: 게시판 화면
444
+
445
+ **DTO:**
446
+ - `BbsSearch.ts`
447
+ - `BbsRes.ts`
448
+
449
+ **Step 1: SearchParams**
450
+ ```json
451
+ {
452
+ "searchDtoPath": "/Users/kyle/Desktop/nh-fe-bo/src/services/@interface/dto/BbsSearch.ts"
453
+ }
454
+ ```
455
+
456
+ **Step 2: ListDataGrid**
457
+ ```json
458
+ {
459
+ "outputPath": "/Users/kyle/Desktop/nh-fe-bo/src/pages/resources/bbs/ListDataGrid.tsx",
460
+ "responseDtoPath": "/Users/kyle/Desktop/nh-fe-bo/src/services/@interface/dto/BbsRes.ts",
461
+ "storeName": "useBbsStore",
462
+ "rowKey": "bbsNo",
463
+ "config": {
464
+ "withFormHeader": true,
465
+ "withExcel": true,
466
+ "onClickType": "modal"
467
+ }
468
+ }
469
+ ```
470
+
471
+ ---
472
+
473
+ ### 예시 5: 코드 관리 화면
474
+
475
+ **DTO:**
476
+ - `CodeSearch.ts`
477
+ - `CodeRes.ts`
478
+
479
+ **Step 1: SearchParams**
480
+ ```json
481
+ {
482
+ "searchDtoPath": "/Users/kyle/Desktop/nh-fe-bo/src/services/@interface/dto/CodeSearch.ts"
483
+ }
484
+ ```
485
+
486
+ **Step 2: ListDataGrid**
487
+ ```json
488
+ {
489
+ "outputPath": "/Users/kyle/Desktop/nh-fe-bo/src/pages/resources/code/ListDataGrid.tsx",
490
+ "responseDtoPath": "/Users/kyle/Desktop/nh-fe-bo/src/services/@interface/dto/CodeRes.ts",
491
+ "storeName": "useCodeStore",
492
+ "rowKey": "cd",
493
+ "config": {
494
+ "withFormHeader": true,
495
+ "withDelete": true,
496
+ "selectionMode": "single",
497
+ "onClickType": "modal"
498
+ }
499
+ }
500
+ ```
501
+
502
+ ---
503
+
504
+ ## 파라미터 이름 구분
505
+
506
+ | 핸들러 | 파라미터 | 용도 | DTO 예시 |
507
+ |--------|---------|------|----------|
508
+ | `axboot_generate_search_params` | `searchDtoPath` | 검색 조건 생성 | `BannerSearch` |
509
+ | `axboot_generate_listdatagrid` | `responseDtoPath` | 목록 테이블 생성 | `BannerRes` |
510
+
511
+ ---
512
+
513
+ ## 추가 기능
514
+
515
+ ### ESLint 자동 실행
516
+
517
+ 모든 생성/수정 작업 후 ESLint가 자동으로 실행됩니다.
518
+
519
+ ### CodeModel 자동 업데이트
520
+
521
+ 코드 필드가 감지되면 `useCodeStore.ts`의 `CodeModel` 인터페이스에 자동 추가됩니다.
522
+
523
+ ---
524
+
525
+ ## 팁
526
+
527
+ 1. **DTO 네이밍 컨벤션 따르기**
528
+ - 검색용: `xxxSearch.ts` → `xxxSearch` 인터페이스
529
+ - 목록용: `xxxRes.ts` → `xxxRes` 인터페이스
530
+
531
+ 2. **주석 활용**
532
+ - DTO 필드에 주석을 달면 라벨로 사용됩니다
533
+ ```typescript
534
+ export interface BannerRes extends Response {
535
+ uuid: string; // UUID
536
+ title: string; // 배너 제목
537
+ statusCd?: string; // 상태
538
+ }
539
+ ```
540
+
541
+ 3. **rowKey 선택**
542
+ - 주로 `uuid`, `id`, `code` 등 고유값 사용
543
+ - DB 기본키와 일치시키기
544
+
545
+ 4. **Store 생성 먼저**
546
+ - `axboot_generate_store_and_hook_interactive`로 Store 생성 후 사용
547
+
548
+ ---
549
+
550
+ ## 문제 해결
551
+
552
+ ### 문제: DTO를 찾을 수 없음
553
+ **해결:** 절대 경로를 사용하세요
554
+ ```
555
+ /Users/kyle/Desktop/nh-fe-bo/src/services/@interface/dto/BannerSearch.ts
556
+ ```
557
+
558
+ ### 문제: CodeModel 이미 존재
559
+ **해결:** 이미 존재하는 코드는 건너뜁니다. 안심하세요.
560
+
561
+ ### 문제: App.tsx 경로 자동 감지 실패
562
+ **해결:** `appPath` 파라미터로 직접 지정하세요.
563
+
564
+ ---
565
+
566
+ ## 참고
567
+
568
+ - Store 생성: `axboot_generate_store_and_hook_interactive`
569
+ - SearchParams 패턴: `src/prompts/search-params.md`
570
+ - ListDataGrid 패턴: `src/prompts/listdatagrid-usage.md`
@@ -1,5 +1,6 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
+ import { execSync } from 'child_process';
3
4
 
4
5
  /**
5
6
  * ListDataGrid 컴포넌트 생성 도구
@@ -8,8 +9,14 @@ import * as path from 'path';
8
9
  export interface GenerateListDataGridParams {
9
10
  /** 생성할 ListDataGrid 파일 경로 (절대 경로) */
10
11
  outputPath: string;
11
- /** ListDataGrid 설정 */
12
- config: ListDataGridConfig;
12
+ /** ListDataGrid 설정 (responseDtoPath를 사용하면 자동 생성됨) */
13
+ config?: ListDataGridConfig;
14
+ /** DTO 파일 경로 (절대 경로) - DTO를 분석해서 자동으로 컬럼 생성 */
15
+ responseDtoPath?: string;
16
+ /** Store 이름 (responseDtoPath 시 필수) */
17
+ storeName?: string;
18
+ /** 행 키 (responseDtoPath 시 필수, 예: id, prdCd) */
19
+ rowKey?: string;
13
20
  }
14
21
 
15
22
  export interface ListDataGridConfig {
@@ -68,10 +75,53 @@ export interface ColumnConfig {
68
75
  export async function generateListDataGrid(params: GenerateListDataGridParams): Promise<{
69
76
  content: Array<{ type: string; text: string }>;
70
77
  }> {
71
- const { outputPath, config } = params;
78
+ const { outputPath, config, responseDtoPath, storeName, rowKey } = params;
79
+
80
+ let finalConfig = config;
81
+
82
+ // DTO 파싱으로 자동 생성
83
+ if (responseDtoPath && !finalConfig) {
84
+ if (!storeName) {
85
+ return {
86
+ content: [{
87
+ type: 'text',
88
+ text: JSON.stringify({
89
+ success: false,
90
+ error: 'responseDtoPath를 사용할 때는 storeName이 필수입니다.',
91
+ }),
92
+ }],
93
+ };
94
+ }
95
+ if (!rowKey) {
96
+ return {
97
+ content: [{
98
+ type: 'text',
99
+ text: JSON.stringify({
100
+ success: false,
101
+ error: 'responseDtoPath를 사용할 때는 rowKey가 필수입니다.',
102
+ }),
103
+ }],
104
+ };
105
+ }
106
+
107
+ const parsed = parseDtoForListDataGrid(responseDtoPath, storeName, rowKey);
108
+ finalConfig = parsed.config;
109
+ }
110
+
111
+ if (!finalConfig) {
112
+ return {
113
+ content: [{
114
+ type: 'text',
115
+ text: JSON.stringify({
116
+ success: false,
117
+ error: 'config 또는 responseDtoPath 중 하나는 필수입니다.',
118
+ }),
119
+ }],
120
+ };
121
+ }
72
122
 
73
123
  // 코드 생성
74
- const code = generateListDataGridCode(config);
124
+ const code = generateListDataGridCode(finalConfig);
75
125
 
76
126
  // 파일에 쓰기
77
127
  const dir = path.dirname(outputPath);
@@ -79,6 +129,15 @@ export async function generateListDataGrid(params: GenerateListDataGridParams):
79
129
  fs.mkdirSync(dir, { recursive: true });
80
130
  }
81
131
  fs.writeFileSync(outputPath, code, 'utf-8');
132
+ console.error(`[MCP] ListDataGrid 파일 생성 완료: ${outputPath}`);
133
+
134
+ // ESLint 실행
135
+ try {
136
+ execSync(`npx eslint "${outputPath}" --fix`, { cwd: dir, stdio: 'pipe' });
137
+ console.error(`[MCP] ESLint 실행 완료: ${outputPath}`);
138
+ } catch (e) {
139
+ console.error(`[MCP] ESLint 실행 실패 (무시): ${e}`);
140
+ }
82
141
 
83
142
  return {
84
143
  content: [
@@ -88,6 +147,7 @@ export async function generateListDataGrid(params: GenerateListDataGridParams):
88
147
  success: true,
89
148
  outputPath,
90
149
  message: `ListDataGrid 파일이 생성되었습니다: ${outputPath}`,
150
+ dtoAnalysis: responseDtoPath ? parseDtoForListDataGrid(responseDtoPath, storeName!, rowKey!).analysis : undefined,
91
151
  nextSteps: `
92
152
  ## 다음 단계
93
153
 
@@ -117,19 +177,19 @@ import { ListDataGrid } from "./ListDataGrid";
117
177
  \`\`\`typescript
118
178
  const { columns } = useDataGridColumns<DtoItem>(
119
179
  [
120
- { key: "${config.rowKey}", label: t("번호"), type: "rowNo" },
180
+ { key: "${finalConfig.rowKey}", label: t("번호"), type: "rowNo" },
121
181
  // TODO: 여기에 컬럼 추가
122
- ${config.columns.map(c => `{ key: "${c.key}", label: t("${c.label}")${c.type ? `, type: "${c.type}"` : ''}${c.width ? `, width: ${c.width}` : ''}${c.align ? `, align: "${c.align}"` : ''} }`).join('\n ')}
182
+ ${finalConfig.columns.map(c => `{ key: "${c.key}", label: t("${c.label}")${c.type ? `, type: "${c.type}"` : ''}${c.width ? `, width: ${c.width}` : ''}${c.align ? `, align: "${c.align}"` : ''} }`).join('\n ')}
123
183
  ],
124
184
  {
125
185
  colWidths: listColWidths,
126
- ${config.columns.some(c => c.codeVar) ? `deps: [${config.columns.filter(c => c.codeVar).map(c => c.codeVar).join(', ')}],` : ''}
186
+ ${finalConfig.columns.some(c => c.codeVar) ? `deps: [${finalConfig.columns.filter(c => c.codeVar).map(c => c.codeVar).join(', ')}],` : ''}
127
187
  }
128
188
  );
129
189
  \`\`\`
130
190
 
131
191
  ### 4. onClickItem 핸들러 구현
132
- onClickType이 "${config.onClickType || 'select'}"이므로 필요한 동작을 구현하세요.
192
+ onClickType이 "${finalConfig.onClickType || 'select'}"이므로 필요한 동작을 구현하세요.
133
193
  `.trim(),
134
194
  }),
135
195
  },
@@ -659,3 +719,247 @@ const FormHeader = styled(PageLayout.FrameHeader)\`\`;`;
659
719
 
660
720
  return styled;
661
721
  }
722
+
723
+ /**
724
+ * DTO 필드 정보
725
+ */
726
+ interface DtoField {
727
+ name: string;
728
+ type: string;
729
+ optional: boolean;
730
+ comment?: string;
731
+ }
732
+
733
+ /**
734
+ * DTO 파일 파싱하여 ListDataGrid 설정 자동 생성
735
+ */
736
+ function parseDtoForListDataGrid(
737
+ responseDtoPath: string,
738
+ storeName: string,
739
+ rowKey: string
740
+ ): { config: ListDataGridConfig; analysis: string } {
741
+ let content = '';
742
+ try {
743
+ content = fs.readFileSync(responseDtoPath, 'utf-8');
744
+ } catch (e) {
745
+ return {
746
+ config: {
747
+ storeName,
748
+ dtoType: 'DtoItem',
749
+ rowKey,
750
+ columns: [],
751
+ },
752
+ analysis: `DTO 파일을 읽을 수 없습니다: ${responseDtoPath}`,
753
+ };
754
+ }
755
+
756
+ // 인터페이스 내용 추출
757
+ const interfaceMatch = content.match(/export\s+interface\s+(\w+)\s+extends\s+[\w.]+\s*\{([\s\S]*?)^\}/m);
758
+ if (!interfaceMatch) {
759
+ return {
760
+ config: {
761
+ storeName,
762
+ dtoType: 'DtoItem',
763
+ rowKey,
764
+ columns: [],
765
+ },
766
+ analysis: 'DTO 인터페이스를 찾을 수 없습니다.',
767
+ };
768
+ }
769
+
770
+ const interfaceBody = interfaceMatch[2];
771
+ const interfaceName = interfaceMatch[1];
772
+
773
+ // 필드 파싱
774
+ const fieldLines = interfaceBody.split('\n').filter(line => line.trim() && !line.trim().startsWith('//'));
775
+
776
+ const fields: DtoField[] = [];
777
+ const columns: ColumnConfig[] = [];
778
+ const detectedCodes: string[] = [];
779
+
780
+ for (const line of fieldLines) {
781
+ // 주석 추출
782
+ const commentMatch = line.match(/\/\/(.*)$/);
783
+ const comment = commentMatch ? commentMatch[1].trim() : '';
784
+
785
+ // 필드 정의 추출
786
+ const fieldMatch = line.match(/^\s*(\w+)\s*:\s*([^;]+);/);
787
+ if (fieldMatch) {
788
+ const name = fieldMatch[1];
789
+ const type = fieldMatch[2].trim();
790
+ const optional = type.endsWith('?');
791
+
792
+ fields.push({ name, type, optional, comment });
793
+ }
794
+ }
795
+
796
+ // 컬럼 설정 생성
797
+ for (const field of fields) {
798
+ const { name, comment } = field;
799
+
800
+ // 페이지네이션 필드 제외
801
+ if (['pageNumber', 'pageSize', 'pageNo', 'rowNum', 'totalCount'].includes(name)) {
802
+ continue;
803
+ }
804
+
805
+ // rowKey 제외 (번호 컬럼으로 자동 생성됨)
806
+ if (name === rowKey) {
807
+ continue;
808
+ }
809
+
810
+ let column: ColumnConfig | null = null;
811
+
812
+ // 코드 필드 감지 (xxxTpcd, xxxClcd, xxxKncd, xxxCd 등)
813
+ if (name.endsWith('Tpcd') || name.endsWith('Clcd') || name.endsWith('Kncd') ||
814
+ (name.endsWith('Cd') && name.length > 2 && !name.endsWith('YnCd'))) {
815
+ const label = comment || name.replace(/Tpcd$|Clcd$|Kncd$|Cd$/, '').replace(/^[a-z]/g, c => c.toUpperCase());
816
+ const codeVar = name.toUpperCase();
817
+ detectedCodes.push(codeVar);
818
+
819
+ column = {
820
+ key: name.charAt(0).toLowerCase() + name.slice(1), // camelCase
821
+ label,
822
+ type: 'custom',
823
+ codeVar,
824
+ width: 120,
825
+ };
826
+ }
827
+ // 날짜 필드 (xxxDt, xxxDate)
828
+ else if (name.endsWith('Dt') || name.endsWith('Date')) {
829
+ const label = comment || name.replace(/Dt$|Date$/, '').replace(/^[a-z]/g, c => c.toUpperCase());
830
+ column = {
831
+ key: name,
832
+ label,
833
+ type: 'date',
834
+ align: 'center',
835
+ width: 100,
836
+ };
837
+ }
838
+ // 날짜시간 필드 (xxxDttm, xxxDateTime)
839
+ else if (name.endsWith('Dttm') || name.endsWith('DateTime')) {
840
+ const label = comment || name.replace(/Dttm$|DateTime$/, '').replace(/^[a-z]/g, c => c.toUpperCase());
841
+ column = {
842
+ key: name,
843
+ label,
844
+ type: 'dateTime',
845
+ align: 'center',
846
+ width: 140,
847
+ };
848
+ }
849
+ // 금액 필드 (xxxAmt, xxxPrice, xxxAmt)
850
+ else if (name.endsWith('Amt') || name.endsWith('Price') || name.endsWith('Cost') || name.endsWith('Amt')) {
851
+ const label = comment || name.replace(/Amt$|Price$|Cost$/, '').replace(/^[a-z]/g, c => c.toUpperCase());
852
+ column = {
853
+ key: name,
854
+ label,
855
+ type: 'money',
856
+ align: 'right',
857
+ width: 120,
858
+ };
859
+ }
860
+ // Y/N 필드 (xxxYn)
861
+ else if (name.endsWith('Yn')) {
862
+ const label = comment || name.replace(/Yn$/, '').replace(/^[a-z]/g, c => c.toUpperCase());
863
+ column = {
864
+ key: name,
865
+ label,
866
+ type: 'selectable',
867
+ align: 'center',
868
+ width: 80,
869
+ };
870
+ }
871
+ // 사용자명 필드 (xxxNm, xxxName, xxxUser)
872
+ else if (name.endsWith('Nm') || name.endsWith('Name') || name.endsWith('User')) {
873
+ const label = comment || name.replace(/Nm$|Name$|User$/, '').replace(/^[a-z]/g, c => c.toUpperCase());
874
+ column = {
875
+ key: name,
876
+ label,
877
+ type: 'user',
878
+ align: 'left',
879
+ width: 100,
880
+ };
881
+ }
882
+ // 이미지 필드 (xxxImg, xxxImage, xxxThumb, xxxPhoto)
883
+ else if (name.includes('img') || name.includes('Img') || name.includes('Image') ||
884
+ name.includes('thumb') || name.includes('Thumb') || name.includes('photo') || name.includes('Photo')) {
885
+ const label = comment || name.replace(/^[a-z]/g, c => c.toUpperCase());
886
+ const imageType = name.includes('thumb') || name.includes('Thumb') ? 'ThumbPreview' : 'ImagePreview';
887
+ column = {
888
+ key: name,
889
+ label,
890
+ type: 'image',
891
+ align: 'center',
892
+ width: 100,
893
+ imageType,
894
+ };
895
+ }
896
+ // 제목 필드 (title, subject, xxxTitle, xxxSubject, bbsTitle)
897
+ else if (name === 'title' || name === 'subject' || name.endsWith('Title') || name.endsWith('Subject') || name === 'bbsTitle') {
898
+ const label = comment || name.replace(/^[a-z]/g, c => c.toUpperCase());
899
+ column = {
900
+ key: name,
901
+ label,
902
+ type: name === 'bbsTitle' ? 'bbsTitle' : 'title',
903
+ align: 'left',
904
+ width: 200,
905
+ };
906
+ }
907
+ // 내용 필드 (content, contents, description) - 넓게
908
+ else if (name === 'content' || name === 'contents' || name === 'description' || name === 'remark') {
909
+ const label = comment || name.replace(/^[a-z]/g, c => c.toUpperCase());
910
+ column = {
911
+ key: name,
912
+ label,
913
+ type: 'custom',
914
+ align: 'left',
915
+ width: 300,
916
+ };
917
+ }
918
+ // 그 외 필드
919
+ else {
920
+ const label = comment || name.replace(/^[a-z]/g, c => c.toUpperCase());
921
+ column = {
922
+ key: name,
923
+ label,
924
+ type: 'custom',
925
+ align: 'left',
926
+ width: 150,
927
+ };
928
+ }
929
+
930
+ if (column) {
931
+ columns.push(column);
932
+ }
933
+ }
934
+
935
+ // 분석 결과 생성
936
+ let analysis = `## DTO: ${interfaceName}\n\n`;
937
+ analysis += `### 필드 분석 결과\n\n`;
938
+ analysis += `- 총 ${fields.length}개 필드 중 ${columns.length}개 컬럼 자동 생성\n\n`;
939
+
940
+ if (detectedCodes.length > 0) {
941
+ analysis += `- ✅ **코드 필드** (${detectedCodes.length}개): ${detectedCodes.join(', ')}\n`;
942
+ }
943
+
944
+ const dateCount = columns.filter(c => c.type === 'date').length;
945
+ const dateTimeCount = columns.filter(c => c.type === 'dateTime').length;
946
+ const moneyCount = columns.filter(c => c.type === 'money').length;
947
+ const ynCount = columns.filter(c => c.type === 'selectable').length;
948
+ const imageCount = columns.filter(c => c.type === 'image').length;
949
+
950
+ if (dateCount > 0) analysis += `- ✅ **날짜 필드**: ${dateCount}개\n`;
951
+ if (dateTimeCount > 0) analysis += `- ✅ **날짜시간 필드**: ${dateTimeCount}개\n`;
952
+ if (moneyCount > 0) analysis += `- ✅ **금액 필드**: ${moneyCount}개\n`;
953
+ if (ynCount > 0) analysis += `- ✅ **Y/N 필드**: ${ynCount}개\n`;
954
+ if (imageCount > 0) analysis += `- ✅ **이미지 필드**: ${imageCount}개\n`;
955
+
956
+ return {
957
+ config: {
958
+ storeName,
959
+ dtoType: interfaceName,
960
+ rowKey,
961
+ columns,
962
+ },
963
+ analysis,
964
+ };
965
+ }
@@ -1,17 +1,18 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
+ import { execSync } from 'child_process';
3
4
 
4
5
  /**
5
6
  * SearchParams 코드 생성 도구
6
7
  * App.tsx에 직접 SearchParams를 추가합니다.
7
8
  */
8
9
  export interface GenerateSearchParamsParams {
9
- /** App.tsx 파일 경로 (절대 경로) - dtoPath를 사용하면 자동 감지 */
10
+ /** App.tsx 파일 경로 (절대 경로) - searchDtoPath를 사용하면 자동 감지 */
10
11
  appPath?: string;
11
12
  /** SearchParams 설정 */
12
13
  searchParams?: SearchParamsConfig;
13
- /** DTO 파일 경로 (절대 경로) - DTO를 분석해서 자동으로 SearchParams 생성 및 App.tsx 수정 */
14
- dtoPath?: string;
14
+ /** 검색용 DTO 파일 경로 (절대 경로) - DTO를 분석해서 자동으로 SearchParams 생성 및 App.tsx 수정 */
15
+ searchDtoPath?: string;
15
16
  }
16
17
 
17
18
  export interface SearchParamsConfig {
@@ -100,15 +101,15 @@ interface DtoField {
100
101
  export async function generateSearchParams(params: GenerateSearchParamsParams): Promise<{
101
102
  content: Array<{ type: string; text: string }>;
102
103
  }> {
103
- const { dtoPath, searchParams } = params;
104
+ const { searchDtoPath, searchParams } = params;
104
105
 
105
106
  // 1. DTO 파싱
106
107
  let config: SearchParamsConfig | null = null;
107
108
  let dtoAnalysis = '';
108
109
  let detectedCodes: string[] = [];
109
110
 
110
- if (dtoPath) {
111
- const dtoResult = parseDtoFile(dtoPath);
111
+ if (searchDtoPath) {
112
+ const dtoResult = parseDtoFile(searchDtoPath);
112
113
  dtoAnalysis = dtoResult.analysis;
113
114
  config = dtoResult.searchParams;
114
115
  detectedCodes = dtoResult.detectedCodes;
@@ -133,12 +134,12 @@ export async function generateSearchParams(params: GenerateSearchParamsParams):
133
134
 
134
135
  // 2. App.tsx 경로 결정
135
136
  let appPath = params.appPath;
136
- if (!appPath && dtoPath) {
137
+ if (!appPath && searchDtoPath) {
137
138
  // DTO 경로에서 App.tsx 경로 추정
138
139
  // 예: /Users/kyle/Desktop/nh-fe-bo/src/services/@interface/dto/BannerSearch.ts
139
140
  // → /Users/kyle/Desktop/nh-fe-bo/src/pages/resources/banner/App.tsx
140
141
  const projectRoot = '/Users/kyle/Desktop/nh-fe-bo';
141
- const dtoName = path.basename(dtoPath, '.ts').replace('Search', '');
142
+ const dtoName = path.basename(searchDtoPath, '.ts').replace('Search', '');
142
143
  const pageName = dtoName.toLowerCase();
143
144
 
144
145
  const possiblePaths = [
@@ -177,7 +178,7 @@ export async function generateSearchParams(params: GenerateSearchParamsParams):
177
178
  dtoAnalysis,
178
179
  appPath,
179
180
  modifications,
180
- message: generateSuccessMessage(dtoPath, finalConfig, appPath),
181
+ message: generateSuccessMessage(searchDtoPath, finalConfig, appPath),
181
182
  }),
182
183
  }],
183
184
  };
@@ -186,7 +187,7 @@ export async function generateSearchParams(params: GenerateSearchParamsParams):
186
187
  /**
187
188
  * DTO 파일 파싱
188
189
  */
189
- function parseDtoFile(dtoPath: string): {
190
+ function parseDtoFile(searchDtoPath: string): {
190
191
  fields: DtoField[];
191
192
  analysis: string;
192
193
  searchParams: SearchParamsConfig;
@@ -194,11 +195,11 @@ function parseDtoFile(dtoPath: string): {
194
195
  } {
195
196
  let content = '';
196
197
  try {
197
- content = fs.readFileSync(dtoPath, 'utf-8');
198
+ content = fs.readFileSync(searchDtoPath, 'utf-8');
198
199
  } catch (e) {
199
200
  return {
200
201
  fields: [],
201
- analysis: `DTO 파일을 읽을 수 없습니다: ${dtoPath}`,
202
+ analysis: `DTO 파일을 읽을 수 없습니다: ${searchDtoPath}`,
202
203
  searchParams: {},
203
204
  detectedCodes: [],
204
205
  };
@@ -517,6 +518,16 @@ async function modifyAppTsx(appPath: string, config: SearchParamsConfig): Promis
517
518
  fs.writeFileSync(appPath, content, 'utf-8');
518
519
  console.error(`[MCP] App.tsx 수정 완료: ${appPath}`);
519
520
 
521
+ // ESLint 실행
522
+ try {
523
+ const appDir = path.dirname(appPath);
524
+ execSync(`npx eslint "${appPath}" --fix`, { cwd: appDir, stdio: 'pipe' });
525
+ console.error(`[MCP] ESLint 실행 완료: ${appPath}`);
526
+ modifications.push('ESLint 실행 완료');
527
+ } catch (e) {
528
+ console.error(`[MCP] ESLint 실행 실패 (무시): ${e}`);
529
+ }
530
+
520
531
  return modifications;
521
532
  }
522
533
 
@@ -671,15 +682,15 @@ function generateSearchParamsJsx(): string {
671
682
  * 성공 메시지 생성
672
683
  */
673
684
  function generateSuccessMessage(
674
- dtoPath: string | undefined,
685
+ searchDtoPath: string | undefined,
675
686
  config: SearchParamsConfig,
676
687
  appPath: string | undefined
677
688
  ): string {
678
689
  let message = '# ✅ SearchParams 자동 생성 완료\n\n';
679
690
 
680
- if (dtoPath) {
691
+ if (searchDtoPath) {
681
692
  message += `## DTO 파일\n`;
682
- message += `\`${dtoPath}\`\n\n`;
693
+ message += `\`${searchDtoPath}\`\n\n`;
683
694
  }
684
695
 
685
696
  message += `## App.tsx 수정 완료\n`;