@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.
|
@@ -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
|