@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,1168 @@
|
|
|
1
|
+
# NH-FE-B Store 패턴 제약 조건 및 규칙
|
|
2
|
+
|
|
3
|
+
> Store 생성/수정 시 준수해야 할 제약 조건과 규칙을 정리한 문서입니다.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 목차
|
|
8
|
+
|
|
9
|
+
1. [필수 규칙](#1-필수-규칙)
|
|
10
|
+
2. [MetaData 제약 조건](#2-metadata-제약-조건)
|
|
11
|
+
3. [States 제약 조건](#3-states-제약-조건)
|
|
12
|
+
4. [Actions 제약 조건](#4-actions-제약-조건)
|
|
13
|
+
5. [Store.subscribe 제약 조건](#5-storesubscribe-제약-조건)
|
|
14
|
+
6. [네이밍 규칙](#6-네이밍-규칙)
|
|
15
|
+
7. [타입 제약 조건](#7-타입-제약-조건)
|
|
16
|
+
8. [필드 조합 규칙](#8-필드-조합-규칙)
|
|
17
|
+
9. [구현 순서 규칙](#9-구현-순서-규칙)
|
|
18
|
+
10. [금지 패턴](#10-금지-패턴)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 1. 필수 규칙
|
|
23
|
+
|
|
24
|
+
### 1.1 모든 Store는 다음을 포함해야 함
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// 1) 반드시 필요한 imports
|
|
28
|
+
import { AXDGDataItem, AXDGPage, AXDGSortParam } from "@axboot/datagrid";
|
|
29
|
+
import { pageStoreActions } from "@core/stores/pageStoreActions";
|
|
30
|
+
import { PageStoreActions, StoreActions } from "@core/stores/types";
|
|
31
|
+
import { ProgramFn } from "@types";
|
|
32
|
+
import { create } from "zustand";
|
|
33
|
+
import { subscribeWithSelector } from "zustand/middleware";
|
|
34
|
+
import { shallow } from "zustand/shallow";
|
|
35
|
+
import { getTabStoreListener } from "stores";
|
|
36
|
+
|
|
37
|
+
// 2) 반드시 필요한 인터페이스
|
|
38
|
+
interface ListRequest extends ... {}
|
|
39
|
+
interface DtoItem extends ... {}
|
|
40
|
+
interface MetaData { ... }
|
|
41
|
+
interface States extends MetaData { ... }
|
|
42
|
+
interface Actions extends PageStoreActions<States> { ... }
|
|
43
|
+
|
|
44
|
+
// 3) 반드시 필요한 상수/초기화
|
|
45
|
+
const createState: States = { ... };
|
|
46
|
+
const createActions: StoreActions<States & Actions, Actions> = (set, get) => ({ ... });
|
|
47
|
+
|
|
48
|
+
// 4) 반드시 필요한 내보내기
|
|
49
|
+
export interface xxxStore extends States, Actions, PageStoreActions<States> {}
|
|
50
|
+
export const useXXXStore = create(subscribeWithSelector<xxxStore>((set, get) => ({ ... })));
|
|
51
|
+
|
|
52
|
+
// 5) 반드시 필요한 subscribe
|
|
53
|
+
useXXXStore.subscribe(...);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 1.2 pageStoreActions는 항상 마지막에 위치
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// ✅ 올바름
|
|
60
|
+
const createActions: StoreActions<States & Actions, Actions> = (set, get) => ({
|
|
61
|
+
// ... 다른 액션들
|
|
62
|
+
...pageStoreActions(set, get, { createState }),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ❌ 잘못됨
|
|
66
|
+
const createActions: StoreActions<States & Actions, Actions> = (set, get) => ({
|
|
67
|
+
...pageStoreActions(set, get, { createState }),
|
|
68
|
+
// ... 다른 액션들
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 2. MetaData 제약 조건
|
|
75
|
+
|
|
76
|
+
### 2.1 programFn은 항상 첫 번째 필드
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// ✅ 올바름
|
|
80
|
+
interface MetaData {
|
|
81
|
+
programFn?: ProgramFn;
|
|
82
|
+
listRequestValue: ListRequest;
|
|
83
|
+
// ...
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ❌ 잘못됨
|
|
87
|
+
interface MetaData {
|
|
88
|
+
listRequestValue: ListRequest;
|
|
89
|
+
programFn?: ProgramFn;
|
|
90
|
+
// ...
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 2.2 MetaData 필드 순서 (권장)
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
interface MetaData {
|
|
98
|
+
// 1) 항상 첫 번째
|
|
99
|
+
programFn?: ProgramFn;
|
|
100
|
+
|
|
101
|
+
// 2) 리스트 관련 (그룹화)
|
|
102
|
+
listRequestValue: ListRequest;
|
|
103
|
+
listColWidths: number[];
|
|
104
|
+
listSortParams: AXDGSortParam[];
|
|
105
|
+
|
|
106
|
+
// 3) 선택 관련 (그룹화)
|
|
107
|
+
listSelectedRowKey?: React.Key;
|
|
108
|
+
listCheckedIndexes?: number[];
|
|
109
|
+
checkedRowKeys?: React.Key[];
|
|
110
|
+
listCheckedKeys?: React.Key[];
|
|
111
|
+
|
|
112
|
+
// 4) 저장/폼 관련 (그룹화)
|
|
113
|
+
saveRequestValue: SaveRequest;
|
|
114
|
+
detail?: SaveRequest;
|
|
115
|
+
selectedItem?: DtoItem;
|
|
116
|
+
saveRequestValues?: DtoItem;
|
|
117
|
+
|
|
118
|
+
// 5) 폼 상태 관련 (그룹화)
|
|
119
|
+
formActive: boolean;
|
|
120
|
+
flexGrow: number;
|
|
121
|
+
|
|
122
|
+
// 6) 트리 관련 (그룹화)
|
|
123
|
+
expandedKeys: number[] | string[];
|
|
124
|
+
|
|
125
|
+
// 7) 탭 관련 (그룹화)
|
|
126
|
+
activeTabKey: string;
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 2.3 MetaData와 States의 관계
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
// ✅ 올바름: States는 MetaData를 확장해야 함
|
|
134
|
+
interface States extends MetaData {
|
|
135
|
+
routePath?: string;
|
|
136
|
+
listSpinning: boolean;
|
|
137
|
+
listData: AXDGDataItem<DtoItem>[];
|
|
138
|
+
listPage: AXDGPage;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ❌ 잘못됨: MetaData를 확장하지 않음
|
|
142
|
+
interface States {
|
|
143
|
+
routePath?: string;
|
|
144
|
+
listSpinning: boolean;
|
|
145
|
+
// ...
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 2.4 MetaData 필드는 States에도 포함되어야 함
|
|
150
|
+
|
|
151
|
+
MetaData에 정의된 필드는 `getTabStoreListener`를 통해 저장되므로 States에 포함되어야 합니다.
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
// ✅ 올바름
|
|
155
|
+
interface MetaData {
|
|
156
|
+
programFn?: ProgramFn;
|
|
157
|
+
listRequestValue: ListRequest;
|
|
158
|
+
saveRequestValue: SaveRequest;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface States extends MetaData { // MetaData의 필드가 모두 포함됨
|
|
162
|
+
routePath?: string;
|
|
163
|
+
listSpinning: boolean;
|
|
164
|
+
// ...
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Store.subscribe에도 모두 포함
|
|
168
|
+
useXXXStore.subscribe(
|
|
169
|
+
(s): Record<keyof MetaData, any> => ({
|
|
170
|
+
programFn: s.programFn,
|
|
171
|
+
listRequestValue: s.listRequestValue,
|
|
172
|
+
saveRequestValue: s.saveRequestValue,
|
|
173
|
+
}),
|
|
174
|
+
getTabStoreListener<MetaData>(createState.routePath),
|
|
175
|
+
{ equalityFn: shallow },
|
|
176
|
+
);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## 3. States 제약 조건
|
|
182
|
+
|
|
183
|
+
### 3.1 States 필드 순서 (권장)
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
interface States extends MetaData {
|
|
187
|
+
// 1) 시스템 필드 (가장 먼저)
|
|
188
|
+
routePath?: string;
|
|
189
|
+
|
|
190
|
+
// 2) MetaData 필드들 (extends로 자동 포함)
|
|
191
|
+
|
|
192
|
+
// 3) 데이터 관련
|
|
193
|
+
listData: AXDGDataItem<DtoItem>[];
|
|
194
|
+
listPage: AXDGPage;
|
|
195
|
+
selectedItem?: DtoItem;
|
|
196
|
+
detail?: DtoItem;
|
|
197
|
+
|
|
198
|
+
// 4) 로딩 상태 (그룹화)
|
|
199
|
+
listSpinning: boolean;
|
|
200
|
+
excelSpinning: boolean;
|
|
201
|
+
saveSpinning: boolean;
|
|
202
|
+
deleteSpinning: boolean;
|
|
203
|
+
detailSpinning: boolean;
|
|
204
|
+
detailLoading: boolean;
|
|
205
|
+
|
|
206
|
+
// 5) UI 상태
|
|
207
|
+
modalOpen: boolean;
|
|
208
|
+
formActive: boolean;
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### 3.2 Spinning 필드 관련 규칙
|
|
213
|
+
|
|
214
|
+
| Spinning 필드 | 용도 | 필수 조건 |
|
|
215
|
+
|--------------|------|----------|
|
|
216
|
+
| `listSpinning` | 리스트 로딩 | 모든 리스트형 Store 필수 |
|
|
217
|
+
| `excelSpinning` | 엑셀 다운로드 | 엑셀 기능 있을 시만 |
|
|
218
|
+
| `saveSpinning` | 저장 작업 | CRUD Store에서 저장 기능 있을 시 |
|
|
219
|
+
| `deleteSpinning` | 삭제 작업 | CRUD Store에서 삭제 기능 있을 시 |
|
|
220
|
+
| `detailSpinning` | 상세 조회 | 상세 정보 별도 조회 시 |
|
|
221
|
+
| `detailLoading` | 상세 로딩 UI 지연용 | `setSelectedItem`에서 delay 사용 시 |
|
|
222
|
+
| `updateSpinning` | 상태 변경 | 일괄 상태 변경 기능 있을 시 |
|
|
223
|
+
| `copySpinning` | 데이터 복사 | 복사 기능 있을 시 |
|
|
224
|
+
| `treeSpinning` | 트리 로딩 | 트리형 Store에서 |
|
|
225
|
+
| `summarySpinning` | 요약 로딩 | 대시보드형 Store에서 |
|
|
226
|
+
| `list01Spinning` | 리스트1 로딩 | 다중 리스트형 Store에서 |
|
|
227
|
+
| `list02Spinning` | 리스트2 로딩 | 다중 리스트형 Store에서 |
|
|
228
|
+
|
|
229
|
+
### 3.3 States 필드 초기값 규칙
|
|
230
|
+
|
|
231
|
+
| 필드명 | 필수 초기값 | 설명 |
|
|
232
|
+
|--------|------------|------|
|
|
233
|
+
| `routePath` | `undefined` | 초기화되지 않은 상태 |
|
|
234
|
+
| `listSpinning` | `false` | 로딩 아님 |
|
|
235
|
+
| `listData` | `[]` | 빈 배열 |
|
|
236
|
+
| `listPage` | `{ currentPage: 0, totalPages: 0 }` | 빈 페이지 상태 |
|
|
237
|
+
| `listColWidths` | `[]` | 빈 배열 (사용자가 설정) |
|
|
238
|
+
| `listSortParams` | `[]` | 빈 배열 (사용자가 설정) |
|
|
239
|
+
| `listSelectedRowKey` | `""` | 빈 문자열 (선택 없음) |
|
|
240
|
+
| `checkedRowKeys` | `[]` | 빈 배열 (체크 없음) |
|
|
241
|
+
| `selectedItem` | `undefined` | 선택 없음 |
|
|
242
|
+
| `detail` | `undefined` | 상세 없음 |
|
|
243
|
+
| `modalOpen` | `false` | 모달 닫힘 |
|
|
244
|
+
| `formActive` | `false` | 폼 비활성 |
|
|
245
|
+
| `expandedKeys` | `[]` | 빈 배열 (모두 접힘) |
|
|
246
|
+
|
|
247
|
+
### 3.4 고정값 필드 규칙
|
|
248
|
+
|
|
249
|
+
고정값이 필요한 경우 상수로 선언 후 States와 listRequestValue에 모두 포함:
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// ✅ 올바름
|
|
253
|
+
const bbsNo = 26; // 상수 선언
|
|
254
|
+
|
|
255
|
+
const createState: States = {
|
|
256
|
+
bbsNo, // States에 포함
|
|
257
|
+
listRequestValue: {
|
|
258
|
+
bbsNo, // listRequestValue에도 포함
|
|
259
|
+
pageNumber: 1,
|
|
260
|
+
pageSize: 100,
|
|
261
|
+
},
|
|
262
|
+
// ...
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// API 호출 시 사용
|
|
266
|
+
callListApi: async (request) => {
|
|
267
|
+
const apiParam = {
|
|
268
|
+
...get().listRequestValue,
|
|
269
|
+
...request,
|
|
270
|
+
bbsNo: get().bbsNo, // 고정값 사용
|
|
271
|
+
};
|
|
272
|
+
// ...
|
|
273
|
+
};
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## 4. Actions 제약 조건
|
|
279
|
+
|
|
280
|
+
### 4.1 Actions는 반드시 PageStoreActions를 확장
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// ✅ 올바름
|
|
284
|
+
interface Actions extends PageStoreActions<States> {
|
|
285
|
+
setListRequestValue: (requestValue: ListRequest) => void;
|
|
286
|
+
// ...
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ❌ 잘못됨
|
|
290
|
+
interface Actions {
|
|
291
|
+
setListRequestValue: (requestValue: ListRequest) => void;
|
|
292
|
+
// ...
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### 4.2 Actions 필드 순서 (권장)
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
interface Actions extends PageStoreActions<States> {
|
|
300
|
+
// 1) Setter들 (그룹화)
|
|
301
|
+
setListRequestValue: ...;
|
|
302
|
+
setListColWidths: ...;
|
|
303
|
+
setListSpinning: ...;
|
|
304
|
+
setListSortParams: ...;
|
|
305
|
+
setExcelSpinning: ...;
|
|
306
|
+
setSaveRequestValue: ...;
|
|
307
|
+
setSelectedItem: ...;
|
|
308
|
+
setCheckedRowKeys: ...;
|
|
309
|
+
setListSelectedRowKey: ...;
|
|
310
|
+
|
|
311
|
+
// 2) API 호출들 (그룹화)
|
|
312
|
+
callListApi: ...;
|
|
313
|
+
callListApi01: ...;
|
|
314
|
+
callListApi02: ...;
|
|
315
|
+
callSummaryApi: ...;
|
|
316
|
+
callTreeApi: ...;
|
|
317
|
+
callSaveApi: ...;
|
|
318
|
+
callDeleteApi: ...;
|
|
319
|
+
callExcelDownloadApi: ...;
|
|
320
|
+
|
|
321
|
+
// 3) 페이지 변경 (그룹화)
|
|
322
|
+
changeListPage: ...;
|
|
323
|
+
changeList01Page: ...;
|
|
324
|
+
changeList02Page: ...;
|
|
325
|
+
|
|
326
|
+
// 4) 기타 액션들
|
|
327
|
+
setModalOpen: ...;
|
|
328
|
+
setFormActive: ...;
|
|
329
|
+
cancelFormActive: ...;
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### 4.3 필수 액션 vs 선택 액션
|
|
334
|
+
|
|
335
|
+
| 액션명 | 필수여부 | 조건 |
|
|
336
|
+
|--------|----------|------|
|
|
337
|
+
| `syncMetadata` | **필수** | 항상 |
|
|
338
|
+
| `onMountApp` | **필수** | 항상 |
|
|
339
|
+
| `setListRequestValue` | **필수** | 리스트형 Store |
|
|
340
|
+
| `setListColWidths` | **필수** | 리스트형 Store |
|
|
341
|
+
| `setListSpinning` | **필수** | 리스트형 Store |
|
|
342
|
+
| `setListSortParams` | **필수** | 리스트형 Store |
|
|
343
|
+
| `callListApi` | **필수** | 리스트형 Store |
|
|
344
|
+
| `changeListPage` | **필수** | 리스트형 Store |
|
|
345
|
+
| `setExcelSpinning` | 선택 | 엑셀 기능 있을 때 |
|
|
346
|
+
| `callExcelDownloadApi` | 선택 | 엑셀 기능 있을 때 |
|
|
347
|
+
| `setSelectedItem` | 선택 | 상세/폼 기능 있을 때 |
|
|
348
|
+
| `setSaveRequestValue` | 선택 | 저장 기능 있을 때 |
|
|
349
|
+
| `setCheckedRowKeys` | 선택 | 체크박스 있을 때 |
|
|
350
|
+
| `setListSelectedRowKey` | 선택 | 단일 선택 기능 있을 때 |
|
|
351
|
+
| `callSaveApi` | 선택 | 저장 기능 있을 때 |
|
|
352
|
+
| `callDeleteApi` | 선택 | 삭제 기능 있을 때 |
|
|
353
|
+
| `setModalOpen` | 선택 | 모달 기능 있을 때 |
|
|
354
|
+
| `setFormActive` | 선택 | 폼 활성화 기능 있을 때 |
|
|
355
|
+
| `cancelFormActive` | 선택 | 폼 활성화 기능 있을 때 |
|
|
356
|
+
|
|
357
|
+
### 4.4 callListApi 구현 규칙
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
// ✅ 올바른 callListApi 구현 패턴
|
|
361
|
+
callListApi: async (request) => {
|
|
362
|
+
// 1) 중복 호출 방지 (필수)
|
|
363
|
+
if (get().listSpinning) return;
|
|
364
|
+
set({ listSpinning: true });
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
// 2) 파라미터 병합
|
|
368
|
+
const apiParam: ListRequest = { ...get().listRequestValue, ...request };
|
|
369
|
+
|
|
370
|
+
// 3) API 호출 (deleteEmptyValue 필수)
|
|
371
|
+
const response = await XXXService.postXXXList(
|
|
372
|
+
deleteEmptyValue({ ...apiParam })
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
// 4) 상태 업데이트 (listRequestValue 포함)
|
|
376
|
+
set({
|
|
377
|
+
listRequestValue: apiParam, // 현재 요청값 저장
|
|
378
|
+
listData: response.ds.map((values) => ({ values })),
|
|
379
|
+
listPage: {
|
|
380
|
+
currentPage: response.page.pageNumber ?? 1,
|
|
381
|
+
pageSize: response.page.pageSize ?? 0,
|
|
382
|
+
totalPages: response.page.pageCount ?? 0,
|
|
383
|
+
totalElements: response.page?.totalCount,
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// 5) 응답 반환 (선택사항)
|
|
388
|
+
return response;
|
|
389
|
+
} finally {
|
|
390
|
+
// 6) 스피닝 해제 (필수)
|
|
391
|
+
set({ listSpinning: false });
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### 4.5 Actions와 States의 일치 규칙
|
|
397
|
+
|
|
398
|
+
모든 Actions에서 set/get하는 필드는 States에 정의되어 있어야 합니다.
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
// ✅ 올바름
|
|
402
|
+
interface States {
|
|
403
|
+
listSpinning: boolean;
|
|
404
|
+
selectedItem?: DtoItem;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const createActions = (set, get) => ({
|
|
408
|
+
setListSpinning: (spinning) => set({ listSpinning: spinning }),
|
|
409
|
+
setSelectedItem: (item) => set({ selectedItem: item }),
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// ❌ 잘못됨: States에 없는 필드를 사용
|
|
413
|
+
interface States {
|
|
414
|
+
listSpinning: boolean;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const createActions = (set, get) => ({
|
|
418
|
+
setSelectedItem: (item) => set({ selectedItem: item }), // States에 없음!
|
|
419
|
+
});
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## 5. Store.subscribe 제약 조건
|
|
425
|
+
|
|
426
|
+
### 5.1 subscribe는 항상 Store 생성 후에 위치
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
// ✅ 올바름
|
|
430
|
+
export const useXXXStore = create(
|
|
431
|
+
subscribeWithSelector<xxxStore>((set, get) => ({
|
|
432
|
+
...createState,
|
|
433
|
+
...createActions(set, get),
|
|
434
|
+
})),
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
useXXXStore.subscribe( // Store 생성 후
|
|
438
|
+
(s): Record<keyof MetaData, any> => ({ ... }),
|
|
439
|
+
getTabStoreListener<MetaData>(createState.routePath),
|
|
440
|
+
{ equalityFn: shallow },
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
// ❌ 잘못됨
|
|
444
|
+
useXXXStore.subscribe( // Store 생성 전
|
|
445
|
+
(s): Record<keyof MetaData, any> => ({ ... }),
|
|
446
|
+
getTabStoreListener<MetaData>(createState.routePath),
|
|
447
|
+
{ equalityFn: shallow },
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
export const useXXXStore = create(
|
|
451
|
+
subscribeWithSelector<xxxStore>((set, get) => ({
|
|
452
|
+
...createState,
|
|
453
|
+
...createActions(set, get),
|
|
454
|
+
})),
|
|
455
|
+
);
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### 5.2 subscribe selector는 MetaData의 모든 필드를 포함해야 함
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
// ✅ 올바름
|
|
462
|
+
interface MetaData {
|
|
463
|
+
programFn?: ProgramFn;
|
|
464
|
+
listRequestValue: ListRequest;
|
|
465
|
+
listColWidths: number[];
|
|
466
|
+
listSortParams: AXDGSortParam[];
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
useXXXStore.subscribe(
|
|
470
|
+
(s): Record<keyof MetaData, any> => ({
|
|
471
|
+
programFn: s.programFn, // ✅
|
|
472
|
+
listRequestValue: s.listRequestValue, // ✅
|
|
473
|
+
listColWidths: s.listColWidths, // ✅
|
|
474
|
+
listSortParams: s.listSortParams, // ✅
|
|
475
|
+
}),
|
|
476
|
+
getTabStoreListener<MetaData>(createState.routePath),
|
|
477
|
+
{ equalityFn: shallow },
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
// ❌ 잘못됨: MetaData 필드 누락
|
|
481
|
+
useXXXStore.subscribe(
|
|
482
|
+
(s): Record<keyof MetaData, any> => ({
|
|
483
|
+
programFn: s.programFn,
|
|
484
|
+
listRequestValue: s.listRequestValue,
|
|
485
|
+
// listColWidths 누락! ❌
|
|
486
|
+
// listSortParams 누락! ❌
|
|
487
|
+
}),
|
|
488
|
+
getTabStoreListener<MetaData>(createState.routePath),
|
|
489
|
+
{ equalityFn: shallow },
|
|
490
|
+
);
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### 5.3 subscribe selector는 순서를 유지해야 함 (권장)
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
// ✅ 권장: MetaData 필드 순서와 동일하게
|
|
497
|
+
interface MetaData {
|
|
498
|
+
programFn?: ProgramFn;
|
|
499
|
+
listRequestValue: ListRequest;
|
|
500
|
+
listColWidths: number[];
|
|
501
|
+
listSortParams: AXDGSortParam[];
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
useXXXStore.subscribe(
|
|
505
|
+
(s): Record<keyof MetaData, any> => ({
|
|
506
|
+
programFn: s.programFn, // 1) 첫 번째
|
|
507
|
+
listSortParams: s.listSortParams, // 2) 정렬 파라미터
|
|
508
|
+
listRequestValue: s.listRequestValue, // 3) 요청값
|
|
509
|
+
listColWidths: s.listColWidths, // 4) 컬럼 너비
|
|
510
|
+
}),
|
|
511
|
+
getTabStoreListener<MetaData>(createState.routePath),
|
|
512
|
+
{ equalityFn: shallow },
|
|
513
|
+
);
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### 5.4 equalityFn은 항상 shallow
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
// ✅ 올바름
|
|
520
|
+
useXXXStore.subscribe(
|
|
521
|
+
(s) => ({ ... }),
|
|
522
|
+
getTabStoreListener<MetaData>(createState.routePath),
|
|
523
|
+
{ equalityFn: shallow }, // 항상 shallow
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
// ❌ 잘못됨
|
|
527
|
+
useXXXStore.subscribe(
|
|
528
|
+
(s) => ({ ... }),
|
|
529
|
+
getTabStoreListener<MetaData>(createState.routePath),
|
|
530
|
+
// equalityFn 누락
|
|
531
|
+
);
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
|
|
536
|
+
## 6. 네이밍 규칙
|
|
537
|
+
|
|
538
|
+
### 6.1 Store 인터페이스 네이밍
|
|
539
|
+
|
|
540
|
+
```
|
|
541
|
+
패턴: [소문자 camelCase] + "Store"
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
```typescript
|
|
545
|
+
// ✅ 올바름
|
|
546
|
+
export interface memberListStore extends States, Actions, PageStoreActions<States> {}
|
|
547
|
+
export interface productMasterStore extends States, Actions, PageStoreActions<States> {}
|
|
548
|
+
export interface orderClaimReturnStore extends States, Actions, PageStoreActions<States> {}
|
|
549
|
+
export interface kdhFinancialHighlightsStore extends States, Actions, PageStoreActions<States> {}
|
|
550
|
+
|
|
551
|
+
// ❌ 잘못됨
|
|
552
|
+
export interface MemberListStore ... // 대문자 시작
|
|
553
|
+
export interface member_list_store ... // 스네이크 케이스
|
|
554
|
+
export interface Memberliststore ... // 잘못된 케이스
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### 6.2 Hook 네이밍
|
|
558
|
+
|
|
559
|
+
```
|
|
560
|
+
패턴: "use" + [PascalCase] + "Store"
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
```typescript
|
|
564
|
+
// ✅ 올바름
|
|
565
|
+
export const useMemberListStore = create(...);
|
|
566
|
+
export const useProductMasterStore = create(...);
|
|
567
|
+
export const useOrderClaimReturnStore = create(...);
|
|
568
|
+
export const useKdhFinancialHighlightsStore = create(...);
|
|
569
|
+
|
|
570
|
+
// ❌ 잘못됨
|
|
571
|
+
export const usememberListStore ... // 소문자 시작
|
|
572
|
+
export const useMemberlistStore ... // 잘못된 케이스
|
|
573
|
+
export const memberListStore ... // use 없음
|
|
574
|
+
export const MemberListStore ... // use 없고 대문자
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### 6.3 Actions 네이밍
|
|
578
|
+
|
|
579
|
+
| 용도 | 네이밍 패턴 | 예시 |
|
|
580
|
+
|------|------------|------|
|
|
581
|
+
| 요청값 설정 | `set[Target]RequestValue` | `setListRequestValue`, `setSummaryRequestValue` |
|
|
582
|
+
| 데이터 설정 | `set[Target]` | `setSelectedItem`, `setTreeData` |
|
|
583
|
+
| 컬럼 너비 설정 | `setListColWidths` | `setListColWidths` (복수형) |
|
|
584
|
+
| 스피닝 설정 | `set[Target]Spinning` | `setListSpinning`, `setExcelSpinning` |
|
|
585
|
+
| 정렬 설정 | `setListSortParams` | `setListSortParams` (복수형) |
|
|
586
|
+
| 체크 설정 | `setChecked[Target]s` | `setCheckedRowKeys` (복수형) |
|
|
587
|
+
| 선택 설정 | `set[Target]Selected` | `setListSelectedRowKey` |
|
|
588
|
+
| API 호출 | `call[Target]Api` | `callListApi`, `callSaveApi`, `callTreeApi` |
|
|
589
|
+
| 페이지 변경 | `change[Target]Page` | `changeListPage` |
|
|
590
|
+
| 폼 활성화 | `setFormActive`, `cancelFormActive` | `setFormActive`, `cancelFormActive` |
|
|
591
|
+
| 모달 열기 | `setModalOpen` | `setModalOpen` |
|
|
592
|
+
|
|
593
|
+
### 6.4 States 필드 네이밍
|
|
594
|
+
|
|
595
|
+
| 용도 | 네이밍 패턴 | 복수형 여부 | 예시 |
|
|
596
|
+
|------|------------|-----------|------|
|
|
597
|
+
| 요청값 | `[target]RequestValue` | 단수 | `listRequestValue`, `summaryRequestValue` |
|
|
598
|
+
| 데이터 | `[target]Data` | 단수 | `listData`, `treeData`, `summaryData` |
|
|
599
|
+
| 페이지 | `[target]Page` | 단수 | `listPage`, `list01Page` |
|
|
600
|
+
| 스피닝 | `[target]Spinning` | 단수 | `listSpinning`, `excelSpinning` |
|
|
601
|
+
| 로딩 | `[target]Loading` | 단수 | `detailLoading` |
|
|
602
|
+
| 정렬 파라미터 | `[target]SortParams` | **복수** | `listSortParams`, `list01SortParams` |
|
|
603
|
+
| 컬럼 너비 | `listColWidths` | **복수** | `listColWidths` |
|
|
604
|
+
| 선택된 행 키 | `listSelectedRowKey` | 단수 | `listSelectedRowKey` |
|
|
605
|
+
| 체크된 키들 | `checkedRowKeys` | **복수** | `checkedRowKeys` |
|
|
606
|
+
| 체크된 인덱스들 | `listCheckedIndexes` | **복수** | `listCheckedIndexes` |
|
|
607
|
+
| 확장된 키들 | `expandedKeys` | **복수** | `expandedKeys` |
|
|
608
|
+
| 선택된 아이템 | `selectedItem` | 단수 | `selectedItem` |
|
|
609
|
+
|
|
610
|
+
### 6.5 상수 네이밍
|
|
611
|
+
|
|
612
|
+
```typescript
|
|
613
|
+
// ✅ 올바름
|
|
614
|
+
const createState: States = { ... };
|
|
615
|
+
const createActions: StoreActions<States & Actions, Actions> = (set, get) => ({ ... });
|
|
616
|
+
|
|
617
|
+
// ❌ 잘못됨
|
|
618
|
+
const initialState: States = { ... }; // createState 아님
|
|
619
|
+
const actions: StoreActions<States & Actions, Actions> = (set, get) => ({ ... }); // createActions 아님
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
---
|
|
623
|
+
|
|
624
|
+
## 7. 타입 제약 조건
|
|
625
|
+
|
|
626
|
+
### 7.1 ListRequest 타입
|
|
627
|
+
|
|
628
|
+
```typescript
|
|
629
|
+
// ✅ 올바름: Repository Request 타입을 확장
|
|
630
|
+
interface ListRequest extends PostXXXListRequest {
|
|
631
|
+
dateRange?: [string, string]; // 추가 필드
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// 또는 Partial 사용
|
|
635
|
+
interface ListRequest extends Partial<PostXXXListRequest> {
|
|
636
|
+
dateRange?: [string, string];
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// ❌ 잘못됨: 확장하지 않음
|
|
640
|
+
interface ListRequest {
|
|
641
|
+
pageNumber: number;
|
|
642
|
+
pageSize: number;
|
|
643
|
+
// 모든 필드를 다시 정의 (비효율)
|
|
644
|
+
}
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### 7.2 DtoItem 타입
|
|
648
|
+
|
|
649
|
+
```typescript
|
|
650
|
+
// ✅ 올바름: Repository Response 타입을 확장
|
|
651
|
+
interface DtoItem extends XXXRes {}
|
|
652
|
+
|
|
653
|
+
// ❌ 잘못됨: 확장하지 않음
|
|
654
|
+
interface DtoItem {
|
|
655
|
+
id: number;
|
|
656
|
+
name: string;
|
|
657
|
+
// 모든 필드를 다시 정의
|
|
658
|
+
}
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
### 7.3 SaveRequest 타입
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
// ✅ 올바름: Repository Save Request 타입을 확장
|
|
665
|
+
interface SaveRequest extends PostXXXSaveRequest {}
|
|
666
|
+
|
|
667
|
+
// 또는 별도 정의
|
|
668
|
+
interface SaveRequest {
|
|
669
|
+
// 필드들...
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// ❌ 잘못됨: Request와 Response를 혼용
|
|
673
|
+
interface SaveRequest extends XXXRes {} // Response를 확장 (잘못됨)
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### 7.4 States 타입
|
|
677
|
+
|
|
678
|
+
```typescript
|
|
679
|
+
// ✅ 올바름: States는 MetaData를 확장
|
|
680
|
+
interface States extends MetaData {
|
|
681
|
+
routePath?: string;
|
|
682
|
+
listSpinning: boolean;
|
|
683
|
+
listData: AXDGDataItem<DtoItem>[];
|
|
684
|
+
listPage: AXDGPage;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// ✅ 올바름: Actions는 PageStoreActions를 확장
|
|
688
|
+
interface Actions extends PageStoreActions<States> {
|
|
689
|
+
setListRequestValue: ...;
|
|
690
|
+
// ...
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// ❌ 잘못됨: States가 MetaData를 확장하지 않음
|
|
694
|
+
interface States {
|
|
695
|
+
routePath?: string;
|
|
696
|
+
listSpinning: boolean;
|
|
697
|
+
// MetaData 필드들이 누락됨
|
|
698
|
+
}
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
### 7.5 Store 타입 (내보내기)
|
|
702
|
+
|
|
703
|
+
```typescript
|
|
704
|
+
// ✅ 올바름: States, Actions, PageStoreActions<States> 모두 확장
|
|
705
|
+
export interface xxxStore extends States, Actions, PageStoreActions<States> {}
|
|
706
|
+
|
|
707
|
+
// ❌ 잘못됨: PageStoreActions 누락
|
|
708
|
+
export interface xxxStore extends States, Actions {}
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
---
|
|
712
|
+
|
|
713
|
+
## 8. 필드 조합 규칙
|
|
714
|
+
|
|
715
|
+
### 8.1 필수 필드 조합
|
|
716
|
+
|
|
717
|
+
다음 필드들은 함께 사용되어야 합니다:
|
|
718
|
+
|
|
719
|
+
| 그룹 | 함께 사용해야 하는 필드 |
|
|
720
|
+
|------|----------------------|
|
|
721
|
+
| **기본 리스트** | `listRequestValue`, `listColWidths`, `listSortParams`, `listSpinning`, `listData`, `listPage`, `callListApi`, `changeListPage` |
|
|
722
|
+
| **엑셀 다운로드** | `excelSpinning`, `setExcelSpinning`, `callExcelDownloadApi` |
|
|
723
|
+
| **상세/폼** | `saveRequestValue`, `selectedItem`, `setSelectedItem`, `setSaveRequestValue` |
|
|
724
|
+
| **단일 선택** | `listSelectedRowKey`, `setListSelectedRowKey` |
|
|
725
|
+
| **다중 선택(체크박스)** | `checkedRowKeys`, `setCheckedRowKeys` |
|
|
726
|
+
| **다중 선택(인덱스)** | `listCheckedIndexes`, `setListCheckedIndexes` |
|
|
727
|
+
| **CRUD 저장** | `saveSpinning`, `setSaveSpinning`, `saveRequestValue`, `callSaveApi` |
|
|
728
|
+
| **CRUD 삭제** | `deleteSpinning`, `callDeleteApi` |
|
|
729
|
+
| **CRUD 폼** | `formActive`, `setFormActive`, `cancelFormActive` |
|
|
730
|
+
| **모달** | `modalOpen`, `setModalOpen` |
|
|
731
|
+
| **트리** | `treeSpinning`, `treeData`, `expandedKeys`, `callTreeApi` |
|
|
732
|
+
| **대시보드** | `summaryRequestValue`, `summaryData`, `callSummaryApi` |
|
|
733
|
+
|
|
734
|
+
### 8.2 상호 배타적 필드 조합
|
|
735
|
+
|
|
736
|
+
다음 필드들은 함께 사용할 수 없습니다:
|
|
737
|
+
|
|
738
|
+
| 그룹 | 선택해야 할 필드 | 함께 사용하면 안 되는 필드 |
|
|
739
|
+
|------|------------------|----------------------|
|
|
740
|
+
| **데이터 구조** | `listData`, `listPage` 또는 `treeData` | `listData` + `treeData` 함께 사용 ❌ |
|
|
741
|
+
| **스피닝** | `listSpinning` 또는 `treeSpinning` | `listSpinning` + `treeSpinning` 함께 사용 ❌ |
|
|
742
|
+
| **요청값** | `listRequestValue` 또는 `summaryRequestValue` | 목적에 따라 선택 |
|
|
743
|
+
|
|
744
|
+
### 8.3 의존 필드 조합
|
|
745
|
+
|
|
746
|
+
특정 필드를 사용할 때 다른 필드도 함께 필요한 경우:
|
|
747
|
+
|
|
748
|
+
| 필드 | 함께 필요한 필드 | 이유 |
|
|
749
|
+
|------|------------------|------|
|
|
750
|
+
| `callExcelDownloadApi` | `excelSpinning`, `setExcelSpinning` | 엑셀 다운로드 로딩 상태 |
|
|
751
|
+
| `setSelectedItem` | `selectedItem`, `detailLoading` | 선택 및 로딩 상태 |
|
|
752
|
+
| `callSaveApi` | `saveSpinning`, `saveRequestValue` | 저장 로딩 및 데이터 |
|
|
753
|
+
| `callDeleteApi` | `deleteSpinning` | 삭제 로딩 상태 |
|
|
754
|
+
| `setListSelectedRowKey` | `listSelectedRowKey` | 선택된 행 키 상태 |
|
|
755
|
+
| `setCheckedRowKeys` | `checkedRowKeys` | 체크된 키 상태 |
|
|
756
|
+
| `setExpandedKeys` | `expandedKeys` | 확장된 키 상태 |
|
|
757
|
+
| `changeListPage` | `callListApi` | 페이지 변경은 리스트 API 호출 |
|
|
758
|
+
| `setFormActive` | `formActive` | 폼 활성화 상태 |
|
|
759
|
+
|
|
760
|
+
### 8.4 MetaData ↔ Store.subscribe 매칭
|
|
761
|
+
|
|
762
|
+
MetaData에 정의된 모든 필드는 Store.subscribe에 포함되어야 합니다.
|
|
763
|
+
|
|
764
|
+
```typescript
|
|
765
|
+
// ✅ 올바름: 매칭됨
|
|
766
|
+
interface MetaData {
|
|
767
|
+
programFn?: ProgramFn;
|
|
768
|
+
listRequestValue: ListRequest;
|
|
769
|
+
listColWidths: number[];
|
|
770
|
+
listSortParams: AXDGSortParam[];
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
useXXXStore.subscribe(
|
|
774
|
+
(s): Record<keyof MetaData, any> => ({
|
|
775
|
+
programFn: s.programFn, // ✅ MetaData에 있음
|
|
776
|
+
listRequestValue: s.listRequestValue, // ✅ MetaData에 있음
|
|
777
|
+
listColWidths: s.listColWidths, // ✅ MetaData에 있음
|
|
778
|
+
listSortParams: s.listSortParams, // ✅ MetaData에 있음
|
|
779
|
+
}),
|
|
780
|
+
getTabStoreListener<MetaData>(createState.routePath),
|
|
781
|
+
{ equalityFn: shallow },
|
|
782
|
+
);
|
|
783
|
+
|
|
784
|
+
// ❌ 잘못됨: 불일치
|
|
785
|
+
interface MetaData {
|
|
786
|
+
programFn?: ProgramFn;
|
|
787
|
+
listRequestValue: ListRequest;
|
|
788
|
+
listColWidths: number[];
|
|
789
|
+
listSortParams: AXDGSortParam[];
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
useXXXStore.subscribe(
|
|
793
|
+
(s): Record<keyof MetaData, any> => ({
|
|
794
|
+
programFn: s.programFn,
|
|
795
|
+
listRequestValue: s.listRequestValue,
|
|
796
|
+
// listColWidths 누락! ❌
|
|
797
|
+
// listSortParams 누락! ❌
|
|
798
|
+
}),
|
|
799
|
+
getTabStoreListener<MetaData>(createState.routePath),
|
|
800
|
+
{ equalityFn: shallow },
|
|
801
|
+
);
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
---
|
|
805
|
+
|
|
806
|
+
## 9. 구현 순서 규칙
|
|
807
|
+
|
|
808
|
+
### 9.1 파일 내 순서
|
|
809
|
+
|
|
810
|
+
```typescript
|
|
811
|
+
// 1) Imports (가장 먼저)
|
|
812
|
+
import { ... } from "...";
|
|
813
|
+
|
|
814
|
+
// 2) Interface 정의
|
|
815
|
+
interface ListRequest ... {}
|
|
816
|
+
interface DtoItem ... {}
|
|
817
|
+
interface SaveRequest ... {} // 필요시
|
|
818
|
+
interface RemoveRequest ... {} // 필요시
|
|
819
|
+
|
|
820
|
+
interface MetaData ... {}
|
|
821
|
+
interface States extends MetaData ... {}
|
|
822
|
+
interface Actions extends PageStoreActions<States> ... {}
|
|
823
|
+
|
|
824
|
+
// 3) 상수/초기화
|
|
825
|
+
const createState: States = { ... };
|
|
826
|
+
const createActions: StoreActions<States & Actions, Actions> = (set, get) => ({ ... });
|
|
827
|
+
|
|
828
|
+
// 4) 내보내기
|
|
829
|
+
export interface xxxStore ... {}
|
|
830
|
+
export const useXXXStore = create(...);
|
|
831
|
+
|
|
832
|
+
// 5) Subscribe (가장 마지막)
|
|
833
|
+
useXXXStore.subscribe(...);
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
### 9.2 createActions 내 순서
|
|
837
|
+
|
|
838
|
+
```typescript
|
|
839
|
+
const createActions: StoreActions<States & Actions, Actions> = (set, get) => ({
|
|
840
|
+
// 1) pageStoreActions 또는 syncMetadata (가장 먼저 또는 마지막)
|
|
841
|
+
...pageStoreActions(set, get, { createState }),
|
|
842
|
+
|
|
843
|
+
// 또는
|
|
844
|
+
syncMetadata: (s = createState) => set(s),
|
|
845
|
+
|
|
846
|
+
// 2) onMountApp
|
|
847
|
+
onMountApp: async () => { ... },
|
|
848
|
+
|
|
849
|
+
// 3) Setter들 (알파벳 순 또는 그룹별)
|
|
850
|
+
setListRequestValue: ...,
|
|
851
|
+
setListColWidths: ...,
|
|
852
|
+
setListSpinning: ...,
|
|
853
|
+
setListSortParams: ...,
|
|
854
|
+
|
|
855
|
+
// 4) API 호출들
|
|
856
|
+
callListApi: ...,
|
|
857
|
+
callSaveApi: ...,
|
|
858
|
+
callDeleteApi: ...,
|
|
859
|
+
callExcelDownloadApi: ...,
|
|
860
|
+
|
|
861
|
+
// 5) 페이지 변경
|
|
862
|
+
changeListPage: ...,
|
|
863
|
+
|
|
864
|
+
// 6) 기타 액션들
|
|
865
|
+
setSelectedItem: ...,
|
|
866
|
+
setCheckedRowKeys: ...,
|
|
867
|
+
});
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
### 9.3 Store 내 요소 순서
|
|
871
|
+
|
|
872
|
+
```typescript
|
|
873
|
+
// 1) createState 내 필드 순서
|
|
874
|
+
const createState: States = {
|
|
875
|
+
// 1) 고정값들 (가장 먼저)
|
|
876
|
+
bbsNo: 26,
|
|
877
|
+
|
|
878
|
+
// 2) MetaData 필드들
|
|
879
|
+
listRequestValue: { ... },
|
|
880
|
+
listColWidths: [],
|
|
881
|
+
listSortParams: [],
|
|
882
|
+
|
|
883
|
+
// 3) UI 상태
|
|
884
|
+
listSpinning: false,
|
|
885
|
+
modalOpen: false,
|
|
886
|
+
formActive: false,
|
|
887
|
+
|
|
888
|
+
// 4) 데이터
|
|
889
|
+
listData: [],
|
|
890
|
+
listPage: { ... },
|
|
891
|
+
selectedItem: undefined,
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
// 2) createActions 내 액션 순서
|
|
895
|
+
const createActions: StoreActions<States & Actions, Actions> = (set, get) => ({
|
|
896
|
+
// 시스템 액션 → Setter → API → 페이지 변경 → 기타
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
// 3) Store.subscribe 내 필드 순서
|
|
900
|
+
useXXXStore.subscribe(
|
|
901
|
+
(s): Record<keyof MetaData, any> => ({
|
|
902
|
+
// MetaData 필드 순서와 동일하게
|
|
903
|
+
programFn: ...,
|
|
904
|
+
listSortParams: ...,
|
|
905
|
+
listRequestValue: ...,
|
|
906
|
+
listColWidths: ...,
|
|
907
|
+
}),
|
|
908
|
+
...
|
|
909
|
+
);
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
---
|
|
913
|
+
|
|
914
|
+
## 10. 금지 패턴
|
|
915
|
+
|
|
916
|
+
### 10.1 금지: States에 MetaData를 확장하지 않음
|
|
917
|
+
|
|
918
|
+
```typescript
|
|
919
|
+
// ❌ 금지
|
|
920
|
+
interface States {
|
|
921
|
+
routePath?: string;
|
|
922
|
+
listSpinning: boolean;
|
|
923
|
+
// MetaData 필드들이 누락됨!
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// ✅ 해결
|
|
927
|
+
interface States extends MetaData {
|
|
928
|
+
routePath?: string;
|
|
929
|
+
listSpinning: boolean;
|
|
930
|
+
}
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
### 10.2 금지: pageStoreActions를 잘못된 위치에 배치
|
|
934
|
+
|
|
935
|
+
```typescript
|
|
936
|
+
// ❌ 금지: pageStoreActions가 첫 번째에 있음
|
|
937
|
+
const createActions = (set, get) => ({
|
|
938
|
+
...pageStoreActions(set, get, { createState }),
|
|
939
|
+
syncMetadata: (s = createState) => set(s),
|
|
940
|
+
onMountApp: async () => {},
|
|
941
|
+
// ...
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
// ✅ 해결: pageStoreActions를 마지막으로
|
|
945
|
+
const createActions = (set, get) => ({
|
|
946
|
+
syncMetadata: (s = createState) => set(s),
|
|
947
|
+
onMountApp: async () => {},
|
|
948
|
+
// ...
|
|
949
|
+
...pageStoreActions(set, get, { createState }),
|
|
950
|
+
});
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
### 10.3 금지: listRequestValue를 업데이트하지 않음
|
|
954
|
+
|
|
955
|
+
```typescript
|
|
956
|
+
// ❌ 금지: callListApi에서 listRequestValue를 업데이트하지 않음
|
|
957
|
+
callListApi: async (request) => {
|
|
958
|
+
const response = await XXXService.postXXXList(...);
|
|
959
|
+
set({
|
|
960
|
+
// listRequestValue 누락! ❌
|
|
961
|
+
listData: response.ds.map((values) => ({ values })),
|
|
962
|
+
listPage: { ... },
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// ✅ 해결: listRequestValue 업데이트 포함
|
|
967
|
+
callListApi: async (request) => {
|
|
968
|
+
const apiParam = { ...get().listRequestValue, ...request };
|
|
969
|
+
const response = await XXXService.postXXXList(...);
|
|
970
|
+
set({
|
|
971
|
+
listRequestValue: apiParam, // ✅ 포함
|
|
972
|
+
listData: response.ds.map((values) => ({ values })),
|
|
973
|
+
listPage: { ... },
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
### 10.4 금지: deleteEmptyValue를 사용하지 않음
|
|
979
|
+
|
|
980
|
+
```typescript
|
|
981
|
+
// ❌ 금지: deleteEmptyValue 미사용
|
|
982
|
+
const response = await XXXService.postXXXList(apiParam);
|
|
983
|
+
|
|
984
|
+
// ✅ 해결: deleteEmptyValue 사용
|
|
985
|
+
const response = await XXXService.postXXXList(
|
|
986
|
+
deleteEmptyValue(apiParam)
|
|
987
|
+
);
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
### 10.5 금지: States에 없는 필드를 Actions에서 사용
|
|
991
|
+
|
|
992
|
+
```typescript
|
|
993
|
+
// ❌ 금지
|
|
994
|
+
interface States {
|
|
995
|
+
listSpinning: boolean;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const createActions = (set, get) => ({
|
|
999
|
+
setSelectedItem: (item) => set({ selectedItem: item }), // States에 없음!
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
// ✅ 해결
|
|
1003
|
+
interface States {
|
|
1004
|
+
listSpinning: boolean;
|
|
1005
|
+
selectedItem?: DtoItem; // 추가
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const createActions = (set, get) => ({
|
|
1009
|
+
setSelectedItem: (item) => set({ selectedItem: item }),
|
|
1010
|
+
});
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
### 10.6 금지: shallow equalityFn 누락
|
|
1014
|
+
|
|
1015
|
+
```typescript
|
|
1016
|
+
// ❌ 금지
|
|
1017
|
+
useXXXStore.subscribe(
|
|
1018
|
+
(s) => ({ ... }),
|
|
1019
|
+
getTabStoreListener<MetaData>(createState.routePath),
|
|
1020
|
+
// equalityFn 누락!
|
|
1021
|
+
);
|
|
1022
|
+
|
|
1023
|
+
// ✅ 해결
|
|
1024
|
+
useXXXStore.subscribe(
|
|
1025
|
+
(s) => ({ ... }),
|
|
1026
|
+
getTabStoreListener<MetaData>(createState.routePath),
|
|
1027
|
+
{ equalityFn: shallow },
|
|
1028
|
+
);
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
### 10.7 금지: Subscribe selector와 MetaData 불일치
|
|
1032
|
+
|
|
1033
|
+
```typescript
|
|
1034
|
+
// ❌ 금지
|
|
1035
|
+
interface MetaData {
|
|
1036
|
+
programFn?: ProgramFn;
|
|
1037
|
+
listRequestValue: ListRequest;
|
|
1038
|
+
listColWidths: number[];
|
|
1039
|
+
listSortParams: AXDGSortParam[];
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
useXXXStore.subscribe(
|
|
1043
|
+
(s): Record<keyof MetaData, any> => ({
|
|
1044
|
+
programFn: s.programFn,
|
|
1045
|
+
listRequestValue: s.listRequestValue,
|
|
1046
|
+
// listColWidths, listSortParams 누락!
|
|
1047
|
+
}),
|
|
1048
|
+
...
|
|
1049
|
+
);
|
|
1050
|
+
|
|
1051
|
+
// ✅ 해결
|
|
1052
|
+
useXXXStore.subscribe(
|
|
1053
|
+
(s): Record<keyof MetaData, any> => ({
|
|
1054
|
+
programFn: s.programFn,
|
|
1055
|
+
listSortParams: s.listSortParams,
|
|
1056
|
+
listRequestValue: s.listRequestValue,
|
|
1057
|
+
listColWidths: s.listColWidths,
|
|
1058
|
+
}),
|
|
1059
|
+
...
|
|
1060
|
+
);
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
### 10.8 금지: Spinning 해제 누락
|
|
1064
|
+
|
|
1065
|
+
```typescript
|
|
1066
|
+
// ❌ 금지: finally 블록 누락
|
|
1067
|
+
callListApi: async (request) => {
|
|
1068
|
+
set({ listSpinning: true });
|
|
1069
|
+
const response = await XXXService.postXXXList(...);
|
|
1070
|
+
set({ listData: response.ds.map((values) => ({ values })) });
|
|
1071
|
+
// spinning 해제 누락! ❌
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// ✅ 해결: finally 블록 사용
|
|
1075
|
+
callListApi: async (request) => {
|
|
1076
|
+
set({ listSpinning: true });
|
|
1077
|
+
try {
|
|
1078
|
+
const response = await XXXService.postXXXList(...);
|
|
1079
|
+
set({ listData: response.ds.map((values) => ({ values })) });
|
|
1080
|
+
} finally {
|
|
1081
|
+
set({ listSpinning: false }); // ✅ 항상 해제
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
### 10.9 금지: 중복 호출 방지 누락
|
|
1087
|
+
|
|
1088
|
+
```typescript
|
|
1089
|
+
// ❌ 금지: 중복 호출 방지 없음
|
|
1090
|
+
callListApi: async (request) => {
|
|
1091
|
+
set({ listSpinning: true });
|
|
1092
|
+
// ...
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// ✅ 해결: 중복 호출 방지 포함
|
|
1096
|
+
callListApi: async (request) => {
|
|
1097
|
+
if (get().listSpinning) return; // ✅ 중복 방지
|
|
1098
|
+
set({ listSpinning: true });
|
|
1099
|
+
// ...
|
|
1100
|
+
}
|
|
1101
|
+
```
|
|
1102
|
+
|
|
1103
|
+
### 10.10 금지: Actions의 반환 타입 누락
|
|
1104
|
+
|
|
1105
|
+
```typescript
|
|
1106
|
+
// ❌ 금지: Actions가 PageStoreActions를 확장하지 않음
|
|
1107
|
+
interface Actions {
|
|
1108
|
+
setListRequestValue: (requestValue: ListRequest) => void;
|
|
1109
|
+
// ...
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// ✅ 해결: PageStoreActions 확장
|
|
1113
|
+
interface Actions extends PageStoreActions<States> {
|
|
1114
|
+
setListRequestValue: (requestValue: ListRequest) => void;
|
|
1115
|
+
// ...
|
|
1116
|
+
}
|
|
1117
|
+
```
|
|
1118
|
+
|
|
1119
|
+
---
|
|
1120
|
+
|
|
1121
|
+
## 11. 빠른 참조: 체크리스트
|
|
1122
|
+
|
|
1123
|
+
### Store 생성 체크리스트
|
|
1124
|
+
|
|
1125
|
+
#### Interface
|
|
1126
|
+
- [ ] `ListRequest`가 Repository Request를 확장함
|
|
1127
|
+
- [ ] `DtoItem`가 Repository Response를 확장함
|
|
1128
|
+
- [ ] `SaveRequest`가 Repository Save Request를 확장함 (필요시)
|
|
1129
|
+
- [ ] `MetaData`에 필요한 필드가 모두 포함됨
|
|
1130
|
+
- [ ] `States`가 `MetaData`를 확장함
|
|
1131
|
+
- [ ] `Actions`가 `PageStoreActions<States>`를 확장함
|
|
1132
|
+
|
|
1133
|
+
#### createState
|
|
1134
|
+
- [ ] `listRequestValue`에 `pageNumber`, `pageSize` 초기값 포함
|
|
1135
|
+
- [ ] `listColWidths`가 빈 배열로 초기화됨
|
|
1136
|
+
- [ ] `listSortParams`가 빈 배열로 초기화됨
|
|
1137
|
+
- [ ] `listSpinning`이 `false`로 초기화됨
|
|
1138
|
+
- [ ] `listData`가 빈 배열로 초기화됨
|
|
1139
|
+
- [ ] `listPage`가 빈 페이지 상태로 초기화됨
|
|
1140
|
+
- [ ] 고정값이 필요한 경우 상수로 선언되고 포함됨
|
|
1141
|
+
|
|
1142
|
+
#### createActions
|
|
1143
|
+
- [ ] `syncMetadata`가 구현됨
|
|
1144
|
+
- [ ] `onMountApp`가 구현됨
|
|
1145
|
+
- [ ] `setListRequestValue`가 구현됨
|
|
1146
|
+
- [ ] `setListColWidths`가 구현됨
|
|
1147
|
+
- [ ] `setListSpinning`가 구현됨
|
|
1148
|
+
- [ ] `setListSortParams`가 구현됨
|
|
1149
|
+
- [ ] `callListApi`가 구현됨 (중복 호출 방지, try-finally 포함)
|
|
1150
|
+
- [ ] `changeListPage`가 구현됨
|
|
1151
|
+
- [ ] `callListApi`에서 `deleteEmptyValue` 사용됨
|
|
1152
|
+
- [ ] `callListApi`에서 `listRequestValue` 업데이트됨
|
|
1153
|
+
- [ ] 추가 기능의 액션들이 모두 구현됨
|
|
1154
|
+
- [ ] `...pageStoreActions(set, get, { createState })`가 마지막에 있음
|
|
1155
|
+
|
|
1156
|
+
#### Store 내보내기
|
|
1157
|
+
- [ ] `xxxStore` 인터페이스가 `States, Actions, PageStoreActions<States>`를 모두 확장함
|
|
1158
|
+
- [ ] `useXXXStore`가 `subscribeWithSelector`로 생성됨
|
|
1159
|
+
|
|
1160
|
+
#### Store.subscribe
|
|
1161
|
+
- [ ] `subscribe`가 Store 생성 후에 위치함
|
|
1162
|
+
- [ ] Selector가 `MetaData`의 모든 필드를 포함함
|
|
1163
|
+
- [ ] `getTabStoreListener<MetaData>`가 사용됨
|
|
1164
|
+
- [ ] `{ equalityFn: shallow }`가 포함됨
|
|
1165
|
+
|
|
1166
|
+
---
|
|
1167
|
+
|
|
1168
|
+
*이 문서는 NH-FE-B 프로젝트의 Store 패턴 제약 조건과 규칙을 정리한 것입니다.*
|