@axboot-mcp/mcp-server 1.0.0 → 1.0.1

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.0",
6
+ "version": "1.0.1",
7
7
  "description": "Axboot MCP Server - Store 생성 및 다른 기능 확장 가능",
8
8
  "main": "./dist/index.js",
9
9
  "types": "./dist/index.d.ts",
@@ -31,6 +31,14 @@ export interface ListDataGridConfig {
31
31
  onClickType?: 'modal' | 'link' | 'select' | 'none';
32
32
  /** 추가적인 import */
33
33
  extraImports?: string[];
34
+ /** 행 높이 */
35
+ itemHeight?: number;
36
+ /** 행별 스타일 (조건 필드명) */
37
+ rowClassNameField?: string;
38
+ /** 삭제 버튼 포함 */
39
+ withDelete?: boolean;
40
+ /** 복사 버튼 포함 */
41
+ withCopy?: boolean;
34
42
  }
35
43
 
36
44
  export interface ColumnConfig {
@@ -39,7 +47,7 @@ export interface ColumnConfig {
39
47
  /** 라벨 */
40
48
  label: string;
41
49
  /** 컬럼 타입 */
42
- type?: 'rowNo' | 'title' | 'money' | 'date' | 'dateTime' | 'user' | 'selectable' | 'custom';
50
+ type?: 'rowNo' | 'title' | 'money' | 'date' | 'dateTime' | 'user' | 'selectable' | 'image' | 'bbsTitle' | 'custom';
43
51
  /** 정렬 */
44
52
  align?: 'left' | 'center' | 'right';
45
53
  /** 너비 */
@@ -48,6 +56,10 @@ export interface ColumnConfig {
48
56
  itemRender?: string;
49
57
  /** 코드 변수명 (코드 변환시 사용) */
50
58
  codeVar?: string;
59
+ /** 이미지 타입 (ThumbPreview 또는 ImagePreview) */
60
+ imageType?: 'ThumbPreview' | 'ImagePreview';
61
+ /** className */
62
+ className?: string;
51
63
  }
52
64
 
53
65
  /**
@@ -139,6 +151,10 @@ function generateListDataGridCode(config: ListDataGridConfig): string {
139
151
  columns,
140
152
  onClickType = 'select',
141
153
  extraImports = [],
154
+ itemHeight,
155
+ rowClassNameField,
156
+ withDelete = false,
157
+ withCopy = false,
142
158
  } = config;
143
159
 
144
160
  // Import 생성
@@ -215,15 +231,21 @@ function generateImports(config: ListDataGridConfig, extraImports: string[] = []
215
231
  ];
216
232
 
217
233
  if (config.withFormHeader) {
218
- imports.push(`import { Button, Flex } from "antd";`);
234
+ imports.push(`import { Button, Flex, Space, Divider } from "antd";`);
219
235
  imports.push(`import { IconDownload } from "components/icon";`);
220
236
  }
221
237
 
222
238
  imports.push(`import styled from "@emotion/styled";`);
223
239
  imports.push(`import { useI18n } from "hooks";`);
224
- imports.push(`import React from "react";`);
240
+ imports.push(`import React, { useCallback, useState } from "react";`);
225
241
  imports.push(`import { PageLayout } from "styles/pageStyled";`);
226
242
  imports.push(`import { errorHandling } from "utils";`);
243
+
244
+ // 모달이나 삭제/복사 기능이 있을 때
245
+ if (config.withFormHeader || config.onClickType === 'modal' || config.withDelete || config.withCopy) {
246
+ imports.push(`import { alertDialog, confirmDialog } from "@core/components/dialogs";`);
247
+ }
248
+
227
249
  imports.push(`import { ${config.dtoType} } from "services";`);
228
250
 
229
251
  // Store import
@@ -231,12 +253,31 @@ function generateImports(config: ListDataGridConfig, extraImports: string[] = []
231
253
  imports.push(`import { ${config.storeName} } from "./${storeFileName}";`);
232
254
 
233
255
  // 컬럼 타입별 필요한 import
234
- if (config.columns.some(c => c.type === 'money')) {
235
- imports.push(`// import { formatterNumber } from "@core/utils"; // 금액 포맷 필요시`);
256
+ const hasMoney = config.columns.some(c => c.type === 'money');
257
+ const hasDate = config.columns.some(c => c.type === 'date' || c.type === 'dateTime');
258
+ const hasImage = config.columns.some(c => c.type === 'image');
259
+ const hasCode = config.columns.some(c => c.codeVar);
260
+
261
+ if (hasMoney || hasDate) {
262
+ imports.push(`import { formatterDate, formatterNumber } from "../../../../@core/utils";`);
263
+ imports.push(`import { DT_FORMAT } from "../../../../@types";`);
264
+ }
265
+
266
+ if (hasImage) {
267
+ const hasThumbPreview = config.columns.some(c => c.imageType === 'ThumbPreview');
268
+ const hasImagePreview = config.columns.some(c => c.imageType === 'ImagePreview');
269
+
270
+ if (hasThumbPreview && hasImagePreview) {
271
+ imports.push(`import { ImagePreview, ThumbPreview } from "../../../../components/common";`);
272
+ } else if (hasThumbPreview) {
273
+ imports.push(`import { ThumbPreview } from "../../../../components/common/ThumbPreview";`);
274
+ } else if (hasImagePreview) {
275
+ imports.push(`import { ImagePreview } from "../../../../components/common";`);
276
+ }
236
277
  }
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"; // 날짜 포맷 필요시`);
278
+
279
+ if (hasCode) {
280
+ imports.push(`import { useCodeStore } from "stores";`);
240
281
  }
241
282
 
242
283
  // 추가 imports
@@ -313,15 +354,29 @@ function generateStoreConnections(config: ListDataGridConfig): string {
313
354
  * 컬럼 정의 생성
314
355
  */
315
356
  function generateColumnsDefinition(config: ListDataGridConfig): string {
316
- const { storeName, columns, rowKey } = config;
357
+ const { storeName, columns, rowKey, withDelete, withCopy } = config;
317
358
 
318
359
  // 컬럼 배열 생성
319
360
  const columnItems = columns.map(col => {
320
- const { key, label, type, align, width, itemRender, codeVar } = col;
361
+ const { key, label, type, align, width, itemRender, codeVar, imageType, className } = col;
321
362
 
322
363
  // itemRender가 있는 경우
323
364
  if (itemRender) {
324
- return ` { key: "${key}", label: t("${label}")${width ? `, width: ${width}` : ''}${align ? `, align: "${align}"` : ''}, itemRender: ${itemRender} }`;
365
+ return ` { key: "${key}", label: t("${label}")${width ? `, width: ${width}` : ''}${align ? `, align: "${align}"` : ''}${className ? `, className: "${className}"` : ''}, itemRender: ${itemRender} }`;
366
+ }
367
+
368
+ // 이미지 타입
369
+ if (type === 'image') {
370
+ if (imageType === 'ThumbPreview') {
371
+ return ` { key: "${key}", label: t("${label}"), align: "center"${width ? `, width: ${width}` : ', width: 100'}, itemRender: ({ value }) => <ThumbPreview url={value?.prevewUrlCtnts} /> }`;
372
+ } else if (imageType === 'ImagePreview') {
373
+ 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 /> }`;
374
+ }
375
+ }
376
+
377
+ // bbsTitle 타입
378
+ if (type === 'bbsTitle') {
379
+ return ` { key: "${key}", label: t("${label}"), type: "bbsTitle"${width ? `, width: ${width}` : ''} }`;
325
380
  }
326
381
 
327
382
  // 코드 변환이 필요한 경우
@@ -340,6 +395,22 @@ function generateColumnsDefinition(config: ListDataGridConfig): string {
340
395
 
341
396
  const hasCodeVar = columns.some(c => c.codeVar);
342
397
 
398
+ // 관리 버튼 컬럼
399
+ if (withDelete || withCopy) {
400
+ const actionButtons: string[] = [];
401
+ if (withCopy) actionButtons.push('handleCopy');
402
+ if (withDelete) actionButtons.push('handleDelete');
403
+
404
+ const actionColumn = ` { key: "", label: t("관리"), align: "center", width: ${actionButtons.length * 80 + 40}, itemRender: (item) => (
405
+ <Space size={8}>
406
+ ${withCopy ? `<Button size={"small"} variant={"outlined"} color={"primary"} onClick={() => handleCopy(item.values.${rowKey})}>{t("복사")}</Button>` : ''}
407
+ ${withDelete ? `<Button size={"small"} variant={"outlined"} color={"danger"} onClick={() => handleDelete(item.values.${rowKey})}>{t("삭제")}</Button>` : ''}
408
+ </Space>
409
+ ) }`;
410
+
411
+ columnItems.push(actionColumn);
412
+ }
413
+
343
414
  return ` const handleColumnsChange = React.useCallback(
344
415
  (columnIndex: number | null, { columns }: AXDGChangeColumnsInfo<DtoItem>) => {
345
416
  setListColWidths(columns.map((column) => column.width));
@@ -347,7 +418,7 @@ function generateColumnsDefinition(config: ListDataGridConfig): string {
347
418
  [setListColWidths],
348
419
  );
349
420
 
350
- const { columns } = useDataGridColumns<DtoItem>(
421
+ const ${hasCodeVar ? '// 코드 의존성\n ' : ''}{ columns } = useDataGridColumns<DtoItem>(
351
422
  [
352
423
  { key: "${rowKey}", label: t("번호"), type: "rowNo" },
353
424
  ${columnItems.join(',\n')}
@@ -362,7 +433,7 @@ ${columnItems.join(',\n')}
362
433
  * 이벤트 핸들러 생성
363
434
  */
364
435
  function generateEventHandlers(config: ListDataGridConfig): string {
365
- const { storeName, onClickType, withExcel } = config;
436
+ const { storeName, onClickType, withExcel, withDelete, withCopy, rowKey } = config;
366
437
  const handlers: string[] = [];
367
438
 
368
439
  // 엑셀 다운로드 핸들러
@@ -379,6 +450,52 @@ function generateEventHandlers(config: ListDataGridConfig): string {
379
450
  );
380
451
  }
381
452
 
453
+ // 삭제 핸들러
454
+ if (withDelete) {
455
+ handlers.push(
456
+ ` const handleDelete = useCallback(
457
+ async (id: string) => {
458
+ try {
459
+ await confirmDialog({
460
+ content: "정말 삭제하시겠습니까?",
461
+ });
462
+
463
+ // TODO: Service.delete 호출
464
+ // await Service.delete({ ${rowKey}: id });
465
+ messageApi.info("삭제되었습니다.");
466
+ await callListApi({ pageNumber: 1 });
467
+ } catch (err) {
468
+ await errorHandling(err);
469
+ }
470
+ },
471
+ [callListApi, messageApi]
472
+ );`,
473
+ );
474
+ }
475
+
476
+ // 복사 핸들러
477
+ if (withCopy) {
478
+ handlers.push(
479
+ ` const handleCopy = useCallback(
480
+ async (id: string) => {
481
+ try {
482
+ await confirmDialog({
483
+ content: "해당 정보를 복사하시겠습니까?",
484
+ });
485
+
486
+ // TODO: Service.copy 호출
487
+ // const result = await Service.copy({ ${rowKey}: id });
488
+ messageApi.success("복사되었습니다.");
489
+ await callListApi();
490
+ } catch (err) {
491
+ await errorHandling(err);
492
+ }
493
+ },
494
+ [callListApi, messageApi]
495
+ );`,
496
+ );
497
+ }
498
+
382
499
  // onClickItem 핸들러
383
500
  if (onClickType === 'modal') {
384
501
  handlers.push(
@@ -407,7 +524,7 @@ function generateEventHandlers(config: ListDataGridConfig): string {
407
524
  try {
408
525
  // TODO: 링크 이동
409
526
  // const menu = MENUS_LIST.find((m) => m.progId === "TARGET_PAGE");
410
- // if (menu) linkByMenu(menu, { id: params.item.${config.rowKey} });
527
+ // if (menu) linkByMenu(menu, ${rowKey}: params.item.${rowKey} });
411
528
  } catch (err) {
412
529
  await errorHandling(err);
413
530
  }
@@ -469,7 +586,7 @@ function generateFormHeader(config: ListDataGridConfig): string {
469
586
  * DataGrid Props 생성
470
587
  */
471
588
  function generateDataGridProps(config: ListDataGridConfig): string {
472
- const { selectionMode, rowKey } = config;
589
+ const { selectionMode, rowKey, itemHeight, rowClassNameField } = config;
473
590
  const props: string[] = [
474
591
  ` onClick={onClickItem}`,
475
592
  ` page={{`,
@@ -501,6 +618,18 @@ function generateDataGridProps(config: ListDataGridConfig): string {
501
618
  );
502
619
  }
503
620
 
621
+ // 행 높이
622
+ if (itemHeight) {
623
+ props.push(` itemHeight={${itemHeight}}`);
624
+ }
625
+
626
+ // 행별 스타일
627
+ if (rowClassNameField) {
628
+ props.push(` getRowClassName={(ri, item) => {`);
629
+ props.push(` return item.values.${rowClassNameField} === "Y" ? "highlight-row" : "";`);
630
+ props.push(` }}`);
631
+ }
632
+
504
633
  return props.join('\n');
505
634
  }
506
635
 
@@ -512,6 +641,17 @@ function generateStyledComponents(config: ListDataGridConfig): string {
512
641
  flex: 1;
513
642
  \`;`;
514
643
 
644
+ if (config.rowClassNameField) {
645
+ styled = `const Container = styled.div\`
646
+ flex: 1;
647
+
648
+ .highlight-row {
649
+ background-color: #fffbe6;
650
+ color: var(--text-display-color);
651
+ }
652
+ \`;`;
653
+ }
654
+
515
655
  if (config.withFormHeader) {
516
656
  styled += `
517
657
  const FormHeader = styled(PageLayout.FrameHeader)\`\`;`;
@@ -0,0 +1,1026 @@
1
+ # ListDataGrid 생성 가이드
2
+
3
+ 이 프롬프트는 NH-FE-B 패턴의 ListDataGrid 컴포넌트를 자동 생성하는 방법을 안내합니다.
4
+
5
+ ## 사용자 요청 인식
6
+
7
+ 사용자가 다음과 같은 요청을 하면 이 가이드를 따르세요:
8
+ - "ListDataGrid 만들어줘"
9
+ - "데이터그리드 생성"
10
+ - "목록 화면 만들어줘"
11
+ - "컬럼 정의해줘"
12
+
13
+ ## 처리 단계
14
+
15
+ ### 1. 요구사항 파싱
16
+
17
+ 사용자 입력에서 다음 패턴을 추출:
18
+ - **컬럼 타입**: `rowNo`, `title`, `money`, `date`, `dateTime`, `user`, `selectable`, `custom`
19
+ - **기능**: `checkbox`, `modal`, `excel`, `delete`, `copy`
20
+ - **선택 모드**: `single`, `multi`, `none`
21
+
22
+ **예시:**
23
+ ```
24
+ 사용자: "ListDataGrid 만들어줘: rowNo title code:STATUS_CODE money date 관리버튼"
25
+
26
+ 파싱 결과:
27
+ - rowNo: { key: "no", label: "번호", type: "rowNo" }
28
+ - title: { key: "name", label: "이름", type: "title", width: 250 }
29
+ - code:STATUS_CODE: { key: "statusCd", label: "상태", codeVar: "STATUS_CODE", align: "center", width: 100 }
30
+ - money: { key: "amount", label: "금액", type: "money", align: "right", width: 120 }
31
+ - date: { key: "creatDtm", label: "등록일", type: "date", width: 150 }
32
+ - 관리버튼: { key: "", label: "관리", itemRender: customButtons }
33
+ ```
34
+
35
+ ### 2. 파일 경로 확인
36
+
37
+ **현재 열린 파일 또는 지정된 경로 사용:**
38
+ ```
39
+ /Users/kyle/Desktop/nh-fe-bo/src/pages/resources/{category}/{page}/ListDataGrid.tsx
40
+ ```
41
+
42
+ ### 3. Store 자동 감지
43
+
44
+ 동일한 경로에서 Store 파일을 찾습니다.
45
+
46
+ **Store 파일 찾기:**
47
+ 1. `use*.ts` 패턴으로 파일 검색
48
+ 2. 찾은 Store에서 필요한 상태 추출
49
+
50
+ **필요한 Store 상태:**
51
+ ```typescript
52
+ // 읽기 전용
53
+ const listColWidths = useStore((s) => s.listColWidths);
54
+ const listSortParams = useStore((s) => s.listSortParams);
55
+ const listData = useStore((s) => s.listData);
56
+ const listPage = useStore((s) => s.listPage);
57
+ const listSpinning = useStore((s) => s.listSpinning);
58
+
59
+ // 쓰기
60
+ const setListColWidths = useStore((s) => s.setListColWidths);
61
+ const setListSortParams = useStore((s) => s.setListSortParams);
62
+ const changeListPage = useStore((s) => s.changeListPage);
63
+ const callListApi = useStore((s) => s.callListApi);
64
+
65
+ // 선택적
66
+ const selectedItem = useStore((s) => s.selectedItem);
67
+ const setSelectedItem = useStore((s) => s.setSelectedItem);
68
+ const listSelectedRowKey = useStore((s) => s.listSelectedRowKey);
69
+ const setListSelectedRowKey = useStore((s) => s.setListSelectedRowKey);
70
+ const programFn = useStore((s) => s.programFn);
71
+ const callExcelDownloadApi = useStore((s) => s.callExcelDownloadApi);
72
+ const excelSpinning = useStore((s) => s.excelSpinning);
73
+ ```
74
+
75
+ ### 4. Import 문 추가
76
+
77
+ ```typescript
78
+ import { AXDGChangeColumnsInfo, AXDGClickParams } from "@axboot/datagrid";
79
+ import { DataGrid } from "@core/components/DataGrid";
80
+ import { useAntApp } from "@core/hooks";
81
+ import { useContainerSize } from "@core/hooks/useContainerSize";
82
+ import { useDataGridColumns } from "@core/hooks/useDataGridColumns";
83
+ import { useDataGridSortedList } from "@core/hooks/useDataGridSortedList";
84
+ import styled from "@emotion/styled";
85
+ import { Button, Flex, Divider } from "antd";
86
+ import { useI18n } from "hooks";
87
+ import React, { useCallback } from "react";
88
+ import { PageLayout } from "styles/pageStyled";
89
+ import { errorHandling } from "utils";
90
+ import { IconDownload } from "components/icon";
91
+ ```
92
+
93
+ ### 5. DTO 타입 정의
94
+
95
+ ```typescript
96
+ // Service 타입을 확장하여 DtoItem 정의
97
+ interface DtoItem extends [ServiceResponseType] {}
98
+
99
+ interface Props {}
100
+ ```
101
+
102
+ ### 6. 컬럼 정의 (useDataGridColumns)
103
+
104
+ ```typescript
105
+ const { columns } = useDataGridColumns<DtoItem>(
106
+ [
107
+ // 컬럼 정의 배열
108
+ ],
109
+ {
110
+ colWidths: listColWidths,
111
+ deps: [CODE_DEPENDENCIES], // 코드 사용 시 추가
112
+ }
113
+ );
114
+ ```
115
+
116
+ ## 컬럼 타입별 정의 규칙
117
+
118
+ ### rowNo (행 번호)
119
+
120
+ ```typescript
121
+ {
122
+ key: "no",
123
+ label: t("번호"),
124
+ type: "rowNo"
125
+ }
126
+ ```
127
+
128
+ ### title (제목 - 클릭 가능)
129
+
130
+ ```typescript
131
+ {
132
+ key: "name",
133
+ label: t("이름"),
134
+ type: "title",
135
+ width: 250,
136
+ align: "left"
137
+ }
138
+ ```
139
+
140
+ ### money (금액)
141
+
142
+ ```typescript
143
+ {
144
+ key: "amount",
145
+ label: t("금액"),
146
+ type: "money",
147
+ align: "right",
148
+ width: 120,
149
+ itemRender: ({ value }) => formatterNumber(value)
150
+ }
151
+ ```
152
+
153
+ ### date (날짜)
154
+
155
+ ```typescript
156
+ {
157
+ key: "creatDtm",
158
+ label: t("등록일"),
159
+ type: "date",
160
+ width: 150,
161
+ itemRender: ({ value }) => formatterDate(value, DT_FORMAT.DATE)
162
+ }
163
+ ```
164
+
165
+ ### dateTime (날짜시간)
166
+
167
+ ```typescript
168
+ {
169
+ key: "creatDtm",
170
+ label: t("등록일시"),
171
+ type: "dateTime",
172
+ width: 150,
173
+ itemRender: ({ value }) => formatterDate(value, DT_FORMAT.DATETIME)
174
+ }
175
+ ```
176
+
177
+ ### user (사용자 정보)
178
+
179
+ ```typescript
180
+ {
181
+ key: "crtrId",
182
+ label: t("등록자"),
183
+ type: "user",
184
+ width: 150,
185
+ itemRender: (item) => {
186
+ return `${item.values.crtrNm}(${item.values.crtrId})`;
187
+ }
188
+ }
189
+ ```
190
+
191
+ ### code (코드 변환)
192
+
193
+ ```typescript
194
+ {
195
+ key: "statusCd",
196
+ label: t("상태"),
197
+ align: "center",
198
+ width: 100,
199
+ itemRender: ({ value }) => {
200
+ return STATUS_CODE?.find(value)?.label;
201
+ }
202
+ }
203
+ ```
204
+
205
+ ### selectable (클릭 가능한 링크)
206
+
207
+ ```typescript
208
+ {
209
+ key: "prdNm",
210
+ label: t("제품명"),
211
+ align: "left",
212
+ width: 200,
213
+ type: "selectable",
214
+ }
215
+ ```
216
+
217
+ ### image (이미지 미리보기)
218
+
219
+ **ThumbPreview (썸네일):**
220
+ ```typescript
221
+ {
222
+ key: "imageFlInfo",
223
+ label: t("이미지"),
224
+ align: "center",
225
+ width: 100,
226
+ itemRender: ({ value }) => {
227
+ return <ThumbPreview url={value?.prevewUrlCtnts} />;
228
+ },
229
+ }
230
+ ```
231
+
232
+ **ImagePreview (이미지 리스트):**
233
+ ```typescript
234
+ {
235
+ key: "imageFlCnt",
236
+ label: t("포토"),
237
+ align: "right",
238
+ width: 80,
239
+ className: "selectable",
240
+ itemRender: (item) => {
241
+ return <ImagePreview files={item.values.imageFlInfoList} height={60} swap rotate zoom thumbList text />;
242
+ },
243
+ }
244
+ ```
245
+
246
+ **Import 추가:**
247
+ ```typescript
248
+ import { ImagePreview } from "../../../../components/common";
249
+ // 또는
250
+ import { ThumbPreview } from "../../../../components/common/ThumbPreview";
251
+ ```
252
+
253
+ ### bbsTitle (게시판 제목)
254
+
255
+ ```typescript
256
+ {
257
+ key: "bbscttSjNm",
258
+ label: t("제목"),
259
+ type: "bbsTitle"
260
+ }
261
+ ```
262
+
263
+ ### custom (사용자 정의)
264
+
265
+ ```typescript
266
+ {
267
+ key: "action",
268
+ label: t("관리"),
269
+ align: "center",
270
+ width: 150,
271
+ itemRender: (item) => {
272
+ return (
273
+ <Flex gap={6} justify={"center"}>
274
+ <Button size={"small"} onClick={() => handleAction(item.values.id)}>
275
+ {t("버튼")}
276
+ </Button>
277
+ </Flex>
278
+ );
279
+ }
280
+ }
281
+ ```
282
+
283
+ ### Space를 사용한 여러 버튼
284
+
285
+ ```typescript
286
+ {
287
+ key: "",
288
+ label: t("관리"),
289
+ align: "center",
290
+ width: 120,
291
+ itemRender: (item) => {
292
+ return (
293
+ <Space size={8}>
294
+ <Button size={"small"} variant={"outlined"} color={"primary"} onClick={() => handleCopy(item.values.id)}>
295
+ {t("복사")}
296
+ </Button>
297
+ <Button size={"small"} variant={"outlined"} color={"danger"} onClick={() => handleDelete(item.values.id)}>
298
+ {t("삭제")}
299
+ </Button>
300
+ </Space>
301
+ );
302
+ },
303
+ }
304
+ ```
305
+
306
+ ### 조건부 렌더링
307
+
308
+ ```typescript
309
+ {
310
+ key: "status",
311
+ label: t("상태"),
312
+ align: "center",
313
+ width: 100,
314
+ itemRender: (item) => {
315
+ const code = item.values.statusCd;
316
+ if (code === "01") {
317
+ return <span style={{ color: "green" }}>활성</span>;
318
+ } else if (code === "02") {
319
+ return <span style={{ color: "red" }}>비활성</span>;
320
+ }
321
+ return "-";
322
+ }
323
+ }
324
+ ```
325
+
326
+ ### 복합 필드 렌더링
327
+
328
+ ```typescript
329
+ {
330
+ key: "period",
331
+ label: t("기간"),
332
+ align: "center",
333
+ width: 200,
334
+ itemRender: (item) => {
335
+ if (item.values.periodType === "01") {
336
+ return `${formatterDate(item.values.startDate)}~${formatterDate(item.values.endDate)}`;
337
+ } else if (item.values.periodType === "02") {
338
+ return `지급 후 ${item.values.days}일까지`;
339
+ }
340
+ return "제한없음";
341
+ }
342
+ }
343
+ ```
344
+
345
+ ## FormHeader 패턴
346
+
347
+ ### 기본 FormHeader
348
+
349
+ ```typescript
350
+ <FormHeader>
351
+ {t("목록")}
352
+ <Flex gap={6} align={"center"}>
353
+ {programFn?.fn02 && (
354
+ <Button size={"small"} type={"primary"} onClick={handleAdd}>
355
+ {t("등록")}
356
+ </Button>
357
+ )}
358
+ </Flex>
359
+ </FormHeader>
360
+ ```
361
+
362
+ ### 엑셀 다운로드 포함
363
+
364
+ ```typescript
365
+ <FormHeader>
366
+ {t("목록")}
367
+ <Flex gap={6} align={"center"}>
368
+ {programFn?.fn02 && (
369
+ <Button size={"small"} type={"primary"} onClick={handleAdd}>
370
+ {t("등록")}
371
+ </Button>
372
+ )}
373
+ <Divider type={"vertical"} />
374
+ {programFn?.fn04 && (
375
+ <Button size={"small"} icon={<IconDownload />} onClick={handleExcelDownload} loading={excelSpinning}>
376
+ {t("엑셀다운로드")}
377
+ </Button>
378
+ )}
379
+ </Flex>
380
+ </FormHeader>
381
+ ```
382
+
383
+ ### 여러 액션 포함
384
+
385
+ ```typescript
386
+ <FormHeader>
387
+ {t("현황")}
388
+ <Flex gap={6} align={"center"}>
389
+ <Button onClick={handleReward}>{t("지급")}</Button>
390
+ <Button onClick={handleAdd} type={"primary"}>
391
+ {t("등록")}
392
+ </Button>
393
+ </Flex>
394
+ </FormHeader>
395
+ ```
396
+
397
+ ### Form 포함 (결과 내 재검색)
398
+
399
+ ```typescript
400
+ <FormHeader>
401
+ 목록
402
+ <Flex gap={6} align={"center"} justify={"flex-end"}>
403
+ <Form<any>
404
+ className={"SIR"}
405
+ form={form}
406
+ layout={"horizontal"}
407
+ colon={false}
408
+ onValuesChange={onValuesChange}
409
+ initialValues={{ rsltSrchwrdType: "PRD_NM" }}
410
+ >
411
+ <Row gutter={8} align={"middle"}>
412
+ <Col>
413
+ <Form.Item name={"rsltSrchwrdType"}>
414
+ <Select
415
+ style={{ width: "100px" }}
416
+ options={[
417
+ { label: t("제품명"), value: "PRD_NM" },
418
+ { label: t("제품코드"), value: "PRD_CD" },
419
+ ]}
420
+ />
421
+ </Form.Item>
422
+ </Col>
423
+ <Col>
424
+ <Form.Item name={"rsltSrchwrd"}>
425
+ <Input />
426
+ </Form.Item>
427
+ </Col>
428
+ <Col>
429
+ <Button size={"small"} type={"primary"} onClick={onSearch}>
430
+ {t("결과 내 재검색")}
431
+ </Button>
432
+ </Col>
433
+ </Row>
434
+ </Form>
435
+ <Divider type={"vertical"} />
436
+ {programFn?.fn02 && (
437
+ <>
438
+ <Divider type={"vertical"} />
439
+ <Button size={"small"} type={"primary"} onClick={() => handleStatus("전시")}>
440
+ {t("전시")}
441
+ </Button>
442
+ <Button size={"small"} type={"primary"} onClick={() => handleStatus("비전시")}>
443
+ {t("비전시")}
444
+ </Button>
445
+ </>
446
+ )}
447
+ </Flex>
448
+ </FormHeader>
449
+ ```
450
+
451
+ **Import 추가:**
452
+ ```typescript
453
+ import { Col, Divider, Form, Input, Row, Select } from "antd";
454
+ ```
455
+
456
+ **Props 추가:**
457
+ ```typescript
458
+ interface Props {
459
+ form: FormInstance<any>;
460
+ onValuesChange: (requestValue, changedValues?) => void;
461
+ onSearch: () => void;
462
+ }
463
+ ```
464
+
465
+ **Styled 컴포넌트:**
466
+ ```typescript
467
+ const FormHeader = styled(PageLayout.FrameHeader)`
468
+ .SIR .ant-form-item {
469
+ margin-bottom: 0;
470
+ }
471
+ `;
472
+ ```
473
+
474
+ ## DataGrid Props 패턴
475
+
476
+ ### 기본 DataGrid
477
+
478
+ ```typescript
479
+ <DataGrid<DtoItem>
480
+ frozenColumnIndex={0}
481
+ width={containerWidth}
482
+ height={containerHeight}
483
+ columns={columns}
484
+ data={sortedListData}
485
+ spinning={listSpinning}
486
+ page={{
487
+ ...listPage,
488
+ loading: false,
489
+ onChange: async (currentPage, pageSize) => {
490
+ await changeListPage(currentPage, pageSize);
491
+ },
492
+ }}
493
+ sort={{
494
+ sortParams: listSortParams,
495
+ onChange: setListSortParams,
496
+ }}
497
+ onChangeColumns={handleColumnsChange}
498
+ />
499
+ ```
500
+
501
+ ### 선택 포함
502
+
503
+ ```typescript
504
+ <DataGrid<DtoItem>
505
+ // ... 기본 props
506
+ rowKey={"id"}
507
+ selectedRowKey={selectedItem?.id ?? ""}
508
+ onClick={onClickItem}
509
+ />
510
+ ```
511
+
512
+ ### 체크박스 포함
513
+
514
+ ```typescript
515
+ <DataGrid<DtoItem>
516
+ // ... 기본 props
517
+ rowChecked={{
518
+ checkedRowKeys,
519
+ onChange: (checkedIndexes, checkedRowKeys, checkedAll) => {
520
+ setCheckedRowKeys(checkedRowKeys);
521
+ },
522
+ }}
523
+ />
524
+ ```
525
+
526
+ ### getRowClassName (행별 스타일)
527
+
528
+ ```typescript
529
+ <DataGrid<DtoItem>
530
+ // ... 기본 props
531
+ getRowClassName={(ri, item) => {
532
+ return item.values.noticeYn === "Y" ? "notice-row" : "";
533
+ }}
534
+ />
535
+ ```
536
+
537
+ **Styled 컴포넌트:**
538
+ ```typescript
539
+ const Container = styled.div`
540
+ flex: 1;
541
+
542
+ .notice-row {
543
+ background-color: #fffbe6;
544
+ color: var(--text-display-color);
545
+ }
546
+ `;
547
+ ```
548
+
549
+ ### itemHeight (행 높이 지정)
550
+
551
+ ```typescript
552
+ <DataGrid<DtoItem
553
+ // ... 기본 props
554
+ itemHeight={60}
555
+ />
556
+ ```
557
+
558
+ ## 이벤트 핸들러 패턴
559
+
560
+ ### handleColumnsChange
561
+
562
+ ```typescript
563
+ const handleColumnsChange = React.useCallback(
564
+ (columnIndex: number | null, { columns }: AXDGChangeColumnsInfo<DtoItem>) => {
565
+ setListColWidths(columns.map((column) => column.width));
566
+ },
567
+ [setListColWidths]
568
+ );
569
+ ```
570
+
571
+ ### onClickItem (모달 오픈)
572
+
573
+ ```typescript
574
+ const onClickItem = React.useCallback(
575
+ async (params: AXDGClickParams<DtoItem>) => {
576
+ try {
577
+ // 특정 컬럼 클릭 시 무시
578
+ if (params.columnIndex === columnIndex) {
579
+ return false;
580
+ }
581
+
582
+ const data = await openFormModal({
583
+ query: params.item,
584
+ });
585
+
586
+ if (data.save) {
587
+ messageApi.success(t("저장되었습니다."));
588
+ await callListApi();
589
+ } else if (data.delete) {
590
+ messageApi.info(t("삭제되었습니다."));
591
+ await callListApi({ pageNumber: 1 });
592
+ }
593
+ } catch (err) {
594
+ await errorHandling(err);
595
+ }
596
+ },
597
+ [callListApi, messageApi, t]
598
+ );
599
+ ```
600
+
601
+ ### handleAdd
602
+
603
+ ```typescript
604
+ const handleAdd = React.useCallback(async () => {
605
+ try {
606
+ await setSelectedItem({ __status__: "C" });
607
+ } catch (err) {
608
+ await errorHandling(err);
609
+ }
610
+ }, [setSelectedItem]);
611
+ ```
612
+
613
+ ### handleDelete
614
+
615
+ ```typescript
616
+ const handleDelete = useCallback(
617
+ async (id: number) => {
618
+ try {
619
+ await confirmDialog({
620
+ content: "정말 삭제하시겠습니까?",
621
+ });
622
+
623
+ await Service.delete({ id });
624
+ messageApi.info("삭제되었습니다.");
625
+ await callListApi({ pageNumber: 1 });
626
+ } catch (err) {
627
+ await errorHandling(err);
628
+ }
629
+ },
630
+ [callListApi, messageApi]
631
+ );
632
+ ```
633
+
634
+ ### handleCopy
635
+
636
+ ```typescript
637
+ const handleCopy = useCallback(
638
+ async (id: number) => {
639
+ try {
640
+ await confirmDialog({
641
+ content: "해당 정보를 복사하시겠습니까?",
642
+ });
643
+
644
+ const result = await Service.copy({ id });
645
+ const data = await openFormModal({
646
+ query: result.rs,
647
+ copyYn: true,
648
+ });
649
+
650
+ if (data.save) {
651
+ messageApi.success(t("저장되었습니다."));
652
+ await callListApi();
653
+ }
654
+ } catch (err) {
655
+ await errorHandling(err);
656
+ }
657
+ },
658
+ [callListApi, messageApi, t]
659
+ );
660
+ ```
661
+
662
+ ### handleExcelDownload
663
+
664
+ ```typescript
665
+ const handleExcelDownload = useCallback(async () => {
666
+ try {
667
+ await callExcelDownloadApi();
668
+ messageApi.success("엑셀 다운로드가 완료되었습니다.");
669
+ } catch (err) {
670
+ await errorHandling(err);
671
+ }
672
+ }, [callExcelDownloadApi, messageApi]);
673
+ ```
674
+
675
+ ## Styled 컴포넌트
676
+
677
+ ### 기본 컨테이너
678
+
679
+ ```typescript
680
+ const Container = styled.div`
681
+ flex: 1;
682
+ `;
683
+ ```
684
+
685
+ ### 링크 스타일 포함
686
+
687
+ ```typescript
688
+ const Container = styled.div`
689
+ flex: 1;
690
+ .selectable {
691
+ text-decoration: underline;
692
+ color: ${(p) => p.theme.link_hover_color};
693
+ }
694
+ `;
695
+ ```
696
+
697
+ ### 테마 오버라이드
698
+
699
+ ```typescript
700
+ const Container = styled.div`
701
+ flex: 1;
702
+
703
+ [role="ax-datagrid"] {
704
+ --axdg-body-bg: #fff !important;
705
+ --axdg-body-hover-bg: #f6f6f6 !important;
706
+ }
707
+ `;
708
+ ```
709
+
710
+ ### FormHeader
711
+
712
+ ```typescript
713
+ const FormHeader = styled(PageLayout.FrameHeader)``;
714
+ ```
715
+
716
+ ## 필수 확인 사항
717
+
718
+ 1. **타입 정의:**
719
+ - `interface DtoItem extends [ServiceType] {}`
720
+
721
+ 2. **Store 상태:**
722
+ - `listColWidths`, `listSortParams`, `listData`, `listPage`, `listSpinning`
723
+ - `setListColWidths`, `setListSortParams`, `changeListPage`
724
+ - 필요에 따라 `selectedItem`, `setSelectedItem`, `callListApi`
725
+
726
+ 3. **필수 Hooks:**
727
+ - `useContainerSize()`: containerRef, containerWidth, containerHeight
728
+ - `useDataGridColumns<DtoItem>()`: columns 정의
729
+ - `useDataGridSortedList<DtoItem>()`: 정렬된 데이터
730
+
731
+ 4. **DataGrid 필수 Props:**
732
+ - `frozenColumnIndex={0}`
733
+ - `width`, `height`
734
+ - `columns`, `data`
735
+ - `spinning`
736
+ - `page`, `sort`, `onChangeColumns`
737
+
738
+ ## 예시 응답
739
+
740
+ ### 케이스 1: 기본 목록 (rowNo + title + date)
741
+
742
+ **사용자:** "ListDataGrid 만들어줘: rowNo title code:STATUS_CODE date"
743
+
744
+ ```typescript
745
+ // 완전한 컴포넌트 코드 생성
746
+ import { AXDGChangeColumnsInfo, AXDGClickParams } from "@axboot/datagrid";
747
+ import { DataGrid } from "@core/components/DataGrid";
748
+ import { useContainerSize } from "@core/hooks/useContainerSize";
749
+ import { useDataGridColumns } from "@core/hooks/useDataGridColumns";
750
+ import { useDataGridSortedList } from "@core/hooks/useDataGridSortedList";
751
+ import { formatterDate } from "@core/utils";
752
+ import styled from "@emotion/styled";
753
+ import { useI18n } from "hooks";
754
+ import React from "react";
755
+ import { PageLayout } from "styles/pageStyled";
756
+ import { errorHandling } from "utils";
757
+ import { useCodeStore } from "stores";
758
+ import { DT_FORMAT } from "@types";
759
+ import { useXxxStore } from "./useXxxStore";
760
+
761
+ interface DtoItem extends XxxRes {}
762
+
763
+ interface Props {}
764
+
765
+ export function ListDataGrid({}: Props) {
766
+ const { t } = useI18n();
767
+
768
+ const listColWidths = useXxxStore((s) => s.listColWidths);
769
+ const listSortParams = useXxxStore((s) => s.listSortParams);
770
+ const listData = useXxxStore((s) => s.listData);
771
+ const listPage = useXxxStore((s) => s.listPage);
772
+ const listSpinning = useXxxStore((s) => s.listSpinning);
773
+ const setListColWidths = useXxxStore((s) => s.setListColWidths);
774
+ const setListSortParams = useXxxStore((s) => s.setListSortParams);
775
+ const changeListPage = useXxxStore((s) => s.changeListPage);
776
+
777
+ const STATUS_CODE = useCodeStore((s) => s.STATUS_CODE);
778
+
779
+ const { containerRef, width: containerWidth, height: containerHeight } = useContainerSize();
780
+
781
+ const handleColumnsChange = React.useCallback(
782
+ (columnIndex: number | null, { columns }: AXDGChangeColumnsInfo<DtoItem>) => {
783
+ setListColWidths(columns.map((column) => column.width));
784
+ },
785
+ [setListColWidths]
786
+ );
787
+
788
+ const { columns } = useDataGridColumns<DtoItem>(
789
+ [
790
+ { key: "no", label: t("번호"), type: "rowNo" },
791
+ { key: "name", label: t("이름"), type: "title", width: 250 },
792
+ {
793
+ key: "statusCd",
794
+ label: t("상태"),
795
+ align: "center",
796
+ width: 100,
797
+ itemRender: ({ value }) => STATUS_CODE?.find(value)?.label
798
+ },
799
+ {
800
+ key: "creatDtm",
801
+ label: t("등록일"),
802
+ type: "date",
803
+ width: 150,
804
+ itemRender: ({ value }) => formatterDate(value, DT_FORMAT.DATE)
805
+ },
806
+ ],
807
+ {
808
+ colWidths: listColWidths,
809
+ deps: [STATUS_CODE],
810
+ }
811
+ );
812
+
813
+ const sortedListData = useDataGridSortedList<DtoItem>(listData, listSortParams);
814
+
815
+ return (
816
+ <Container ref={containerRef}>
817
+ <DataGrid<DtoItem>
818
+ frozenColumnIndex={0}
819
+ width={containerWidth}
820
+ height={containerHeight}
821
+ columns={columns}
822
+ data={sortedListData}
823
+ spinning={listSpinning}
824
+ page={{
825
+ ...listPage,
826
+ loading: false,
827
+ onChange: async (currentPage, pageSize) => {
828
+ await changeListPage(currentPage, pageSize);
829
+ },
830
+ }}
831
+ sort={{
832
+ sortParams: listSortParams,
833
+ onChange: setListSortParams,
834
+ }}
835
+ onChangeColumns={handleColumnsChange}
836
+ />
837
+ </Container>
838
+ );
839
+ }
840
+
841
+ const Container = styled.div`
842
+ flex: 1;
843
+ `;
844
+ ```
845
+
846
+ ### 케이스 2: 관리 버튼 포함 (등록 + 엑셀 + 삭제/복사)
847
+
848
+ **사용자:** "ListDataGrid 만들어줘: rowNo title money 관리:복사,삭제 excel"
849
+
850
+ ```typescript
851
+ // FormHeader 추가 및 관리 버튼 컬럼 포함
852
+ // 위 케이스 1에 다음 추가:
853
+
854
+ // 1. FormHeader
855
+ <FormHeader>
856
+ {t("목록")}
857
+ <Flex gap={6} align={"center"}>
858
+ {programFn?.fn02 && (
859
+ <Button size={"small"} type={"primary"} onClick={handleAdd}>
860
+ {t("등록")}
861
+ </Button>
862
+ )}
863
+ <Divider type={"vertical"} />
864
+ {programFn?.fn04 && (
865
+ <Button size={"small"} icon={<IconDownload />} onClick={handleExcelDownload} loading={excelSpinning}>
866
+ {t("엑셀다운로드")}
867
+ </Button>
868
+ )}
869
+ </Flex>
870
+ </FormHeader>
871
+
872
+ // 2. 관리 컬럼 추가
873
+ {
874
+ key: "",
875
+ label: t("관리"),
876
+ align: "center",
877
+ width: 150,
878
+ itemRender: (item) => {
879
+ return (
880
+ <Flex gap={6} justify={"center"}>
881
+ <Button size='small' onClick={() => handleCopy(item.values.id)}>
882
+ {t("복사")}
883
+ </Button>
884
+ <Button
885
+ size='small'
886
+ variant={"outlined"}
887
+ color={"danger"}
888
+ onClick={() => handleDelete(item.values.id)}
889
+ >
890
+ {t("삭제")}
891
+ </Button>
892
+ </Flex>
893
+ );
894
+ }
895
+ }
896
+ ```
897
+
898
+ ### 케이스 3: 체크박스 포함
899
+
900
+ **사용자:** "ListDataGrid 만들어줘: checkbox rowNo title date"
901
+
902
+ ```typescript
903
+ // Store 상태 추가
904
+ const checkedRowKeys = useXxxStore((s) => s.checkedRowKeys);
905
+ const setCheckedRowKeys = useXxxStore((s) => s.setCheckedRowKeys);
906
+
907
+ // DataGrid에 rowChecked 추가
908
+ <DataGrid<DtoItem>
909
+ // ... 기본 props
910
+ rowChecked={{
911
+ checkedRowKeys,
912
+ onChange: (checkedIndexes, checkedRowKeys, checkedAll) => {
913
+ setCheckedRowKeys(checkedRowKeys);
914
+ },
915
+ }}
916
+ />
917
+ ```
918
+
919
+ ### 케이스 4: 사용자 정의 렌더링
920
+
921
+ **사용자:** "ListDataGrid 만들어줘: rowNo title 기간(시작일~종료일) 상태(활성/비활성 색상)"
922
+
923
+ ```typescript
924
+ const { columns } = useDataGridColumns<DtoItem>(
925
+ [
926
+ { key: "no", label: t("번호"), type: "rowNo" },
927
+ { key: "name", label: t("이름"), type: "title", width: 250 },
928
+ {
929
+ key: "period",
930
+ label: t("기간"),
931
+ align: "center",
932
+ width: 200,
933
+ itemRender: (item) => {
934
+ if (item.values.periodType === "01") {
935
+ return `${formatterDate(item.values.startDate, "YYYY-MM-DD")}~${formatterDate(item.values.endDate, "YYYY-MM-DD")}`;
936
+ } else if (item.values.periodType === "02") {
937
+ return `지급 후 ${item.values.days}일까지`;
938
+ }
939
+ return "제한없음";
940
+ }
941
+ },
942
+ {
943
+ key: "status",
944
+ label: t("상태"),
945
+ align: "center",
946
+ width: 100,
947
+ itemRender: (item) => {
948
+ const code = item.values.statusCd;
949
+ if (code === "01") {
950
+ return <span style={{ color: "green", fontWeight: "bold" }}>활성</span>;
951
+ } else if (code === "02") {
952
+ return <span style={{ color: "red" }}>비활성</span>;
953
+ }
954
+ return "-";
955
+ }
956
+ },
957
+ ],
958
+ {
959
+ colWidths: listColWidths,
960
+ }
961
+ );
962
+ ```
963
+
964
+ ## 복잡한 itemRender 패턴
965
+
966
+ ### 복수 조건 분기
967
+
968
+ ```typescript
969
+ itemRender: (item) => {
970
+ const { field1, field2, field3 } = item.values;
971
+
972
+ if (field1 === "A" && field2 === "X") {
973
+ return "A-X";
974
+ } else if (field1 === "B") {
975
+ return `B: ${formatterNumber(field3)}원`;
976
+ } else if (field1 === "C") {
977
+ return (
978
+ <Button size={"small"} onClick={() => handleClick(field3)}>
979
+ {t("상세보기")}
980
+ </Button>
981
+ );
982
+ }
983
+ return "-";
984
+ }
985
+ ```
986
+
987
+ ### 배열 매핑
988
+
989
+ ```typescript
990
+ itemRender: (item) => {
991
+ const tags = item.values.tags || [];
992
+ return (
993
+ <Flex gap={4}>
994
+ {tags.map((tag, idx) => (
995
+ <span key={idx} style={{ padding: "2px 8px", background: "#f0f0f0", borderRadius: "4px" }}>
996
+ {tag.label}
997
+ </span>
998
+ ))}
999
+ </Flex>
1000
+ );
1001
+ }
1002
+ ```
1003
+
1004
+ ### 중첩 필드 접근
1005
+
1006
+ ```typescript
1007
+ itemRender: (item) => {
1008
+ const user = item.values.user;
1009
+ const dept = item.values.department;
1010
+ return (
1011
+ <div>
1012
+ <div>{user?.name || "-"}</div>
1013
+ <div style={{ fontSize: "12px", color: "#888" }}>{dept?.deptNm || ""}</div>
1014
+ </div>
1015
+ );
1016
+ }
1017
+ ```
1018
+
1019
+ ## 주의사항
1020
+
1021
+ 1. **코드 사용 시 useCodeStore에서 반드시 import:** `import { useCodeStore } from "stores";`
1022
+ 2. **DTO 타입은 Service 응답 타입을 확장:** `interface DtoItem extends XxxRes {}`
1023
+ 3. **itemRender에서 값 접근:** `item.values.fieldName` 또는 `item.value`
1024
+ 4. **컬럼 너비는 colWidths로 관리:** `useDataGridColumns`의 `colWidths` 옵션 사용
1025
+ 5. **onClickItem에서 특정 컬럼 무시:** `params.columnIndex === X` 체크
1026
+ 6. **에러 처리는 errorHandling 함수 사용:** 모든 비동기 함수에서 try-catch