@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,2570 @@
|
|
|
1
|
+
# SearchParams 대화형 생성 툴 계획
|
|
2
|
+
|
|
3
|
+
> **⚠️ MCP 구현 시 필독**
|
|
4
|
+
>
|
|
5
|
+
> 이 문서는 **SearchParams 대화형 생성 MCP**를 구현하기 위한 설계서입니다.
|
|
6
|
+
>
|
|
7
|
+
> **핵심 컨셉**: 이 MD는 AI를 위한 "매뉴얼 + 설계서"입니다.
|
|
8
|
+
> - 사용자가 어떤 입력을 하면
|
|
9
|
+
> - AI가 어떤 질문을 해야 하고
|
|
10
|
+
> - 그 답변을 어떻게 코드로 변환할지가 정의되어 있어야 합니다.
|
|
11
|
+
>
|
|
12
|
+
> **목표**: 자연어로 대화하면서 사용자 의도에 최대한 맞는 코드가 나오도록 설계
|
|
13
|
+
|
|
14
|
+
## 개요
|
|
15
|
+
|
|
16
|
+
검색 폼(SearchParams)을 대화형으로 생성합니다.
|
|
17
|
+
|
|
18
|
+
## 입력
|
|
19
|
+
|
|
20
|
+
- `repositoryPath`: Repository 파일 경로 (절대 경로)
|
|
21
|
+
- `dtoPath`: DTO 파일 경로 (선택, 없으면 자동 검색)
|
|
22
|
+
|
|
23
|
+
## 처리 흐름
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
1. Repository의 List API 메서드 찾기
|
|
27
|
+
2. Request 타입 분석 → 필드 목록 추출
|
|
28
|
+
3. 연동 설정 (선택)
|
|
29
|
+
4. 필드별로 패턴 선택 (대화형)
|
|
30
|
+
5. 그룹핑 설정
|
|
31
|
+
6. 기본값 설정
|
|
32
|
+
7. SearchParams JSX 코드 생성
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 필드별 패턴 선택
|
|
38
|
+
|
|
39
|
+
### 패턴 카탈로그 (NH 프로젝트 실사용 패턴 기반)
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
[MCP] usrNm (회원명) 필드를 발견했습니다.
|
|
43
|
+
어떤 검색 컴포넌트를 사용할까요?
|
|
44
|
+
|
|
45
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
46
|
+
│ 1. 🤖 AI에게 맡기기 (자연어로 설명) │
|
|
47
|
+
├─────────────────────────────────────────────────────────────────────┤
|
|
48
|
+
│ 2. 📝 일반 텍스트 입력 (INPUT) │
|
|
49
|
+
│ 예: 회원명, 이메일, 전화번호 검색 │
|
|
50
|
+
├─────────────────────────────────────────────────────────────────────┤
|
|
51
|
+
│ 3. 🔍 검색구분 + 텍스트 (GROUP) │
|
|
52
|
+
│ 예: 제목/내용 검색, 이름/ID 검색 │
|
|
53
|
+
├─────────────────────────────────────────────────────────────────────┤
|
|
54
|
+
│ 4. 📋 드롭다운 선택 (SELECT) │
|
|
55
|
+
│ 예: 상태코드, 카테고리, 주문유형 │
|
|
56
|
+
├─────────────────────────────────────────────────────────────────────┤
|
|
57
|
+
│ 5. ✓ 체크박스 다중 선택 (CHECKBOX) │
|
|
58
|
+
│ 예: 회원가입채널, 관심사 │
|
|
59
|
+
├─────────────────────────────────────────────────────────────────────┤
|
|
60
|
+
│ 6. ☐ 멀티 셀렉트 (SELECT_MULTI) │
|
|
61
|
+
│ 예: 주문상태, 배송상태 (체크박스 대용) │
|
|
62
|
+
├─────────────────────────────────────────────────────────────────────┤
|
|
63
|
+
│ 7. ○ 라디오 버튼 (RADIO) │
|
|
64
|
+
│ 예: 사용여부(Y/N), 이메일수신동의, 답변상태 │
|
|
65
|
+
├─────────────────────────────────────────────────────────────────────┤
|
|
66
|
+
│ 8. 📅 단일 날짜 (DTPICKER_DATE) │
|
|
67
|
+
│ 예: 등록일, 수정일 │
|
|
68
|
+
├─────────────────────────────────────────────────────────────────────┤
|
|
69
|
+
│ 9. 📆 날짜 범위 (DTPICKER_DATE_RANGE) │
|
|
70
|
+
│ 예: 가입기간, 주문기간, 기간검색 │
|
|
71
|
+
├─────────────────────────────────────────────────────────────────────┤
|
|
72
|
+
│ 10. 🔢 숫자 범위 (NUMBER_RANGE) │
|
|
73
|
+
│ 예: 방문회수, 구매금액, 나이 │
|
|
74
|
+
├─────────────────────────────────────────────────────────────────────┤
|
|
75
|
+
│ 11. ⏭️ 스킵 (SKIP) │
|
|
76
|
+
│ 이 필드는 SearchParams에 포함하지 않음 │
|
|
77
|
+
├─────────────────────────────────────────────────────────────────────┤
|
|
78
|
+
│ 12. 👁️ 숨김 (HIDDEN) │
|
|
79
|
+
│ 필드는 존재하지만 UI에는 표시하지 않음 │
|
|
80
|
+
├─────────────────────────────────────────────────────────────────────┤
|
|
81
|
+
│ 13. 🔀 조건부 필드 (CONDITIONAL) │
|
|
82
|
+
│ 특정 필드값에 따라 동적으로 표시되는 필드 │
|
|
83
|
+
│ 예: 리뷰유형이 '포토'일 때만 상세옵션 표시 │
|
|
84
|
+
├─────────────────────────────────────────────────────────────────────┤
|
|
85
|
+
│ 14. 🔍 자동완성 (AUTOCOMPLETE) │
|
|
86
|
+
│ 키워드 입력 시 실시간 API 호출로 검색 │
|
|
87
|
+
│ 예: 고객사 검색, 제품 검색 │
|
|
88
|
+
├─────────────────────────────────────────────────────────────────────┤
|
|
89
|
+
│ 15. 🌊 계층 선택 (CASCADE) │
|
|
90
|
+
│ 계층적 구조에서 단계별 선택 (조직 > 팀 > 파트) │
|
|
91
|
+
│ 예: 조직도, 카테고리 > 하위카테고리 │
|
|
92
|
+
├─────────────────────────────────────────────────────────────────────┤
|
|
93
|
+
│ 16. 🪟 모달 검색 (MODAL) │
|
|
94
|
+
│ 버튼 클릭 시 모달 오픈하여 선택 │
|
|
95
|
+
│ 예: 고객사 검색, 영업사원 검색 │
|
|
96
|
+
└─────────────────────────────────────────────────────────────────────┘
|
|
97
|
+
|
|
98
|
+
번호를 입력하세요:
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
### 13번 조건부 필드 (CONDITIONAL) 상세
|
|
104
|
+
|
|
105
|
+
다른 필드의 값에 따라 동적으로 표시되는 필드입니다.
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
// product.review/App.tsx 예시
|
|
109
|
+
params={[
|
|
110
|
+
{
|
|
111
|
+
label: t("리뷰유형"),
|
|
112
|
+
type: SearchParamType.SELECT,
|
|
113
|
+
name: "prdBbsRvwTpcd",
|
|
114
|
+
options: [{ label: t("전체"), value: "" }, ...(PRD_BBS_RVW_TPCD?.options ?? [])],
|
|
115
|
+
},
|
|
116
|
+
// prdBbsRvwTpcd가 "1"일 때만 표시
|
|
117
|
+
...(listRequestValue.prdBbsRvwTpcd === "1"
|
|
118
|
+
? [
|
|
119
|
+
{
|
|
120
|
+
label: t(" "),
|
|
121
|
+
name: "prdBbsRvwDtlTpcdArray",
|
|
122
|
+
type: SearchParamType.CHECKBOX,
|
|
123
|
+
width: 120,
|
|
124
|
+
options: PRD_BBS_RVW_DTL_TPCD?.options,
|
|
125
|
+
},
|
|
126
|
+
]
|
|
127
|
+
: []),
|
|
128
|
+
]}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
#### 대화형 설정 흐름
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
[MCP] CONDITIONAL을 선택했습니다.
|
|
135
|
+
이 필드는 조건부로 표시됩니다.
|
|
136
|
+
|
|
137
|
+
1️⃣ 조건 필드 선택
|
|
138
|
+
어느 필드의 값에 따라 표시할까요?
|
|
139
|
+
(이미 정의된 필드 목록에서 선택)
|
|
140
|
+
|
|
141
|
+
┌─────────────────────────────────────────────┐
|
|
142
|
+
│ 1. usrNm (회원명) │
|
|
143
|
+
│ 2. prdBbsRvwTpcd (리뷰유형) │
|
|
144
|
+
│ 3. useYn (사용여부) │
|
|
145
|
+
│ 4. statusCd (상태) │
|
|
146
|
+
└─────────────────────────────────────────────┘
|
|
147
|
+
|
|
148
|
+
사용자: 2
|
|
149
|
+
|
|
150
|
+
[MCP] 'prdBbsRvwTpcd' 필드를 조건 필드로 선택했습니다.
|
|
151
|
+
|
|
152
|
+
2️⃣ 이 필드의 실제 타입 선택
|
|
153
|
+
이 필드의 실제 타입을 선택하세요:
|
|
154
|
+
|
|
155
|
+
┌─────────────────────────────────────────────┐
|
|
156
|
+
│ 2. INPUT 3. GROUP 4. SELECT │
|
|
157
|
+
│ 5. CHECKBOX 6. MULTI 7. RADIO │
|
|
158
|
+
│ 8. DATE 9. DATE_RANGE 10. NUMBER_RANGE │
|
|
159
|
+
└─────────────────────────────────────────────┘
|
|
160
|
+
|
|
161
|
+
사용자: 5
|
|
162
|
+
|
|
163
|
+
[MCP] CHECKBOX를 선택했습니다.
|
|
164
|
+
prdBbsRvwDtlTpcdArray (상세옵션) 필드에 대해 추가 설정을 진행합니다.
|
|
165
|
+
|
|
166
|
+
[CHECKBOX 타입의 일반 설정 진행...]
|
|
167
|
+
- 옵션 소스 선택 (Code Store / 직접 입력)
|
|
168
|
+
- 코드 변수명 입력
|
|
169
|
+
- 전체 옵션 포함 여부
|
|
170
|
+
- 기본값 설정
|
|
171
|
+
|
|
172
|
+
3️⃣ 조건을 자연어로 입력
|
|
173
|
+
조건을 자연어로 설명해주세요.
|
|
174
|
+
AI가 조건식을 생성합니다.
|
|
175
|
+
|
|
176
|
+
사용자: prdBbsRvwTpcd가 1일 때만 표시해줘
|
|
177
|
+
|
|
178
|
+
[MCP] 다음 조건식으로 생성합니다:
|
|
179
|
+
|
|
180
|
+
...(listRequestValue.prdBbsRvwTpcd === "1" ? [{
|
|
181
|
+
label: t('상세옵션'),
|
|
182
|
+
name: 'prdBbsRvwDtlTpcdArray',
|
|
183
|
+
type: SearchParamType.CHECKBOX,
|
|
184
|
+
width: 120,
|
|
185
|
+
options: PRD_BBS_RVW_DTL_TPCD?.options ?? [],
|
|
186
|
+
checkAllItem: true,
|
|
187
|
+
}] : [])
|
|
188
|
+
|
|
189
|
+
확인되나요? (Y/n)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
#### 자연어 조건 예시
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
// 단일 값 조건
|
|
196
|
+
"prdBbsRvwTpcd가 1일 때만 표시"
|
|
197
|
+
→ listRequestValue.prdBbsRvwTpcd === "1"
|
|
198
|
+
|
|
199
|
+
// 다중 값 조건
|
|
200
|
+
"prdBbsRvwTpcd가 1 또는 2일 때 표시"
|
|
201
|
+
→ ["1", "2"].includes(listRequestValue.prdBbsRvwTpcd)
|
|
202
|
+
|
|
203
|
+
// 참/거짓 조건
|
|
204
|
+
"useYn이 Y일 때만 표시"
|
|
205
|
+
→ listRequestValue.useYn === "Y"
|
|
206
|
+
|
|
207
|
+
// 복합 조건
|
|
208
|
+
"prdBbsRvwTpcd가 1이고 useYn이 Y일 때만 표시"
|
|
209
|
+
→ listRequestValue.prdBbsRvwTpcd === "1" && listRequestValue.useYn === "Y"
|
|
210
|
+
|
|
211
|
+
// 존재 여부
|
|
212
|
+
"searchText가 있을 때만 표시"
|
|
213
|
+
→ !!listRequestValue.searchText
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
### 14번 자동완성 (AUTOCOMPLETE) 상세
|
|
219
|
+
|
|
220
|
+
키워드 입력 시 실시간으로 API를 호출하여 검색 결과를 표시하는 컴포넌트입니다.
|
|
221
|
+
|
|
222
|
+
```tsx
|
|
223
|
+
// /wefor-sales-server/front/src/pages/sales/pipeline/App.tsx 예시
|
|
224
|
+
{
|
|
225
|
+
placeholder: t("고객사"),
|
|
226
|
+
name: "custDto",
|
|
227
|
+
type: SearchParamType.AUTOCOMPLETE,
|
|
228
|
+
onSearch: async (filter?: string) => {
|
|
229
|
+
const { ds } = await CustomerService.getCustomerList({
|
|
230
|
+
filter, // ← 입력한 키워드
|
|
231
|
+
filterCond: "CONTAINS",
|
|
232
|
+
pageSize: 100,
|
|
233
|
+
});
|
|
234
|
+
return ds.map((n) => ({
|
|
235
|
+
key: n.id ?? "",
|
|
236
|
+
value: { custId: n.id, custNm: n.custNm, inputValue: n.custNm },
|
|
237
|
+
label: n.custNm + ` (${bizNumFormatter(bizNumParser(n.bizNum))})`,
|
|
238
|
+
}));
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
#### 특징
|
|
244
|
+
|
|
245
|
+
| 항목 | 설명 |
|
|
246
|
+
|------|------|
|
|
247
|
+
| **onSearch** | 키워드 입력 시 호출되는 함수 |
|
|
248
|
+
| **filter** | 사용자가 입력한 키워드 |
|
|
249
|
+
| **반환값** | `{ key, value, label }` 형태의 배열 |
|
|
250
|
+
| **value** | 객체 저장 가능 (예: `{ custId, custNm }`) |
|
|
251
|
+
|
|
252
|
+
#### 대화형 설정 흐름
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
[MCP] AUTOCOMPLETE를 선택했습니다.
|
|
256
|
+
키워드 입력 시 실시간 검색을 수행합니다.
|
|
257
|
+
|
|
258
|
+
1️⃣ 검색 API 서비스 선택
|
|
259
|
+
검색에 사용할 API 서비스를 입력하세요:
|
|
260
|
+
|
|
261
|
+
사용자: CustomerService.getCustomerList
|
|
262
|
+
|
|
263
|
+
2️⃣ 검색 필드 설정
|
|
264
|
+
검색할 필드명을 입력하세요 (엔터: filter):
|
|
265
|
+
|
|
266
|
+
사용자: custNm
|
|
267
|
+
|
|
268
|
+
3️⃣ 필터 조건 설정 (선택)
|
|
269
|
+
필터 조건을 선택하세요:
|
|
270
|
+
1. CONTAINS (포함)
|
|
271
|
+
2. STARTS_WITH (시작)
|
|
272
|
+
3. ENDS_WITH (종료)
|
|
273
|
+
4. EQUAL (일치)
|
|
274
|
+
|
|
275
|
+
사용자: 1
|
|
276
|
+
|
|
277
|
+
4️⃣ 표시 라벨 설정
|
|
278
|
+
결과에 표시할 라벨 형식을 입력하세요.
|
|
279
|
+
변수: {custNm}, {bizNum}, {custOwner} 등
|
|
280
|
+
|
|
281
|
+
사용자: {custNm} ({bizNum})
|
|
282
|
+
|
|
283
|
+
5️⃣ 저장 값 설정
|
|
284
|
+
결과 값으로 저장할 필드를 선택하세요:
|
|
285
|
+
1. id만 저장
|
|
286
|
+
2. 전체 객체 저장
|
|
287
|
+
|
|
288
|
+
사용자: 2
|
|
289
|
+
|
|
290
|
+
생성될 코드:
|
|
291
|
+
{
|
|
292
|
+
placeholder: t('고객사'),
|
|
293
|
+
name: 'custDto',
|
|
294
|
+
type: SearchParamType.AUTOCOMPLETE,
|
|
295
|
+
onSearch: async (filter?: string) => {
|
|
296
|
+
const { ds } = await CustomerService.getCustomerList({
|
|
297
|
+
filter,
|
|
298
|
+
filterCond: 'CONTAINS',
|
|
299
|
+
pageSize: 100,
|
|
300
|
+
});
|
|
301
|
+
return ds.map((n) => ({
|
|
302
|
+
key: n.id ?? '',
|
|
303
|
+
value: n, // 전체 객체 저장
|
|
304
|
+
label: `${n.custNm} (${bizNumFormatter(n.bizNum)})`,
|
|
305
|
+
}));
|
|
306
|
+
},
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
### 15번 계층 선택 (CASCADE) 상세
|
|
313
|
+
|
|
314
|
+
계층적 구조에서 단계별로 선택하는 컴포넌트입니다. (예: 조직 > 팀 > 파트)
|
|
315
|
+
|
|
316
|
+
```tsx
|
|
317
|
+
// /wefor-sales-server/front/src/pages/sales/pipeline/App.tsx 예시
|
|
318
|
+
const { list: organizationOption } = useOrganizationCascadeData();
|
|
319
|
+
|
|
320
|
+
{
|
|
321
|
+
placeholder: t("조직"),
|
|
322
|
+
name: "_orgCd",
|
|
323
|
+
width: 200,
|
|
324
|
+
type: SearchParamType.CASCADE,
|
|
325
|
+
options: organizationOption as SearchParamOption[],
|
|
326
|
+
displayRender: (labels: string[]) => labels[labels.length - 1], // 마지막 레벨만 표시
|
|
327
|
+
onSelect: (option) => {
|
|
328
|
+
setUserComboParams({ ...option }); // 선택 시 다른 필드 업데이트
|
|
329
|
+
},
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
#### 특징
|
|
334
|
+
|
|
335
|
+
| 항목 | 설명 |
|
|
336
|
+
|------|------|
|
|
337
|
+
| **options** | 계층적 구조의 옵션 (children 포함) |
|
|
338
|
+
| **displayRender** | 표시할 라벨 커스터마이징 |
|
|
339
|
+
| **onSelect** | 선택 시 콜백 (다른 필드 연동) |
|
|
340
|
+
|
|
341
|
+
#### 계층 옵션 구조
|
|
342
|
+
|
|
343
|
+
```tsx
|
|
344
|
+
// useOrganizationCascadeData 훅이 반환하는 옵션 구조
|
|
345
|
+
[
|
|
346
|
+
{
|
|
347
|
+
value: "ORG001",
|
|
348
|
+
label: "본사",
|
|
349
|
+
children: [
|
|
350
|
+
{
|
|
351
|
+
value: "TEAM001",
|
|
352
|
+
label: "영업팀",
|
|
353
|
+
children: [
|
|
354
|
+
{ value: "PART001", label: "1파트" },
|
|
355
|
+
{ value: "PART002", label: "2파트" },
|
|
356
|
+
],
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
},
|
|
360
|
+
]
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
#### 대화형 설정 흐름
|
|
364
|
+
|
|
365
|
+
```
|
|
366
|
+
[MCP] CASCADE를 선택했습니다.
|
|
367
|
+
계층적 선택 컴포넌트를 생성합니다.
|
|
368
|
+
|
|
369
|
+
1️⃣ 계층 데이터 훅 선택
|
|
370
|
+
계층 데이터를 가져올 훅을 입력하세요:
|
|
371
|
+
|
|
372
|
+
사용자: useOrganizationCascadeData
|
|
373
|
+
|
|
374
|
+
2️⃣ 표시 레벨 설정
|
|
375
|
+
선택된 경로 중 어떤 레벨을 표시할까요?
|
|
376
|
+
1. 전체 경로 (본사 > 영업팀 > 1파트)
|
|
377
|
+
2. 마지막 레벨만 (1파트)
|
|
378
|
+
3. 첫 번째 레벨만 (본사)
|
|
379
|
+
|
|
380
|
+
사용자: 2
|
|
381
|
+
|
|
382
|
+
3️⃣ 연동 필드 설정 (선택)
|
|
383
|
+
선택 시 다른 필드를 업데이트할까요?
|
|
384
|
+
(이미 정의된 필드 목록, 엔터: 없음)
|
|
385
|
+
|
|
386
|
+
┌─────────────────────────────────────────────┐
|
|
387
|
+
│ 1. salesUserCd (영업사원) │
|
|
388
|
+
│ 2. someOtherField │
|
|
389
|
+
└─────────────────────────────────────────────┘
|
|
390
|
+
|
|
391
|
+
사용자: 1
|
|
392
|
+
|
|
393
|
+
[MCP] 조직 선택 시 salesUserCd 필드를 업데이트합니다.
|
|
394
|
+
|
|
395
|
+
생성될 코드:
|
|
396
|
+
// API 훅
|
|
397
|
+
const { list: organizationOption } = useOrganizationCascadeData();
|
|
398
|
+
const [userComboParams, setUserComboParams] = useState({});
|
|
399
|
+
|
|
400
|
+
// SearchParams
|
|
401
|
+
{
|
|
402
|
+
placeholder: t('조직'),
|
|
403
|
+
name: '_orgCd',
|
|
404
|
+
width: 200,
|
|
405
|
+
type: SearchParamType.CASCADE,
|
|
406
|
+
options: organizationOption,
|
|
407
|
+
displayRender: (labels: string[]) => labels[labels.length - 1],
|
|
408
|
+
onSelect: (option) => {
|
|
409
|
+
setUserComboParams({ ...option });
|
|
410
|
+
},
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// 연동 필드
|
|
414
|
+
{
|
|
415
|
+
placeholder: t('영업사원'),
|
|
416
|
+
name: 'salesUserCd',
|
|
417
|
+
type: SearchParamType.SELECT,
|
|
418
|
+
options: userComboList, // onSelect로 업데이트됨
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
#### 계층 데이터 전송
|
|
423
|
+
|
|
424
|
+
```tsx
|
|
425
|
+
// API 호출 시 계층 값을 처리하는 유틸리티
|
|
426
|
+
import { collectCascadeValue } from 'utils/collectOptions';
|
|
427
|
+
|
|
428
|
+
const apiParams = {
|
|
429
|
+
...
|
|
430
|
+
orgCd: collectCascadeValue(listRequestValue._orgCd),
|
|
431
|
+
// _orgCd: ["ORG001", "TEAM001", "PART001"]
|
|
432
|
+
// → orgCd: "PART001" (마지막 값만 전송)
|
|
433
|
+
// 또는 "ORG001>TEAM001>PART001" (전체 경로)
|
|
434
|
+
};
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
### 16번 모달 검색 (MODAL) 상세
|
|
440
|
+
|
|
441
|
+
버튼 클릭 시 모달을 열어서 선택하는 컴포넌트입니다.
|
|
442
|
+
|
|
443
|
+
```tsx
|
|
444
|
+
// /wefor-sales-server/front/src/pages/sales/estimate/App.tsx 예시
|
|
445
|
+
import { openCustomerModal } from "modals/customer/CustomerModal";
|
|
446
|
+
import { openEmployeeModal } from "modals/employee/EmployeeModal";
|
|
447
|
+
|
|
448
|
+
// 고객사 선택 (단일)
|
|
449
|
+
{
|
|
450
|
+
placeholder: t("고객사코드"),
|
|
451
|
+
name: "custId",
|
|
452
|
+
type: SearchParamType.MODAL,
|
|
453
|
+
onSearch: async () => {
|
|
454
|
+
const data = await openCustomerModal();
|
|
455
|
+
return [{
|
|
456
|
+
label: data.customer?.custNm,
|
|
457
|
+
value: JSON.stringify({
|
|
458
|
+
custId: data.customer?.id,
|
|
459
|
+
custNm: data.customer?.custNm,
|
|
460
|
+
}),
|
|
461
|
+
}];
|
|
462
|
+
},
|
|
463
|
+
config: {
|
|
464
|
+
single: true, // 단일 선택
|
|
465
|
+
},
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// 영업사원 선택 (단일)
|
|
469
|
+
{
|
|
470
|
+
placeholder: t("영업사원"),
|
|
471
|
+
name: "salesUserCd",
|
|
472
|
+
type: SearchParamType.MODAL,
|
|
473
|
+
onSearch: async () => {
|
|
474
|
+
const data = await openEmployeeModal();
|
|
475
|
+
return [{
|
|
476
|
+
label: data.user?.userNm,
|
|
477
|
+
value: JSON.stringify({
|
|
478
|
+
salesUserCd: data.user?.userCd,
|
|
479
|
+
salesUserNm: data.user?.userNm,
|
|
480
|
+
}),
|
|
481
|
+
}];
|
|
482
|
+
},
|
|
483
|
+
config: {
|
|
484
|
+
single: true,
|
|
485
|
+
},
|
|
486
|
+
width: 150,
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
#### 특징
|
|
491
|
+
|
|
492
|
+
| 항목 | 설명 |
|
|
493
|
+
|------|------|
|
|
494
|
+
| **onSearch** | 버튼 클릭 시 호출되는 함수 (모달 오픈) |
|
|
495
|
+
| **modal 함수** | `openXxxModal()` 형태의 함수 |
|
|
496
|
+
| **반환값** | `[{ label, value }]` 형태의 배열 |
|
|
497
|
+
| **value** | JSON 문자열로 객체 저장 |
|
|
498
|
+
| **config.single** | 단일 선택 여부 (`true`: 단일, `false`/없음: 다중) |
|
|
499
|
+
|
|
500
|
+
#### 대화형 설정 흐름
|
|
501
|
+
|
|
502
|
+
```
|
|
503
|
+
[MCP] MODAL(모달형 검색)을 선택했습니다.
|
|
504
|
+
버튼 클릭 시 모달을 열어서 선택합니다.
|
|
505
|
+
|
|
506
|
+
1️⃣ 모달 함수 선택
|
|
507
|
+
사용할 모달 오픈 함수를 입력하세요:
|
|
508
|
+
예: openCustomerModal, openEmployeeModal, openProductModal
|
|
509
|
+
|
|
510
|
+
사용자: openCustomerModal
|
|
511
|
+
|
|
512
|
+
2️⃣ 표시 라벨 설정
|
|
513
|
+
모달에서 선택한 결과를 어떻게 표시할까요?
|
|
514
|
+
사용 가능 변수: 모달이 반환하는 데이터의 필드들
|
|
515
|
+
예: {custNm}, {bizNum}, {custOwner} 등
|
|
516
|
+
|
|
517
|
+
형식: {필드명} 또는 조합 가능
|
|
518
|
+
예: {custNm} ({bizNum})
|
|
519
|
+
|
|
520
|
+
사용자: {custNm}
|
|
521
|
+
|
|
522
|
+
3️⃣ 저장 값 설정
|
|
523
|
+
어떤 값을 저장할까요?
|
|
524
|
+
1. id만 저장 (단일 값)
|
|
525
|
+
2. 전체 객체를 JSON으로 저장
|
|
526
|
+
|
|
527
|
+
사용자: 2
|
|
528
|
+
|
|
529
|
+
[MCP] 저장할 필드를 선택하세요:
|
|
530
|
+
쉼표로 구분하여 입력하세요.
|
|
531
|
+
예: custId, custNm, bizNum
|
|
532
|
+
|
|
533
|
+
사용자: custId, custNm
|
|
534
|
+
|
|
535
|
+
4️⃣ 단일/다중 선택
|
|
536
|
+
단일 선택인가요? (Y/n):
|
|
537
|
+
|
|
538
|
+
사용자: y
|
|
539
|
+
|
|
540
|
+
5️⃣ 너비 설정 (선택)
|
|
541
|
+
너비를 입력하세요 (엔터: 기본값 200):
|
|
542
|
+
|
|
543
|
+
사용자: 150
|
|
544
|
+
|
|
545
|
+
생성될 코드:
|
|
546
|
+
{
|
|
547
|
+
placeholder: t('고객사코드'),
|
|
548
|
+
name: 'custId',
|
|
549
|
+
type: SearchParamType.MODAL,
|
|
550
|
+
onSearch: async () => {
|
|
551
|
+
const data = await openCustomerModal();
|
|
552
|
+
return [{
|
|
553
|
+
label: data.customer?.custNm,
|
|
554
|
+
value: JSON.stringify({
|
|
555
|
+
custId: data.customer?.id,
|
|
556
|
+
custNm: data.customer?.custNm,
|
|
557
|
+
}),
|
|
558
|
+
}];
|
|
559
|
+
},
|
|
560
|
+
config: {
|
|
561
|
+
single: true,
|
|
562
|
+
},
|
|
563
|
+
width: 150,
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
#### 다중 선택 예시
|
|
568
|
+
|
|
569
|
+
```
|
|
570
|
+
사용자: n (다중 선택)
|
|
571
|
+
|
|
572
|
+
[MCP] config.single을 설정하지 않습니다 (다중 선택).
|
|
573
|
+
|
|
574
|
+
생성될 코드:
|
|
575
|
+
{
|
|
576
|
+
placeholder: t('고객사'),
|
|
577
|
+
name: 'custId',
|
|
578
|
+
type: SearchParamType.MODAL,
|
|
579
|
+
onSearch: async () => {
|
|
580
|
+
const data = await openCustomerModal();
|
|
581
|
+
return data.customers.map((c) => ({
|
|
582
|
+
label: c.custNm,
|
|
583
|
+
value: JSON.stringify({
|
|
584
|
+
custId: c.id,
|
|
585
|
+
custNm: c.custNm,
|
|
586
|
+
}),
|
|
587
|
+
}));
|
|
588
|
+
},
|
|
589
|
+
// config: { single: true } 없음 → 다중 선택
|
|
590
|
+
}
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
#### 저장 값 처리
|
|
594
|
+
|
|
595
|
+
```tsx
|
|
596
|
+
// API 호출 시 JSON 문자열을 파싱
|
|
597
|
+
const custId = listRequestValue.custId;
|
|
598
|
+
|
|
599
|
+
// 단일 선택
|
|
600
|
+
if (custId) {
|
|
601
|
+
const parsed = JSON.parse(custId);
|
|
602
|
+
// parsed: { custId: 123, custNm: "ABC고객사" }
|
|
603
|
+
apiParams.custId = parsed.custId;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// 다중 선택
|
|
607
|
+
if (custId && Array.isArray(custId)) {
|
|
608
|
+
const parsedList = custId.map((c) => JSON.parse(c));
|
|
609
|
+
// parsedList: [{ custId: 123, custNm: "A" }, { custId: 456, custNm: "B" }]
|
|
610
|
+
apiParams.custIds = parsedList.map((p) => p.custId);
|
|
611
|
+
}
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
### 1번 선택 시 (AI에게 맡기기)
|
|
617
|
+
|
|
618
|
+
```
|
|
619
|
+
사용자: "이름으로 검색하는데, placeholder는 '이름을 입력하세요'로 해주고"
|
|
620
|
+
|
|
621
|
+
[MCP] "알겠습니다. 검색어 INPUT으로 생성합니다.
|
|
622
|
+
placeholder는 '이름을 입력하세요'로 설정합니다.
|
|
623
|
+
|
|
624
|
+
다음과 같이 생성됩니다:
|
|
625
|
+
|
|
626
|
+
{
|
|
627
|
+
label: t('회원명'),
|
|
628
|
+
name: 'usrNm',
|
|
629
|
+
type: SearchParamType.INPUT,
|
|
630
|
+
placeholder: t('이름을 입력하세요'),
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
확인되나요? (y/n)"
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
### 자연어 설명 예시
|
|
637
|
+
|
|
638
|
+
```
|
|
639
|
+
사용자: "이메일로 검색하고, 이메일 형식 검증도 추가해줘"
|
|
640
|
+
|
|
641
|
+
[MCP] "검색어 INPUT으로 생성합니다.
|
|
642
|
+
이메일 형식 검증 rules를 추가합니다.
|
|
643
|
+
|
|
644
|
+
{
|
|
645
|
+
label: t('이메일'),
|
|
646
|
+
name: 'usrEmailAddr',
|
|
647
|
+
type: SearchParamType.INPUT,
|
|
648
|
+
inputType: 'email',
|
|
649
|
+
rules: [{ type: 'email', message: t('이메일 형식이 아닙니다.') }],
|
|
650
|
+
}"
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
```
|
|
654
|
+
사용자: "사용여부는 드롭다운으로, 기본값은 Y로"
|
|
655
|
+
|
|
656
|
+
[MCP] "SELECT으로 생성합니다.
|
|
657
|
+
기본값을 'Y'로 설정합니다.
|
|
658
|
+
|
|
659
|
+
{
|
|
660
|
+
label: t('사용여부'),
|
|
661
|
+
name: 'useYn',
|
|
662
|
+
type: SearchParamType.SELECT,
|
|
663
|
+
codeVar: 'USE_YN',
|
|
664
|
+
initialValue: 'Y',
|
|
665
|
+
}"
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
```
|
|
669
|
+
사용자: "가입일은 날짜 범위 picker로, 기본값은 최근 30일로"
|
|
670
|
+
|
|
671
|
+
[MCP] "DATE_RANGE로 생성합니다.
|
|
672
|
+
기본값을 최근 30일로 설정합니다.
|
|
673
|
+
|
|
674
|
+
{
|
|
675
|
+
label: t('가입일'),
|
|
676
|
+
name: 'joinDt',
|
|
677
|
+
type: SearchParamType.DATE_RANGE,
|
|
678
|
+
defaultValue: [dayjs().subtract(30, 'day'), dayjs()],
|
|
679
|
+
}"
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
### 자연어 처리 규칙
|
|
683
|
+
|
|
684
|
+
| 키워드 | 컴포넌트 | 예시 |
|
|
685
|
+
|--------|----------|------|
|
|
686
|
+
| 검색어, 텍스트, 입력 | INPUT | "이름으로 검색" |
|
|
687
|
+
| 드롭다운, 셀렉트, 선택 | SELECT | "드롭다운으로" |
|
|
688
|
+
| 체크박스, 다중 선택 | CHECKBOX | "여러 개 선택 가능" |
|
|
689
|
+
| 멀티 셀렉트 | SELECT_MULTI | "여러 상태 선택" |
|
|
690
|
+
| 라디오, Y/N | RADIO | "사용여부 Y/N" |
|
|
691
|
+
| 날짜, 일자 | DTPICKER_DATE | "가입일자" |
|
|
692
|
+
| 기간, 범위, ~부터 ~까지 | DTPICKER_DATE_RANGE | "가입기간" |
|
|
693
|
+
| 숫자 범위, 최소 최대 | NUMBER_RANGE | "방문회수 범위" |
|
|
694
|
+
| 검색구분 + 텍스트 | GROUP | "제목/내용 검색" |
|
|
695
|
+
| 코드, 분류 | SELECT + codeVar | "상태코드" |
|
|
696
|
+
| 필수, 필수입력 | required: true | "필수 입력" |
|
|
697
|
+
| placeholder ~ | placeholder: "~" | "placeholder는 '검색어'" |
|
|
698
|
+
|
|
699
|
+
---
|
|
700
|
+
|
|
701
|
+
### 타입별 대화형 설정 (타입 선택 후 추가 질문)
|
|
702
|
+
|
|
703
|
+
타입을 선택하면 해당 타입에 맞는 추가 설정을 대화형으로 진행합니다.
|
|
704
|
+
|
|
705
|
+
#### 2번 INPUT 선택 시
|
|
706
|
+
|
|
707
|
+
```
|
|
708
|
+
[MCP] INPUT을 선택했습니다.
|
|
709
|
+
usrNm (회원명) 필드에 대해 추가 설정을 진행합니다.
|
|
710
|
+
|
|
711
|
+
1️⃣ placeholder 설정 (선택)
|
|
712
|
+
placeholder를 입력하세요 (엔터: 설정 안함):
|
|
713
|
+
|
|
714
|
+
사용자: [엔터]
|
|
715
|
+
|
|
716
|
+
[MCP] placeholder를 설정하지 않습니다.
|
|
717
|
+
|
|
718
|
+
2️⃣ 너비 설정 (선택)
|
|
719
|
+
너비를 입력하세요 (엔터: 기본값 200):
|
|
720
|
+
|
|
721
|
+
사용자: 250
|
|
722
|
+
|
|
723
|
+
[MCP] width: 250으로 설정되었습니다.
|
|
724
|
+
|
|
725
|
+
3️⃣ 기본값 설정 (선택)
|
|
726
|
+
기본값을 입력하세요 (엔터: 설정 안함):
|
|
727
|
+
|
|
728
|
+
사용자: [엔터]
|
|
729
|
+
|
|
730
|
+
[MCP] 기본값을 설정하지 않습니다.
|
|
731
|
+
|
|
732
|
+
생성될 코드:
|
|
733
|
+
{
|
|
734
|
+
label: t('회원명'),
|
|
735
|
+
name: 'usrNm',
|
|
736
|
+
type: SearchParamType.INPUT,
|
|
737
|
+
width: 250,
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
확인되나요? (Y/n)
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
#### 4번 SELECT 선택 시
|
|
744
|
+
|
|
745
|
+
```
|
|
746
|
+
[MCP] SELECT를 선택했습니다.
|
|
747
|
+
statusCd (상태코드) 필드에 대해 추가 설정을 진행합니다.
|
|
748
|
+
|
|
749
|
+
1️⃣ 옵션 소스 선택
|
|
750
|
+
옵션을 어떻게 제공할까요?
|
|
751
|
+
|
|
752
|
+
┌─────────────────────────────────────────────┐
|
|
753
|
+
│ 1. 📦 Code Store 사용 (codeVar) │
|
|
754
|
+
│ 2. ✏️ 직접 옵션 입력 (options 배열) │
|
|
755
|
+
└─────────────────────────────────────────────┘
|
|
756
|
+
|
|
757
|
+
사용자: 1
|
|
758
|
+
|
|
759
|
+
2️⃣ 코드 변수명
|
|
760
|
+
코드 변수명을 입력하세요 (예: STATUS_CD):
|
|
761
|
+
|
|
762
|
+
사용자: STATUS_CD
|
|
763
|
+
|
|
764
|
+
[MCP] codeVar: 'STATUS_CD'로 설정되었습니다.
|
|
765
|
+
|
|
766
|
+
3️⃣ 너비 설정 (엔터: 기본값 150):
|
|
767
|
+
|
|
768
|
+
사용자: [엔터]
|
|
769
|
+
|
|
770
|
+
4️⃣ 다중 선택 여부
|
|
771
|
+
다중 선택을 지원할까요? (y/N):
|
|
772
|
+
|
|
773
|
+
사용자: n
|
|
774
|
+
|
|
775
|
+
5️⃣ 기본값 설정 (선택)
|
|
776
|
+
기본값을 입력하세요 (엔터: 설정 안함):
|
|
777
|
+
코드 값으로 입력 (예: 01, Y):
|
|
778
|
+
|
|
779
|
+
사용자: 01
|
|
780
|
+
|
|
781
|
+
[MCP] 기본값 '01'로 설정되었습니다.
|
|
782
|
+
|
|
783
|
+
생성될 코드:
|
|
784
|
+
{
|
|
785
|
+
label: t('상태'),
|
|
786
|
+
name: 'statusCd',
|
|
787
|
+
type: SearchParamType.SELECT,
|
|
788
|
+
width: 150,
|
|
789
|
+
options: [
|
|
790
|
+
{ label: t('전체'), value: '' },
|
|
791
|
+
...(STATUS_CD?.options ?? [])
|
|
792
|
+
],
|
|
793
|
+
initialValue: '01',
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
확인되나요? (Y/n)
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
#### 7번 RADIO 선택 시
|
|
800
|
+
|
|
801
|
+
```
|
|
802
|
+
[MCP] RADIO를 선택했습니다.
|
|
803
|
+
useYn (사용여부) 필드에 대해 추가 설정을 진행합니다.
|
|
804
|
+
|
|
805
|
+
1️⃣ 옵션 패턴 선택
|
|
806
|
+
어떤 옵션 패턴을 사용할까요?
|
|
807
|
+
|
|
808
|
+
┌─────────────────────────────────────────────┐
|
|
809
|
+
│ 1. Y/N 패턴 (전체/Y/N) │
|
|
810
|
+
│ 2. 직접 옵션 입력 │
|
|
811
|
+
└─────────────────────────────────────────────┘
|
|
812
|
+
|
|
813
|
+
사용자: 1
|
|
814
|
+
|
|
815
|
+
[MCP] Y/N 패턴을 사용합니다.
|
|
816
|
+
|
|
817
|
+
2️⃣ 기본값 설정 (선택)
|
|
818
|
+
기본값을 선택하세요:
|
|
819
|
+
1. 전체 (빈값)
|
|
820
|
+
2. Y
|
|
821
|
+
3. N
|
|
822
|
+
|
|
823
|
+
사용자: 2
|
|
824
|
+
|
|
825
|
+
[MCP] 기본값 'Y'로 설정되었습니다.
|
|
826
|
+
|
|
827
|
+
생성될 코드:
|
|
828
|
+
{
|
|
829
|
+
label: t('사용여부'),
|
|
830
|
+
name: 'useYn',
|
|
831
|
+
type: SearchParamType.RADIO,
|
|
832
|
+
options: [
|
|
833
|
+
{ label: t('전체'), value: '' },
|
|
834
|
+
{ label: t('Y'), value: 'Y' },
|
|
835
|
+
{ label: t('N'), value: 'N' },
|
|
836
|
+
],
|
|
837
|
+
initialValue: 'Y',
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
확인되나요? (Y/n)
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
#### 9번 DTPICKER_DATE_RANGE 선택 시
|
|
844
|
+
|
|
845
|
+
```
|
|
846
|
+
[MCP] DATE_RANGE를 선택했습니다.
|
|
847
|
+
dateRange (등록일) 필드에 대해 추가 설정을 진행합니다.
|
|
848
|
+
|
|
849
|
+
1️⃣ 프리셋 타입
|
|
850
|
+
프리셋 타입을 선택하세요:
|
|
851
|
+
|
|
852
|
+
┌─────────────────────────────────────────────┐
|
|
853
|
+
│ 1. all (전체: 오늘, 7일, 30일, 90일, 1년) │
|
|
854
|
+
│ 2. default (기본: 오늘, 7일, 30일) │
|
|
855
|
+
│ 3. 없음 │
|
|
856
|
+
└─────────────────────────────────────────────┘
|
|
857
|
+
|
|
858
|
+
사용자: 1
|
|
859
|
+
|
|
860
|
+
2️⃣ 날짜 포맷 선택
|
|
861
|
+
날짜 포맷을 선택하세요:
|
|
862
|
+
|
|
863
|
+
┌─────────────────────────────────────────────┐
|
|
864
|
+
│ 1. DATE (년월일) │
|
|
865
|
+
│ 예: 2024-01-15 │
|
|
866
|
+
├─────────────────────────────────────────────┤
|
|
867
|
+
│ 2. DATETIME (년월일시분초) │
|
|
868
|
+
│ 예: 2024-01-15 14:30:00 │
|
|
869
|
+
├─────────────────────────────────────────────┤
|
|
870
|
+
│ 3. DATETIME_HHMM (년월일시분) │
|
|
871
|
+
│ 예: 2024-01-15 14:30 │
|
|
872
|
+
└─────────────────────────────────────────────┘
|
|
873
|
+
|
|
874
|
+
사용자: 1
|
|
875
|
+
|
|
876
|
+
[MCP] format: DT_FORMAT.DATE, isTimeSelector: false로 설정합니다.
|
|
877
|
+
|
|
878
|
+
3️⃣ 기간구분 포함 여부
|
|
879
|
+
기간구분(예: 가입일/수정일)을 포함할까요? (y/N):
|
|
880
|
+
|
|
881
|
+
사용자: n
|
|
882
|
+
|
|
883
|
+
4️⃣ 기본 날짜 범위 설정 (선택)
|
|
884
|
+
기본 날짜 범위를 선택하세요:
|
|
885
|
+
1. 설정 안함
|
|
886
|
+
2. 오늘
|
|
887
|
+
3. 최근 7일
|
|
888
|
+
4. 최근 30일
|
|
889
|
+
5. 최근 90일
|
|
890
|
+
6. 최근 1년
|
|
891
|
+
|
|
892
|
+
사용자: 4
|
|
893
|
+
|
|
894
|
+
[MCP] 기본값을 최근 30일로 설정합니다.
|
|
895
|
+
|
|
896
|
+
생성될 코드:
|
|
897
|
+
{
|
|
898
|
+
label: t('등록일'),
|
|
899
|
+
placeholder: [t('년월일'), t('년월일')],
|
|
900
|
+
name: 'dateRange',
|
|
901
|
+
type: SearchParamType.DTPICKER_DATE_RANGE,
|
|
902
|
+
format: DT_FORMAT.DATE,
|
|
903
|
+
isTimeSelector: false,
|
|
904
|
+
presetRender: DtPickerPresetRender({ type: 'all' }),
|
|
905
|
+
defaultValue: [dayjs().subtract(30, 'day'), dayjs()],
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
확인되나요? (Y/n)
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
#### DATETIME_HHMM 선택 시 예시
|
|
912
|
+
|
|
913
|
+
```
|
|
914
|
+
사용자: 3 (DATETIME_HHMM 선택)
|
|
915
|
+
|
|
916
|
+
[MCP] format: DT_FORMAT.DATETIME_HHMM, isTimeSelector: true로 설정합니다.
|
|
917
|
+
placeholder는 '년월일시분'으로 설정됩니다.
|
|
918
|
+
|
|
919
|
+
생성될 코드:
|
|
920
|
+
{
|
|
921
|
+
label: t('작성기간'),
|
|
922
|
+
placeholder: [t('년월일시분'), t('년월일시분')],
|
|
923
|
+
name: 'dateRange',
|
|
924
|
+
type: SearchParamType.DTPICKER_DATE_RANGE,
|
|
925
|
+
format: DT_FORMAT.DATETIME_HHMM,
|
|
926
|
+
isTimeSelector: true,
|
|
927
|
+
presetRender: DtPickerPresetRender({ type: 'all' }),
|
|
928
|
+
}
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
#### 10번 NUMBER_RANGE 선택 시
|
|
932
|
+
|
|
933
|
+
```
|
|
934
|
+
[MCP] NUMBER_RANGE를 선택했습니다.
|
|
935
|
+
visitCount (방문회수) 필드에 대해 추가 설정을 진행합니다.
|
|
936
|
+
|
|
937
|
+
1️⃣ MIN placeholder 설정 (선택)
|
|
938
|
+
MIN placeholder를 입력하세요 (엔터: 'MIN'):
|
|
939
|
+
|
|
940
|
+
사용자: [엔터]
|
|
941
|
+
|
|
942
|
+
2️⃣ MAX placeholder 설정 (선택)
|
|
943
|
+
MAX placeholder를 입력하세요 (엔터: 'MAX'):
|
|
944
|
+
|
|
945
|
+
사용자: [엔터]
|
|
946
|
+
|
|
947
|
+
3️⃣ 기본값 설정 (선택)
|
|
948
|
+
MIN 기본값을 입력하세요 (엔터: 설정 안함):
|
|
949
|
+
|
|
950
|
+
사용자: 10
|
|
951
|
+
|
|
952
|
+
4️⃣ 기본값 설정 (선택)
|
|
953
|
+
MAX 기본값을 입력하세요 (엔터: 설정 안함):
|
|
954
|
+
|
|
955
|
+
사용자: 100
|
|
956
|
+
|
|
957
|
+
[MCP] 기본값을 MIN: 10, MAX: 100으로 설정합니다.
|
|
958
|
+
|
|
959
|
+
생성될 코드:
|
|
960
|
+
{
|
|
961
|
+
label: t('방문회수'),
|
|
962
|
+
placeholder: [t('MIN'), t('MAX')],
|
|
963
|
+
name: 'visitCount',
|
|
964
|
+
type: SearchParamType.NUMBER_RANGE,
|
|
965
|
+
defaultValue: [10, 100],
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
확인되나요? (Y/n)
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
#### 11번 SKIP 선택 시
|
|
972
|
+
|
|
973
|
+
```
|
|
974
|
+
[MCP] SKIP을 선택했습니다.
|
|
975
|
+
usrNm (회원명) 필드는 SearchParams에 포함하지 않습니다.
|
|
976
|
+
|
|
977
|
+
다음 필드로 넘어갑니다...
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
#### 12번 HIDDEN 선택 시
|
|
981
|
+
|
|
982
|
+
```
|
|
983
|
+
[MCP] HIDDEN을 선택했습니다.
|
|
984
|
+
usrNm (회원명) 필드는 UI에 표시하지 않지만,
|
|
985
|
+
listRequestValue에는 유지됩니다.
|
|
986
|
+
|
|
987
|
+
⚠️ HIDDEN 필드는 별도로 초기값 설정이 필요할 수 있습니다.
|
|
988
|
+
|
|
989
|
+
다음 필드로 넘어갈까요? (Y/n)
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
#### 5번 CHECKBOX 선택 시
|
|
993
|
+
|
|
994
|
+
```
|
|
995
|
+
[MCP] CHECKBOX를 선택했습니다.
|
|
996
|
+
sbscrbPathClcdArr (회원가입채널) 필드에 대해 추가 설정을 진행합니다.
|
|
997
|
+
|
|
998
|
+
1️⃣ 옵션 소스 선택
|
|
999
|
+
옵션을 어떻게 제공할까요?
|
|
1000
|
+
|
|
1001
|
+
┌─────────────────────────────────────────────┐
|
|
1002
|
+
│ 1. 📦 Code Store 사용 (codeVar) │
|
|
1003
|
+
│ 2. ✏️ 직접 옵션 입력 (options 배열) │
|
|
1004
|
+
└─────────────────────────────────────────────┘
|
|
1005
|
+
|
|
1006
|
+
사용자: 1
|
|
1007
|
+
|
|
1008
|
+
2️⃣ 코드 변수명
|
|
1009
|
+
코드 변수명을 입력하세요 (예: SBSCRB_PATH_CLCD):
|
|
1010
|
+
|
|
1011
|
+
사용자: SBSCRB_PATH_CLCD
|
|
1012
|
+
|
|
1013
|
+
[MCP] codeVar: 'SBSCRB_PATH_CLCD'로 설정되었습니다.
|
|
1014
|
+
|
|
1015
|
+
3️⃣ 전체 옵션 포함 여부
|
|
1016
|
+
"전체" 옵션을 포함할까요? (Y/n):
|
|
1017
|
+
|
|
1018
|
+
사용자: y
|
|
1019
|
+
|
|
1020
|
+
[MCP] checkAllItem: true로 설정되었습니다.
|
|
1021
|
+
|
|
1022
|
+
4️⃣ 기본값 설정 (선택)
|
|
1023
|
+
기본으로 체크할 옵션을 입력하세요.
|
|
1024
|
+
형식: 값,값 (쉼표로 구분, 엔터: 설정 안함):
|
|
1025
|
+
|
|
1026
|
+
사용자: Y,02
|
|
1027
|
+
|
|
1028
|
+
[MCP] 기본값 ['Y', '02']로 설정되었습니다.
|
|
1029
|
+
|
|
1030
|
+
생성될 코드:
|
|
1031
|
+
{
|
|
1032
|
+
label: t('회원가입채널'),
|
|
1033
|
+
name: 'sbscrbPathClcdArr',
|
|
1034
|
+
type: SearchParamType.CHECKBOX,
|
|
1035
|
+
options: SBSCRB_PATH_CLCD?.options ?? [],
|
|
1036
|
+
checkAllItem: true,
|
|
1037
|
+
defaultValue: ['Y', '02'],
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
확인되나요? (Y/n)
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
---
|
|
1044
|
+
|
|
1045
|
+
### 각 패턴별 코드 예시 (NH 프로젝트 실사용)
|
|
1046
|
+
|
|
1047
|
+
#### 2. 📝 일반 텍스트 입력 (INPUT)
|
|
1048
|
+
|
|
1049
|
+
```tsx
|
|
1050
|
+
// 기본 텍스트 입력
|
|
1051
|
+
{
|
|
1052
|
+
label: t("검색어"),
|
|
1053
|
+
placeholder: t("검색어"),
|
|
1054
|
+
name: "searchText",
|
|
1055
|
+
type: SearchParamType.INPUT,
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// 너비 지정
|
|
1059
|
+
{
|
|
1060
|
+
label: t("회원명"),
|
|
1061
|
+
placeholder: t("회원명을 입력하세요"),
|
|
1062
|
+
name: "usrNm",
|
|
1063
|
+
type: SearchParamType.INPUT,
|
|
1064
|
+
width: 200,
|
|
1065
|
+
}
|
|
1066
|
+
```
|
|
1067
|
+
|
|
1068
|
+
#### 3. 🔍 검색구분 + 텍스트 (GROUP)
|
|
1069
|
+
|
|
1070
|
+
```tsx
|
|
1071
|
+
// 검색어 구분 + 텍스트 입력
|
|
1072
|
+
{
|
|
1073
|
+
type: SearchParamType.GROUP,
|
|
1074
|
+
children: [
|
|
1075
|
+
{
|
|
1076
|
+
label: t("검색어구분"),
|
|
1077
|
+
name: "searchType",
|
|
1078
|
+
type: SearchParamType.SELECT,
|
|
1079
|
+
options: [
|
|
1080
|
+
{ label: t("제목"), value: "TITLE" },
|
|
1081
|
+
{ label: t("내용"), value: "CONTENT" },
|
|
1082
|
+
{ label: t("작성자"), value: "WRITER" },
|
|
1083
|
+
],
|
|
1084
|
+
width: 120,
|
|
1085
|
+
},
|
|
1086
|
+
{
|
|
1087
|
+
label: t("검색어"),
|
|
1088
|
+
name: "searchText",
|
|
1089
|
+
type: SearchParamType.INPUT,
|
|
1090
|
+
placeholder: t("검색어를 입력하세요"),
|
|
1091
|
+
},
|
|
1092
|
+
],
|
|
1093
|
+
}
|
|
1094
|
+
```
|
|
1095
|
+
|
|
1096
|
+
#### 4. 📋 드롭다운 선택 (SELECT)
|
|
1097
|
+
|
|
1098
|
+
```tsx
|
|
1099
|
+
// Code Store 사용
|
|
1100
|
+
{
|
|
1101
|
+
label: t("주문상태"),
|
|
1102
|
+
name: "ordSttusTpcd",
|
|
1103
|
+
type: SearchParamType.SELECT,
|
|
1104
|
+
width: 200,
|
|
1105
|
+
options: ORD_STTUS_TPCD?.options ?? [],
|
|
1106
|
+
showSearch: true,
|
|
1107
|
+
allowClear: true,
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// 직접 옵션 입력
|
|
1111
|
+
{
|
|
1112
|
+
label: t("정렬"),
|
|
1113
|
+
name: "sortType",
|
|
1114
|
+
type: SearchParamType.SELECT,
|
|
1115
|
+
width: 150,
|
|
1116
|
+
options: [
|
|
1117
|
+
{ label: t("최신순"), value: "latest" },
|
|
1118
|
+
{ label: t("이름순"), value: "name" },
|
|
1119
|
+
{ label: t("가나다순"), value: "abc" },
|
|
1120
|
+
],
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// API 로딩 상태
|
|
1124
|
+
{
|
|
1125
|
+
label: t("제품군"),
|
|
1126
|
+
name: "ctgrNo",
|
|
1127
|
+
type: SearchParamType.SELECT,
|
|
1128
|
+
width: 250,
|
|
1129
|
+
options: [...prodgrpNo],
|
|
1130
|
+
showSearch: false,
|
|
1131
|
+
allowClear: true,
|
|
1132
|
+
loading: prodgrpSpinning,
|
|
1133
|
+
}
|
|
1134
|
+
```
|
|
1135
|
+
|
|
1136
|
+
#### 5. ✓ 체크박스 다중 선택 (CHECKBOX)
|
|
1137
|
+
|
|
1138
|
+
```tsx
|
|
1139
|
+
// 코드 기반 체크박스
|
|
1140
|
+
{
|
|
1141
|
+
label: t("회원가입채널"),
|
|
1142
|
+
name: "sbscrbPathClcdArr",
|
|
1143
|
+
type: SearchParamType.CHECKBOX,
|
|
1144
|
+
options: SBSCRB_PATH_CLCD?.options ?? [],
|
|
1145
|
+
checkAllItem: true, // "전체" 옵션 추가
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// 직접 옵션 입력
|
|
1149
|
+
{
|
|
1150
|
+
label: t("통합회원여부"),
|
|
1151
|
+
name: "unityMberYnArr",
|
|
1152
|
+
type: SearchParamType.CHECKBOX,
|
|
1153
|
+
options: [
|
|
1154
|
+
{ label: t("통합회원"), value: "Y" },
|
|
1155
|
+
{ label: t("임직원"), value: "02" },
|
|
1156
|
+
{ label: t("미전환"), value: "N" },
|
|
1157
|
+
],
|
|
1158
|
+
checkAllItem: true,
|
|
1159
|
+
}
|
|
1160
|
+
```
|
|
1161
|
+
|
|
1162
|
+
#### 6. ☐ 멀티 셀렉트 (SELECT_MULTI)
|
|
1163
|
+
|
|
1164
|
+
```tsx
|
|
1165
|
+
// 상태 다중 선택
|
|
1166
|
+
{
|
|
1167
|
+
label: t("주문상태"),
|
|
1168
|
+
name: "ordSttusTpcds",
|
|
1169
|
+
type: SearchParamType.SELECT_MULTI,
|
|
1170
|
+
width: 200,
|
|
1171
|
+
options: ORD_STTUS_TPCD?.options ?? [],
|
|
1172
|
+
checkAllItem: true,
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// 동적 옵션 생성
|
|
1176
|
+
{
|
|
1177
|
+
label: t("년도"),
|
|
1178
|
+
name: "bbscttMgmtYys",
|
|
1179
|
+
type: SearchParamType.SELECT_MULTI,
|
|
1180
|
+
options: Array.from({ length: dayjs().year() - 2010 + 1 })
|
|
1181
|
+
.map((_, i) => ({
|
|
1182
|
+
label: `${i + 2010}년`,
|
|
1183
|
+
value: `${i + 2010}`,
|
|
1184
|
+
})),
|
|
1185
|
+
}
|
|
1186
|
+
```
|
|
1187
|
+
|
|
1188
|
+
#### 7. ○ 라디오 버튼 (RADIO)
|
|
1189
|
+
|
|
1190
|
+
```tsx
|
|
1191
|
+
// Y/N 선택 (가장 일반적)
|
|
1192
|
+
{
|
|
1193
|
+
label: t("사용여부"),
|
|
1194
|
+
name: "useYn",
|
|
1195
|
+
type: SearchParamType.RADIO,
|
|
1196
|
+
options: [
|
|
1197
|
+
{ label: t("전체"), value: "" },
|
|
1198
|
+
{ label: t("Y"), value: "Y" },
|
|
1199
|
+
{ label: t("N"), value: "N" },
|
|
1200
|
+
],
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// 이메일 수신 동의
|
|
1204
|
+
{
|
|
1205
|
+
label: t("이메일수신동의"),
|
|
1206
|
+
name: "emailRecptnYn",
|
|
1207
|
+
type: SearchParamType.RADIO,
|
|
1208
|
+
options: [
|
|
1209
|
+
{ label: t("전체"), value: "" },
|
|
1210
|
+
{ label: t("동의"), value: "Y" },
|
|
1211
|
+
{ label: t("미동의"), value: "N" },
|
|
1212
|
+
],
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// 답변 상태
|
|
1216
|
+
{
|
|
1217
|
+
label: t("답변상태"),
|
|
1218
|
+
name: "answerSttusYn",
|
|
1219
|
+
type: SearchParamType.RADIO,
|
|
1220
|
+
options: [
|
|
1221
|
+
{ label: t("전체"), value: "" },
|
|
1222
|
+
{ label: t("답변대기"), value: "N" },
|
|
1223
|
+
{ label: t("답변완료"), value: "Y" },
|
|
1224
|
+
],
|
|
1225
|
+
}
|
|
1226
|
+
```
|
|
1227
|
+
|
|
1228
|
+
#### 8. 📅 단일 날짜 (DTPICKER_DATE)
|
|
1229
|
+
|
|
1230
|
+
```tsx
|
|
1231
|
+
{
|
|
1232
|
+
label: t("등록일"),
|
|
1233
|
+
name: "regDt",
|
|
1234
|
+
type: SearchParamType.DTPICKER_DATE,
|
|
1235
|
+
format: DT_FORMAT.DATE,
|
|
1236
|
+
}
|
|
1237
|
+
```
|
|
1238
|
+
|
|
1239
|
+
#### 9. 📆 날짜 범위 (DTPICKER_DATE_RANGE)
|
|
1240
|
+
|
|
1241
|
+
```tsx
|
|
1242
|
+
// 기본 날짜 범위
|
|
1243
|
+
{
|
|
1244
|
+
label: t("등록일"),
|
|
1245
|
+
placeholder: [t("년월일"), t("년월일")],
|
|
1246
|
+
name: "dateRange",
|
|
1247
|
+
type: SearchParamType.DTPICKER_DATE_RANGE,
|
|
1248
|
+
format: DT_FORMAT.DATE,
|
|
1249
|
+
isTimeSelector: false,
|
|
1250
|
+
presetRender: DtPickerPresetRender({ type: "all" }),
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// 기간구분 + 날짜 범위 (단일 달력으로 처리)
|
|
1254
|
+
{
|
|
1255
|
+
type: SearchParamType.GROUP,
|
|
1256
|
+
children: [
|
|
1257
|
+
{
|
|
1258
|
+
label: t("기간구분"),
|
|
1259
|
+
name: "periodType",
|
|
1260
|
+
type: SearchParamType.SELECT,
|
|
1261
|
+
options: [
|
|
1262
|
+
{ label: t("주문일"), value: "01" },
|
|
1263
|
+
{ label: t("결제일"), value: "02" },
|
|
1264
|
+
{ label: t("배송일"), value: "03" },
|
|
1265
|
+
],
|
|
1266
|
+
width: 120,
|
|
1267
|
+
},
|
|
1268
|
+
{
|
|
1269
|
+
label: t("기간"),
|
|
1270
|
+
name: "dateRange",
|
|
1271
|
+
type: SearchParamType.DTPICKER_DATE_RANGE,
|
|
1272
|
+
format: DT_FORMAT.DATE,
|
|
1273
|
+
presetRender: DtPickerPresetRender({ type: "all" }),
|
|
1274
|
+
},
|
|
1275
|
+
],
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// 날짜 범위 (시간 포함)
|
|
1279
|
+
{
|
|
1280
|
+
label: t("작성일시"),
|
|
1281
|
+
name: "datetimeRange",
|
|
1282
|
+
type: SearchParamType.DTPICKER_DATE_RANGE,
|
|
1283
|
+
format: DT_FORMAT.DATETIME,
|
|
1284
|
+
isTimeSelector: true,
|
|
1285
|
+
presetRender: DtPickerPresetRender({ type: "all" }),
|
|
1286
|
+
}
|
|
1287
|
+
```
|
|
1288
|
+
|
|
1289
|
+
#### 10. 🔢 숫자 범위 (NUMBER_RANGE)
|
|
1290
|
+
|
|
1291
|
+
```tsx
|
|
1292
|
+
// 방문회수 범위
|
|
1293
|
+
{
|
|
1294
|
+
label: t("방문회수"),
|
|
1295
|
+
placeholder: [t("MIN"), t("MAX")],
|
|
1296
|
+
name: "visitCount",
|
|
1297
|
+
type: SearchParamType.NUMBER_RANGE,
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// 구매금액 범위
|
|
1301
|
+
{
|
|
1302
|
+
label: t("구매금액"),
|
|
1303
|
+
placeholder: [t("최소금액"), t("최대금액")],
|
|
1304
|
+
name: "purchaseAmount",
|
|
1305
|
+
type: SearchParamType.NUMBER_RANGE,
|
|
1306
|
+
}
|
|
1307
|
+
```
|
|
1308
|
+
|
|
1309
|
+
---
|
|
1310
|
+
|
|
1311
|
+
### 추가 레이아웃 요소
|
|
1312
|
+
|
|
1313
|
+
#### 줄바꿈 (LINE_BREAK)
|
|
1314
|
+
|
|
1315
|
+
```tsx
|
|
1316
|
+
// 줄바꿈으로 섹션 분리
|
|
1317
|
+
{ type: SearchParamType.LINE_BREAK }
|
|
1318
|
+
```
|
|
1319
|
+
|
|
1320
|
+
#### 간격 조절 (GAP)
|
|
1321
|
+
|
|
1322
|
+
```tsx
|
|
1323
|
+
// 필드 간 간격 조절
|
|
1324
|
+
{ type: SearchParamType.GAP }
|
|
1325
|
+
```
|
|
1326
|
+
|
|
1327
|
+
---
|
|
1328
|
+
|
|
1329
|
+
### Code Store vs 직접 옵션
|
|
1330
|
+
|
|
1331
|
+
```
|
|
1332
|
+
[MCP] statusCd (상태코드) 필드입니다.
|
|
1333
|
+
어떤 방식으로 옵션을 제공할까요?
|
|
1334
|
+
|
|
1335
|
+
┌─────────────────────────────────────────────┐
|
|
1336
|
+
│ 1. 📦 Code Store 사용 (codeVar) │
|
|
1337
|
+
│ → USE_YN, STATUS_CD 등 │
|
|
1338
|
+
├─────────────────────────────────────────────┤
|
|
1339
|
+
│ 2. ✏️ 직접 옵션 입력 (options 배열) │
|
|
1340
|
+
│ → [{ label: '이름', value: 'name' }] │
|
|
1341
|
+
└─────────────────────────────────────────────┘
|
|
1342
|
+
|
|
1343
|
+
번호를 입력하세요:
|
|
1344
|
+
```
|
|
1345
|
+
|
|
1346
|
+
#### 1번 선택 시 (Code Store)
|
|
1347
|
+
```
|
|
1348
|
+
[MCP] "코드 변수명을 입력하세요:"
|
|
1349
|
+
사용자: "STATUS_CD"
|
|
1350
|
+
|
|
1351
|
+
[MCP] "자동 추론: STATUS_CD (확인되었습니다)"
|
|
1352
|
+
```
|
|
1353
|
+
|
|
1354
|
+
#### 2번 선택 시 (직접 옵션)
|
|
1355
|
+
```
|
|
1356
|
+
[MCP] "옵션을 입력하세요.
|
|
1357
|
+
형식: label,value (줄바리로 구분)"
|
|
1358
|
+
|
|
1359
|
+
사용자:
|
|
1360
|
+
활성,active
|
|
1361
|
+
비활성,inactive
|
|
1362
|
+
대기,pending
|
|
1363
|
+
|
|
1364
|
+
[MCP] "다음과 같이 변환됩니다:
|
|
1365
|
+
[
|
|
1366
|
+
{ label: '활성', value: 'active' },
|
|
1367
|
+
{ label: '비활성', value: 'inactive' },
|
|
1368
|
+
{ label: '대기', value: 'pending' },
|
|
1369
|
+
]
|
|
1370
|
+
|
|
1371
|
+
확인되나요? (y/n)"
|
|
1372
|
+
```
|
|
1373
|
+
|
|
1374
|
+
---
|
|
1375
|
+
|
|
1376
|
+
## 그룹핑 설정
|
|
1377
|
+
|
|
1378
|
+
### 그룹핑 방식
|
|
1379
|
+
|
|
1380
|
+
| 방식 | 설명 | 코드 예시 |
|
|
1381
|
+
|------|------|---------|
|
|
1382
|
+
| 한 줄 | 기본, 한 줄에 표시 | `<Params.Input />` |
|
|
1383
|
+
| 그룹 | 여러 필드를 같은 줄 | `<Params.Group>...</Params.Group>` |
|
|
1384
|
+
| 독립 줄 | 그룹 없이 독립 | `lineBreak: true` |
|
|
1385
|
+
|
|
1386
|
+
### 대화형 흐름
|
|
1387
|
+
|
|
1388
|
+
```
|
|
1389
|
+
[MCP] "필드 그룹핑을 설정합니다.
|
|
1390
|
+
|
|
1391
|
+
형식 1 (줄바꿈):
|
|
1392
|
+
- 필드명 (독립 줄)
|
|
1393
|
+
|
|
1394
|
+
형식 2 (그룹):
|
|
1395
|
+
- 필드1, 필드2 (한 줄)
|
|
1396
|
+
|
|
1397
|
+
예시:
|
|
1398
|
+
- usrNm, usrEmailAddr
|
|
1399
|
+
- useYn, statusCd
|
|
1400
|
+
- joinDt (줄바꿈)"
|
|
1401
|
+
```
|
|
1402
|
+
|
|
1403
|
+
### 생성 코드 예시 (그룹핑)
|
|
1404
|
+
|
|
1405
|
+
```tsx
|
|
1406
|
+
<SearchParams
|
|
1407
|
+
form={searchForm}
|
|
1408
|
+
params={[
|
|
1409
|
+
// 그룹 1: 한 줄에 2개
|
|
1410
|
+
{ label: '이름', name: 'usrNm', type: SearchParamType.INPUT },
|
|
1411
|
+
{ label: '이메일', name: 'usrEmailAddr', type: SearchParamType.INPUT },
|
|
1412
|
+
|
|
1413
|
+
// 줄바꿈
|
|
1414
|
+
null,
|
|
1415
|
+
|
|
1416
|
+
// 그룹 2: 한 줄에 2개
|
|
1417
|
+
{ label: '사용여부', name: 'useYn', type: SearchParamType.SELECT, codeVar: 'USE_YN' },
|
|
1418
|
+
{ label: '상태', name: 'statusCd', type: SearchParamType.SELECT, codeVar: 'STATUS_CD' },
|
|
1419
|
+
|
|
1420
|
+
// 줄바꿈
|
|
1421
|
+
null,
|
|
1422
|
+
|
|
1423
|
+
// 독립 필드
|
|
1424
|
+
{ label: '가입일', name: 'joinDt', type: SearchParamType.DATE_PICKER },
|
|
1425
|
+
]}
|
|
1426
|
+
paramsValue={listRequestValue}
|
|
1427
|
+
onChangeParamsValue={setListRequestValue}
|
|
1428
|
+
onSearch={handleSearch}
|
|
1429
|
+
spinning={listSpinning}
|
|
1430
|
+
/>
|
|
1431
|
+
```
|
|
1432
|
+
|
|
1433
|
+
---
|
|
1434
|
+
|
|
1435
|
+
## 기본값 처리
|
|
1436
|
+
|
|
1437
|
+
### 기본값 설정 방식
|
|
1438
|
+
|
|
1439
|
+
| 방식 | 설명 | 예시 |
|
|
1440
|
+
|------|------|------|
|
|
1441
|
+
| 고정값 | 항상 같은 값 | `useYn: 'Y'` |
|
|
1442
|
+
| 동적값 | 실행 시 계산 | `joinDt: dayjs()` |
|
|
1443
|
+
| 코드 첫번째 | 코드 옵션의 첫 값 | `statusCd: CODE.options[0]?.value` |
|
|
1444
|
+
| 빈값 | 초기에 비워둠 | `searchText: ''` |
|
|
1445
|
+
|
|
1446
|
+
### 대화형 흐름
|
|
1447
|
+
|
|
1448
|
+
```
|
|
1449
|
+
[MCP] "각 필드의 기본값을 설정합니다.
|
|
1450
|
+
기본값이 필요한 필드를 말씀해주세요:"
|
|
1451
|
+
|
|
1452
|
+
사용자:
|
|
1453
|
+
- useYn: 'Y'
|
|
1454
|
+
- joinDt: 오늘날짜
|
|
1455
|
+
- statusCd: 첫번째코드
|
|
1456
|
+
|
|
1457
|
+
[MCP] "다음과 같이 설정합니다:
|
|
1458
|
+
|
|
1459
|
+
const initialSearchValues = {
|
|
1460
|
+
useYn: 'Y',
|
|
1461
|
+
joinDt: dayjs(),
|
|
1462
|
+
statusCd: STATUS_CD?.options[0]?.value,
|
|
1463
|
+
};
|
|
1464
|
+
|
|
1465
|
+
확인되나요? (y/n)"
|
|
1466
|
+
```
|
|
1467
|
+
|
|
1468
|
+
### 기본값 입력 형식
|
|
1469
|
+
|
|
1470
|
+
```
|
|
1471
|
+
필드명: 값
|
|
1472
|
+
|
|
1473
|
+
예시:
|
|
1474
|
+
- useYn: 'Y' // 문자열
|
|
1475
|
+
- pageNumber: 1 // 숫자
|
|
1476
|
+
- joinDt: 오늘날짜 // 동적 값
|
|
1477
|
+
- statusCd: 첫번째코드 // 코드 첫 값
|
|
1478
|
+
- searchText: '' // 빈값 (명시)
|
|
1479
|
+
```
|
|
1480
|
+
|
|
1481
|
+
---
|
|
1482
|
+
|
|
1483
|
+
## 연동 선택박스 설정 (Cascading Select)
|
|
1484
|
+
|
|
1485
|
+
### 연동 그룹 종류
|
|
1486
|
+
|
|
1487
|
+
| 구분 | 설명 | 예시 |
|
|
1488
|
+
|------|------|------|
|
|
1489
|
+
| 2단계 연동 | 부모 → 자식 | 배송사 → 배송비 방식 |
|
|
1490
|
+
| 3단계 연동 | 부모 → 자식 → 손자 | 대분류 → 중분류 → 소분류 |
|
|
1491
|
+
|
|
1492
|
+
### 연동 그룹 설정 프롬프트
|
|
1493
|
+
|
|
1494
|
+
```
|
|
1495
|
+
연동 그룹 1:
|
|
1496
|
+
- 단계: 2단계
|
|
1497
|
+
- 필드: dlvyClTpcd → dlcstMthdCd
|
|
1498
|
+
- 훅: useCodeStore
|
|
1499
|
+
- 코드: DLCMPY_TPCD
|
|
1500
|
+
- 필터: uprDtlCd
|
|
1501
|
+
```
|
|
1502
|
+
|
|
1503
|
+
### 대화형 흐름
|
|
1504
|
+
|
|
1505
|
+
#### 1단계: 연동 설정 질문
|
|
1506
|
+
```
|
|
1507
|
+
[MCP] 모든 필드 설정이 완료되었습니다.
|
|
1508
|
+
연동된 선택박스가 있나요?
|
|
1509
|
+
|
|
1510
|
+
┌─────────────────────────────────────────────┐
|
|
1511
|
+
│ 1. 🔗 연동 설정이 있다 │
|
|
1512
|
+
├─────────────────────────────────────────────┤
|
|
1513
|
+
│ 2. ✅ 연동 없음, 다음 단계로 │
|
|
1514
|
+
└─────────────────────────────────────────────┘
|
|
1515
|
+
```
|
|
1516
|
+
|
|
1517
|
+
#### 2단계: 연동 그룹 입력
|
|
1518
|
+
```
|
|
1519
|
+
[MCP] "연동 그룹을 입력해주세요.
|
|
1520
|
+
형식:
|
|
1521
|
+
|
|
1522
|
+
연동 그룹 N:
|
|
1523
|
+
- 단계: 2단계 또는 3단계
|
|
1524
|
+
- 필드: 부모 → 자식 (→ 손자)
|
|
1525
|
+
- 훅: useCodeStore
|
|
1526
|
+
- 코드: CODE_NAME
|
|
1527
|
+
- 필터: 필터링 필드명"
|
|
1528
|
+
|
|
1529
|
+
사용자:
|
|
1530
|
+
연동 그룹 1:
|
|
1531
|
+
- 단계: 2단계
|
|
1532
|
+
- 필드: dlvyClTpcd → dlcstMthdCd
|
|
1533
|
+
- 훅: useCodeStore
|
|
1534
|
+
- 코드: DLCMPY_TPCD
|
|
1535
|
+
- 필터: uprDtlCd
|
|
1536
|
+
```
|
|
1537
|
+
|
|
1538
|
+
#### 3단계: 코드 미리보기
|
|
1539
|
+
```
|
|
1540
|
+
[MCP] "다음과 같이 연동 코드를 생성합니다:
|
|
1541
|
+
|
|
1542
|
+
const DLCMPY_TPCD = useCodeStore((s) => s.DLCMPY_TPCD);
|
|
1543
|
+
|
|
1544
|
+
const DLCMPY_TPCD_OPTS = useMemo(() => {
|
|
1545
|
+
const selected = form.getFieldValue('dlvyClTpcd');
|
|
1546
|
+
return DLCMPY_TPCD?.options.filter(
|
|
1547
|
+
(o) => o.uprDtlCd === selected
|
|
1548
|
+
);
|
|
1549
|
+
}, [form.getFieldValue('dlvyClTpcd')]);
|
|
1550
|
+
|
|
1551
|
+
확인되나요? (y/n)"
|
|
1552
|
+
```
|
|
1553
|
+
|
|
1554
|
+
---
|
|
1555
|
+
|
|
1556
|
+
## 대화형 생성 절차 (전체)
|
|
1557
|
+
|
|
1558
|
+
### 1단계: 필드 분석
|
|
1559
|
+
```
|
|
1560
|
+
[MCP] "N개의 필드를 발견했습니다.
|
|
1561
|
+
하나씩 어떻게 처리할지 물어볼게요."
|
|
1562
|
+
```
|
|
1563
|
+
|
|
1564
|
+
### 2단계: 연동 설정 (선택)
|
|
1565
|
+
```
|
|
1566
|
+
[MCP] "연동된 선택박스가 있나요?
|
|
1567
|
+
연동 그룹을 입력해주세요.
|
|
1568
|
+
|
|
1569
|
+
형식:
|
|
1570
|
+
연동 그룹 N:
|
|
1571
|
+
- 단계: 2단계 또는 3단계
|
|
1572
|
+
- 필드: 부모 → 자식 (→ 손자)
|
|
1573
|
+
- 훅: useCodeStore
|
|
1574
|
+
- 코드: CODE_NAME
|
|
1575
|
+
- 필터: 필터링 필드명"
|
|
1576
|
+
```
|
|
1577
|
+
|
|
1578
|
+
### 3단계: 필드별 패턴 선택
|
|
1579
|
+
```
|
|
1580
|
+
[MCP] "fieldName (한글명) 필드입니다.
|
|
1581
|
+
다음 중 선택해주세요:"
|
|
1582
|
+
|
|
1583
|
+
[패턴 카탈로그 표시]
|
|
1584
|
+
|
|
1585
|
+
사용자: "1"
|
|
1586
|
+
```
|
|
1587
|
+
|
|
1588
|
+
### 4단계: 추가 질문 (필요시)
|
|
1589
|
+
```
|
|
1590
|
+
[MCP] "코드 변수명이 필요합니다.
|
|
1591
|
+
USE_YN, STATUS_CD 등:"
|
|
1592
|
+
```
|
|
1593
|
+
|
|
1594
|
+
### 5단계: 그룹핑 설정
|
|
1595
|
+
```
|
|
1596
|
+
[MCP] "필드 그룹핑을 설정합니다.
|
|
1597
|
+
같은 줄에 표시할 필드들을 콤마로 구분해주세요:"
|
|
1598
|
+
|
|
1599
|
+
사용자:
|
|
1600
|
+
- usrNm, usrEmailAddr
|
|
1601
|
+
- useYn, statusCd
|
|
1602
|
+
- joinDt
|
|
1603
|
+
```
|
|
1604
|
+
|
|
1605
|
+
### 6단계: 기본값 설정
|
|
1606
|
+
```
|
|
1607
|
+
[MCP] "기본값이 필요한 필드가 있나요?
|
|
1608
|
+
형식: 필드명: 값"
|
|
1609
|
+
|
|
1610
|
+
사용자:
|
|
1611
|
+
- useYn: 'Y'
|
|
1612
|
+
- joinDt: 오늘날짜
|
|
1613
|
+
```
|
|
1614
|
+
|
|
1615
|
+
### 7단계: 확인 및 생성
|
|
1616
|
+
```
|
|
1617
|
+
[MCP] "모든 설정 완료!
|
|
1618
|
+
다음과 같이 생성합니다:"
|
|
1619
|
+
|
|
1620
|
+
[요약 표시]
|
|
1621
|
+
|
|
1622
|
+
생성할까요? (y/n)"
|
|
1623
|
+
```
|
|
1624
|
+
|
|
1625
|
+
---
|
|
1626
|
+
|
|
1627
|
+
## SearchParams 컴포넌트 패턴 (NH 프로젝트 기반)
|
|
1628
|
+
|
|
1629
|
+
| # | 패턴 | SearchParamType | 주요 용도 | 빈도 |
|
|
1630
|
+
|---|------|-----------------|----------|------|
|
|
1631
|
+
| 1 | 🤖 AI 추론 | - | 자연어로 설명 | - |
|
|
1632
|
+
| 2 | 📝 텍스트 입력 | INPUT | 회원명, 이메일, 전화번호 | 115회 |
|
|
1633
|
+
| 3 | 🔍 검색구분+텍스트 | GROUP | 제목/내용, 이름/ID 검색 | 110회 |
|
|
1634
|
+
| 4 | 📋 드롭다운 | SELECT | 상태코드, 카테고리 | 191회 |
|
|
1635
|
+
| 5 | ✓ 체크박스 | CHECKBOX | 회원가입채널, 관심사 | 14회 |
|
|
1636
|
+
| 6 | ☐ 멀티 셀렉트 | SELECT_MULTI | 주문상태, 배송상태 | 38회 |
|
|
1637
|
+
| 7 | ○ 라디오 | RADIO | 사용여부, 이메일수신동의 | 127회 |
|
|
1638
|
+
| 8 | 📅 단일 날짜 | DTPICKER_DATE | 등록일, 수정일 | 1회 |
|
|
1639
|
+
| 9 | 📆 날짜 범위 | DTPICKER_DATE_RANGE | 가입기간, 주문기간 | 100회 |
|
|
1640
|
+
| 10 | 🔢 숫자 범위 | NUMBER_RANGE | 방문회수, 구매금액 | 1회 |
|
|
1641
|
+
| - | 줄바꿈 | LINE_BREAK | 섹션 분리 | 15회 |
|
|
1642
|
+
| - | 간격 조절 | GAP | 필드 간 간격 | 2회 |
|
|
1643
|
+
|
|
1644
|
+
---
|
|
1645
|
+
|
|
1646
|
+
## 출력 코드 구조
|
|
1647
|
+
|
|
1648
|
+
### 기본 예시
|
|
1649
|
+
|
|
1650
|
+
```tsx
|
|
1651
|
+
// App.tsx에 추가할 SearchParams JSX
|
|
1652
|
+
|
|
1653
|
+
import { SearchParams, SearchParamType } from "@core/components/search";
|
|
1654
|
+
import { useI18n } from "hooks";
|
|
1655
|
+
|
|
1656
|
+
function App() {
|
|
1657
|
+
const { t } = useI18n("_screenName"); // 다국어 지원
|
|
1658
|
+
|
|
1659
|
+
// 필수 검증이 있는 경우
|
|
1660
|
+
const requiredFields = ['usrNm', 'usrEmailAddr'];
|
|
1661
|
+
|
|
1662
|
+
<SearchParams
|
|
1663
|
+
form={searchForm}
|
|
1664
|
+
params={[
|
|
1665
|
+
// 그룹 1: 텍스트 입력 2개
|
|
1666
|
+
{ label: t("이름"), name: 'usrNm', type: SearchParamType.INPUT },
|
|
1667
|
+
{ label: t("이메일"), name: 'usrEmailAddr', type: SearchParamType.INPUT },
|
|
1668
|
+
|
|
1669
|
+
// 줄바꿈
|
|
1670
|
+
null,
|
|
1671
|
+
|
|
1672
|
+
// 그룹 2: 라디오 + 셀렉트
|
|
1673
|
+
{ label: t("사용여부"), name: 'useYn', type: SearchParamType.RADIO,
|
|
1674
|
+
options: [{ label: t("전체"), value: "" }, { label: t("Y"), value: "Y" }, { label: t("N"), value: "N" }] },
|
|
1675
|
+
{ label: t("상태"), name: 'statusCd', type: SearchParamType.SELECT, codeVar: 'STATUS_CD' },
|
|
1676
|
+
|
|
1677
|
+
// 줄바꿈
|
|
1678
|
+
null,
|
|
1679
|
+
|
|
1680
|
+
// 독립 필드: 날짜 범위
|
|
1681
|
+
{ label: t("가입일"), name: 'dateRange', type: SearchParamType.DTPICKER_DATE_RANGE,
|
|
1682
|
+
format: DT_FORMAT.DATE, presetRender: DtPickerPresetRender({ type: "all" }) },
|
|
1683
|
+
]}
|
|
1684
|
+
requiredFields={requiredFields}
|
|
1685
|
+
paramsValue={listRequestValue}
|
|
1686
|
+
onChangeParamsValue={setListRequestValue}
|
|
1687
|
+
onSearch={handleSearch}
|
|
1688
|
+
spinning={listSpinning}
|
|
1689
|
+
/>
|
|
1690
|
+
}
|
|
1691
|
+
```
|
|
1692
|
+
|
|
1693
|
+
### 복합 예시 (NH 프로젝트 실사용 패턴)
|
|
1694
|
+
|
|
1695
|
+
```tsx
|
|
1696
|
+
import { SearchParams, SearchParamType } from "@core/components/search";
|
|
1697
|
+
import { useI18n } from "hooks";
|
|
1698
|
+
import { DtPickerPresetRender, DT_FORMAT } from "@core/components/datetime";
|
|
1699
|
+
|
|
1700
|
+
function OrderList() {
|
|
1701
|
+
const { t } = useI18n("_orderList");
|
|
1702
|
+
const ORD_STTUS_TPCD = useCodeStore((s) => s.ORD_STTUS_TPCD);
|
|
1703
|
+
|
|
1704
|
+
<SearchParams
|
|
1705
|
+
form={searchForm}
|
|
1706
|
+
params={[
|
|
1707
|
+
// 검색구분 + 검색어 그룹
|
|
1708
|
+
{
|
|
1709
|
+
type: SearchParamType.GROUP,
|
|
1710
|
+
children: [
|
|
1711
|
+
{
|
|
1712
|
+
label: t("검색구분"),
|
|
1713
|
+
name: "searchType",
|
|
1714
|
+
type: SearchParamType.SELECT,
|
|
1715
|
+
options: [
|
|
1716
|
+
{ label: t("주문번호"), value: "ORD_NO" },
|
|
1717
|
+
{ label: t("상품명"), value: "PRD_NM" },
|
|
1718
|
+
{ label: t("구매자명"), value: "BUYER_NM" },
|
|
1719
|
+
],
|
|
1720
|
+
width: 120,
|
|
1721
|
+
},
|
|
1722
|
+
{
|
|
1723
|
+
label: t("검색어"),
|
|
1724
|
+
name: "searchText",
|
|
1725
|
+
type: SearchParamType.INPUT,
|
|
1726
|
+
placeholder: t("검색어를 입력하세요"),
|
|
1727
|
+
},
|
|
1728
|
+
],
|
|
1729
|
+
},
|
|
1730
|
+
|
|
1731
|
+
// 줄바꿈
|
|
1732
|
+
null,
|
|
1733
|
+
|
|
1734
|
+
// 상태 다중 선택 + 사용여부
|
|
1735
|
+
{
|
|
1736
|
+
label: t("주문상태"),
|
|
1737
|
+
name: "ordSttusTpcds",
|
|
1738
|
+
type: SearchParamType.SELECT_MULTI,
|
|
1739
|
+
width: 200,
|
|
1740
|
+
options: ORD_STTUS_TPCD?.options ?? [],
|
|
1741
|
+
checkAllItem: true,
|
|
1742
|
+
},
|
|
1743
|
+
{
|
|
1744
|
+
label: t("사용여부"),
|
|
1745
|
+
name: "useYn",
|
|
1746
|
+
type: SearchParamType.RADIO,
|
|
1747
|
+
options: [
|
|
1748
|
+
{ label: t("전체"), value: "" },
|
|
1749
|
+
{ label: t("Y"), value: "Y" },
|
|
1750
|
+
{ label: t("N"), value: "N" },
|
|
1751
|
+
],
|
|
1752
|
+
},
|
|
1753
|
+
|
|
1754
|
+
// 줄바꿈
|
|
1755
|
+
null,
|
|
1756
|
+
|
|
1757
|
+
// 기간구분 + 날짜 범위 (단일 달력)
|
|
1758
|
+
{
|
|
1759
|
+
type: SearchParamType.GROUP,
|
|
1760
|
+
children: [
|
|
1761
|
+
{
|
|
1762
|
+
label: t("기간구분"),
|
|
1763
|
+
name: "periodType",
|
|
1764
|
+
type: SearchParamType.SELECT,
|
|
1765
|
+
options: [
|
|
1766
|
+
{ label: t("주문일"), value: "01" },
|
|
1767
|
+
{ label: t("결제일"), value: "02" },
|
|
1768
|
+
],
|
|
1769
|
+
width: 120,
|
|
1770
|
+
},
|
|
1771
|
+
{
|
|
1772
|
+
label: t("기간"),
|
|
1773
|
+
name: "dateRange",
|
|
1774
|
+
type: SearchParamType.DTPICKER_DATE_RANGE,
|
|
1775
|
+
format: DT_FORMAT.DATE,
|
|
1776
|
+
presetRender: DtPickerPresetRender({ type: "all" }),
|
|
1777
|
+
},
|
|
1778
|
+
],
|
|
1779
|
+
},
|
|
1780
|
+
|
|
1781
|
+
// 줄바꿈
|
|
1782
|
+
null,
|
|
1783
|
+
|
|
1784
|
+
// 숫자 범위
|
|
1785
|
+
{
|
|
1786
|
+
label: t("구매금액"),
|
|
1787
|
+
placeholder: [t("최소금액"), t("최대금액")],
|
|
1788
|
+
name: "purchaseAmount",
|
|
1789
|
+
type: SearchParamType.NUMBER_RANGE,
|
|
1790
|
+
},
|
|
1791
|
+
]}
|
|
1792
|
+
paramsValue={listRequestValue}
|
|
1793
|
+
onChangeParamsValue={setListRequestValue}
|
|
1794
|
+
onSearch={handleSearch}
|
|
1795
|
+
spinning={listSpinning}
|
|
1796
|
+
/>
|
|
1797
|
+
}
|
|
1798
|
+
```
|
|
1799
|
+
|
|
1800
|
+
---
|
|
1801
|
+
|
|
1802
|
+
## 다국어 지원 (i18n)
|
|
1803
|
+
|
|
1804
|
+
모든 라벨은 `t()` 함수로 감싸서 다국어를 지원해야 합니다.
|
|
1805
|
+
|
|
1806
|
+
### 라벨 형식
|
|
1807
|
+
|
|
1808
|
+
```tsx
|
|
1809
|
+
// 기본 형식
|
|
1810
|
+
label: t("라벨 텍스트")
|
|
1811
|
+
|
|
1812
|
+
// 예시
|
|
1813
|
+
{ label: t("회원명"), name: 'usrNm' }
|
|
1814
|
+
{ label: t("사용여부"), name: 'useYn' }
|
|
1815
|
+
{ label: t("가입일자"), name: 'joinDt' }
|
|
1816
|
+
```
|
|
1817
|
+
|
|
1818
|
+
### useI18n 훅
|
|
1819
|
+
|
|
1820
|
+
```tsx
|
|
1821
|
+
import { useI18n } from "hooks";
|
|
1822
|
+
|
|
1823
|
+
function App() {
|
|
1824
|
+
// 화면별 네임스페이스
|
|
1825
|
+
const { t } = useI18n("_memberList");
|
|
1826
|
+
// 또는
|
|
1827
|
+
const { t } = useI18n("_productCategory");
|
|
1828
|
+
}
|
|
1829
|
+
```
|
|
1830
|
+
|
|
1831
|
+
### 네임스페이스 규칙
|
|
1832
|
+
|
|
1833
|
+
| 화면 | 네임스페이스 | 예시 |
|
|
1834
|
+
|------|-------------|------|
|
|
1835
|
+
| 회원 관리 | `_memberList` | `t("회원명")` |
|
|
1836
|
+
| 상품 관리 | `_productList` | `t("상품명")` |
|
|
1837
|
+
| 주문 관리 | `_orderList` | `t("주문번호")` |
|
|
1838
|
+
|
|
1839
|
+
---
|
|
1840
|
+
|
|
1841
|
+
## 줄바꿈 방식
|
|
1842
|
+
|
|
1843
|
+
| 방식 | 설명 | 코드 |
|
|
1844
|
+
|------|------|------|
|
|
1845
|
+
| `null` | 줄바꿈 | params 배열에 `null` 추가 |
|
|
1846
|
+
| `lineBreak: true` | 명시적 줄바꿈 | params 옵션에 설정 |
|
|
1847
|
+
|
|
1848
|
+
---
|
|
1849
|
+
|
|
1850
|
+
## 개선 계획 (v2)
|
|
1851
|
+
|
|
1852
|
+
### 변경 사항
|
|
1853
|
+
|
|
1854
|
+
1. **`outPath` 필수화**: 출력 파일 경로를 입력으로 추가
|
|
1855
|
+
2. **Repository 분석 제거**: `dtoPath`와 `outPath`를 필수로 처리
|
|
1856
|
+
3. **처리 흐름 간소화**: 1,2단계를 DTO 직접 분석으로 대체
|
|
1857
|
+
|
|
1858
|
+
### 변경된 입력
|
|
1859
|
+
|
|
1860
|
+
| 파라미터 | 타입 | 필수 | 설명 |
|
|
1861
|
+
|----------|------|------|------|
|
|
1862
|
+
| `dtoPath` | string | ✅ | Request DTO 파일 경로 (절대 경로) |
|
|
1863
|
+
| `outPath` | string | ✅ | 출력 SearchParams 파일 경로 (절대 경로) |
|
|
1864
|
+
|
|
1865
|
+
### 변경된 처리 흐름
|
|
1866
|
+
|
|
1867
|
+
```
|
|
1868
|
+
[기존] [변경 후]
|
|
1869
|
+
───────────── ─────────────
|
|
1870
|
+
1. Repository의 List API
|
|
1871
|
+
메서드 찾기 → (제거)
|
|
1872
|
+
2. Request 타입 분석 → 1. DTO 파일 분석 → 필드 목록 추출
|
|
1873
|
+
→ 필드 목록 추출
|
|
1874
|
+
3. 연동 설정 (선택) → 2. 연동 설정 (선택)
|
|
1875
|
+
4. 필드별 패턴 선택 → 3. 필드별 패턴 선택 (대화형)
|
|
1876
|
+
(대화형)
|
|
1877
|
+
5. 그룹핑 설정 → 4. 그룹핑 설정
|
|
1878
|
+
6. 기본값 설정 → 5. 기본값 설정
|
|
1879
|
+
7. 코드 생성 → 6. SearchParams JSX 코드를 outPath로 파일 생성
|
|
1880
|
+
```
|
|
1881
|
+
|
|
1882
|
+
### 구현 포인트
|
|
1883
|
+
|
|
1884
|
+
1. **DTO 파일 분석**
|
|
1885
|
+
- `analyze.ts`의 `extractRequestFields()` 함수 재사용
|
|
1886
|
+
- DTO 파일을 직접 읽어서 필드 목록 추출
|
|
1887
|
+
|
|
1888
|
+
2. **출력 파일 생성**
|
|
1889
|
+
- `outPath` 경로로 JSX 코드를 파일로 직접 생성
|
|
1890
|
+
- 기존 `generate-search-params.ts`의 코드 생성 로직 활용
|
|
1891
|
+
|
|
1892
|
+
3. **MCP 도구 스키마 변경**
|
|
1893
|
+
```typescript
|
|
1894
|
+
{
|
|
1895
|
+
name: 'generate_search_params_interactive',
|
|
1896
|
+
description: 'DTO를 기반으로 SearchParams를 대화형으로 생성',
|
|
1897
|
+
inputSchema: {
|
|
1898
|
+
type: 'object',
|
|
1899
|
+
properties: {
|
|
1900
|
+
dtoPath: {
|
|
1901
|
+
type: 'string',
|
|
1902
|
+
description: 'Request DTO 파일 경로 (절대 경로)',
|
|
1903
|
+
},
|
|
1904
|
+
outPath: {
|
|
1905
|
+
type: 'string',
|
|
1906
|
+
description: '출력 SearchParams 파일 경로 (절대 경로)',
|
|
1907
|
+
},
|
|
1908
|
+
},
|
|
1909
|
+
required: ['dtoPath', 'outPath'],
|
|
1910
|
+
},
|
|
1911
|
+
}
|
|
1912
|
+
```
|
|
1913
|
+
|
|
1914
|
+
### 핸들러 파일 구조
|
|
1915
|
+
|
|
1916
|
+
```typescript
|
|
1917
|
+
// src/handlers/generate-search-params-interactive.ts
|
|
1918
|
+
|
|
1919
|
+
export interface GenerateSearchParamsInteractiveParams {
|
|
1920
|
+
dtoPath: string; // Request DTO 파일 경로
|
|
1921
|
+
outPath: string; // 출력 파일 경로
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
export async function generateSearchParamsInteractive(
|
|
1925
|
+
params: GenerateSearchParamsInteractiveParams
|
|
1926
|
+
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
|
1927
|
+
// 1. DTO 파일 읽기 및 필드 분석
|
|
1928
|
+
const fields = await extractRequestFieldsFromDto(params.dtoPath);
|
|
1929
|
+
|
|
1930
|
+
// 2. 대화형으로 필드별 패턴 선택 (현재는 JSON 응답, 향후 true 대화형)
|
|
1931
|
+
const selections = await promptFieldSelections(fields);
|
|
1932
|
+
|
|
1933
|
+
// 3. SearchParams JSX 코드 생성
|
|
1934
|
+
const jsxCodes = generateSearchParamsJsx(selections);
|
|
1935
|
+
|
|
1936
|
+
// 4. outPath로 파일 쓰기
|
|
1937
|
+
await fs.writeFile(params.outPath, jsxCodes, 'utf-8');
|
|
1938
|
+
|
|
1939
|
+
return { content: [{ type: 'text', text: '생성 완료' }] };
|
|
1940
|
+
}
|
|
1941
|
+
```
|
|
1942
|
+
|
|
1943
|
+
### 활용 예시
|
|
1944
|
+
|
|
1945
|
+
```typescript
|
|
1946
|
+
// MCP 호출 예시
|
|
1947
|
+
mcpAxboot.generateSearchParamsInteractive({
|
|
1948
|
+
dtoPath: '/path/to/dto/MemberListReq.ts',
|
|
1949
|
+
outPath: '/path/to/src/pages/resources/NH/member.list/App.tsx',
|
|
1950
|
+
});
|
|
1951
|
+
```
|
|
1952
|
+
|
|
1953
|
+
### App.tsx 파일 삽입 위치
|
|
1954
|
+
|
|
1955
|
+
검색 영역 코드는 `<PageSearchBar>` 컴포넌트 내부에 삽입됩니다.
|
|
1956
|
+
|
|
1957
|
+
#### NH 프로젝트 App.tsx 구조
|
|
1958
|
+
|
|
1959
|
+
```tsx
|
|
1960
|
+
<Container stretch role={"page-container"}>
|
|
1961
|
+
<Header>
|
|
1962
|
+
<ProgramTitle>...</ProgramTitle>
|
|
1963
|
+
|
|
1964
|
+
{/* 검색 버튼 영역 */}
|
|
1965
|
+
<Flex align={"center"} gap={6}>
|
|
1966
|
+
<Button onClick={handleSearch}>{t("btn.검색")}</Button>
|
|
1967
|
+
</Flex>
|
|
1968
|
+
|
|
1969
|
+
{/* 👇 SearchParams 삽입 위치 (PageSearchBar 내부) */}
|
|
1970
|
+
<PageSearchBar>
|
|
1971
|
+
<SearchParams
|
|
1972
|
+
form={searchForm}
|
|
1973
|
+
params={[/* 생성된 params 배열 */}
|
|
1974
|
+
]}
|
|
1975
|
+
paramsValue={listRequestValue}
|
|
1976
|
+
onChangeParamsValue={(value, changedValues) => setListRequestValue(value, changedValues)}
|
|
1977
|
+
onSearch={handleSearch}
|
|
1978
|
+
spinning={listSpinning}
|
|
1979
|
+
/>
|
|
1980
|
+
</PageSearchBar>
|
|
1981
|
+
</Header>
|
|
1982
|
+
|
|
1983
|
+
<Body ref={resizerContainerRef}>
|
|
1984
|
+
<Frame>
|
|
1985
|
+
<ListDataGrid />
|
|
1986
|
+
</Frame>
|
|
1987
|
+
</Body>
|
|
1988
|
+
</Container>
|
|
1989
|
+
```
|
|
1990
|
+
|
|
1991
|
+
#### 파일 처리 로직
|
|
1992
|
+
|
|
1993
|
+
1. **파일 존재 시**
|
|
1994
|
+
- `<PageSearchBar>` 태그를 찾아 내부에 SearchParams를 삽입
|
|
1995
|
+
- 기존 SearchParams가 있으면 교체
|
|
1996
|
+
|
|
1997
|
+
2. **파일 없는 경우**
|
|
1998
|
+
- 기본 App.tsx 템플릿으로 파일 생성
|
|
1999
|
+
- `<PageSearchBar>` 내부에 SearchParams 포함
|
|
2000
|
+
|
|
2001
|
+
#### 삽입 예시
|
|
2002
|
+
|
|
2003
|
+
```tsx
|
|
2004
|
+
// 기존 파일에서 PageSearchBar를 찾아 내부에 삽입
|
|
2005
|
+
<PageSearchBar>
|
|
2006
|
+
<SearchParams
|
|
2007
|
+
form={searchForm}
|
|
2008
|
+
params={[
|
|
2009
|
+
// DTO에서 추출한 필드 기반으로 생성된 params
|
|
2010
|
+
{ label: t("회원명"), name: "usrNm", type: SearchParamType.INPUT },
|
|
2011
|
+
{ label: t("사용여부"), name: "useYn", type: SearchParamType.RADIO, ... },
|
|
2012
|
+
// ...
|
|
2013
|
+
]}
|
|
2014
|
+
paramsValue={listRequestValue}
|
|
2015
|
+
onChangeParamsValue={(value, changedValues) => setListRequestValue(value, changedValues)}
|
|
2016
|
+
onSearch={handleSearch}
|
|
2017
|
+
spinning={listSpinning}
|
|
2018
|
+
/>
|
|
2019
|
+
</PageSearchBar>
|
|
2020
|
+
```
|
|
2021
|
+
|
|
2022
|
+
#### 필요한 styled 컴포넌트
|
|
2023
|
+
|
|
2024
|
+
파일 생성 시 필요한 styled 컴포넌트:
|
|
2025
|
+
|
|
2026
|
+
```tsx
|
|
2027
|
+
const Container = styled(PageLayout)``;
|
|
2028
|
+
const Header = styled(PageLayout.Header)``;
|
|
2029
|
+
const PageSearchBar = styled(PageLayout.PageSearchBar)``; // 필수
|
|
2030
|
+
const Body = styled(PageLayout.FrameRow)``;
|
|
2031
|
+
const Frame = styled(PageLayout.FrameColumn)``;
|
|
2032
|
+
```
|
|
2033
|
+
|
|
2034
|
+
---
|
|
2035
|
+
|
|
2036
|
+
## 사전 수집 MCP 툴 계획
|
|
2037
|
+
|
|
2038
|
+
SearchParams 생성 전, 프로젝트의 메타데이터를 수집하는 별도 MCP 툴을 제공합니다.
|
|
2039
|
+
|
|
2040
|
+
### 개요
|
|
2041
|
+
|
|
2042
|
+
| 툴 | 목적 |
|
|
2043
|
+
|-----|------|
|
|
2044
|
+
| **SCAN 툴** | 프로젝트 메타데이터 수집 (DTO, Repository API, 모달 함수, 서비스, 훅, 코드) |
|
|
2045
|
+
| **GENERATE 툴** | 수집된 메타데이터 기반으로 SearchParams 대화형 생성 |
|
|
2046
|
+
|
|
2047
|
+
### SCAN 툴: 메타데이터 수집
|
|
2048
|
+
|
|
2049
|
+
#### 수집 항목
|
|
2050
|
+
|
|
2051
|
+
| 항목 | 설명 | 사용처 |
|
|
2052
|
+
|------|------|--------|
|
|
2053
|
+
| **DTO 필드** | Request/Response DTO의 모든 필드 | 필드 목록 자동 완성 |
|
|
2054
|
+
| **Repository API** | Repository 인터페이스의 메서드 시그니처 | API 연동 패턴 |
|
|
2055
|
+
| **모달 함수** | `openXxxModal()` 형태의 함수 | MODAL 타입 검색 |
|
|
2056
|
+
| **서비스** | API 서비스 메서드 | AUTOCOMPLETE 타입 |
|
|
2057
|
+
| **훅** | 계층 데이터 훅 | CASCADE 타입 |
|
|
2058
|
+
| **코드** | Code Store의 코드 변수명 | SELECT, CHECKBOX 옵션 |
|
|
2059
|
+
|
|
2060
|
+
#### 수집 경로 (고정 패턴)
|
|
2061
|
+
|
|
2062
|
+
```typescript
|
|
2063
|
+
const SCAN_PATTERNS = {
|
|
2064
|
+
// DTO (1순위)
|
|
2065
|
+
dto: [
|
|
2066
|
+
'src/services/@interface/dto/**/*.ts', // DTO 전용 폴더
|
|
2067
|
+
'src/services/**/dto/**/*.ts', // 서비스 하위 DTO
|
|
2068
|
+
'src/types/**/*Dto*.ts', // Types 폴더 DTO
|
|
2069
|
+
'src/interfaces/**/*Request*.ts', // Request 인터페이스
|
|
2070
|
+
'src/interfaces/**/*Response*.ts', // Response 인터페이스
|
|
2071
|
+
],
|
|
2072
|
+
|
|
2073
|
+
// Repository
|
|
2074
|
+
repository: [
|
|
2075
|
+
'src/services/**/*Repository.ts',
|
|
2076
|
+
'src/services/**/repository/*.ts',
|
|
2077
|
+
],
|
|
2078
|
+
|
|
2079
|
+
// 모달
|
|
2080
|
+
modal: [
|
|
2081
|
+
'src/modals/**/*Modal.ts',
|
|
2082
|
+
'src/components/**/*Modal.ts',
|
|
2083
|
+
],
|
|
2084
|
+
|
|
2085
|
+
// 서비스
|
|
2086
|
+
service: [
|
|
2087
|
+
'src/services/**/*.ts',
|
|
2088
|
+
],
|
|
2089
|
+
|
|
2090
|
+
// 훅
|
|
2091
|
+
hook: [
|
|
2092
|
+
'src/hooks/**/*.ts',
|
|
2093
|
+
'src/hooks/**/*Cascade*.ts', // 계층 데이터 훅
|
|
2094
|
+
],
|
|
2095
|
+
};
|
|
2096
|
+
```
|
|
2097
|
+
|
|
2098
|
+
#### DTO 스캔: 경로 패턴 기반 전체 스캔
|
|
2099
|
+
|
|
2100
|
+
**DTO 파일이 여기저기 산재해 있어도, 전체를 스캔하여 인덱싱합니다.**
|
|
2101
|
+
|
|
2102
|
+
##### 스캔 경로 패턴 (고정)
|
|
2103
|
+
|
|
2104
|
+
```typescript
|
|
2105
|
+
const DTO_PATTERNS = [
|
|
2106
|
+
'src/services/@interface/dto/**/*.ts', // 1순위: DTO 전용 폴더
|
|
2107
|
+
'src/services/**/dto/**/*.ts', // 2순위: 서비스 하위 DTO
|
|
2108
|
+
'src/types/**/*Dto*.ts', // 3순위: Types 폴더
|
|
2109
|
+
'src/interfaces/**/*Request*.ts', // 4순위: Request 인터페이스
|
|
2110
|
+
'src/interfaces/**/*Response*.ts', // 5순위: Response 인터페이스
|
|
2111
|
+
];
|
|
2112
|
+
```
|
|
2113
|
+
|
|
2114
|
+
##### 추출 정보
|
|
2115
|
+
|
|
2116
|
+
```typescript
|
|
2117
|
+
interface DtoIndex {
|
|
2118
|
+
// 인터페이스 기본 정보
|
|
2119
|
+
name: string; // 인터페이스 명 (예: MemberListRequest)
|
|
2120
|
+
filePath: string; // 파일 경로 (절대 경로)
|
|
2121
|
+
isExport: boolean; // export 여부
|
|
2122
|
+
|
|
2123
|
+
// 상속 정보 (체인 추적 지원)
|
|
2124
|
+
extends?: string[]; // 상속받은 인터페이스 배열 (순서 중요!)
|
|
2125
|
+
// 예: ["DefaultDto", "MemberSearch"]
|
|
2126
|
+
|
|
2127
|
+
// 필드 목록 (extends 체인을 모두 포함한 필드)
|
|
2128
|
+
fields: DtoField[];
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
interface DtoField {
|
|
2132
|
+
name: string; // 필드명 (예: usrNm)
|
|
2133
|
+
type: string; // 타입 (예: string, number, boolean)
|
|
2134
|
+
isOptional: boolean; // 선택적 여부 (?:)
|
|
2135
|
+
comment?: string; // 주석 (있는 경우)
|
|
2136
|
+
source?: string; // 필드가 정의된 위치 (예: "MemberSearch", "DefaultDto")
|
|
2137
|
+
}
|
|
2138
|
+
```
|
|
2139
|
+
|
|
2140
|
+
##### extends 체인 추적 (중요!)
|
|
2141
|
+
|
|
2142
|
+
**실제 프로젝트에서는 Repository 인터페이스에서 정의한 Request/Response 타입이 실제 DTO를 `extends` 합니다.**
|
|
2143
|
+
|
|
2144
|
+
> **이미 구현됨**: `analyze_interface` MCP 툴(`src/handlers/analyze.ts`)에서 `extractRequestFields()` 함수가 이미 extends 체인 추적을 구현하고 있습니다. SearchParams 생성 시 이 기능을 활용하면 됩니다.
|
|
2145
|
+
|
|
2146
|
+
```typescript
|
|
2147
|
+
// 예시: MemberInterface.ts
|
|
2148
|
+
export interface PostMemberListMemberRequest extends MemberSearch {
|
|
2149
|
+
pageNumber?: number;
|
|
2150
|
+
pageSize?: number;
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
// 예시: MemberSearch.ts (DTO)
|
|
2154
|
+
export interface MemberSearch extends DefaultDto {
|
|
2155
|
+
usrNm?: string;
|
|
2156
|
+
usrTpcd?: string;
|
|
2157
|
+
// ...
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
// 예시: types/index.ts
|
|
2161
|
+
export interface DefaultDto {
|
|
2162
|
+
__status__?: DtoItemStatus;
|
|
2163
|
+
rowId?: string;
|
|
2164
|
+
crtrId?: string;
|
|
2165
|
+
crtrNm?: string;
|
|
2166
|
+
creatDtm?: string;
|
|
2167
|
+
upusrId?: string;
|
|
2168
|
+
upusrNm?: string;
|
|
2169
|
+
upddeDtm?: string;
|
|
2170
|
+
}
|
|
2171
|
+
```
|
|
2172
|
+
|
|
2173
|
+
**체인 추적 알고리즘:**
|
|
2174
|
+
|
|
2175
|
+
```typescript
|
|
2176
|
+
/**
|
|
2177
|
+
* extends 체인을 추적하여 모든 필드를 수집합니다.
|
|
2178
|
+
*
|
|
2179
|
+
* 1. 인터페이스 파싱 시 extends 정보 수집
|
|
2180
|
+
* 2. 재귀적으로 extends 체인을 탐색
|
|
2181
|
+
* 3. 순환 참조 방지 (방문 기록)
|
|
2182
|
+
* 4. 필드 중복 시 하위 클래스가 우선 (override)
|
|
2183
|
+
*/
|
|
2184
|
+
async function resolveExtendsChain(
|
|
2185
|
+
interfaceName: string,
|
|
2186
|
+
cache: MetadataCache,
|
|
2187
|
+
visited = new Set<string>()
|
|
2188
|
+
): Promise<DtoIndex> {
|
|
2189
|
+
// 순환 참조 방지
|
|
2190
|
+
if (visited.has(interfaceName)) {
|
|
2191
|
+
console.warn(`[MCP] 순환 참조 감지: ${interfaceName}`);
|
|
2192
|
+
return { name: interfaceName, fields: [] };
|
|
2193
|
+
}
|
|
2194
|
+
visited.add(interfaceName);
|
|
2195
|
+
|
|
2196
|
+
// 캐시에서 현재 인터페이스查找
|
|
2197
|
+
const current = cache.dtos[interfaceName];
|
|
2198
|
+
if (!current) {
|
|
2199
|
+
console.warn(`[MCP] 인터페이스를 찾을 수 없음: ${interfaceName}`);
|
|
2200
|
+
return { name: interfaceName, fields: [] };
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
// 자체 필드 수집 (extends 제외)
|
|
2204
|
+
const ownFields = current.fields.filter(f =>
|
|
2205
|
+
!f.source?.includes('extends')
|
|
2206
|
+
);
|
|
2207
|
+
|
|
2208
|
+
// extends 체인 처리
|
|
2209
|
+
const allFields = [...ownFields];
|
|
2210
|
+
const extendsChain: string[] = [];
|
|
2211
|
+
|
|
2212
|
+
if (current.extends && current.extends.length > 0) {
|
|
2213
|
+
for (const parent of current.extends) {
|
|
2214
|
+
extendsChain.push(parent);
|
|
2215
|
+
|
|
2216
|
+
// 재귀적으로 부모 인터페이스 처리
|
|
2217
|
+
const parentDto = await resolveExtendsChain(parent, cache, visited);
|
|
2218
|
+
allFields.push(...parentDto.fields);
|
|
2219
|
+
|
|
2220
|
+
// 부모의 extends 체인도 병합
|
|
2221
|
+
if (parentDto.extends) {
|
|
2222
|
+
extendsChain.push(...parentDto.extends);
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
// 필드 중복 제거 (하위 클래스가 우선)
|
|
2228
|
+
const uniqueFields = Array.from(
|
|
2229
|
+
new Map(allFields.map(f => [f.name, f])).values()
|
|
2230
|
+
);
|
|
2231
|
+
|
|
2232
|
+
return {
|
|
2233
|
+
...current,
|
|
2234
|
+
extends: [...new Set(extendsChain)], // 중복 제거
|
|
2235
|
+
fields: uniqueFields,
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
```
|
|
2239
|
+
|
|
2240
|
+
**스캔 시 extends 정보 수집:**
|
|
2241
|
+
|
|
2242
|
+
```typescript
|
|
2243
|
+
// 인터페이스 파싱 시 extends 추출
|
|
2244
|
+
function parseInterfaceFile(sourceCode: string): DtoIndex[] {
|
|
2245
|
+
const results: DtoIndex[] = [];
|
|
2246
|
+
|
|
2247
|
+
// 정규식으로 인터페이스 선언 추출
|
|
2248
|
+
const interfaceRegex = /export\s+interface\s+(\w+)\s+(?:extends\s+([^{]+))?/g;
|
|
2249
|
+
|
|
2250
|
+
let match;
|
|
2251
|
+
while ((match = interfaceRegex.exec(sourceCode)) !== null) {
|
|
2252
|
+
const [, name, extendsClause] = match;
|
|
2253
|
+
|
|
2254
|
+
const extendsList: string[] = [];
|
|
2255
|
+
if (extendsClause) {
|
|
2256
|
+
// extends A, B, C → ["A", "B", "C"]
|
|
2257
|
+
extendsList.push(
|
|
2258
|
+
...extendsClause.split(',').map(e => e.trim())
|
|
2259
|
+
);
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
results.push({
|
|
2263
|
+
name,
|
|
2264
|
+
extends: extendsList.length > 0 ? extendsList : undefined,
|
|
2265
|
+
filePath: currentFile,
|
|
2266
|
+
isExport: true,
|
|
2267
|
+
fields: parseFields(sourceCode, name),
|
|
2268
|
+
});
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
return results;
|
|
2272
|
+
}
|
|
2273
|
+
```
|
|
2274
|
+
|
|
2275
|
+
**캐시에 해결된 필드 저장:**
|
|
2276
|
+
|
|
2277
|
+
```json
|
|
2278
|
+
{
|
|
2279
|
+
"dtos": {
|
|
2280
|
+
"PostMemberListMemberRequest": {
|
|
2281
|
+
"name": "PostMemberListMemberRequest",
|
|
2282
|
+
"filePath": "/Users/kyle/Desktop/nh-fe-bo/src/services/@interface/interface/MemberInterface.ts",
|
|
2283
|
+
"extends": ["MemberSearch", "DefaultDto"],
|
|
2284
|
+
"fields": [
|
|
2285
|
+
{ "name": "__status__", "type": "DtoItemStatus", "isOptional": true, "source": "DefaultDto" },
|
|
2286
|
+
{ "name": "rowId", "type": "string", "isOptional": true, "source": "DefaultDto" },
|
|
2287
|
+
{ "name": "crtrId", "type": "string", "isOptional": true, "source": "DefaultDto" },
|
|
2288
|
+
{ "name": "crtrNm", "type": "string", "isOptional": true, "source": "DefaultDto" },
|
|
2289
|
+
{ "name": "creatDtm", "type": "string", "isOptional": true, "source": "DefaultDto" },
|
|
2290
|
+
{ "name": "upusrId", "type": "string", "isOptional": true, "source": "DefaultDto" },
|
|
2291
|
+
{ "name": "upusrNm", "type": "string", "isOptional": true, "source": "DefaultDto" },
|
|
2292
|
+
{ "name": "upddeDtm", "type": "string", "isOptional": true, "source": "DefaultDto" },
|
|
2293
|
+
{ "name": "usrNm", "type": "string", "isOptional": true, "comment": "사용자명", "source": "MemberSearch" },
|
|
2294
|
+
{ "name": "usrTpcd", "type": "string", "isOptional": true, "source": "MemberSearch" },
|
|
2295
|
+
{ "name": "usrNo", "type": "number", "isOptional": true, "source": "MemberSearch" },
|
|
2296
|
+
{ "name": "usrHpNo", "type": "string", "isOptional": true, "source": "MemberSearch" },
|
|
2297
|
+
{ "name": "usrLoginId", "type": "string", "isOptional": true, "source": "MemberSearch" },
|
|
2298
|
+
{ "name": "usrSttusTpcd", "type": "string", "isOptional": true, "source": "MemberSearch" },
|
|
2299
|
+
{ "name": "pageNumber", "type": "number", "isOptional": true, "source": "PostMemberListMemberRequest" },
|
|
2300
|
+
{ "name": "pageSize", "type": "number", "isOptional": true, "source": "PostMemberListMemberRequest" }
|
|
2301
|
+
]
|
|
2302
|
+
},
|
|
2303
|
+
"MemberSearch": {
|
|
2304
|
+
"name": "MemberSearch",
|
|
2305
|
+
"filePath": "/Users/kyle/Desktop/nh-fe-bo/src/services/@interface/dto/MemberSearch.ts",
|
|
2306
|
+
"extends": ["DefaultDto"],
|
|
2307
|
+
"fields": [
|
|
2308
|
+
{ "name": "usrNm", "type": "string", "isOptional": true, "source": "MemberSearch" },
|
|
2309
|
+
{ "name": "usrTpcd", "type": "string", "isOptional": true, "source": "MemberSearch" },
|
|
2310
|
+
{ "name": "usrNo", "type": "number", "isOptional": true, "source": "MemberSearch" },
|
|
2311
|
+
{ "name": "usrHpNo", "type": "string", "isOptional": true, "source": "MemberSearch" },
|
|
2312
|
+
{ "name": "usrLoginId", "type": "string", "isOptional": true, "source": "MemberSearch" },
|
|
2313
|
+
{ "name": "usrSttusTpcd", "type": "string", "isOptional": true, "source": "MemberSearch" }
|
|
2314
|
+
]
|
|
2315
|
+
},
|
|
2316
|
+
"DefaultDto": {
|
|
2317
|
+
"name": "DefaultDto",
|
|
2318
|
+
"filePath": "/Users/kyle/Desktop/nh-fe-bo/src/services/@interface/types/index.ts",
|
|
2319
|
+
"extends": undefined,
|
|
2320
|
+
"fields": [
|
|
2321
|
+
{ "name": "__status__", "type": "DtoItemStatus", "isOptional": true, "source": "DefaultDto" },
|
|
2322
|
+
{ "name": "rowId", "type": "string", "isOptional": true, "source": "DefaultDto" },
|
|
2323
|
+
{ "name": "crtrId", "type": "string", "isOptional": true, "source": "DefaultDto" },
|
|
2324
|
+
{ "name": "crtrNm", "type": "string", "isOptional": true, "source": "DefaultDto" },
|
|
2325
|
+
{ "name": "creatDtm", "type": "string", "isOptional": true, "source": "DefaultDto" },
|
|
2326
|
+
{ "name": "upusrId", "type": "string", "isOptional": true, "source": "DefaultDto" },
|
|
2327
|
+
{ "name": "upusrNm", "type": "string", "isOptional": true, "source": "DefaultDto" },
|
|
2328
|
+
{ "name": "upddeDtm", "type": "string", "isOptional": true, "source": "DefaultDto" }
|
|
2329
|
+
]
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
```
|
|
2334
|
+
|
|
2335
|
+
##### Repository와 DTO 자동 매칭
|
|
2336
|
+
|
|
2337
|
+
```typescript
|
|
2338
|
+
// Repository 메서드의 Request 타입 → DTO 인덱스에서 찾기
|
|
2339
|
+
interface RepositoryMethod {
|
|
2340
|
+
name: string;
|
|
2341
|
+
requestType: string; // 예: "PostMemberListMemberRequest"
|
|
2342
|
+
responseType: string;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
// Request 타입명으로 DTO 인덱스 검색
|
|
2346
|
+
// (이미 extends 체인이 해결되어 모든 필드가 포함됨)
|
|
2347
|
+
function findDtoByRequestType(requestType: string): DtoIndex | undefined {
|
|
2348
|
+
return cache.dtos[requestType];
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
// 예: MemberRepository.postMemberListMember(PostMemberListMemberRequest)
|
|
2352
|
+
// ↓
|
|
2353
|
+
// cache.dtos["PostMemberListMemberRequest"] → extends 체인이 해결된 필드 목록 제공
|
|
2354
|
+
// ↓
|
|
2355
|
+
// fields: [__status__, rowId, ..., usrNm, usrTpcd, ..., pageNumber, pageSize]
|
|
2356
|
+
// (DefaultDto 포함) (MemberSearch 포함) (자체 필드)
|
|
2357
|
+
```
|
|
2358
|
+
|
|
2359
|
+
**Repository 스캔 시 Request 타입 추출:**
|
|
2360
|
+
|
|
2361
|
+
```typescript
|
|
2362
|
+
// Repository 파일 파싱
|
|
2363
|
+
function parseRepositoryFile(sourceCode: string): RepositoryInfo {
|
|
2364
|
+
const methods: RepositoryMethod[] = [];
|
|
2365
|
+
|
|
2366
|
+
// 메서드 시그니처 추출
|
|
2367
|
+
// 예: async postMemberListMember(params: PostMemberListMemberRequest, config?: ApiRequestConfig)
|
|
2368
|
+
const methodRegex = /async\s+(\w+)\([^)]*:\s*(\w+Request)[^)]*\)/g;
|
|
2369
|
+
|
|
2370
|
+
let match;
|
|
2371
|
+
while ((match = methodRegex.exec(sourceCode)) !== null) {
|
|
2372
|
+
const [, methodName, requestType] = match;
|
|
2373
|
+
|
|
2374
|
+
// Response 타입도 추출 (선택)
|
|
2375
|
+
const responseMatch = sourceCode.match(
|
|
2376
|
+
new RegExp(`${methodName}[^{]*{[^}]*_apiWrapper<(${methodName}Response)>`)
|
|
2377
|
+
);
|
|
2378
|
+
const responseType = responseMatch?.[1];
|
|
2379
|
+
|
|
2380
|
+
methods.push({
|
|
2381
|
+
name: methodName,
|
|
2382
|
+
requestType,
|
|
2383
|
+
responseType,
|
|
2384
|
+
});
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
return {
|
|
2388
|
+
name: repositoryName,
|
|
2389
|
+
filePath,
|
|
2390
|
+
methods,
|
|
2391
|
+
};
|
|
2392
|
+
}
|
|
2393
|
+
```
|
|
2394
|
+
|
|
2395
|
+
#### 캐시 저장 위치
|
|
2396
|
+
|
|
2397
|
+
```
|
|
2398
|
+
프젝트 루트/
|
|
2399
|
+
├── .mcp-cache/
|
|
2400
|
+
│ ├── search-params-meta.json # 전체 메타데이터
|
|
2401
|
+
│ ├── .gitignore # (git에 커밋 안됨)
|
|
2402
|
+
│ └── README.md # 캐시 폴더 설명
|
|
2403
|
+
├── src/
|
|
2404
|
+
└── package.json
|
|
2405
|
+
```
|
|
2406
|
+
|
|
2407
|
+
#### .gitignore
|
|
2408
|
+
|
|
2409
|
+
```
|
|
2410
|
+
# MCP Cache
|
|
2411
|
+
.mcp-cache/
|
|
2412
|
+
```
|
|
2413
|
+
|
|
2414
|
+
### GENERATE 툴: SearchParams 대화형 생성
|
|
2415
|
+
|
|
2416
|
+
#### 입력 파라미터
|
|
2417
|
+
|
|
2418
|
+
| 파라미터 | 타입 | 필수 | 설명 |
|
|
2419
|
+
|----------|------|------|------|
|
|
2420
|
+
| `outPath` | string | ✅ | 출력 SearchParams 파일 경로 (절대 경로) |
|
|
2421
|
+
| `dtoName` | string | ❌ | Request DTO 명 (선택 시 캐시에서 필드 목록 자동 완성) |
|
|
2422
|
+
|
|
2423
|
+
#### 캐시 없는 경우: Fail-safe 자동 스캔
|
|
2424
|
+
|
|
2425
|
+
```typescript
|
|
2426
|
+
// GENERATE 툴 실행 시 캐시 확인
|
|
2427
|
+
async function generateSearchParams(params: GenerateParams) {
|
|
2428
|
+
const cache = await loadCache();
|
|
2429
|
+
|
|
2430
|
+
if (!cache || isCacheStale(cache)) {
|
|
2431
|
+
// 캐시 없음 or 오래됨 → 백그라운드 자동 스캔
|
|
2432
|
+
console.log('[MCP] 캐시 없음. 백그라운드에서 DTO 스캔을 시작합니다...');
|
|
2433
|
+
|
|
2434
|
+
const newCache = await scanProjectMetadata();
|
|
2435
|
+
await saveCache(newCache);
|
|
2436
|
+
|
|
2437
|
+
return newCache;
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
return cache;
|
|
2441
|
+
}
|
|
2442
|
+
```
|
|
2443
|
+
|
|
2444
|
+
#### 대화형 필드 선택 (캐시 기반)
|
|
2445
|
+
|
|
2446
|
+
```
|
|
2447
|
+
[MCP] MemberListRequest DTO에서 6개 필드를 발견했습니다.
|
|
2448
|
+
(캐시: 2024-01-15 10:30:00)
|
|
2449
|
+
|
|
2450
|
+
┌─────────────────────────────────────────────┐
|
|
2451
|
+
│ 1. usrNm (회원명) - string, optional │
|
|
2452
|
+
│ 2. usrEmailAddr (이메일) - string, optional│
|
|
2453
|
+
│ 3. useYn (사용여부) - 'Y'|'N'|'' │
|
|
2454
|
+
│ 4. statusCd (상태코드) - string, optional │
|
|
2455
|
+
│ 5. pageNumber (페이지번호) - number │
|
|
2456
|
+
│ 6. pageSize (페이지크기) - number │
|
|
2457
|
+
└─────────────────────────────────────────────┘
|
|
2458
|
+
|
|
2459
|
+
어떤 필드로 SearchParams를 생성할까요?
|
|
2460
|
+
(번호 입력, 엔터: 전체, q: 종료)
|
|
2461
|
+
```
|
|
2462
|
+
|
|
2463
|
+
### 파일 감시 + 증분 스캔
|
|
2464
|
+
|
|
2465
|
+
#### 자동 업데이트 흐름
|
|
2466
|
+
|
|
2467
|
+
```
|
|
2468
|
+
1. MCP 서버 시작
|
|
2469
|
+
↓
|
|
2470
|
+
2. chokidar watch 시작
|
|
2471
|
+
↓
|
|
2472
|
+
3. DTO 파일 변경 감지
|
|
2473
|
+
↓
|
|
2474
|
+
4. 변경된 DTO만 재스캔 (증분 업데이트)
|
|
2475
|
+
↓
|
|
2476
|
+
5. 캐시 업데이트
|
|
2477
|
+
↓
|
|
2478
|
+
6. 다음 SearchParams 생성 시 최신 캐시 사용
|
|
2479
|
+
```
|
|
2480
|
+
|
|
2481
|
+
#### watch 대상
|
|
2482
|
+
|
|
2483
|
+
```typescript
|
|
2484
|
+
const WATCH_PATTERNS = [
|
|
2485
|
+
'src/services/@interface/dto/**/*.ts',
|
|
2486
|
+
'src/services/**/*Repository.ts',
|
|
2487
|
+
'src/modals/**/*Modal.ts',
|
|
2488
|
+
];
|
|
2489
|
+
```
|
|
2490
|
+
|
|
2491
|
+
#### 증분 업데이트
|
|
2492
|
+
|
|
2493
|
+
```typescript
|
|
2494
|
+
// 변경된 파일만 재스캔
|
|
2495
|
+
watcher.on('change', (filePath) => {
|
|
2496
|
+
if (filePath.includes('/dto/')) {
|
|
2497
|
+
// DTO 변경: 해당 DTO만 재스캔
|
|
2498
|
+
const dto = await scanSingleDto(filePath);
|
|
2499
|
+
cache.dtos[dto.name] = dto;
|
|
2500
|
+
await saveCache(cache);
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
if (filePath.includes('Repository.ts')) {
|
|
2504
|
+
// Repository 변경: 해당 Repository만 재스캔
|
|
2505
|
+
const repo = await scanSingleRepository(filePath);
|
|
2506
|
+
cache.repositories[repo.name] = repo;
|
|
2507
|
+
await saveCache(cache);
|
|
2508
|
+
}
|
|
2509
|
+
});
|
|
2510
|
+
```
|
|
2511
|
+
|
|
2512
|
+
### 장점
|
|
2513
|
+
|
|
2514
|
+
| 장점 | 설명 |
|
|
2515
|
+
|------|------|
|
|
2516
|
+
| **누락 방지** | 전체 DTO를 스캔하여 Request 타입 누락 없음 |
|
|
2517
|
+
| **위치 독립적** | DTO가 어디에 있든 상관없이 자동 발견 |
|
|
2518
|
+
| **Repository 연동** | Repository 메서드 시그니처와 DTO 자동 매칭 |
|
|
2519
|
+
| **빠른 대화형** | 캐시된 데이터로 즉시 필드 선택 가능 |
|
|
2520
|
+
| **자동 업데이트** | 파일 감시로 최신 상태 유지 |
|
|
2521
|
+
| **Fail-safe** | 캐시 없으면 백그라운드 자동 스캔 |
|
|
2522
|
+
|
|
2523
|
+
### MCP 툴 스키마
|
|
2524
|
+
|
|
2525
|
+
#### SCAN 툴
|
|
2526
|
+
|
|
2527
|
+
```typescript
|
|
2528
|
+
{
|
|
2529
|
+
name: 'scan_search_params_metadata',
|
|
2530
|
+
description: 'SearchParams 생성을 위한 프로젝트 메타데이터 수집',
|
|
2531
|
+
inputSchema: {
|
|
2532
|
+
type: 'object',
|
|
2533
|
+
properties: {
|
|
2534
|
+
projectRoot: {
|
|
2535
|
+
type: 'string',
|
|
2536
|
+
description: '프로젝트 루트 경로 (절대 경로)',
|
|
2537
|
+
},
|
|
2538
|
+
forceRescan: {
|
|
2539
|
+
type: 'boolean',
|
|
2540
|
+
description: '강제 재스캔 여부',
|
|
2541
|
+
},
|
|
2542
|
+
},
|
|
2543
|
+
required: ['projectRoot'],
|
|
2544
|
+
},
|
|
2545
|
+
}
|
|
2546
|
+
```
|
|
2547
|
+
|
|
2548
|
+
#### GENERATE 툴
|
|
2549
|
+
|
|
2550
|
+
```typescript
|
|
2551
|
+
{
|
|
2552
|
+
name: 'generate_search_params_interactive',
|
|
2553
|
+
description: 'SearchParams를 대화형으로 생성 (캐시 기반)',
|
|
2554
|
+
inputSchema: {
|
|
2555
|
+
type: 'object',
|
|
2556
|
+
properties: {
|
|
2557
|
+
outPath: {
|
|
2558
|
+
type: 'string',
|
|
2559
|
+
description: '출력 SearchParams 파일 경로 (절대 경로)',
|
|
2560
|
+
},
|
|
2561
|
+
dtoName: {
|
|
2562
|
+
type: 'string',
|
|
2563
|
+
description: 'Request DTO 명 (선택 시 캐시에서 필드 목록 자동 완성)',
|
|
2564
|
+
},
|
|
2565
|
+
},
|
|
2566
|
+
required: ['outPath'],
|
|
2567
|
+
},
|
|
2568
|
+
}
|
|
2569
|
+
```
|
|
2570
|
+
```
|