@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
|
@@ -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
|
-
|
|
235
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
imports.push(
|
|
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, {
|
|
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
|