@axboot-mcp/mcp-server 1.0.0 → 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.
@@ -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 {
@@ -31,6 +38,14 @@ export interface ListDataGridConfig {
31
38
  onClickType?: 'modal' | 'link' | 'select' | 'none';
32
39
  /** 추가적인 import */
33
40
  extraImports?: string[];
41
+ /** 행 높이 */
42
+ itemHeight?: number;
43
+ /** 행별 스타일 (조건 필드명) */
44
+ rowClassNameField?: string;
45
+ /** 삭제 버튼 포함 */
46
+ withDelete?: boolean;
47
+ /** 복사 버튼 포함 */
48
+ withCopy?: boolean;
34
49
  }
35
50
 
36
51
  export interface ColumnConfig {
@@ -39,7 +54,7 @@ export interface ColumnConfig {
39
54
  /** 라벨 */
40
55
  label: string;
41
56
  /** 컬럼 타입 */
42
- type?: 'rowNo' | 'title' | 'money' | 'date' | 'dateTime' | 'user' | 'selectable' | 'custom';
57
+ type?: 'rowNo' | 'title' | 'money' | 'date' | 'dateTime' | 'user' | 'selectable' | 'image' | 'bbsTitle' | 'custom';
43
58
  /** 정렬 */
44
59
  align?: 'left' | 'center' | 'right';
45
60
  /** 너비 */
@@ -48,6 +63,10 @@ export interface ColumnConfig {
48
63
  itemRender?: string;
49
64
  /** 코드 변수명 (코드 변환시 사용) */
50
65
  codeVar?: string;
66
+ /** 이미지 타입 (ThumbPreview 또는 ImagePreview) */
67
+ imageType?: 'ThumbPreview' | 'ImagePreview';
68
+ /** className */
69
+ className?: string;
51
70
  }
52
71
 
53
72
  /**
@@ -56,10 +75,53 @@ export interface ColumnConfig {
56
75
  export async function generateListDataGrid(params: GenerateListDataGridParams): Promise<{
57
76
  content: Array<{ type: string; text: string }>;
58
77
  }> {
59
- 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
+ }
60
122
 
61
123
  // 코드 생성
62
- const code = generateListDataGridCode(config);
124
+ const code = generateListDataGridCode(finalConfig);
63
125
 
64
126
  // 파일에 쓰기
65
127
  const dir = path.dirname(outputPath);
@@ -67,6 +129,15 @@ export async function generateListDataGrid(params: GenerateListDataGridParams):
67
129
  fs.mkdirSync(dir, { recursive: true });
68
130
  }
69
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
+ }
70
141
 
71
142
  return {
72
143
  content: [
@@ -76,6 +147,7 @@ export async function generateListDataGrid(params: GenerateListDataGridParams):
76
147
  success: true,
77
148
  outputPath,
78
149
  message: `ListDataGrid 파일이 생성되었습니다: ${outputPath}`,
150
+ dtoAnalysis: responseDtoPath ? parseDtoForListDataGrid(responseDtoPath, storeName!, rowKey!).analysis : undefined,
79
151
  nextSteps: `
80
152
  ## 다음 단계
81
153
 
@@ -105,19 +177,19 @@ import { ListDataGrid } from "./ListDataGrid";
105
177
  \`\`\`typescript
106
178
  const { columns } = useDataGridColumns<DtoItem>(
107
179
  [
108
- { key: "${config.rowKey}", label: t("번호"), type: "rowNo" },
180
+ { key: "${finalConfig.rowKey}", label: t("번호"), type: "rowNo" },
109
181
  // TODO: 여기에 컬럼 추가
110
- ${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 ')}
111
183
  ],
112
184
  {
113
185
  colWidths: listColWidths,
114
- ${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(', ')}],` : ''}
115
187
  }
116
188
  );
117
189
  \`\`\`
118
190
 
119
191
  ### 4. onClickItem 핸들러 구현
120
- onClickType이 "${config.onClickType || 'select'}"이므로 필요한 동작을 구현하세요.
192
+ onClickType이 "${finalConfig.onClickType || 'select'}"이므로 필요한 동작을 구현하세요.
121
193
  `.trim(),
122
194
  }),
123
195
  },
@@ -139,6 +211,10 @@ function generateListDataGridCode(config: ListDataGridConfig): string {
139
211
  columns,
140
212
  onClickType = 'select',
141
213
  extraImports = [],
214
+ itemHeight,
215
+ rowClassNameField,
216
+ withDelete = false,
217
+ withCopy = false,
142
218
  } = config;
143
219
 
144
220
  // Import 생성
@@ -215,15 +291,21 @@ function generateImports(config: ListDataGridConfig, extraImports: string[] = []
215
291
  ];
216
292
 
217
293
  if (config.withFormHeader) {
218
- imports.push(`import { Button, Flex } from "antd";`);
294
+ imports.push(`import { Button, Flex, Space, Divider } from "antd";`);
219
295
  imports.push(`import { IconDownload } from "components/icon";`);
220
296
  }
221
297
 
222
298
  imports.push(`import styled from "@emotion/styled";`);
223
299
  imports.push(`import { useI18n } from "hooks";`);
224
- imports.push(`import React from "react";`);
300
+ imports.push(`import React, { useCallback, useState } from "react";`);
225
301
  imports.push(`import { PageLayout } from "styles/pageStyled";`);
226
302
  imports.push(`import { errorHandling } from "utils";`);
303
+
304
+ // 모달이나 삭제/복사 기능이 있을 때
305
+ if (config.withFormHeader || config.onClickType === 'modal' || config.withDelete || config.withCopy) {
306
+ imports.push(`import { alertDialog, confirmDialog } from "@core/components/dialogs";`);
307
+ }
308
+
227
309
  imports.push(`import { ${config.dtoType} } from "services";`);
228
310
 
229
311
  // Store import
@@ -231,12 +313,31 @@ function generateImports(config: ListDataGridConfig, extraImports: string[] = []
231
313
  imports.push(`import { ${config.storeName} } from "./${storeFileName}";`);
232
314
 
233
315
  // 컬럼 타입별 필요한 import
234
- if (config.columns.some(c => c.type === 'money')) {
235
- imports.push(`// import { formatterNumber } from "@core/utils"; // 금액 포맷 필요시`);
316
+ const hasMoney = config.columns.some(c => c.type === 'money');
317
+ const hasDate = config.columns.some(c => c.type === 'date' || c.type === 'dateTime');
318
+ const hasImage = config.columns.some(c => c.type === 'image');
319
+ const hasCode = config.columns.some(c => c.codeVar);
320
+
321
+ if (hasMoney || hasDate) {
322
+ imports.push(`import { formatterDate, formatterNumber } from "../../../../@core/utils";`);
323
+ imports.push(`import { DT_FORMAT } from "../../../../@types";`);
236
324
  }
237
- if (config.columns.some(c => c.type === 'date' || c.type === 'dateTime')) {
238
- imports.push(`// import { formatterDate } from "@core/utils"; // 날짜 포맷 필요시`);
239
- imports.push(`// import { DT_FORMAT } from "@types"; // 날짜 포맷 필요시`);
325
+
326
+ if (hasImage) {
327
+ const hasThumbPreview = config.columns.some(c => c.imageType === 'ThumbPreview');
328
+ const hasImagePreview = config.columns.some(c => c.imageType === 'ImagePreview');
329
+
330
+ if (hasThumbPreview && hasImagePreview) {
331
+ imports.push(`import { ImagePreview, ThumbPreview } from "../../../../components/common";`);
332
+ } else if (hasThumbPreview) {
333
+ imports.push(`import { ThumbPreview } from "../../../../components/common/ThumbPreview";`);
334
+ } else if (hasImagePreview) {
335
+ imports.push(`import { ImagePreview } from "../../../../components/common";`);
336
+ }
337
+ }
338
+
339
+ if (hasCode) {
340
+ imports.push(`import { useCodeStore } from "stores";`);
240
341
  }
241
342
 
242
343
  // 추가 imports
@@ -313,15 +414,29 @@ function generateStoreConnections(config: ListDataGridConfig): string {
313
414
  * 컬럼 정의 생성
314
415
  */
315
416
  function generateColumnsDefinition(config: ListDataGridConfig): string {
316
- const { storeName, columns, rowKey } = config;
417
+ const { storeName, columns, rowKey, withDelete, withCopy } = config;
317
418
 
318
419
  // 컬럼 배열 생성
319
420
  const columnItems = columns.map(col => {
320
- const { key, label, type, align, width, itemRender, codeVar } = col;
421
+ const { key, label, type, align, width, itemRender, codeVar, imageType, className } = col;
321
422
 
322
423
  // itemRender가 있는 경우
323
424
  if (itemRender) {
324
- return ` { key: "${key}", label: t("${label}")${width ? `, width: ${width}` : ''}${align ? `, align: "${align}"` : ''}, itemRender: ${itemRender} }`;
425
+ return ` { key: "${key}", label: t("${label}")${width ? `, width: ${width}` : ''}${align ? `, align: "${align}"` : ''}${className ? `, className: "${className}"` : ''}, itemRender: ${itemRender} }`;
426
+ }
427
+
428
+ // 이미지 타입
429
+ if (type === 'image') {
430
+ if (imageType === 'ThumbPreview') {
431
+ return ` { key: "${key}", label: t("${label}"), align: "center"${width ? `, width: ${width}` : ', width: 100'}, itemRender: ({ value }) => <ThumbPreview url={value?.prevewUrlCtnts} /> }`;
432
+ } else if (imageType === 'ImagePreview') {
433
+ return ` { key: "${key}", label: t("${label}"), align: "right"${width ? `, width: ${width}` : ', width: 80'}${className ? `, className: "${className}"` : ''}, itemRender: (item) => <ImagePreview files={item.values.${key}List} height={60} swap rotate zoom thumbList text /> }`;
434
+ }
435
+ }
436
+
437
+ // bbsTitle 타입
438
+ if (type === 'bbsTitle') {
439
+ return ` { key: "${key}", label: t("${label}"), type: "bbsTitle"${width ? `, width: ${width}` : ''} }`;
325
440
  }
326
441
 
327
442
  // 코드 변환이 필요한 경우
@@ -340,6 +455,22 @@ function generateColumnsDefinition(config: ListDataGridConfig): string {
340
455
 
341
456
  const hasCodeVar = columns.some(c => c.codeVar);
342
457
 
458
+ // 관리 버튼 컬럼
459
+ if (withDelete || withCopy) {
460
+ const actionButtons: string[] = [];
461
+ if (withCopy) actionButtons.push('handleCopy');
462
+ if (withDelete) actionButtons.push('handleDelete');
463
+
464
+ const actionColumn = ` { key: "", label: t("관리"), align: "center", width: ${actionButtons.length * 80 + 40}, itemRender: (item) => (
465
+ <Space size={8}>
466
+ ${withCopy ? `<Button size={"small"} variant={"outlined"} color={"primary"} onClick={() => handleCopy(item.values.${rowKey})}>{t("복사")}</Button>` : ''}
467
+ ${withDelete ? `<Button size={"small"} variant={"outlined"} color={"danger"} onClick={() => handleDelete(item.values.${rowKey})}>{t("삭제")}</Button>` : ''}
468
+ </Space>
469
+ ) }`;
470
+
471
+ columnItems.push(actionColumn);
472
+ }
473
+
343
474
  return ` const handleColumnsChange = React.useCallback(
344
475
  (columnIndex: number | null, { columns }: AXDGChangeColumnsInfo<DtoItem>) => {
345
476
  setListColWidths(columns.map((column) => column.width));
@@ -347,7 +478,7 @@ function generateColumnsDefinition(config: ListDataGridConfig): string {
347
478
  [setListColWidths],
348
479
  );
349
480
 
350
- const { columns } = useDataGridColumns<DtoItem>(
481
+ const ${hasCodeVar ? '// 코드 의존성\n ' : ''}{ columns } = useDataGridColumns<DtoItem>(
351
482
  [
352
483
  { key: "${rowKey}", label: t("번호"), type: "rowNo" },
353
484
  ${columnItems.join(',\n')}
@@ -362,7 +493,7 @@ ${columnItems.join(',\n')}
362
493
  * 이벤트 핸들러 생성
363
494
  */
364
495
  function generateEventHandlers(config: ListDataGridConfig): string {
365
- const { storeName, onClickType, withExcel } = config;
496
+ const { storeName, onClickType, withExcel, withDelete, withCopy, rowKey } = config;
366
497
  const handlers: string[] = [];
367
498
 
368
499
  // 엑셀 다운로드 핸들러
@@ -379,6 +510,52 @@ function generateEventHandlers(config: ListDataGridConfig): string {
379
510
  );
380
511
  }
381
512
 
513
+ // 삭제 핸들러
514
+ if (withDelete) {
515
+ handlers.push(
516
+ ` const handleDelete = useCallback(
517
+ async (id: string) => {
518
+ try {
519
+ await confirmDialog({
520
+ content: "정말 삭제하시겠습니까?",
521
+ });
522
+
523
+ // TODO: Service.delete 호출
524
+ // await Service.delete({ ${rowKey}: id });
525
+ messageApi.info("삭제되었습니다.");
526
+ await callListApi({ pageNumber: 1 });
527
+ } catch (err) {
528
+ await errorHandling(err);
529
+ }
530
+ },
531
+ [callListApi, messageApi]
532
+ );`,
533
+ );
534
+ }
535
+
536
+ // 복사 핸들러
537
+ if (withCopy) {
538
+ handlers.push(
539
+ ` const handleCopy = useCallback(
540
+ async (id: string) => {
541
+ try {
542
+ await confirmDialog({
543
+ content: "해당 정보를 복사하시겠습니까?",
544
+ });
545
+
546
+ // TODO: Service.copy 호출
547
+ // const result = await Service.copy({ ${rowKey}: id });
548
+ messageApi.success("복사되었습니다.");
549
+ await callListApi();
550
+ } catch (err) {
551
+ await errorHandling(err);
552
+ }
553
+ },
554
+ [callListApi, messageApi]
555
+ );`,
556
+ );
557
+ }
558
+
382
559
  // onClickItem 핸들러
383
560
  if (onClickType === 'modal') {
384
561
  handlers.push(
@@ -407,7 +584,7 @@ function generateEventHandlers(config: ListDataGridConfig): string {
407
584
  try {
408
585
  // TODO: 링크 이동
409
586
  // const menu = MENUS_LIST.find((m) => m.progId === "TARGET_PAGE");
410
- // if (menu) linkByMenu(menu, { id: params.item.${config.rowKey} });
587
+ // if (menu) linkByMenu(menu, ${rowKey}: params.item.${rowKey} });
411
588
  } catch (err) {
412
589
  await errorHandling(err);
413
590
  }
@@ -469,7 +646,7 @@ function generateFormHeader(config: ListDataGridConfig): string {
469
646
  * DataGrid Props 생성
470
647
  */
471
648
  function generateDataGridProps(config: ListDataGridConfig): string {
472
- const { selectionMode, rowKey } = config;
649
+ const { selectionMode, rowKey, itemHeight, rowClassNameField } = config;
473
650
  const props: string[] = [
474
651
  ` onClick={onClickItem}`,
475
652
  ` page={{`,
@@ -501,6 +678,18 @@ function generateDataGridProps(config: ListDataGridConfig): string {
501
678
  );
502
679
  }
503
680
 
681
+ // 행 높이
682
+ if (itemHeight) {
683
+ props.push(` itemHeight={${itemHeight}}`);
684
+ }
685
+
686
+ // 행별 스타일
687
+ if (rowClassNameField) {
688
+ props.push(` getRowClassName={(ri, item) => {`);
689
+ props.push(` return item.values.${rowClassNameField} === "Y" ? "highlight-row" : "";`);
690
+ props.push(` }}`);
691
+ }
692
+
504
693
  return props.join('\n');
505
694
  }
506
695
 
@@ -512,6 +701,17 @@ function generateStyledComponents(config: ListDataGridConfig): string {
512
701
  flex: 1;
513
702
  \`;`;
514
703
 
704
+ if (config.rowClassNameField) {
705
+ styled = `const Container = styled.div\`
706
+ flex: 1;
707
+
708
+ .highlight-row {
709
+ background-color: #fffbe6;
710
+ color: var(--text-display-color);
711
+ }
712
+ \`;`;
713
+ }
714
+
515
715
  if (config.withFormHeader) {
516
716
  styled += `
517
717
  const FormHeader = styled(PageLayout.FrameHeader)\`\`;`;
@@ -519,3 +719,247 @@ const FormHeader = styled(PageLayout.FrameHeader)\`\`;`;
519
719
 
520
720
  return styled;
521
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
+ }