@axboot-mcp/mcp-server 1.0.0
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/CLAUDE.md +119 -0
- package/MCP_TOOL_PLAN.md +710 -0
- package/MCP_USAGE.md +914 -0
- package/README.md +168 -0
- package/REPOSITORY_CONVENTIONS.md +250 -0
- package/SEARCH_PARAMS_MCP_TOOL_COMPLETE_PLAN.md +646 -0
- package/SEARCH_PARAMS_PLAN.md +2570 -0
- package/STORE_PATTERNS.md +1178 -0
- package/debug-dto.js +72 -0
- package/generate-banner-store.js +62 -0
- package/generation-plan.json +2176 -0
- package/generation-results.json +1817 -0
- package/package.json +45 -0
- package/scripts/batch-generate-all.js +159 -0
- package/scripts/batch-generate-mcp.js +329 -0
- package/scripts/batch-generate-stores-v2.js +272 -0
- package/scripts/batch-generate-stores.js +179 -0
- package/scripts/batch-plan.json +3810 -0
- package/scripts/batch-process.py +90 -0
- package/scripts/batch-regenerate.js +356 -0
- package/scripts/direct-generate.js +227 -0
- package/scripts/execute-batches.js +1911 -0
- package/scripts/generate-all-stores.js +144 -0
- package/scripts/generate-stores-mcp.js +161 -0
- package/scripts/generate-stores-v2.js +450 -0
- package/scripts/generate-stores-v3.js +412 -0
- package/scripts/generate-stores-v4.js +521 -0
- package/scripts/generate-stores.js +382 -0
- package/scripts/repos-to-process.json +1899 -0
- package/src/config/nh-layout-patterns.ts +166 -0
- package/src/docs/HOOK_GENERATION_PLAN.md +2226 -0
- package/src/docs/NH_STORE_PATTERNS.md +297 -0
- package/src/docs/README.md +216 -0
- package/src/docs/index.ts +28 -0
- package/src/docs/loader.ts +568 -0
- package/src/docs/patterns.json +419 -0
- package/src/docs/practical-examples.md +732 -0
- package/src/docs/quick-start.md +257 -0
- package/src/docs/requirements-analysis-guide.md +364 -0
- package/src/docs/rules.json +321 -0
- package/src/docs/store-pattern-analysis.md +664 -0
- package/src/docs/store-patterns-rules.md +1168 -0
- package/src/docs/store-patterns-usage-guide.md +1835 -0
- package/src/docs/troubleshooting.md +544 -0
- package/src/docs/type-selection-guide.md +572 -0
- package/src/docs//354/202/254/354/232/251/353/262/225/AntD-/354/273/264/355/217/254/353/204/214/355/212/270-/354/202/254/354/232/251/353/262/225.md +1515 -0
- package/src/docs//354/202/254/354/232/251/353/262/225/DataGrid-/354/202/254/354/232/251/353/262/225.md +866 -0
- package/src/docs//354/202/254/354/232/251/353/262/225/FormItem-/354/202/254/354/232/251/353/262/225.md +903 -0
- package/src/docs//354/202/254/354/232/251/353/262/225/FormModal-/354/202/254/354/232/251/353/262/225.md +1155 -0
- package/src/docs//354/202/254/354/232/251/353/262/225/MCP-/353/260/224/354/235/264/353/270/214/354/275/224/353/224/251-/352/260/200/354/235/264/353/223/234.md +1133 -0
- package/src/docs//354/202/254/354/232/251/353/262/225/MSW-Mock-/353/215/260/354/235/264/355/204/260-/354/202/254/354/232/251/353/262/225.md +579 -0
- package/src/docs//354/202/254/354/232/251/353/262/225/Search-/354/273/264/355/217/254/353/204/214/355/212/270-/354/202/254/354/232/251/353/262/225.md +738 -0
- package/src/docs//354/202/254/354/232/251/353/262/225/Store-/355/214/250/355/204/264-/354/202/254/354/232/251/353/262/225.md +1135 -0
- package/src/docs//354/202/254/354/232/251/353/262/225//355/231/224/353/251/264/352/265/254/354/204/261-/355/203/200/354/236/205/353/263/204-/352/260/234/353/260/234/354/210/234/354/204/234.md +1805 -0
- package/src/docs//354/202/254/354/232/251/353/262/225//355/231/224/353/251/264/355/203/200/354/236/205/353/263/204-/352/260/234/353/260/234-/355/224/204/353/241/254/355/224/204/355/212/270-/352/260/200/354/235/264/353/223/234.md +946 -0
- package/src/docs//354/202/254/354/232/251/353/262/225//355/231/225/354/236/245/355/231/224/353/251/264/355/203/200/354/236/205/353/263/204-/354/203/201/354/204/270-/355/224/204/353/241/254/355/224/204/355/212/270/352/260/200/354/235/264/353/223/234.md +2422 -0
- package/src/features/store-features.ts +232 -0
- package/src/handlers/analyze-requirements.ts +403 -0
- package/src/handlers/analyze.ts +1373 -0
- package/src/handlers/generate-from-requirements.ts +250 -0
- package/src/handlers/generate-hook.ts +950 -0
- package/src/handlers/generate-interactive.ts +840 -0
- package/src/handlers/generate-listdatagrid.ts +521 -0
- package/src/handlers/generate-multi-stores.ts +577 -0
- package/src/handlers/generate-requirements-from-layout.ts +160 -0
- package/src/handlers/generate-search-params.ts +717 -0
- package/src/handlers/generate.ts +911 -0
- package/src/handlers/list-templates.ts +104 -0
- package/src/handlers/scan-metadata.ts +485 -0
- package/src/handlers/suggest-layout.ts +326 -0
- package/src/index.ts +959 -0
- package/src/prompts/search-params.md +793 -0
- package/src/templates/index.ts +107 -0
- package/src/templates/unified.ts +462 -0
- package/store-generation-error-patterns.md +225 -0
- package/test/useAgentStore.ts +136 -0
- package/test-server.js +78 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,1515 @@
|
|
|
1
|
+
# Ant Design 컴포넌트 사용법 가이드
|
|
2
|
+
|
|
3
|
+
## 개요
|
|
4
|
+
|
|
5
|
+
이 문서는 네이비언하우스 FE-BO 프로젝트에서 실제 사용되고 있는 Ant Design 컴포넌트들의 사용 사례와 패턴을 정리한 가이드입니다.
|
|
6
|
+
|
|
7
|
+
## 1. 폼 관련 컴포넌트 (Form Components)
|
|
8
|
+
|
|
9
|
+
### 1.1 Form & Form.Item
|
|
10
|
+
|
|
11
|
+
#### 기본 사용 패턴
|
|
12
|
+
```tsx
|
|
13
|
+
// 프로젝트 표준 패턴
|
|
14
|
+
const FormItem = Form.Item<DtoItem>;
|
|
15
|
+
|
|
16
|
+
function FormModal() {
|
|
17
|
+
const [form] = Form.useForm<DtoItem>();
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Form
|
|
21
|
+
form={form}
|
|
22
|
+
layout="vertical"
|
|
23
|
+
onFinish={handleSave}
|
|
24
|
+
initialValues={{ useYn: "Y" }}
|
|
25
|
+
>
|
|
26
|
+
<FormItem
|
|
27
|
+
name="itemName"
|
|
28
|
+
label={t("항목명")}
|
|
29
|
+
rules={[{ required: true }]}
|
|
30
|
+
>
|
|
31
|
+
<Input />
|
|
32
|
+
</FormItem>
|
|
33
|
+
</Form>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
#### 실제 사용 예시 (department/FormModal.tsx)
|
|
39
|
+
```tsx
|
|
40
|
+
<Form form={form} layout="vertical" onFinish={handleSave} initialValues={{ useYn: "Y" }}>
|
|
41
|
+
<FormItem name="trtmntDeptNo" noStyle>
|
|
42
|
+
<Input type="hidden" />
|
|
43
|
+
</FormItem>
|
|
44
|
+
|
|
45
|
+
<Row gutter={12}>
|
|
46
|
+
<Col span={24}>
|
|
47
|
+
<FormItem name="trtmntDeptNm" label={t("부서명")} rules={[{ required: true }]}>
|
|
48
|
+
<Input />
|
|
49
|
+
</FormItem>
|
|
50
|
+
</Col>
|
|
51
|
+
<Col span={24}>
|
|
52
|
+
<FormItem
|
|
53
|
+
name="pgSvcMidValue"
|
|
54
|
+
label={t("MID")}
|
|
55
|
+
rules={[{ required: true }]}
|
|
56
|
+
tooltip="KG이니시스의 MID정보를 입력해 주세요."
|
|
57
|
+
>
|
|
58
|
+
<Input />
|
|
59
|
+
</FormItem>
|
|
60
|
+
</Col>
|
|
61
|
+
</Row>
|
|
62
|
+
</Form>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 1.2 Input
|
|
66
|
+
|
|
67
|
+
#### 기본 텍스트 입력
|
|
68
|
+
```tsx
|
|
69
|
+
<FormItem name="title" label={t("제목")} rules={[{ required: true }]}>
|
|
70
|
+
<Input placeholder={t("제목을 입력하세요")} />
|
|
71
|
+
</FormItem>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### TextArea 사용
|
|
75
|
+
```tsx
|
|
76
|
+
<FormItem name="description" label={t("설명")}>
|
|
77
|
+
<Input.TextArea
|
|
78
|
+
rows={3}
|
|
79
|
+
maxLength={2000}
|
|
80
|
+
placeholder={t("설명을 입력하세요")}
|
|
81
|
+
/>
|
|
82
|
+
</FormItem>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
#### Password 입력
|
|
86
|
+
```tsx
|
|
87
|
+
<FormItem name="password" label={t("비밀번호")}>
|
|
88
|
+
<Input.Password />
|
|
89
|
+
</FormItem>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
#### 숨김 필드
|
|
93
|
+
```tsx
|
|
94
|
+
<FormItem name="hiddenId" noStyle>
|
|
95
|
+
<Input type="hidden" />
|
|
96
|
+
</FormItem>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 1.3 InputNumber
|
|
100
|
+
|
|
101
|
+
#### 기본 숫자 입력
|
|
102
|
+
```tsx
|
|
103
|
+
<FormItem name="quantity" label={t("수량")} rules={[{ required: true }]}>
|
|
104
|
+
<InputNumber
|
|
105
|
+
min={1}
|
|
106
|
+
max={999}
|
|
107
|
+
style={{ width: "100%" }}
|
|
108
|
+
/>
|
|
109
|
+
</FormItem>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
#### 단위가 있는 숫자 입력 (system.app-banner/FormModal.tsx)
|
|
113
|
+
```tsx
|
|
114
|
+
<FormItem name="expsrSecndValue" label={t("배너노출시간설정")} rules={[{ required: true }]}>
|
|
115
|
+
<InputNumber
|
|
116
|
+
min={1}
|
|
117
|
+
max={60}
|
|
118
|
+
addonAfter="초"
|
|
119
|
+
placeholder={t("노출할 초값을 입력하세요")}
|
|
120
|
+
style={{ width: "20%" }}
|
|
121
|
+
/>
|
|
122
|
+
</FormItem>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 1.4 Select
|
|
126
|
+
|
|
127
|
+
#### 기본 셀렉트 박스
|
|
128
|
+
```tsx
|
|
129
|
+
<FormItem name="categoryCode" label={t("카테고리")} rules={[{ required: true }]}>
|
|
130
|
+
<Select
|
|
131
|
+
options={categoryOptions}
|
|
132
|
+
placeholder={t("카테고리를 선택하세요")}
|
|
133
|
+
/>
|
|
134
|
+
</FormItem>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
#### 컴팩트 스타일의 셀렉트 (benefit.time-deal/FormModal.tsx)
|
|
138
|
+
```tsx
|
|
139
|
+
<Space.Compact>
|
|
140
|
+
<Select
|
|
141
|
+
value={getFieldValue("bnefDscntStupTpcd")}
|
|
142
|
+
options={[
|
|
143
|
+
{ label: "%", value: "10" },
|
|
144
|
+
{ label: "원", value: "20" }
|
|
145
|
+
]}
|
|
146
|
+
style={{ width: 80 }}
|
|
147
|
+
onChange={(value) => setFieldValue("bnefDscntStupTpcd", value)}
|
|
148
|
+
/>
|
|
149
|
+
<Button>{t("할인")}</Button>
|
|
150
|
+
</Space.Compact>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 1.5 Radio
|
|
154
|
+
|
|
155
|
+
#### 기본 라디오 그룹
|
|
156
|
+
```tsx
|
|
157
|
+
<FormItem name="useYn" label={t("사용여부")} rules={[{ required: true }]}>
|
|
158
|
+
<Radio.Group
|
|
159
|
+
options={[
|
|
160
|
+
{ value: "Y", label: "Y" },
|
|
161
|
+
{ value: "N", label: "N" }
|
|
162
|
+
]}
|
|
163
|
+
/>
|
|
164
|
+
</FormItem>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
#### 인라인 라디오 그룹 (system.app-banner/FormModal.tsx)
|
|
168
|
+
```tsx
|
|
169
|
+
<FormItem name="urlTrgtTpcd" label={t("링크 대상")}>
|
|
170
|
+
<Radio.Group>
|
|
171
|
+
<Radio value="">없음</Radio>
|
|
172
|
+
{URL_TRGT_TPCD?.options?.map((option) => (
|
|
173
|
+
<Radio key={option.value} value={option.value}>
|
|
174
|
+
{option.label}
|
|
175
|
+
</Radio>
|
|
176
|
+
))}
|
|
177
|
+
</Radio.Group>
|
|
178
|
+
</FormItem>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### 1.6 Switch
|
|
182
|
+
|
|
183
|
+
#### 토글 스위치 (system.app-banner/FormModal.tsx)
|
|
184
|
+
```tsx
|
|
185
|
+
<FormItem name="dspyYn" label={t("전시여부")} valuePropName="checked">
|
|
186
|
+
<Switch
|
|
187
|
+
checkedChildren="전시"
|
|
188
|
+
unCheckedChildren="미전시"
|
|
189
|
+
/>
|
|
190
|
+
</FormItem>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### 1.7 날짜/시간 컴포넌트 (Date & Time Components)
|
|
194
|
+
|
|
195
|
+
프로젝트에서는 Ant Design의 기본 DatePicker 대신 커스텀 `DtPicker` 컴포넌트를 주로 사용합니다.
|
|
196
|
+
|
|
197
|
+
#### DtPicker 타입 정의
|
|
198
|
+
```typescript
|
|
199
|
+
export enum DtPickerType {
|
|
200
|
+
DATE = 0, // 단일 날짜
|
|
201
|
+
DATE_RANGE = 1, // 날짜 범위
|
|
202
|
+
MONTH = 2, // 월 선택
|
|
203
|
+
YEAR = 3, // 연도 선택
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
#### 날짜 범위 선택 (system.app-banner/FormModal.tsx)
|
|
208
|
+
```tsx
|
|
209
|
+
import { DtPicker } from "@core/components/dtPicker";
|
|
210
|
+
import { DtPickerType } from "@core/components/dtPicker/types";
|
|
211
|
+
|
|
212
|
+
<FormItem
|
|
213
|
+
name="dt_range"
|
|
214
|
+
label={t("노출기간")}
|
|
215
|
+
rules={[{ required: true, message: t("노출기간을 선택해주세요") }]}
|
|
216
|
+
>
|
|
217
|
+
<DtPicker
|
|
218
|
+
type={DtPickerType.DATE_RANGE}
|
|
219
|
+
placeholder={[t("노출 시작일시"), t("노출 종료일시")]}
|
|
220
|
+
/>
|
|
221
|
+
</FormItem>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
#### 단일 날짜 선택 (press-media/PressMediaModal.tsx)
|
|
225
|
+
```tsx
|
|
226
|
+
<FormItem name="bbscttBgnDt" label={t("노출 일자 지정")} rules={[{ required: true }]}>
|
|
227
|
+
<DtPicker
|
|
228
|
+
type={DtPickerType.DATE}
|
|
229
|
+
isTimeSelector // 시간 선택 포함
|
|
230
|
+
allowClear // 클리어 버튼 표시
|
|
231
|
+
/>
|
|
232
|
+
</FormItem>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
#### 월/연도 선택
|
|
236
|
+
```tsx
|
|
237
|
+
// 월 선택
|
|
238
|
+
<FormItem name="targetMonth" label={t("대상월")}>
|
|
239
|
+
<DtPicker type={DtPickerType.MONTH} />
|
|
240
|
+
</FormItem>
|
|
241
|
+
|
|
242
|
+
// 연도 선택
|
|
243
|
+
<FormItem name="targetYear" label={t("대상연도")}>
|
|
244
|
+
<DtPicker type={DtPickerType.YEAR} />
|
|
245
|
+
</FormItem>
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
#### 검색 화면용 날짜 프리셋 (DtPickerPresetRender)
|
|
249
|
+
검색 화면에서는 `DtPickerPresetRender`를 사용하여 일반적인 날짜 범위 프리셋을 제공합니다.
|
|
250
|
+
|
|
251
|
+
```tsx
|
|
252
|
+
import { DtPickerPresetRender } from "components/presetRender/DtPickerPresetRender";
|
|
253
|
+
|
|
254
|
+
// 검색 파라미터에서 사용
|
|
255
|
+
const searchParams: SearchParams = [
|
|
256
|
+
{
|
|
257
|
+
prop: "dateRange",
|
|
258
|
+
label: t("기간"),
|
|
259
|
+
type: SearchParamType.DATE_RANGE,
|
|
260
|
+
defaultValue: [],
|
|
261
|
+
presetRender: DtPickerPresetRender({ type: "all" }),
|
|
262
|
+
},
|
|
263
|
+
// 기타 검색 파라미터들...
|
|
264
|
+
];
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
#### DtPickerPresetRender 타입
|
|
268
|
+
```tsx
|
|
269
|
+
// 모든 프리셋 (오늘, 어제, 이번주, 지난주, 이번달, 지난달, 최근 3개월)
|
|
270
|
+
presetRender: DtPickerPresetRender({ type: "all" })
|
|
271
|
+
|
|
272
|
+
// 기본 프리셋 (오늘, 어제, 이번주, 이번달)
|
|
273
|
+
presetRender: DtPickerPresetRender({ type: "basic" })
|
|
274
|
+
|
|
275
|
+
// 커스텀 프리셋
|
|
276
|
+
presetRender: DtPickerPresetRender({})
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
#### 날짜 데이터 처리 패턴
|
|
280
|
+
```tsx
|
|
281
|
+
// 폼에서 날짜 범위를 API 파라미터로 변환
|
|
282
|
+
import { dateRangeToDts } from "utils";
|
|
283
|
+
import { DT_FORMAT } from "@types";
|
|
284
|
+
|
|
285
|
+
const handleSave = async () => {
|
|
286
|
+
const values = await form.validateFields();
|
|
287
|
+
|
|
288
|
+
const saveItem = {
|
|
289
|
+
...values,
|
|
290
|
+
// 날짜 범위를 시작일/종료일로 분리
|
|
291
|
+
...dateRangeToDts(values.dt_range, {
|
|
292
|
+
expsrBgnDtm: DT_FORMAT.DATETIME,
|
|
293
|
+
expsrEndDtm: DT_FORMAT.DATETIME,
|
|
294
|
+
}),
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
await ApiService.save(saveItem);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// 폼 초기값 설정 시
|
|
301
|
+
const formValues = {
|
|
302
|
+
...params.query,
|
|
303
|
+
// API에서 받은 시작일/종료일을 날짜 범위로 결합
|
|
304
|
+
dt_range: params.query.expsrBgnDtm && params.query.expsrEndDtm
|
|
305
|
+
? [params.query.expsrBgnDtm, params.query.expsrEndDtm]
|
|
306
|
+
: undefined,
|
|
307
|
+
};
|
|
308
|
+
form.setFieldsValue(formValues);
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
#### 날짜 제한 옵션들
|
|
312
|
+
|
|
313
|
+
프로젝트에서는 다양한 날짜 제한 패턴을 사용할 수 있습니다.
|
|
314
|
+
|
|
315
|
+
```tsx
|
|
316
|
+
import dayjs from "dayjs";
|
|
317
|
+
|
|
318
|
+
// 과거 날짜 선택 불가 (오늘 이후만 선택 가능)
|
|
319
|
+
<FormItem name="futureDate" label={t("미래 날짜")}>
|
|
320
|
+
<DatePicker
|
|
321
|
+
disabledDate={(date) => date && date < dayjs().startOf('day')}
|
|
322
|
+
placeholder={t("오늘 이후 날짜만 선택 가능")}
|
|
323
|
+
/>
|
|
324
|
+
</FormItem>
|
|
325
|
+
|
|
326
|
+
// 미래 날짜 선택 불가 (오늘 이전만 선택 가능)
|
|
327
|
+
<FormItem name="pastDate" label={t("과거 날짜")}>
|
|
328
|
+
<DatePicker
|
|
329
|
+
disabledDate={(date) => date && date > dayjs().endOf('day')}
|
|
330
|
+
placeholder={t("오늘 이전 날짜만 선택 가능")}
|
|
331
|
+
/>
|
|
332
|
+
</FormItem>
|
|
333
|
+
|
|
334
|
+
// 특정 기간 내에서만 선택 가능
|
|
335
|
+
<FormItem name="limitedDate" label={t("제한 날짜")}>
|
|
336
|
+
<DatePicker
|
|
337
|
+
disabledDate={(date) => {
|
|
338
|
+
const startDate = dayjs().subtract(30, 'day');
|
|
339
|
+
const endDate = dayjs().add(30, 'day');
|
|
340
|
+
return date && (date < startDate || date > endDate);
|
|
341
|
+
}}
|
|
342
|
+
placeholder={t("30일 전후 범위에서만 선택 가능")}
|
|
343
|
+
/>
|
|
344
|
+
</FormItem>
|
|
345
|
+
|
|
346
|
+
// 주말 선택 불가
|
|
347
|
+
<FormItem name="weekdayOnly" label={t("평일만")}>
|
|
348
|
+
<DatePicker
|
|
349
|
+
disabledDate={(date) => {
|
|
350
|
+
const day = date.day();
|
|
351
|
+
return day === 0 || day === 6; // 일요일(0), 토요일(6)
|
|
352
|
+
}}
|
|
353
|
+
placeholder={t("평일만 선택 가능")}
|
|
354
|
+
/>
|
|
355
|
+
</FormItem>
|
|
356
|
+
|
|
357
|
+
// 특정 날짜들 선택 불가
|
|
358
|
+
<FormItem name="excludeSpecificDates" label={t("제외 날짜")}>
|
|
359
|
+
<DatePicker
|
|
360
|
+
disabledDate={(date) => {
|
|
361
|
+
const disabledDates = [
|
|
362
|
+
'2024-01-01', // 신정
|
|
363
|
+
'2024-12-25', // 크리스마스
|
|
364
|
+
];
|
|
365
|
+
return disabledDates.includes(date.format('YYYY-MM-DD'));
|
|
366
|
+
}}
|
|
367
|
+
placeholder={t("공휴일 제외")}
|
|
368
|
+
/>
|
|
369
|
+
</FormItem>
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
#### 날짜 범위 제한 패턴
|
|
373
|
+
|
|
374
|
+
```tsx
|
|
375
|
+
// 날짜 범위에서 시작일 이후에만 종료일 선택 가능
|
|
376
|
+
const [startDate, setStartDate] = useState(null);
|
|
377
|
+
|
|
378
|
+
<Row gutter={16}>
|
|
379
|
+
<Col span={12}>
|
|
380
|
+
<FormItem name="startDate" label={t("시작일")}>
|
|
381
|
+
<DatePicker
|
|
382
|
+
onChange={(date) => setStartDate(date)}
|
|
383
|
+
disabledDate={(date) => date && date < dayjs().startOf('day')}
|
|
384
|
+
/>
|
|
385
|
+
</FormItem>
|
|
386
|
+
</Col>
|
|
387
|
+
<Col span={12}>
|
|
388
|
+
<FormItem name="endDate" label={t("종료일")}>
|
|
389
|
+
<DatePicker
|
|
390
|
+
disabledDate={(date) => {
|
|
391
|
+
if (!startDate) return date && date < dayjs().startOf('day');
|
|
392
|
+
return date && (date < dayjs(startDate) || date < dayjs().startOf('day'));
|
|
393
|
+
}}
|
|
394
|
+
/>
|
|
395
|
+
</FormItem>
|
|
396
|
+
</Col>
|
|
397
|
+
</Row>
|
|
398
|
+
|
|
399
|
+
// RangePicker에서 날짜 제한
|
|
400
|
+
<FormItem name="dateRange" label={t("기간")}>
|
|
401
|
+
<DatePicker.RangePicker
|
|
402
|
+
disabledDate={(date) => date && date < dayjs().startOf('day')}
|
|
403
|
+
placeholder={[t("시작일"), t("종료일")]}
|
|
404
|
+
/>
|
|
405
|
+
</FormItem>
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
#### 시간 제한 옵션들
|
|
409
|
+
|
|
410
|
+
```tsx
|
|
411
|
+
// 특정 시간대만 선택 가능
|
|
412
|
+
<FormItem name="businessHours" label={t("업무시간")}>
|
|
413
|
+
<DatePicker
|
|
414
|
+
showTime
|
|
415
|
+
disabledTime={(date) => ({
|
|
416
|
+
disabledHours: () => {
|
|
417
|
+
const hours = [];
|
|
418
|
+
for (let i = 0; i < 9; i++) hours.push(i); // 9시 이전
|
|
419
|
+
for (let i = 18; i < 24; i++) hours.push(i); // 18시 이후
|
|
420
|
+
return hours;
|
|
421
|
+
},
|
|
422
|
+
disabledMinutes: () => [],
|
|
423
|
+
disabledSeconds: () => [],
|
|
424
|
+
})}
|
|
425
|
+
/>
|
|
426
|
+
</FormItem>
|
|
427
|
+
|
|
428
|
+
// 현재 시간 이후만 선택 가능
|
|
429
|
+
<FormItem name="futureDateTime" label={t("미래 일시")}>
|
|
430
|
+
<DatePicker
|
|
431
|
+
showTime
|
|
432
|
+
disabledDate={(date) => date && date < dayjs().startOf('day')}
|
|
433
|
+
disabledTime={(date) => {
|
|
434
|
+
if (date && dayjs(date).isSame(dayjs(), 'day')) {
|
|
435
|
+
const currentHour = dayjs().hour();
|
|
436
|
+
const currentMinute = dayjs().minute();
|
|
437
|
+
return {
|
|
438
|
+
disabledHours: () => Array.from({ length: currentHour }, (_, i) => i),
|
|
439
|
+
disabledMinutes: (selectedHour) =>
|
|
440
|
+
selectedHour === currentHour
|
|
441
|
+
? Array.from({ length: currentMinute }, (_, i) => i)
|
|
442
|
+
: [],
|
|
443
|
+
disabledSeconds: () => [],
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
return {};
|
|
447
|
+
}}
|
|
448
|
+
/>
|
|
449
|
+
</FormItem>
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
#### DtPicker에서 날짜 제한 (프로젝트 커스텀)
|
|
453
|
+
|
|
454
|
+
프로젝트의 DtPicker는 내부적으로 일부 제한사항이 구현되어 있습니다:
|
|
455
|
+
|
|
456
|
+
```tsx
|
|
457
|
+
// 내부 구현 예시 (DateRangePicker.tsx 참고)
|
|
458
|
+
disabledDate={(date) => {
|
|
459
|
+
return !!dayjs(date).isAfter(dayjs("9999-12-31")) || !!dayjs(date).isBefore(dayjs(firstDate));
|
|
460
|
+
}}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
#### Ant Design 기본 DatePicker 사용 (필요시)
|
|
464
|
+
```tsx
|
|
465
|
+
import { DatePicker } from "antd";
|
|
466
|
+
|
|
467
|
+
// 단일 날짜
|
|
468
|
+
<FormItem name="date" label={t("날짜")}>
|
|
469
|
+
<DatePicker />
|
|
470
|
+
</FormItem>
|
|
471
|
+
|
|
472
|
+
// 날짜 범위
|
|
473
|
+
<FormItem name="dateRange" label={t("기간")}>
|
|
474
|
+
<DatePicker.RangePicker />
|
|
475
|
+
</FormItem>
|
|
476
|
+
|
|
477
|
+
// 시간 포함 날짜
|
|
478
|
+
<FormItem name="datetime" label={t("일시")}>
|
|
479
|
+
<DatePicker showTime />
|
|
480
|
+
</FormItem>
|
|
481
|
+
|
|
482
|
+
// 월 선택
|
|
483
|
+
<FormItem name="month" label={t("월")}>
|
|
484
|
+
<DatePicker picker="month" />
|
|
485
|
+
</FormItem>
|
|
486
|
+
|
|
487
|
+
// 연도 선택
|
|
488
|
+
<FormItem name="year" label={t("연도")}>
|
|
489
|
+
<DatePicker picker="year" />
|
|
490
|
+
</FormItem>
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
## 2. 레이아웃 컴포넌트 (Layout Components)
|
|
494
|
+
|
|
495
|
+
### 2.1 Row & Col
|
|
496
|
+
|
|
497
|
+
#### 반응형 그리드 레이아웃
|
|
498
|
+
```tsx
|
|
499
|
+
<Row gutter={16}>
|
|
500
|
+
<Col xs={24} sm={12} md={8}>
|
|
501
|
+
<FormItem name="field1" label={t("필드1")}>
|
|
502
|
+
<Input />
|
|
503
|
+
</FormItem>
|
|
504
|
+
</Col>
|
|
505
|
+
<Col xs={24} sm={12} md={8}>
|
|
506
|
+
<FormItem name="field2" label={t("필드2")}>
|
|
507
|
+
<Input />
|
|
508
|
+
</FormItem>
|
|
509
|
+
</Col>
|
|
510
|
+
<Col xs={24} sm={24} md={8}>
|
|
511
|
+
<FormItem name="field3" label={t("필드3")}>
|
|
512
|
+
<Input />
|
|
513
|
+
</FormItem>
|
|
514
|
+
</Col>
|
|
515
|
+
</Row>
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
#### 기본 2컬럼 레이아웃 (department/FormModal.tsx)
|
|
519
|
+
```tsx
|
|
520
|
+
<Row gutter={12}>
|
|
521
|
+
<Col span={24}>
|
|
522
|
+
<FormItem name="trtmntDeptNm" label={t("부서명")}>
|
|
523
|
+
<Input />
|
|
524
|
+
</FormItem>
|
|
525
|
+
</Col>
|
|
526
|
+
<Col xs={24} sm={12}>
|
|
527
|
+
<FormItem name="useYn" label={t("사용여부")}>
|
|
528
|
+
<Radio.Group options={[...]} />
|
|
529
|
+
</FormItem>
|
|
530
|
+
</Col>
|
|
531
|
+
</Row>
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### 2.2 Space
|
|
535
|
+
|
|
536
|
+
#### 기본 간격 조정
|
|
537
|
+
```tsx
|
|
538
|
+
<Space>
|
|
539
|
+
<Button type="primary" onClick={handleSave}>
|
|
540
|
+
{t("저장")}
|
|
541
|
+
</Button>
|
|
542
|
+
<Button onClick={onCancel}>
|
|
543
|
+
{t("취소")}
|
|
544
|
+
</Button>
|
|
545
|
+
</Space>
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
#### 컴팩트 스타일 (benefit.time-deal/FormModal.tsx)
|
|
549
|
+
```tsx
|
|
550
|
+
<Space.Compact>
|
|
551
|
+
<Select options={options} style={{ width: 80 }} />
|
|
552
|
+
<Button>{t("할인")}</Button>
|
|
553
|
+
</Space.Compact>
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
### 2.3 Flex
|
|
557
|
+
|
|
558
|
+
#### 정렬과 간격 조정 (benefit.time-deal/FormModal.tsx)
|
|
559
|
+
```tsx
|
|
560
|
+
<Flex gap={8} align="center">
|
|
561
|
+
<FormItem name="field1" style={{ margin: 0 }}>
|
|
562
|
+
<Input />
|
|
563
|
+
</FormItem>
|
|
564
|
+
<span>~</span>
|
|
565
|
+
<FormItem name="field2" style={{ margin: 0 }}>
|
|
566
|
+
<Input />
|
|
567
|
+
</FormItem>
|
|
568
|
+
</Flex>
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### 2.4 Divider
|
|
572
|
+
|
|
573
|
+
#### 섹션 구분
|
|
574
|
+
```tsx
|
|
575
|
+
<Divider orientation="left">{t("기본 정보")}</Divider>
|
|
576
|
+
<Form>
|
|
577
|
+
{/* 폼 필드들 */}
|
|
578
|
+
</Form>
|
|
579
|
+
|
|
580
|
+
<Divider orientation="left">{t("추가 정보")}</Divider>
|
|
581
|
+
<Form>
|
|
582
|
+
{/* 추가 폼 필드들 */}
|
|
583
|
+
</Form>
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
## 3. 데이터 표시 컴포넌트 (Data Display)
|
|
587
|
+
|
|
588
|
+
### 3.1 Table
|
|
589
|
+
|
|
590
|
+
#### 기본 테이블 (StatisticsListData.tsx)
|
|
591
|
+
```tsx
|
|
592
|
+
const columns: TableColumnsType<DtoItem> = [
|
|
593
|
+
{
|
|
594
|
+
title: <div style={{ textAlign: "center" }}>번호</div>,
|
|
595
|
+
dataIndex: "num",
|
|
596
|
+
key: "num",
|
|
597
|
+
width: 80,
|
|
598
|
+
align: "center",
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
title: <div style={{ textAlign: "center" }}>세부항목</div>,
|
|
602
|
+
dataIndex: "cstItmNm",
|
|
603
|
+
key: "cstItmNm",
|
|
604
|
+
sorter: (a, b) => (a.cstItmNm ?? "").localeCompare(b.cstItmNm ?? ""),
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
title: <div style={{ textAlign: "center" }}>제품금액(원)</div>,
|
|
608
|
+
dataIndex: "amt",
|
|
609
|
+
key: "amt",
|
|
610
|
+
width: 150,
|
|
611
|
+
align: "right",
|
|
612
|
+
render: (value) => toMoney(value),
|
|
613
|
+
sorter: (a, b) => (a.amt ?? 0) - (b.amt ?? 0),
|
|
614
|
+
}
|
|
615
|
+
];
|
|
616
|
+
|
|
617
|
+
<Table<DtoItem>
|
|
618
|
+
columns={columns}
|
|
619
|
+
dataSource={listData}
|
|
620
|
+
pagination={false}
|
|
621
|
+
size="small"
|
|
622
|
+
/>
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
### 3.2 Descriptions
|
|
626
|
+
|
|
627
|
+
#### 상세 정보 표시 (order.subscription.direct-rental/FormModal.tsx)
|
|
628
|
+
```tsx
|
|
629
|
+
<Descriptions
|
|
630
|
+
bordered
|
|
631
|
+
column={3}
|
|
632
|
+
size="small"
|
|
633
|
+
style={{ width: "100%" }}
|
|
634
|
+
styles={{
|
|
635
|
+
label: { textAlign: "left", minWidth: 100, width: 140 },
|
|
636
|
+
content: { textAlign: "left" }
|
|
637
|
+
}}
|
|
638
|
+
className="fixed-descriptions"
|
|
639
|
+
items={[
|
|
640
|
+
{ label: t("주문번호"), children: params.query?.ifId },
|
|
641
|
+
{
|
|
642
|
+
label: t("주문일자"),
|
|
643
|
+
children: params.query?.creatDtm
|
|
644
|
+
? dayjs(params.query.creatDtm).format("YYYY-MM-DD HH:mm")
|
|
645
|
+
: "-",
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
label: t("주문자명/아이디"),
|
|
649
|
+
children: (
|
|
650
|
+
<Space>
|
|
651
|
+
{`${params.query?.usrNm || ""}(${params.query?.rmsUsrId || ""})`}
|
|
652
|
+
<Button type="default" size="small" onClick={handleLink}>
|
|
653
|
+
CRM
|
|
654
|
+
</Button>
|
|
655
|
+
</Space>
|
|
656
|
+
),
|
|
657
|
+
},
|
|
658
|
+
{ label: t("휴대폰"), children: params.query?.usrHpNo },
|
|
659
|
+
{ label: t("이메일"), span: 2, children: params.query?.email || "-" },
|
|
660
|
+
]}
|
|
661
|
+
/>
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
#### 중첩된 Descriptions.Item
|
|
665
|
+
```tsx
|
|
666
|
+
<Descriptions.Item
|
|
667
|
+
label={
|
|
668
|
+
<span>
|
|
669
|
+
<span style={{ color: "#ff4d4f", fontSize: "12px", marginRight: "4px" }}>*</span>
|
|
670
|
+
{t("할인 설정")}
|
|
671
|
+
</span>
|
|
672
|
+
}
|
|
673
|
+
>
|
|
674
|
+
<Flex gap={8} align="center">
|
|
675
|
+
<FormItem name="discountType" style={{ margin: 0 }}>
|
|
676
|
+
<Select options={discountOptions} />
|
|
677
|
+
</FormItem>
|
|
678
|
+
</Flex>
|
|
679
|
+
</Descriptions.Item>
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
## 4. 내비게이션 컴포넌트 (Navigation)
|
|
683
|
+
|
|
684
|
+
### 4.1 Tabs
|
|
685
|
+
|
|
686
|
+
#### 기본 탭 (MemberInstallerDetailDrawer.tsx)
|
|
687
|
+
```tsx
|
|
688
|
+
<Tabs
|
|
689
|
+
items={Object.values(MemberInstallerTabType).map((value) => ({
|
|
690
|
+
key: value,
|
|
691
|
+
label: value,
|
|
692
|
+
}))}
|
|
693
|
+
activeKey={activeTabType}
|
|
694
|
+
type="line"
|
|
695
|
+
size="small"
|
|
696
|
+
onChange={(key) => setActiveTabType(key as MemberInstallerTabType)}
|
|
697
|
+
/>
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
#### 동적 탭 콘텐츠
|
|
701
|
+
```tsx
|
|
702
|
+
const tabItems = [
|
|
703
|
+
{
|
|
704
|
+
key: '1',
|
|
705
|
+
label: t('기본정보'),
|
|
706
|
+
children: <BasicInfoPanel />,
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
key: '2',
|
|
710
|
+
label: t('상세정보'),
|
|
711
|
+
children: <DetailInfoPanel />,
|
|
712
|
+
},
|
|
713
|
+
];
|
|
714
|
+
|
|
715
|
+
<Tabs items={tabItems} />
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
## 5. 피드백 컴포넌트 (Feedback)
|
|
719
|
+
|
|
720
|
+
### 5.1 Button
|
|
721
|
+
|
|
722
|
+
#### 기본 버튼 스타일
|
|
723
|
+
```tsx
|
|
724
|
+
// Primary 버튼
|
|
725
|
+
<Button type="primary" onClick={handleSave} loading={saveSpinning}>
|
|
726
|
+
{t("저장")}
|
|
727
|
+
</Button>
|
|
728
|
+
|
|
729
|
+
// Default 버튼
|
|
730
|
+
<Button onClick={onCancel}>
|
|
731
|
+
{t("취소")}
|
|
732
|
+
</Button>
|
|
733
|
+
|
|
734
|
+
// Danger 버튼
|
|
735
|
+
<Button onClick={handleDelete} loading={deleteSpinning} danger>
|
|
736
|
+
{t("삭제")}
|
|
737
|
+
</Button>
|
|
738
|
+
|
|
739
|
+
// Link 스타일 버튼
|
|
740
|
+
<Button type="link" onClick={handleEdit}>
|
|
741
|
+
{t("수정")}
|
|
742
|
+
</Button>
|
|
743
|
+
|
|
744
|
+
// Text 버튼 (아이콘과 함께)
|
|
745
|
+
<Button type="text" onClick={onClose} icon={<IconClose />} />
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
#### 크기별 버튼
|
|
749
|
+
```tsx
|
|
750
|
+
// 작은 버튼
|
|
751
|
+
<Button type="default" size="small" onClick={handleAction}>
|
|
752
|
+
CRM
|
|
753
|
+
</Button>
|
|
754
|
+
|
|
755
|
+
// 큰 버튼
|
|
756
|
+
<Button type="primary" size="large">
|
|
757
|
+
{t("확인")}
|
|
758
|
+
</Button>
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
### 5.2 Modal (AXModal 사용)
|
|
762
|
+
|
|
763
|
+
프로젝트에서는 Ant Design Modal 대신 커스텀 AXModal을 주로 사용하지만, 기본 패턴은 유사합니다.
|
|
764
|
+
|
|
765
|
+
```tsx
|
|
766
|
+
<AXModal width={1400} {...{ open, onCancel, onOk: onOk as any, afterClose }}>
|
|
767
|
+
<AXModal.Header title={t("제목")}>
|
|
768
|
+
<Button type="primary" onClick={handleSave}>
|
|
769
|
+
{t("저장")}
|
|
770
|
+
</Button>
|
|
771
|
+
<Button onClick={onCancel}>
|
|
772
|
+
{t("취소")}
|
|
773
|
+
</Button>
|
|
774
|
+
</AXModal.Header>
|
|
775
|
+
|
|
776
|
+
<AXModal.Body>
|
|
777
|
+
{/* 모달 콘텐츠 */}
|
|
778
|
+
</AXModal.Body>
|
|
779
|
+
|
|
780
|
+
<AXModal.Footer>
|
|
781
|
+
{/* 푸터 버튼들 */}
|
|
782
|
+
</AXModal.Footer>
|
|
783
|
+
</AXModal>
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
## 6. 기타 유틸리티 컴포넌트
|
|
787
|
+
|
|
788
|
+
### 6.1 Popover
|
|
789
|
+
|
|
790
|
+
#### 정보 표시용 팝오버
|
|
791
|
+
```tsx
|
|
792
|
+
<Popover
|
|
793
|
+
content={<div>{t("도움말 내용")}</div>}
|
|
794
|
+
title={t("도움말")}
|
|
795
|
+
trigger="hover"
|
|
796
|
+
>
|
|
797
|
+
<Button type="link" icon={<InfoIcon />} />
|
|
798
|
+
</Popover>
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
### 6.2 Tooltip
|
|
802
|
+
|
|
803
|
+
#### 간단한 툴팁
|
|
804
|
+
```tsx
|
|
805
|
+
<Tooltip title={t("이 기능에 대한 설명입니다.")}>
|
|
806
|
+
<Button icon={<QuestionIcon />} />
|
|
807
|
+
</Tooltip>
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
### 6.3 Tag
|
|
811
|
+
|
|
812
|
+
#### 상태 표시용 태그
|
|
813
|
+
```tsx
|
|
814
|
+
<Tag color={status === 'active' ? 'green' : 'red'}>
|
|
815
|
+
{status === 'active' ? t('활성') : t('비활성')}
|
|
816
|
+
</Tag>
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
### 6.4 Badge
|
|
820
|
+
|
|
821
|
+
#### 알림 배지
|
|
822
|
+
```tsx
|
|
823
|
+
<Badge count={5} size="small">
|
|
824
|
+
<Button icon={<NotificationIcon />} />
|
|
825
|
+
</Badge>
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
## 7. 누락되었던 주요 컴포넌트들
|
|
829
|
+
|
|
830
|
+
### 7.1 Tree
|
|
831
|
+
|
|
832
|
+
Tree 컴포넌트는 계층적 데이터를 트리 구조로 표시하는 데 사용됩니다. 프로젝트에서는 메뉴 관리, 카테고리 관리 등에 활용됩니다.
|
|
833
|
+
|
|
834
|
+
#### 기본 트리 구조 (MenuDirectory.tsx 참고)
|
|
835
|
+
```tsx
|
|
836
|
+
import { Tree } from "antd";
|
|
837
|
+
import { TreeProps } from "antd/lib/tree";
|
|
838
|
+
|
|
839
|
+
interface TreeNode {
|
|
840
|
+
key: string;
|
|
841
|
+
title: string;
|
|
842
|
+
children?: TreeNode[];
|
|
843
|
+
isLeaf?: boolean;
|
|
844
|
+
icon?: string;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const treeData: TreeNode[] = [
|
|
848
|
+
{
|
|
849
|
+
key: '0',
|
|
850
|
+
title: '루트 메뉴',
|
|
851
|
+
children: [
|
|
852
|
+
{
|
|
853
|
+
key: '0:0',
|
|
854
|
+
title: '하위 메뉴 1',
|
|
855
|
+
isLeaf: true,
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
key: '0:1',
|
|
859
|
+
title: '하위 메뉴 2',
|
|
860
|
+
children: [
|
|
861
|
+
{
|
|
862
|
+
key: '0:1:0',
|
|
863
|
+
title: '세부 메뉴',
|
|
864
|
+
isLeaf: true,
|
|
865
|
+
},
|
|
866
|
+
],
|
|
867
|
+
},
|
|
868
|
+
],
|
|
869
|
+
},
|
|
870
|
+
];
|
|
871
|
+
|
|
872
|
+
<Tree
|
|
873
|
+
treeData={treeData}
|
|
874
|
+
defaultExpandAll
|
|
875
|
+
onSelect={(selectedKeys, info) => {
|
|
876
|
+
console.log('선택된 노드:', selectedKeys, info);
|
|
877
|
+
}}
|
|
878
|
+
/>
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
#### 드래그 앤 드롭 지원 트리 (CategoryDirectory.tsx 참고)
|
|
882
|
+
```tsx
|
|
883
|
+
const onDrop: TreeProps["onDrop"] = useCallback((info) => {
|
|
884
|
+
const {
|
|
885
|
+
dropPosition,
|
|
886
|
+
dropToGap,
|
|
887
|
+
node: { seqPath: dropNodeKey },
|
|
888
|
+
dragNode: { seqPath: dragNodeKey },
|
|
889
|
+
} = info;
|
|
890
|
+
|
|
891
|
+
console.log('드래그 앤 드롭:', { dropPosition, dropToGap, dropNodeKey, dragNodeKey });
|
|
892
|
+
|
|
893
|
+
// 트리 구조 업데이트 로직
|
|
894
|
+
const newTreeData = updateTreeData(treeData, dragNodeKey, dropNodeKey);
|
|
895
|
+
setTreeData(newTreeData);
|
|
896
|
+
}, [treeData]);
|
|
897
|
+
|
|
898
|
+
<Tree
|
|
899
|
+
draggable
|
|
900
|
+
blockNode
|
|
901
|
+
onDrop={onDrop}
|
|
902
|
+
treeData={treeData}
|
|
903
|
+
expandedKeys={expandedKeys}
|
|
904
|
+
onExpand={(keys) => setExpandedKeys(keys)}
|
|
905
|
+
onSelect={(selectedKeys, info) => {
|
|
906
|
+
setSelectedItem(info.node);
|
|
907
|
+
}}
|
|
908
|
+
/>
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
#### 커스텀 트리 노드 렌더링
|
|
912
|
+
```tsx
|
|
913
|
+
const renderTreeNode = (nodeData) => (
|
|
914
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
915
|
+
<MenuIcon type={nodeData.icon} />
|
|
916
|
+
<span>{nodeData.title}</span>
|
|
917
|
+
<Space size={4}>
|
|
918
|
+
<Tooltip title={t("추가")}>
|
|
919
|
+
<Button
|
|
920
|
+
type="text"
|
|
921
|
+
size="small"
|
|
922
|
+
icon={<IconAdd />}
|
|
923
|
+
onClick={(e) => {
|
|
924
|
+
e.stopPropagation();
|
|
925
|
+
handleAddNode(nodeData.key);
|
|
926
|
+
}}
|
|
927
|
+
/>
|
|
928
|
+
</Tooltip>
|
|
929
|
+
<Tooltip title={t("삭제")}>
|
|
930
|
+
<Button
|
|
931
|
+
type="text"
|
|
932
|
+
size="small"
|
|
933
|
+
icon={<IconBin />}
|
|
934
|
+
onClick={(e) => {
|
|
935
|
+
e.stopPropagation();
|
|
936
|
+
handleDelNode(nodeData.key);
|
|
937
|
+
}}
|
|
938
|
+
/>
|
|
939
|
+
</Tooltip>
|
|
940
|
+
</Space>
|
|
941
|
+
</div>
|
|
942
|
+
);
|
|
943
|
+
|
|
944
|
+
<Tree
|
|
945
|
+
treeData={treeData.map(node => ({
|
|
946
|
+
...node,
|
|
947
|
+
title: renderTreeNode(node),
|
|
948
|
+
}))}
|
|
949
|
+
/>
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
### 7.2 Drawer
|
|
953
|
+
|
|
954
|
+
Drawer는 화면 측면에서 슬라이드되는 패널입니다. 프로젝트에서는 AXDrawer로 커스터마이징되어 사용됩니다.
|
|
955
|
+
|
|
956
|
+
#### 기본 Drawer (AXDrawer.tsx 참고)
|
|
957
|
+
```tsx
|
|
958
|
+
import { AXDrawer } from "@core/components/AXDrawer";
|
|
959
|
+
|
|
960
|
+
function DetailDrawer({ open, onClose, selectedItem }) {
|
|
961
|
+
const [width, setWidth] = useState(70);
|
|
962
|
+
|
|
963
|
+
return (
|
|
964
|
+
<AXDrawer
|
|
965
|
+
widthPercent={width}
|
|
966
|
+
onResize={(w) => setWidth(w)}
|
|
967
|
+
styles={{
|
|
968
|
+
minWidth: 1000,
|
|
969
|
+
}}
|
|
970
|
+
open={open}
|
|
971
|
+
onClose={onClose}
|
|
972
|
+
loading={!selectedItem}
|
|
973
|
+
>
|
|
974
|
+
<AXDrawer.Header>
|
|
975
|
+
<h2>{t("상세 정보")}</h2>
|
|
976
|
+
<Space>
|
|
977
|
+
<Button type="primary" onClick={handleSave}>
|
|
978
|
+
{t("저장")}
|
|
979
|
+
</Button>
|
|
980
|
+
<Button type="text" onClick={onClose} icon={<IconClose />} />
|
|
981
|
+
</Space>
|
|
982
|
+
</AXDrawer.Header>
|
|
983
|
+
|
|
984
|
+
<AXDrawer.Body>
|
|
985
|
+
{/* 드로어 내용 */}
|
|
986
|
+
<Form form={form}>
|
|
987
|
+
<FormItem name="title" label={t("제목")}>
|
|
988
|
+
<Input />
|
|
989
|
+
</FormItem>
|
|
990
|
+
{/* 기타 폼 필드들 */}
|
|
991
|
+
</Form>
|
|
992
|
+
</AXDrawer.Body>
|
|
993
|
+
</AXDrawer>
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
#### 리사이즈 가능한 Drawer
|
|
999
|
+
```tsx
|
|
1000
|
+
const [drawerWidth, setDrawerWidth] = useState(50);
|
|
1001
|
+
|
|
1002
|
+
<AXDrawer
|
|
1003
|
+
widthPercent={drawerWidth}
|
|
1004
|
+
onResize={(width) => setDrawerWidth(width)}
|
|
1005
|
+
open={open}
|
|
1006
|
+
onClose={onClose}
|
|
1007
|
+
>
|
|
1008
|
+
<AXDrawer.Header>
|
|
1009
|
+
<h2>리사이즈 가능한 드로어</h2>
|
|
1010
|
+
</AXDrawer.Header>
|
|
1011
|
+
<AXDrawer.Body>
|
|
1012
|
+
<p>드로어 우측 경계를 드래그하여 크기를 조절할 수 있습니다.</p>
|
|
1013
|
+
</AXDrawer.Body>
|
|
1014
|
+
</AXDrawer>
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
### 7.3 Upload
|
|
1018
|
+
|
|
1019
|
+
파일 업로드 컴포넌트는 다양한 형태로 활용됩니다.
|
|
1020
|
+
|
|
1021
|
+
#### 기본 파일 업로드 (MultiFileUploader.tsx 참고)
|
|
1022
|
+
```tsx
|
|
1023
|
+
import { Upload, Button } from "antd";
|
|
1024
|
+
import { UploadOutlined } from "@ant-design/icons";
|
|
1025
|
+
import type { UploadProps } from "antd";
|
|
1026
|
+
|
|
1027
|
+
const uploadProps: UploadProps = {
|
|
1028
|
+
action: '/api/upload',
|
|
1029
|
+
listType: 'text',
|
|
1030
|
+
multiple: true,
|
|
1031
|
+
onChange: (info) => {
|
|
1032
|
+
const { status } = info.file;
|
|
1033
|
+
if (status === 'done') {
|
|
1034
|
+
console.log('업로드 성공:', info.file.name);
|
|
1035
|
+
} else if (status === 'error') {
|
|
1036
|
+
console.log('업로드 실패:', info.file.name);
|
|
1037
|
+
}
|
|
1038
|
+
},
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
<Upload {...uploadProps}>
|
|
1042
|
+
<Button icon={<UploadOutlined />}>
|
|
1043
|
+
{t("파일 업로드")}
|
|
1044
|
+
</Button>
|
|
1045
|
+
</Upload>
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
#### 이미지 업로드 (드래그 앤 드롭)
|
|
1049
|
+
```tsx
|
|
1050
|
+
const { Dragger } = Upload;
|
|
1051
|
+
|
|
1052
|
+
<Dragger
|
|
1053
|
+
name="file"
|
|
1054
|
+
multiple
|
|
1055
|
+
action="/api/upload"
|
|
1056
|
+
accept="image/*"
|
|
1057
|
+
listType="picture"
|
|
1058
|
+
onChange={(info) => {
|
|
1059
|
+
const { status } = info.file;
|
|
1060
|
+
if (status === 'uploading') {
|
|
1061
|
+
console.log('업로드 중:', info.fileList);
|
|
1062
|
+
}
|
|
1063
|
+
if (status === 'done') {
|
|
1064
|
+
console.log('업로드 완료:', info.file.name);
|
|
1065
|
+
}
|
|
1066
|
+
}}
|
|
1067
|
+
>
|
|
1068
|
+
<p className="ant-upload-drag-icon">
|
|
1069
|
+
<UploadOutlined />
|
|
1070
|
+
</p>
|
|
1071
|
+
<p className="ant-upload-text">
|
|
1072
|
+
{t("클릭하거나 파일을 여기로 드래그하세요")}
|
|
1073
|
+
</p>
|
|
1074
|
+
<p className="ant-upload-hint">
|
|
1075
|
+
{t("단일 또는 다중 파일 업로드를 지원합니다")}
|
|
1076
|
+
</p>
|
|
1077
|
+
</Dragger>
|
|
1078
|
+
```
|
|
1079
|
+
|
|
1080
|
+
#### 커스텀 업로드 버튼
|
|
1081
|
+
```tsx
|
|
1082
|
+
<Upload
|
|
1083
|
+
customRequest={({ file, onSuccess, onError }) => {
|
|
1084
|
+
// 커스텀 업로드 로직
|
|
1085
|
+
uploadFile(file)
|
|
1086
|
+
.then(response => {
|
|
1087
|
+
onSuccess(response);
|
|
1088
|
+
})
|
|
1089
|
+
.catch(error => {
|
|
1090
|
+
onError(error);
|
|
1091
|
+
});
|
|
1092
|
+
}}
|
|
1093
|
+
showUploadList={false}
|
|
1094
|
+
>
|
|
1095
|
+
<Button type="primary" icon={<UploadOutlined />}>
|
|
1096
|
+
{t("커스텀 업로드")}
|
|
1097
|
+
</Button>
|
|
1098
|
+
</Upload>
|
|
1099
|
+
```
|
|
1100
|
+
|
|
1101
|
+
### 7.4 Transfer
|
|
1102
|
+
|
|
1103
|
+
Transfer 컴포넌트는 두 그룹 간의 데이터 이동을 위해 사용됩니다.
|
|
1104
|
+
|
|
1105
|
+
#### 기본 Transfer
|
|
1106
|
+
```tsx
|
|
1107
|
+
import { Transfer } from "antd";
|
|
1108
|
+
|
|
1109
|
+
const [targetKeys, setTargetKeys] = useState<string[]>([]);
|
|
1110
|
+
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
|
|
1111
|
+
|
|
1112
|
+
const mockData = Array.from({ length: 20 }).map((_, i) => ({
|
|
1113
|
+
key: i.toString(),
|
|
1114
|
+
title: `항목 ${i + 1}`,
|
|
1115
|
+
description: `설명 ${i + 1}`,
|
|
1116
|
+
}));
|
|
1117
|
+
|
|
1118
|
+
<Transfer
|
|
1119
|
+
dataSource={mockData}
|
|
1120
|
+
titles={[t('사용 가능'), t('선택된 항목')]}
|
|
1121
|
+
targetKeys={targetKeys}
|
|
1122
|
+
selectedKeys={selectedKeys}
|
|
1123
|
+
onChange={(nextTargetKeys) => {
|
|
1124
|
+
setTargetKeys(nextTargetKeys);
|
|
1125
|
+
}}
|
|
1126
|
+
onSelectChange={(sourceSelectedKeys, targetSelectedKeys) => {
|
|
1127
|
+
setSelectedKeys([...sourceSelectedKeys, ...targetSelectedKeys]);
|
|
1128
|
+
}}
|
|
1129
|
+
render={(item) => item.title}
|
|
1130
|
+
/>
|
|
1131
|
+
```
|
|
1132
|
+
|
|
1133
|
+
### 7.5 Cascader
|
|
1134
|
+
|
|
1135
|
+
Cascader는 계층적 선택을 위한 컴포넌트입니다.
|
|
1136
|
+
|
|
1137
|
+
#### 기본 Cascader
|
|
1138
|
+
```tsx
|
|
1139
|
+
import { Cascader } from "antd";
|
|
1140
|
+
|
|
1141
|
+
const options = [
|
|
1142
|
+
{
|
|
1143
|
+
value: 'seoul',
|
|
1144
|
+
label: '서울',
|
|
1145
|
+
children: [
|
|
1146
|
+
{
|
|
1147
|
+
value: 'gangnam',
|
|
1148
|
+
label: '강남구',
|
|
1149
|
+
},
|
|
1150
|
+
{
|
|
1151
|
+
value: 'gangbuk',
|
|
1152
|
+
label: '강북구',
|
|
1153
|
+
},
|
|
1154
|
+
],
|
|
1155
|
+
},
|
|
1156
|
+
{
|
|
1157
|
+
value: 'busan',
|
|
1158
|
+
label: '부산',
|
|
1159
|
+
children: [
|
|
1160
|
+
{
|
|
1161
|
+
value: 'haeundae',
|
|
1162
|
+
label: '해운대구',
|
|
1163
|
+
},
|
|
1164
|
+
],
|
|
1165
|
+
},
|
|
1166
|
+
];
|
|
1167
|
+
|
|
1168
|
+
<FormItem name="location" label={t("지역")}>
|
|
1169
|
+
<Cascader
|
|
1170
|
+
options={options}
|
|
1171
|
+
placeholder={t("지역을 선택하세요")}
|
|
1172
|
+
changeOnSelect
|
|
1173
|
+
/>
|
|
1174
|
+
</FormItem>
|
|
1175
|
+
```
|
|
1176
|
+
|
|
1177
|
+
### 7.6 TreeSelect
|
|
1178
|
+
|
|
1179
|
+
TreeSelect는 트리 형태의 드롭다운 선택기입니다.
|
|
1180
|
+
|
|
1181
|
+
#### 기본 TreeSelect
|
|
1182
|
+
```tsx
|
|
1183
|
+
import { TreeSelect } from "antd";
|
|
1184
|
+
|
|
1185
|
+
const treeData = [
|
|
1186
|
+
{
|
|
1187
|
+
title: '전체',
|
|
1188
|
+
value: 'all',
|
|
1189
|
+
children: [
|
|
1190
|
+
{
|
|
1191
|
+
title: '카테고리 1',
|
|
1192
|
+
value: 'category1',
|
|
1193
|
+
children: [
|
|
1194
|
+
{
|
|
1195
|
+
title: '하위 카테고리 1-1',
|
|
1196
|
+
value: 'subcategory1-1',
|
|
1197
|
+
},
|
|
1198
|
+
],
|
|
1199
|
+
},
|
|
1200
|
+
{
|
|
1201
|
+
title: '카테고리 2',
|
|
1202
|
+
value: 'category2',
|
|
1203
|
+
},
|
|
1204
|
+
],
|
|
1205
|
+
},
|
|
1206
|
+
];
|
|
1207
|
+
|
|
1208
|
+
<FormItem name="category" label={t("카테고리")}>
|
|
1209
|
+
<TreeSelect
|
|
1210
|
+
style={{ width: '100%' }}
|
|
1211
|
+
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
|
1212
|
+
treeData={treeData}
|
|
1213
|
+
placeholder={t("카테고리를 선택하세요")}
|
|
1214
|
+
treeDefaultExpandAll
|
|
1215
|
+
/>
|
|
1216
|
+
</FormItem>
|
|
1217
|
+
```
|
|
1218
|
+
|
|
1219
|
+
### 7.7 AutoComplete
|
|
1220
|
+
|
|
1221
|
+
AutoComplete는 자동 완성 입력 컴포넌트입니다.
|
|
1222
|
+
|
|
1223
|
+
#### 기본 AutoComplete
|
|
1224
|
+
```tsx
|
|
1225
|
+
import { AutoComplete } from "antd";
|
|
1226
|
+
|
|
1227
|
+
const [options, setOptions] = useState<{value: string}[]>([]);
|
|
1228
|
+
|
|
1229
|
+
const onSearch = (searchText: string) => {
|
|
1230
|
+
const filteredOptions = mockData
|
|
1231
|
+
.filter(item => item.includes(searchText))
|
|
1232
|
+
.map(item => ({ value: item }));
|
|
1233
|
+
setOptions(filteredOptions);
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
<FormItem name="search" label={t("검색")}>
|
|
1237
|
+
<AutoComplete
|
|
1238
|
+
options={options}
|
|
1239
|
+
onSearch={onSearch}
|
|
1240
|
+
placeholder={t("검색어를 입력하세요")}
|
|
1241
|
+
filterOption={(inputValue, option) =>
|
|
1242
|
+
option!.value.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
|
|
1243
|
+
}
|
|
1244
|
+
/>
|
|
1245
|
+
</FormItem>
|
|
1246
|
+
```
|
|
1247
|
+
|
|
1248
|
+
### 7.8 기타 유용한 컴포넌트들
|
|
1249
|
+
|
|
1250
|
+
#### Slider (범위 슬라이더)
|
|
1251
|
+
```tsx
|
|
1252
|
+
import { Slider } from "antd";
|
|
1253
|
+
|
|
1254
|
+
<FormItem name="range" label={t("범위")}>
|
|
1255
|
+
<Slider
|
|
1256
|
+
range
|
|
1257
|
+
min={0}
|
|
1258
|
+
max={100}
|
|
1259
|
+
defaultValue={[20, 50]}
|
|
1260
|
+
marks={{
|
|
1261
|
+
0: '0',
|
|
1262
|
+
25: '25',
|
|
1263
|
+
50: '50',
|
|
1264
|
+
75: '75',
|
|
1265
|
+
100: '100',
|
|
1266
|
+
}}
|
|
1267
|
+
/>
|
|
1268
|
+
</FormItem>
|
|
1269
|
+
```
|
|
1270
|
+
|
|
1271
|
+
#### Rate (평점)
|
|
1272
|
+
```tsx
|
|
1273
|
+
import { Rate } from "antd";
|
|
1274
|
+
|
|
1275
|
+
<FormItem name="rating" label={t("평점")}>
|
|
1276
|
+
<Rate allowHalf defaultValue={2.5} />
|
|
1277
|
+
</FormItem>
|
|
1278
|
+
```
|
|
1279
|
+
|
|
1280
|
+
#### Mention (멘션)
|
|
1281
|
+
```tsx
|
|
1282
|
+
import { Mentions } from "antd";
|
|
1283
|
+
|
|
1284
|
+
const { Option } = Mentions;
|
|
1285
|
+
|
|
1286
|
+
<FormItem name="comment" label={t("댓글")}>
|
|
1287
|
+
<Mentions
|
|
1288
|
+
rows={3}
|
|
1289
|
+
placeholder={t("@사용자명을 입력하면 멘션됩니다")}
|
|
1290
|
+
>
|
|
1291
|
+
<Option value="user1">사용자1</Option>
|
|
1292
|
+
<Option value="user2">사용자2</Option>
|
|
1293
|
+
<Option value="user3">사용자3</Option>
|
|
1294
|
+
</Mentions>
|
|
1295
|
+
</FormItem>
|
|
1296
|
+
```
|
|
1297
|
+
|
|
1298
|
+
## 8. 고급 사용 패턴
|
|
1299
|
+
|
|
1300
|
+
### 7.1 조건부 렌더링과 함께 사용
|
|
1301
|
+
|
|
1302
|
+
```tsx
|
|
1303
|
+
const watchedValue = Form.useWatch("parentField", form);
|
|
1304
|
+
|
|
1305
|
+
return (
|
|
1306
|
+
<>
|
|
1307
|
+
<FormItem name="parentField" label={t("부모필드")}>
|
|
1308
|
+
<Select options={parentOptions} />
|
|
1309
|
+
</FormItem>
|
|
1310
|
+
|
|
1311
|
+
{watchedValue === "specific_value" && (
|
|
1312
|
+
<Row gutter={16}>
|
|
1313
|
+
<Col span={12}>
|
|
1314
|
+
<FormItem name="childField1" label={t("자식필드1")}>
|
|
1315
|
+
<Input />
|
|
1316
|
+
</FormItem>
|
|
1317
|
+
</Col>
|
|
1318
|
+
<Col span={12}>
|
|
1319
|
+
<FormItem name="childField2" label={t("자식필드2")}>
|
|
1320
|
+
<Select options={childOptions} />
|
|
1321
|
+
</FormItem>
|
|
1322
|
+
</Col>
|
|
1323
|
+
</Row>
|
|
1324
|
+
)}
|
|
1325
|
+
</>
|
|
1326
|
+
);
|
|
1327
|
+
```
|
|
1328
|
+
|
|
1329
|
+
### 7.2 shouldUpdate 패턴
|
|
1330
|
+
|
|
1331
|
+
```tsx
|
|
1332
|
+
<Form.Item
|
|
1333
|
+
shouldUpdate={(prevValues, currentValues) =>
|
|
1334
|
+
prevValues.triggerField !== currentValues.triggerField
|
|
1335
|
+
}
|
|
1336
|
+
>
|
|
1337
|
+
{({ getFieldValue }) => {
|
|
1338
|
+
const triggerValue = getFieldValue("triggerField");
|
|
1339
|
+
return (
|
|
1340
|
+
<Row gutter={16}>
|
|
1341
|
+
<Col span={triggerValue === "Y" ? 12 : 24}>
|
|
1342
|
+
<FormItem name="conditionalField1">
|
|
1343
|
+
<Input />
|
|
1344
|
+
</FormItem>
|
|
1345
|
+
</Col>
|
|
1346
|
+
{triggerValue === "Y" && (
|
|
1347
|
+
<Col span={12}>
|
|
1348
|
+
<FormItem name="conditionalField2">
|
|
1349
|
+
<Select options={options} />
|
|
1350
|
+
</FormItem>
|
|
1351
|
+
</Col>
|
|
1352
|
+
)}
|
|
1353
|
+
</Row>
|
|
1354
|
+
);
|
|
1355
|
+
}}
|
|
1356
|
+
</Form.Item>
|
|
1357
|
+
```
|
|
1358
|
+
|
|
1359
|
+
### 7.3 복합 입력 컴포넌트
|
|
1360
|
+
|
|
1361
|
+
```tsx
|
|
1362
|
+
<FormItem label={t("기간 설정")}>
|
|
1363
|
+
<Input.Group compact>
|
|
1364
|
+
<FormItem name="startDate" noStyle rules={[{ required: true }]}>
|
|
1365
|
+
<DatePicker placeholder="시작일" style={{ width: '50%' }} />
|
|
1366
|
+
</FormItem>
|
|
1367
|
+
<FormItem name="endDate" noStyle rules={[{ required: true }]}>
|
|
1368
|
+
<DatePicker placeholder="종료일" style={{ width: '50%' }} />
|
|
1369
|
+
</FormItem>
|
|
1370
|
+
</Input.Group>
|
|
1371
|
+
</FormItem>
|
|
1372
|
+
```
|
|
1373
|
+
|
|
1374
|
+
### 7.4 동적 리스트 관리
|
|
1375
|
+
|
|
1376
|
+
```tsx
|
|
1377
|
+
<Form.List name="items">
|
|
1378
|
+
{(fields, { add, remove }) => (
|
|
1379
|
+
<>
|
|
1380
|
+
{fields.map(({ key, name, ...restField }) => (
|
|
1381
|
+
<Row key={key} gutter={16} align="middle">
|
|
1382
|
+
<Col span={20}>
|
|
1383
|
+
<FormItem
|
|
1384
|
+
{...restField}
|
|
1385
|
+
name={[name, 'itemName']}
|
|
1386
|
+
label={t("항목명")}
|
|
1387
|
+
rules={[{ required: true }]}
|
|
1388
|
+
>
|
|
1389
|
+
<Input />
|
|
1390
|
+
</FormItem>
|
|
1391
|
+
</Col>
|
|
1392
|
+
<Col span={4}>
|
|
1393
|
+
<Button
|
|
1394
|
+
type="link"
|
|
1395
|
+
danger
|
|
1396
|
+
onClick={() => remove(name)}
|
|
1397
|
+
icon={<DeleteIcon />}
|
|
1398
|
+
>
|
|
1399
|
+
삭제
|
|
1400
|
+
</Button>
|
|
1401
|
+
</Col>
|
|
1402
|
+
</Row>
|
|
1403
|
+
))}
|
|
1404
|
+
<Button
|
|
1405
|
+
type="dashed"
|
|
1406
|
+
onClick={() => add()}
|
|
1407
|
+
block
|
|
1408
|
+
icon={<PlusIcon />}
|
|
1409
|
+
>
|
|
1410
|
+
+ 항목 추가
|
|
1411
|
+
</Button>
|
|
1412
|
+
</>
|
|
1413
|
+
)}
|
|
1414
|
+
</Form.List>
|
|
1415
|
+
```
|
|
1416
|
+
|
|
1417
|
+
## 8. 성능 최적화 팁
|
|
1418
|
+
|
|
1419
|
+
### 8.1 Form.Item preserve 속성
|
|
1420
|
+
```tsx
|
|
1421
|
+
<FormItem
|
|
1422
|
+
name="temporaryField"
|
|
1423
|
+
preserve={false} // 컴포넌트 언마운트 시 값 제거
|
|
1424
|
+
>
|
|
1425
|
+
<Input />
|
|
1426
|
+
</FormItem>
|
|
1427
|
+
```
|
|
1428
|
+
|
|
1429
|
+
### 8.2 Table 가상화
|
|
1430
|
+
```tsx
|
|
1431
|
+
<Table
|
|
1432
|
+
virtual
|
|
1433
|
+
scroll={{ y: 400 }} // 높이 제한으로 가상 스크롤 활성화
|
|
1434
|
+
dataSource={largeDataset}
|
|
1435
|
+
columns={columns}
|
|
1436
|
+
/>
|
|
1437
|
+
```
|
|
1438
|
+
|
|
1439
|
+
### 8.3 Select 검색 최적화
|
|
1440
|
+
```tsx
|
|
1441
|
+
<Select
|
|
1442
|
+
showSearch
|
|
1443
|
+
filterOption={(input, option) =>
|
|
1444
|
+
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
|
1445
|
+
}
|
|
1446
|
+
options={options}
|
|
1447
|
+
/>
|
|
1448
|
+
```
|
|
1449
|
+
|
|
1450
|
+
## 9. 접근성 개선
|
|
1451
|
+
|
|
1452
|
+
### 9.1 적절한 라벨링
|
|
1453
|
+
```tsx
|
|
1454
|
+
<FormItem
|
|
1455
|
+
name="field"
|
|
1456
|
+
label={t("필수 입력 필드")}
|
|
1457
|
+
required // 시각적 필수 표시
|
|
1458
|
+
rules={[{ required: true, message: t("이 필드는 필수입니다.") }]}
|
|
1459
|
+
>
|
|
1460
|
+
<Input aria-describedby="field-help" />
|
|
1461
|
+
</FormItem>
|
|
1462
|
+
<div id="field-help">{t("이 필드에 대한 추가 설명")}</div>
|
|
1463
|
+
```
|
|
1464
|
+
|
|
1465
|
+
### 9.2 키보드 내비게이션
|
|
1466
|
+
```tsx
|
|
1467
|
+
<Space>
|
|
1468
|
+
<Button tabIndex={1} onClick={handleSave}>
|
|
1469
|
+
{t("저장")}
|
|
1470
|
+
</Button>
|
|
1471
|
+
<Button tabIndex={2} onClick={handleCancel}>
|
|
1472
|
+
{t("취소")}
|
|
1473
|
+
</Button>
|
|
1474
|
+
</Space>
|
|
1475
|
+
```
|
|
1476
|
+
|
|
1477
|
+
## 10. 프로젝트 특화 패턴
|
|
1478
|
+
|
|
1479
|
+
### 10.1 다국어 지원 (i18n)
|
|
1480
|
+
모든 텍스트는 `useI18n` 훅을 통해 다국어 처리:
|
|
1481
|
+
```tsx
|
|
1482
|
+
const { t } = useI18n();
|
|
1483
|
+
|
|
1484
|
+
<FormItem name="title" label={t("제목")}>
|
|
1485
|
+
<Input placeholder={t("제목을 입력하세요")} />
|
|
1486
|
+
</FormItem>
|
|
1487
|
+
```
|
|
1488
|
+
|
|
1489
|
+
### 10.2 코드 스토어 활용
|
|
1490
|
+
공통 코드는 `useCodeStore`를 통해 관리:
|
|
1491
|
+
```tsx
|
|
1492
|
+
const SLE_TPCD = useCodeStore((s) => s.SLE_TPCD);
|
|
1493
|
+
|
|
1494
|
+
<FormItem name="saleType" label={t("판매유형")}>
|
|
1495
|
+
<Select options={SLE_TPCD?.options} />
|
|
1496
|
+
</FormItem>
|
|
1497
|
+
```
|
|
1498
|
+
|
|
1499
|
+
### 10.3 에러 처리 패턴
|
|
1500
|
+
```tsx
|
|
1501
|
+
const handleSave = useCallback(async () => {
|
|
1502
|
+
try {
|
|
1503
|
+
setSaveSpinning(true);
|
|
1504
|
+
const values = await form.validateFields();
|
|
1505
|
+
await ApiService.save(values);
|
|
1506
|
+
messageApi.success(t("저장되었습니다."));
|
|
1507
|
+
} catch (err) {
|
|
1508
|
+
await errorHandling(err); // 프로젝트 공통 에러 처리
|
|
1509
|
+
} finally {
|
|
1510
|
+
setSaveSpinning(false);
|
|
1511
|
+
}
|
|
1512
|
+
}, [form, messageApi, t]);
|
|
1513
|
+
```
|
|
1514
|
+
|
|
1515
|
+
이 가이드를 참고하여 일관성 있고 효율적인 Ant Design 컴포넌트를 사용하시기 바랍니다.
|