@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