@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,903 @@
|
|
|
1
|
+
# FormItem 사용법 가이드
|
|
2
|
+
|
|
3
|
+
## 개요
|
|
4
|
+
|
|
5
|
+
FormItem은 Ant Design의 Form.Item 컴포넌트를 TypeScript 제네릭과 함께 사용하는 패턴입니다. AXBoot 프로젝트에서는 타입 안전성을 위해 `const FormItem = Form.Item<DtoItem>;` 형태로 정의하여 사용합니다.
|
|
6
|
+
|
|
7
|
+
## 기본 정의 패턴
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
interface DtoItem extends SomeInterface {}
|
|
11
|
+
const FormItem = Form.Item<DtoItem>;
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## 사용 케이스별 정리
|
|
15
|
+
|
|
16
|
+
### 1. 기본 입력 필드 (Basic Input Fields)
|
|
17
|
+
|
|
18
|
+
#### 1.1 필수 입력 필드
|
|
19
|
+
```tsx
|
|
20
|
+
<FormItem
|
|
21
|
+
name={"itemName"}
|
|
22
|
+
label={t("항목명")}
|
|
23
|
+
rules={[{ required: true }]}
|
|
24
|
+
>
|
|
25
|
+
<Input />
|
|
26
|
+
</FormItem>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
#### 1.2 최대 길이 제한
|
|
30
|
+
```tsx
|
|
31
|
+
<FormItem
|
|
32
|
+
name={"description"}
|
|
33
|
+
label={t("설명")}
|
|
34
|
+
rules={[
|
|
35
|
+
{ required: true },
|
|
36
|
+
{ max: 100, message: t("최대 100자까지 입력 가능합니다.") }
|
|
37
|
+
]}
|
|
38
|
+
>
|
|
39
|
+
<Input.TextArea rows={3} maxLength={100} />
|
|
40
|
+
</FormItem>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
#### 1.3 정규식 패턴 검증
|
|
44
|
+
```tsx
|
|
45
|
+
<FormItem
|
|
46
|
+
name={"email"}
|
|
47
|
+
label={t("이메일")}
|
|
48
|
+
rules={[
|
|
49
|
+
{ required: true },
|
|
50
|
+
{ type: 'email', message: t("올바른 이메일 형식을 입력하세요.") }
|
|
51
|
+
]}
|
|
52
|
+
>
|
|
53
|
+
<Input />
|
|
54
|
+
</FormItem>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 2. 선택 필드 (Selection Fields)
|
|
58
|
+
|
|
59
|
+
#### 2.1 Select 드롭다운
|
|
60
|
+
```tsx
|
|
61
|
+
<FormItem
|
|
62
|
+
name={"categoryCode"}
|
|
63
|
+
label={t("카테고리")}
|
|
64
|
+
rules={[{ required: true }]}
|
|
65
|
+
>
|
|
66
|
+
<Select
|
|
67
|
+
options={categoryOptions}
|
|
68
|
+
placeholder={t("카테고리를 선택하세요")}
|
|
69
|
+
/>
|
|
70
|
+
</FormItem>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
#### 2.2 Radio Group
|
|
74
|
+
```tsx
|
|
75
|
+
<FormItem
|
|
76
|
+
name={"useYn"}
|
|
77
|
+
label={t("사용여부")}
|
|
78
|
+
rules={[{ required: true }]}
|
|
79
|
+
>
|
|
80
|
+
<Radio.Group
|
|
81
|
+
options={[
|
|
82
|
+
{ value: "Y", label: "Y" },
|
|
83
|
+
{ value: "N", label: "N" }
|
|
84
|
+
]}
|
|
85
|
+
/>
|
|
86
|
+
</FormItem>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### 2.3 Checkbox (Switch)
|
|
90
|
+
```tsx
|
|
91
|
+
<FormItem
|
|
92
|
+
name={"displayYn"}
|
|
93
|
+
label={t("전시여부")}
|
|
94
|
+
valuePropName="checked"
|
|
95
|
+
>
|
|
96
|
+
<Switch
|
|
97
|
+
checkedChildren="전시"
|
|
98
|
+
unCheckedChildren="미전시"
|
|
99
|
+
/>
|
|
100
|
+
</FormItem>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 3. 숫자 입력 필드 (Number Fields)
|
|
104
|
+
|
|
105
|
+
#### 3.1 기본 숫자 입력
|
|
106
|
+
```tsx
|
|
107
|
+
<FormItem
|
|
108
|
+
name={"quantity"}
|
|
109
|
+
label={t("수량")}
|
|
110
|
+
rules={[{ required: true }]}
|
|
111
|
+
>
|
|
112
|
+
<InputNumber
|
|
113
|
+
min={1}
|
|
114
|
+
max={999}
|
|
115
|
+
style={{ width: "100%" }}
|
|
116
|
+
/>
|
|
117
|
+
</FormItem>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### 3.2 단위가 있는 숫자 입력
|
|
121
|
+
```tsx
|
|
122
|
+
<FormItem
|
|
123
|
+
name={"exposureTime"}
|
|
124
|
+
label={t("노출시간")}
|
|
125
|
+
rules={[{ required: true }]}
|
|
126
|
+
>
|
|
127
|
+
<InputNumber
|
|
128
|
+
min={1}
|
|
129
|
+
max={60}
|
|
130
|
+
addonAfter="초"
|
|
131
|
+
placeholder={t("노출할 초값을 입력하세요")}
|
|
132
|
+
style={{ width: "50%" }}
|
|
133
|
+
/>
|
|
134
|
+
</FormItem>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 4. 날짜/시간 필드 (Date/Time Fields)
|
|
138
|
+
|
|
139
|
+
#### 4.1 날짜 범위 선택
|
|
140
|
+
```tsx
|
|
141
|
+
<FormItem
|
|
142
|
+
name={"dateRange"}
|
|
143
|
+
label={t("기간")}
|
|
144
|
+
rules={[{ required: true }]}
|
|
145
|
+
>
|
|
146
|
+
<DtPicker
|
|
147
|
+
type={DtPickerType.DATE_RANGE}
|
|
148
|
+
placeholder={[t("시작일"), t("종료일")]}
|
|
149
|
+
/>
|
|
150
|
+
</FormItem>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
#### 4.2 단일 날짜 선택
|
|
154
|
+
```tsx
|
|
155
|
+
<FormItem
|
|
156
|
+
name={"targetDate"}
|
|
157
|
+
label={t("대상일자")}
|
|
158
|
+
rules={[{ required: true }]}
|
|
159
|
+
>
|
|
160
|
+
<DtPicker
|
|
161
|
+
type={DtPickerType.DATE}
|
|
162
|
+
/>
|
|
163
|
+
</FormItem>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### 5. 파일 업로드 (File Upload)
|
|
167
|
+
|
|
168
|
+
#### 5.1 이미지 파일 업로드
|
|
169
|
+
```tsx
|
|
170
|
+
<FormItem
|
|
171
|
+
label={t("이미지")}
|
|
172
|
+
required
|
|
173
|
+
>
|
|
174
|
+
<div style={{ display: "flex", alignItems: "flex-start", gap: "16px" }}>
|
|
175
|
+
<div style={{ flex: "0 0 auto" }}>
|
|
176
|
+
<ImgFileUploaderById
|
|
177
|
+
value={files}
|
|
178
|
+
spinning={uploading}
|
|
179
|
+
onChange={setFiles}
|
|
180
|
+
handleUpload={uploadFile}
|
|
181
|
+
handleDelete={setDeleted}
|
|
182
|
+
handleDownload={handleDownload}
|
|
183
|
+
accept="image/png,image/svg+xml"
|
|
184
|
+
/>
|
|
185
|
+
</div>
|
|
186
|
+
<div style={{ flex: "1 1 auto" }}>
|
|
187
|
+
<UploadGuideLine type="banner" />
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</FormItem>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### 6. 조건부 렌더링 (Conditional Rendering)
|
|
194
|
+
|
|
195
|
+
#### 6.1 Form.useWatch를 활용한 조건부 필드
|
|
196
|
+
```tsx
|
|
197
|
+
const watchedValue = Form.useWatch("parentField", form);
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<>
|
|
201
|
+
<FormItem name={"parentField"} label={t("부모필드")}>
|
|
202
|
+
<Select options={parentOptions} />
|
|
203
|
+
</FormItem>
|
|
204
|
+
|
|
205
|
+
{watchedValue === "specific_value" && (
|
|
206
|
+
<FormItem
|
|
207
|
+
name={"childField"}
|
|
208
|
+
label={t("자식필드")}
|
|
209
|
+
rules={[{ required: true }]}
|
|
210
|
+
>
|
|
211
|
+
<Input />
|
|
212
|
+
</FormItem>
|
|
213
|
+
)}
|
|
214
|
+
</>
|
|
215
|
+
);
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
#### 6.2 조건부 필수 입력 검증
|
|
219
|
+
```tsx
|
|
220
|
+
<FormItem
|
|
221
|
+
name={"conditionalField"}
|
|
222
|
+
label={t("조건부 필드")}
|
|
223
|
+
rules={[{ required: watchedValue === "Y" }]}
|
|
224
|
+
>
|
|
225
|
+
<Input />
|
|
226
|
+
</FormItem>
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### 7. 동적 폼 필드 (Dynamic Form Fields)
|
|
230
|
+
|
|
231
|
+
#### 7.1 shouldUpdate를 사용한 동적 렌더링
|
|
232
|
+
```tsx
|
|
233
|
+
<FormItem
|
|
234
|
+
shouldUpdate={(prevValues, currentValues) =>
|
|
235
|
+
prevValues.triggerField !== currentValues.triggerField
|
|
236
|
+
}
|
|
237
|
+
>
|
|
238
|
+
{({ getFieldValue }) => {
|
|
239
|
+
const triggerValue = getFieldValue("triggerField");
|
|
240
|
+
return triggerValue === "show" ? (
|
|
241
|
+
<FormItem name={"dynamicField"} label={t("동적필드")}>
|
|
242
|
+
<Input />
|
|
243
|
+
</FormItem>
|
|
244
|
+
) : null;
|
|
245
|
+
}}
|
|
246
|
+
</FormItem>
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
#### 7.2 배열 형태의 동적 필드
|
|
250
|
+
```tsx
|
|
251
|
+
<Form.List name="items">
|
|
252
|
+
{(fields, { add, remove }) => (
|
|
253
|
+
<>
|
|
254
|
+
{fields.map(({ key, name, ...restField }) => (
|
|
255
|
+
<Row key={key} gutter={16}>
|
|
256
|
+
<Col span={20}>
|
|
257
|
+
<FormItem
|
|
258
|
+
{...restField}
|
|
259
|
+
name={[name, 'itemName']}
|
|
260
|
+
label={t("항목명")}
|
|
261
|
+
rules={[{ required: true }]}
|
|
262
|
+
>
|
|
263
|
+
<Input />
|
|
264
|
+
</FormItem>
|
|
265
|
+
</Col>
|
|
266
|
+
<Col span={4}>
|
|
267
|
+
<Button
|
|
268
|
+
type="link"
|
|
269
|
+
danger
|
|
270
|
+
onClick={() => remove(name)}
|
|
271
|
+
>
|
|
272
|
+
삭제
|
|
273
|
+
</Button>
|
|
274
|
+
</Col>
|
|
275
|
+
</Row>
|
|
276
|
+
))}
|
|
277
|
+
<Button type="dashed" onClick={() => add()} block>
|
|
278
|
+
+ 항목 추가
|
|
279
|
+
</Button>
|
|
280
|
+
</>
|
|
281
|
+
)}
|
|
282
|
+
</Form.List>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### 8. 숨김 필드 (Hidden Fields)
|
|
286
|
+
|
|
287
|
+
#### 8.1 noStyle 속성 사용
|
|
288
|
+
```tsx
|
|
289
|
+
<FormItem name={"hiddenId"} noStyle>
|
|
290
|
+
<Input type="hidden" />
|
|
291
|
+
</FormItem>
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
#### 8.2 기본값이 있는 숨김 필드
|
|
295
|
+
```tsx
|
|
296
|
+
<FormItem
|
|
297
|
+
name={"defaultValue"}
|
|
298
|
+
noStyle
|
|
299
|
+
initialValue="default"
|
|
300
|
+
>
|
|
301
|
+
<Input type="hidden" />
|
|
302
|
+
</FormItem>
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### 9. 복합 입력 필드 (Complex Input Fields)
|
|
306
|
+
|
|
307
|
+
#### 9.1 여러 컴포넌트 조합
|
|
308
|
+
```tsx
|
|
309
|
+
<FormItem label={t("가격 범위")}>
|
|
310
|
+
<Input.Group compact>
|
|
311
|
+
<FormItem
|
|
312
|
+
name={"minPrice"}
|
|
313
|
+
noStyle
|
|
314
|
+
rules={[{ required: true }]}
|
|
315
|
+
>
|
|
316
|
+
<InputNumber
|
|
317
|
+
placeholder="최소가격"
|
|
318
|
+
style={{ width: '50%' }}
|
|
319
|
+
/>
|
|
320
|
+
</FormItem>
|
|
321
|
+
<FormItem
|
|
322
|
+
name={"maxPrice"}
|
|
323
|
+
noStyle
|
|
324
|
+
rules={[{ required: true }]}
|
|
325
|
+
>
|
|
326
|
+
<InputNumber
|
|
327
|
+
placeholder="최대가격"
|
|
328
|
+
style={{ width: '50%' }}
|
|
329
|
+
/>
|
|
330
|
+
</FormItem>
|
|
331
|
+
</Input.Group>
|
|
332
|
+
</FormItem>
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
#### 9.2 주소 입력 필드
|
|
336
|
+
```tsx
|
|
337
|
+
<FormItem label={t("주소")}>
|
|
338
|
+
<Row gutter={8}>
|
|
339
|
+
<Col span={6}>
|
|
340
|
+
<FormItem name={"zipCode"} noStyle>
|
|
341
|
+
<Input placeholder="우편번호" />
|
|
342
|
+
</FormItem>
|
|
343
|
+
</Col>
|
|
344
|
+
<Col span={4}>
|
|
345
|
+
<Button onClick={openAddressModal}>
|
|
346
|
+
주소검색
|
|
347
|
+
</Button>
|
|
348
|
+
</Col>
|
|
349
|
+
<Col span={14}>
|
|
350
|
+
<FormItem name={"address"} noStyle>
|
|
351
|
+
<Input placeholder="상세주소" />
|
|
352
|
+
</FormItem>
|
|
353
|
+
</Col>
|
|
354
|
+
</Row>
|
|
355
|
+
</FormItem>
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### 10. 커스텀 검증 (Custom Validation)
|
|
359
|
+
|
|
360
|
+
#### 10.1 비동기 검증
|
|
361
|
+
```tsx
|
|
362
|
+
<FormItem
|
|
363
|
+
name={"username"}
|
|
364
|
+
label={t("사용자명")}
|
|
365
|
+
rules={[
|
|
366
|
+
{ required: true },
|
|
367
|
+
{
|
|
368
|
+
validator: async (_, value) => {
|
|
369
|
+
if (value) {
|
|
370
|
+
const exists = await checkUsernameExists(value);
|
|
371
|
+
if (exists) {
|
|
372
|
+
return Promise.reject(new Error('이미 존재하는 사용자명입니다.'));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return Promise.resolve();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
]}
|
|
379
|
+
>
|
|
380
|
+
<Input />
|
|
381
|
+
</FormItem>
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
#### 10.2 다른 필드와의 비교 검증
|
|
385
|
+
```tsx
|
|
386
|
+
<FormItem
|
|
387
|
+
name={"confirmPassword"}
|
|
388
|
+
label={t("비밀번호 확인")}
|
|
389
|
+
dependencies={['password']}
|
|
390
|
+
rules={[
|
|
391
|
+
{ required: true },
|
|
392
|
+
({ getFieldValue }) => ({
|
|
393
|
+
validator(_, value) {
|
|
394
|
+
if (!value || getFieldValue('password') === value) {
|
|
395
|
+
return Promise.resolve();
|
|
396
|
+
}
|
|
397
|
+
return Promise.reject(new Error('비밀번호가 일치하지 않습니다.'));
|
|
398
|
+
},
|
|
399
|
+
})
|
|
400
|
+
]}
|
|
401
|
+
>
|
|
402
|
+
<Input.Password />
|
|
403
|
+
</FormItem>
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
## 고급 활용 패턴
|
|
407
|
+
|
|
408
|
+
### 1. 레이아웃과 스타일링
|
|
409
|
+
|
|
410
|
+
#### 1.1 반응형 레이아웃
|
|
411
|
+
```tsx
|
|
412
|
+
<Row gutter={16}>
|
|
413
|
+
<Col xs={24} sm={12} md={8}>
|
|
414
|
+
<FormItem name={"field1"} label={t("필드1")}>
|
|
415
|
+
<Input />
|
|
416
|
+
</FormItem>
|
|
417
|
+
</Col>
|
|
418
|
+
<Col xs={24} sm={12} md={8}>
|
|
419
|
+
<FormItem name={"field2"} label={t("필드2")}>
|
|
420
|
+
<Input />
|
|
421
|
+
</FormItem>
|
|
422
|
+
</Col>
|
|
423
|
+
<Col xs={24} sm={24} md={8}>
|
|
424
|
+
<FormItem name={"field3"} label={t("필드3")}>
|
|
425
|
+
<Input />
|
|
426
|
+
</FormItem>
|
|
427
|
+
</Col>
|
|
428
|
+
</Row>
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
#### 1.2 인라인 폼 항목
|
|
432
|
+
```tsx
|
|
433
|
+
<FormItem label={t("기간 설정")}>
|
|
434
|
+
<Flex gap="small" align="center">
|
|
435
|
+
<FormItem name={"startDate"} noStyle>
|
|
436
|
+
<DatePicker />
|
|
437
|
+
</FormItem>
|
|
438
|
+
<span>~</span>
|
|
439
|
+
<FormItem name={"endDate"} noStyle>
|
|
440
|
+
<DatePicker />
|
|
441
|
+
</FormItem>
|
|
442
|
+
</Flex>
|
|
443
|
+
</FormItem>
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### 2. 접근성과 UX 향상
|
|
447
|
+
|
|
448
|
+
#### 2.1 툴팁과 도움말
|
|
449
|
+
```tsx
|
|
450
|
+
<FormItem
|
|
451
|
+
name={"complexField"}
|
|
452
|
+
label={t("복잡한 필드")}
|
|
453
|
+
tooltip="이 필드는 특별한 규칙이 있습니다."
|
|
454
|
+
rules={[{ required: true }]}
|
|
455
|
+
>
|
|
456
|
+
<Input placeholder="규칙에 맞게 입력하세요" />
|
|
457
|
+
</FormItem>
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
#### 2.2 Extra 정보 제공
|
|
461
|
+
```tsx
|
|
462
|
+
<FormItem
|
|
463
|
+
name={"passwordField"}
|
|
464
|
+
label={t("비밀번호")}
|
|
465
|
+
extra="8자 이상, 영문/숫자/특수문자 포함"
|
|
466
|
+
rules={[
|
|
467
|
+
{ required: true },
|
|
468
|
+
{ min: 8, message: "8자 이상 입력하세요" }
|
|
469
|
+
]}
|
|
470
|
+
>
|
|
471
|
+
<Input.Password />
|
|
472
|
+
</FormItem>
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### 3. 성능 최적화
|
|
476
|
+
|
|
477
|
+
#### 3.1 preserve 속성 사용
|
|
478
|
+
```tsx
|
|
479
|
+
<FormItem
|
|
480
|
+
name={"preservedField"}
|
|
481
|
+
label={t("보존 필드")}
|
|
482
|
+
preserve={false} // 언마운트 시 값 제거
|
|
483
|
+
>
|
|
484
|
+
<Input />
|
|
485
|
+
</FormItem>
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
#### 3.2 dependencies 최적화
|
|
489
|
+
```tsx
|
|
490
|
+
<FormItem
|
|
491
|
+
name={"dependentField"}
|
|
492
|
+
label={t("의존 필드")}
|
|
493
|
+
dependencies={['triggerField']} // 특정 필드 변경시만 리렌더링
|
|
494
|
+
rules={[
|
|
495
|
+
({ getFieldValue }) => ({
|
|
496
|
+
validator(_, value) {
|
|
497
|
+
const trigger = getFieldValue('triggerField');
|
|
498
|
+
// 조건부 검증 로직
|
|
499
|
+
return Promise.resolve();
|
|
500
|
+
}
|
|
501
|
+
})
|
|
502
|
+
]}
|
|
503
|
+
>
|
|
504
|
+
<Input />
|
|
505
|
+
</FormItem>
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
## 프로젝트 실제 폼 검증 로직 (Real-world Validation Patterns)
|
|
509
|
+
|
|
510
|
+
프로젝트에서 실제 사용되는 다양한 폼 검증 패턴들을 케이스별로 정리했습니다.
|
|
511
|
+
|
|
512
|
+
### 1. 기본 검증 규칙들
|
|
513
|
+
|
|
514
|
+
#### 필수 입력 검증
|
|
515
|
+
```tsx
|
|
516
|
+
// 기본 필수 입력
|
|
517
|
+
<FormItem name="itemName" label={t("항목명")} rules={[{ required: true }]}>
|
|
518
|
+
<Input />
|
|
519
|
+
</FormItem>
|
|
520
|
+
|
|
521
|
+
// 커스텀 메시지가 있는 필수 입력
|
|
522
|
+
<FormItem
|
|
523
|
+
name="mngrMemoCtnts"
|
|
524
|
+
rules={[
|
|
525
|
+
{
|
|
526
|
+
required: true,
|
|
527
|
+
message: t("관리자 메모를 입력해주세요."),
|
|
528
|
+
}
|
|
529
|
+
]}
|
|
530
|
+
>
|
|
531
|
+
<Input.TextArea />
|
|
532
|
+
</FormItem>
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
#### 길이 제한 검증 (order.subscription.direct-rental/FormModal.tsx)
|
|
536
|
+
```tsx
|
|
537
|
+
<FormItem
|
|
538
|
+
name="mngrMemoCtnts"
|
|
539
|
+
rules={[
|
|
540
|
+
{
|
|
541
|
+
required: true,
|
|
542
|
+
message: t("관리자 메모를 입력해주세요."),
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
max: 2000,
|
|
546
|
+
message: t("관리자 메모는 최대 2000자까지 입력 가능합니다."),
|
|
547
|
+
},
|
|
548
|
+
]}
|
|
549
|
+
>
|
|
550
|
+
<Input.TextArea rows={3} maxLength={2000} placeholder="관리자 메모를 입력하세요..." />
|
|
551
|
+
</FormItem>
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
### 2. 비밀번호 검증 패턴
|
|
555
|
+
|
|
556
|
+
#### 비밀번호 복잡도 검증 (ResetPwdModal.tsx)
|
|
557
|
+
```tsx
|
|
558
|
+
<FormItem
|
|
559
|
+
name="usrPw"
|
|
560
|
+
label={t("새비밀번호")}
|
|
561
|
+
rules={[
|
|
562
|
+
{ required: true },
|
|
563
|
+
({}) => ({
|
|
564
|
+
validator(_, value) {
|
|
565
|
+
const checkList = checkPasswordValidation(
|
|
566
|
+
value,
|
|
567
|
+
form.getFieldsValue().usrLoginId,
|
|
568
|
+
form.getFieldsValue().usrHpNo,
|
|
569
|
+
);
|
|
570
|
+
if (checkList.length === 0) {
|
|
571
|
+
return Promise.resolve();
|
|
572
|
+
}
|
|
573
|
+
return Promise.reject(new Error(checkList[0].message));
|
|
574
|
+
},
|
|
575
|
+
}),
|
|
576
|
+
]}
|
|
577
|
+
hasFeedback
|
|
578
|
+
>
|
|
579
|
+
<Input.Password placeholder="영문자, 숫자, 특수문자를 조합하여 8자리 이상을 입력해 주세요" />
|
|
580
|
+
</FormItem>
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
#### 비밀번호 확인 검증 (ChangePasswordModal.tsx)
|
|
584
|
+
```tsx
|
|
585
|
+
<FormItem
|
|
586
|
+
name="usrPw2"
|
|
587
|
+
label={t("새비밀번호 확인")}
|
|
588
|
+
dependencies={["usrPw"]}
|
|
589
|
+
hasFeedback
|
|
590
|
+
rules={[
|
|
591
|
+
{ required: true },
|
|
592
|
+
({ getFieldValue }) => ({
|
|
593
|
+
validator(_, value) {
|
|
594
|
+
if (!value || getFieldValue("usrPw") === value) {
|
|
595
|
+
return Promise.resolve();
|
|
596
|
+
}
|
|
597
|
+
return Promise.reject(new Error("비밀번호가 일치하지 않습니다."));
|
|
598
|
+
},
|
|
599
|
+
}),
|
|
600
|
+
]}
|
|
601
|
+
>
|
|
602
|
+
<Input.Password placeholder="새비밀번호를 다시한번 입력해 주세요" />
|
|
603
|
+
</FormItem>
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
### 3. 날짜/시간 검증 패턴
|
|
607
|
+
|
|
608
|
+
#### 날짜 범위 검증 (benefit.live-deal/FormModal.tsx)
|
|
609
|
+
```tsx
|
|
610
|
+
<FormItem
|
|
611
|
+
name="dateRange"
|
|
612
|
+
style={{ margin: 0 }}
|
|
613
|
+
rules={[
|
|
614
|
+
{ required: true, message: "나비엔라이브 기간을 설정해주세요." },
|
|
615
|
+
({ getFieldValue: _ }) => ({
|
|
616
|
+
validator(__, value) {
|
|
617
|
+
if (value && value[0] && value[1]) {
|
|
618
|
+
const startDate = dayjs(value[0]);
|
|
619
|
+
const endDate = dayjs(value[1]);
|
|
620
|
+
const now = dayjs();
|
|
621
|
+
|
|
622
|
+
// 신규 등록 시 과거 날짜 체크
|
|
623
|
+
if (startDate.isBefore(now, "minute") && !params.query?.bnefNo) {
|
|
624
|
+
return Promise.reject(new Error("시작일시는 현재 시간 이후로 설정해주세요."));
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// 종료일이 시작일보다 늦어야 함
|
|
628
|
+
if (endDate.isBefore(startDate) || endDate.isSame(startDate)) {
|
|
629
|
+
return Promise.reject(new Error("종료일시는 시작일시보다 늦어야 합니다."));
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return Promise.resolve();
|
|
633
|
+
},
|
|
634
|
+
}),
|
|
635
|
+
]}
|
|
636
|
+
>
|
|
637
|
+
<DtPicker type={DtPickerType.DATE_RANGE} isTimeSelector allowClear />
|
|
638
|
+
</FormItem>
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### 4. 조건부 검증 패턴
|
|
642
|
+
|
|
643
|
+
#### 다른 필드 값에 따른 조건부 검증 (benefit.live-deal/FormModal.tsx)
|
|
644
|
+
```tsx
|
|
645
|
+
<FormItem
|
|
646
|
+
name="sleLmttStupYn"
|
|
647
|
+
rules={[
|
|
648
|
+
{ required: true },
|
|
649
|
+
{
|
|
650
|
+
validator: () => {
|
|
651
|
+
// 제품이 선택되지 않은 경우 검증 실패
|
|
652
|
+
if (products.length === 0) {
|
|
653
|
+
return Promise.reject(new Error(t("제품을 선택해주세요.")));
|
|
654
|
+
}
|
|
655
|
+
return Promise.resolve();
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
]}
|
|
659
|
+
style={{ margin: 0 }}
|
|
660
|
+
>
|
|
661
|
+
<Radio.Group
|
|
662
|
+
options={[
|
|
663
|
+
{ label: "미사용", value: "N" },
|
|
664
|
+
{ label: "사용", value: "Y" },
|
|
665
|
+
]}
|
|
666
|
+
/>
|
|
667
|
+
</FormItem>
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
#### Watch를 활용한 조건부 필수 검증
|
|
671
|
+
```tsx
|
|
672
|
+
const watchedValue = Form.useWatch("parentField", form);
|
|
673
|
+
|
|
674
|
+
<FormItem
|
|
675
|
+
name="childField"
|
|
676
|
+
label={t("자식 필드")}
|
|
677
|
+
rules={[{ required: watchedValue === "Y" }]}
|
|
678
|
+
>
|
|
679
|
+
<Input />
|
|
680
|
+
</FormItem>
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
### 5. 복합 검증 패턴
|
|
684
|
+
|
|
685
|
+
#### 배열/리스트 검증
|
|
686
|
+
```tsx
|
|
687
|
+
<FormItem
|
|
688
|
+
name="itemList"
|
|
689
|
+
rules={[
|
|
690
|
+
{
|
|
691
|
+
validator: (_, value) => {
|
|
692
|
+
if (!value || value.length === 0) {
|
|
693
|
+
return Promise.reject(new Error(t("최소 1개 이상 선택해주세요.")));
|
|
694
|
+
}
|
|
695
|
+
if (value.length > 10) {
|
|
696
|
+
return Promise.reject(new Error(t("최대 10개까지 선택 가능합니다.")));
|
|
697
|
+
}
|
|
698
|
+
return Promise.resolve();
|
|
699
|
+
},
|
|
700
|
+
},
|
|
701
|
+
]}
|
|
702
|
+
>
|
|
703
|
+
<Select mode="multiple" options={options} />
|
|
704
|
+
</FormItem>
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
#### 숫자 범위 검증
|
|
708
|
+
```tsx
|
|
709
|
+
<FormItem
|
|
710
|
+
name="quantity"
|
|
711
|
+
label={t("수량")}
|
|
712
|
+
rules={[
|
|
713
|
+
{ required: true },
|
|
714
|
+
{
|
|
715
|
+
type: "number",
|
|
716
|
+
min: 1,
|
|
717
|
+
max: 999,
|
|
718
|
+
message: t("1~999 사이의 숫자를 입력해주세요."),
|
|
719
|
+
},
|
|
720
|
+
]}
|
|
721
|
+
>
|
|
722
|
+
<InputNumber style={{ width: "100%" }} />
|
|
723
|
+
</FormItem>
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
### 6. 실시간 검증과 피드백
|
|
727
|
+
|
|
728
|
+
#### hasFeedback 속성 활용
|
|
729
|
+
```tsx
|
|
730
|
+
<FormItem
|
|
731
|
+
name="email"
|
|
732
|
+
label={t("이메일")}
|
|
733
|
+
rules={[
|
|
734
|
+
{ required: true },
|
|
735
|
+
{ type: "email", message: t("올바른 이메일 형식을 입력하세요.") },
|
|
736
|
+
]}
|
|
737
|
+
hasFeedback // 실시간 검증 결과 표시
|
|
738
|
+
>
|
|
739
|
+
<Input />
|
|
740
|
+
</FormItem>
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
#### 정규식 패턴 검증 (주석 처리된 예시)
|
|
744
|
+
```tsx
|
|
745
|
+
<FormItem
|
|
746
|
+
name="password"
|
|
747
|
+
rules={[
|
|
748
|
+
{ required: true },
|
|
749
|
+
// 정규식 패턴 예시 (실제 프로젝트에서는 커스텀 validator 사용)
|
|
750
|
+
// {
|
|
751
|
+
// pattern: /^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[!@#$%^&*()_+|~\-={}[\]:";'<>?,./\\]).{8,16}$/,
|
|
752
|
+
// message: "8~16자 영문+숫자+특수문자를 조합하여 사용하세요.",
|
|
753
|
+
// },
|
|
754
|
+
]}
|
|
755
|
+
>
|
|
756
|
+
<Input.Password />
|
|
757
|
+
</FormItem>
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
### 7. 에러 처리 및 사용자 경험
|
|
761
|
+
|
|
762
|
+
#### 검증 실패 시 첫 번째 필드로 스크롤
|
|
763
|
+
```tsx
|
|
764
|
+
<Form
|
|
765
|
+
form={form}
|
|
766
|
+
layout="vertical"
|
|
767
|
+
onFinish={handleSubmit}
|
|
768
|
+
scrollToFirstError // 검증 실패 시 첫 번째 오류 필드로 스크롤
|
|
769
|
+
>
|
|
770
|
+
{/* FormItem들 */}
|
|
771
|
+
</Form>
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
#### 동적 검증 메시지
|
|
775
|
+
```tsx
|
|
776
|
+
<FormItem
|
|
777
|
+
name="customField"
|
|
778
|
+
rules={[
|
|
779
|
+
{
|
|
780
|
+
validator: (_, value) => {
|
|
781
|
+
if (!value) {
|
|
782
|
+
return Promise.reject(new Error(t("필수 입력 항목입니다.")));
|
|
783
|
+
}
|
|
784
|
+
if (value.length < 3) {
|
|
785
|
+
return Promise.reject(new Error(t("최소 3자 이상 입력해주세요.")));
|
|
786
|
+
}
|
|
787
|
+
if (!/^[a-zA-Z0-9]+$/.test(value)) {
|
|
788
|
+
return Promise.reject(new Error(t("영문과 숫자만 입력 가능합니다.")));
|
|
789
|
+
}
|
|
790
|
+
return Promise.resolve();
|
|
791
|
+
},
|
|
792
|
+
},
|
|
793
|
+
]}
|
|
794
|
+
>
|
|
795
|
+
<Input />
|
|
796
|
+
</FormItem>
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
### 8. 검증 유틸리티 활용
|
|
800
|
+
|
|
801
|
+
프로젝트에서는 공통 검증 함수들을 활용합니다:
|
|
802
|
+
|
|
803
|
+
```tsx
|
|
804
|
+
import { checkPasswordValidation } from "utils/validation";
|
|
805
|
+
|
|
806
|
+
// 비밀번호 검증 함수 활용
|
|
807
|
+
<FormItem
|
|
808
|
+
name="password"
|
|
809
|
+
rules={[
|
|
810
|
+
{ required: true },
|
|
811
|
+
({}) => ({
|
|
812
|
+
validator(_, value) {
|
|
813
|
+
const checkList = checkPasswordValidation(value);
|
|
814
|
+
if (checkList.length === 0) {
|
|
815
|
+
return Promise.resolve();
|
|
816
|
+
}
|
|
817
|
+
return Promise.reject(new Error(checkList[0].message));
|
|
818
|
+
},
|
|
819
|
+
}),
|
|
820
|
+
]}
|
|
821
|
+
>
|
|
822
|
+
<Input.Password />
|
|
823
|
+
</FormItem>
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
### 9. 종합 검증 예시
|
|
827
|
+
|
|
828
|
+
실제 프로젝트에서 사용되는 복합적인 검증 패턴:
|
|
829
|
+
|
|
830
|
+
```tsx
|
|
831
|
+
<FormItem
|
|
832
|
+
name="complexField"
|
|
833
|
+
label={t("복합 검증 필드")}
|
|
834
|
+
dependencies={["relatedField"]}
|
|
835
|
+
rules={[
|
|
836
|
+
{ required: true, message: t("필수 입력 항목입니다.") },
|
|
837
|
+
{ max: 100, message: t("최대 100자까지 입력 가능합니다.") },
|
|
838
|
+
({ getFieldValue }) => ({
|
|
839
|
+
validator: async (_, value) => {
|
|
840
|
+
// 관련 필드 값 확인
|
|
841
|
+
const relatedValue = getFieldValue("relatedField");
|
|
842
|
+
|
|
843
|
+
// 조건부 검증
|
|
844
|
+
if (relatedValue === "special" && value.length < 10) {
|
|
845
|
+
return Promise.reject(new Error(t("특수 모드에서는 10자 이상 입력해야 합니다.")));
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// 비동기 검증 (서버 확인)
|
|
849
|
+
if (value) {
|
|
850
|
+
try {
|
|
851
|
+
const isValid = await validateFieldOnServer(value);
|
|
852
|
+
if (!isValid) {
|
|
853
|
+
return Promise.reject(new Error(t("서버 검증에 실패했습니다.")));
|
|
854
|
+
}
|
|
855
|
+
} catch (error) {
|
|
856
|
+
return Promise.reject(new Error(t("검증 중 오류가 발생했습니다.")));
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
return Promise.resolve();
|
|
861
|
+
},
|
|
862
|
+
}),
|
|
863
|
+
]}
|
|
864
|
+
hasFeedback
|
|
865
|
+
>
|
|
866
|
+
<Input />
|
|
867
|
+
</FormItem>
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
## 모범 사례 (Best Practices)
|
|
871
|
+
|
|
872
|
+
### 1. 타입 안전성 확보
|
|
873
|
+
- 항상 `const FormItem = Form.Item<DtoItem>` 패턴 사용
|
|
874
|
+
- 인터페이스 확장을 통한 타입 정의
|
|
875
|
+
|
|
876
|
+
### 2. 검증 규칙 일관성
|
|
877
|
+
- 공통 검증 규칙을 상수로 정의
|
|
878
|
+
- 메시지는 i18n을 통한 다국어 지원
|
|
879
|
+
- 프로젝트의 `checkPasswordValidation` 같은 공통 유틸리티 활용
|
|
880
|
+
|
|
881
|
+
### 3. 성능 고려사항
|
|
882
|
+
- shouldUpdate 사용 시 필요한 경우에만 적용
|
|
883
|
+
- dependencies를 정확히 명시하여 불필요한 리렌더링 방지
|
|
884
|
+
|
|
885
|
+
### 4. 접근성 향상
|
|
886
|
+
- label, placeholder 적절히 활용
|
|
887
|
+
- 필수 필드 명시적 표현
|
|
888
|
+
- 에러 메시지 명확하게 제공
|
|
889
|
+
- `hasFeedback` 속성으로 실시간 피드백 제공
|
|
890
|
+
|
|
891
|
+
### 5. 검증 패턴별 활용 가이드
|
|
892
|
+
- **기본 검증**: required, max, min, type 등 기본 규칙 활용
|
|
893
|
+
- **비밀번호**: dependencies와 getFieldValue를 활용한 확인 필드 구현
|
|
894
|
+
- **날짜**: dayjs를 활용한 시간 비교 로직
|
|
895
|
+
- **조건부**: 다른 상태나 필드에 따른 동적 검증
|
|
896
|
+
- **복합**: 여러 조건을 조합한 복잡한 검증 로직
|
|
897
|
+
|
|
898
|
+
### 6. 코드 재사용성
|
|
899
|
+
- 공통 FormItem 패턴을 컴포넌트화
|
|
900
|
+
- 비슷한 검증 로직의 경우 훅으로 분리
|
|
901
|
+
- 검증 유틸리티 함수 활용
|
|
902
|
+
|
|
903
|
+
이러한 검증 패턴들을 조합하여 사용자에게 명확한 피드백을 제공하고, 데이터 무결성을 보장할 수 있습니다.
|