@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,1135 @@
|
|
|
1
|
+
# Zustand Store 패턴 사용법 가이드
|
|
2
|
+
|
|
3
|
+
AXBoot에서 사용하는 Zustand 기반 Store 패턴에 대한 상세 가이드입니다. NH 프로젝트의 실제 Store 파일들을 분석하여 다양한 케이스별 사용법을 제시합니다.
|
|
4
|
+
|
|
5
|
+
## 목차
|
|
6
|
+
1. [개요](#개요)
|
|
7
|
+
2. [기본 구조](#기본-구조)
|
|
8
|
+
3. [케이스별 Store 패턴](#케이스별-store-패턴)
|
|
9
|
+
4. [공통 인터페이스](#공통-인터페이스)
|
|
10
|
+
5. [메타데이터 관리](#메타데이터-관리)
|
|
11
|
+
6. [실제 구현 예제](#실제-구현-예제)
|
|
12
|
+
7. [고급 패턴](#고급-패턴)
|
|
13
|
+
8. [성능 최적화](#성능-최적화)
|
|
14
|
+
|
|
15
|
+
## 개요
|
|
16
|
+
|
|
17
|
+
### Store 시스템의 특징
|
|
18
|
+
- **Zustand 기반**: 가벼운 상태 관리 라이브러리
|
|
19
|
+
- **TypeScript 완전 지원**: 타입 안정성 보장
|
|
20
|
+
- **페이지별 분리**: 각 페이지마다 독립적인 Store
|
|
21
|
+
- **메타데이터 영속화**: 탭 간 상태 유지
|
|
22
|
+
- **표준화된 패턴**: 일관성 있는 구조
|
|
23
|
+
|
|
24
|
+
### 핵심 컴포넌트
|
|
25
|
+
- **States**: 상태 인터페이스 정의
|
|
26
|
+
- **Actions**: 액션 인터페이스 정의
|
|
27
|
+
- **MetaData**: 영속화할 데이터 정의
|
|
28
|
+
- **PageStoreActions**: 공통 페이지 액션
|
|
29
|
+
- **TabStoreListener**: 탭 상태 동기화
|
|
30
|
+
|
|
31
|
+
## 기본 구조
|
|
32
|
+
|
|
33
|
+
### 1. Store 파일 구조
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// 1. 필요한 imports
|
|
37
|
+
import { AXDGDataItem, AXDGPage, AXDGSortParam } from "@axboot/datagrid";
|
|
38
|
+
import { pageStoreActions } from "@core/stores/pageStoreActions";
|
|
39
|
+
import { PageStoreActions, StoreActions } from "@core/stores/types";
|
|
40
|
+
import { create } from "zustand";
|
|
41
|
+
import { subscribeWithSelector } from "zustand/middleware";
|
|
42
|
+
import { shallow } from "zustand/shallow";
|
|
43
|
+
|
|
44
|
+
// 2. Request/Response 인터페이스
|
|
45
|
+
interface ListRequest extends ServiceRequestType {}
|
|
46
|
+
interface DtoItem extends ServiceResponseType {}
|
|
47
|
+
interface SaveRequest {}
|
|
48
|
+
|
|
49
|
+
// 3. MetaData 정의 (영속화 대상)
|
|
50
|
+
interface MetaData {
|
|
51
|
+
programFn?: ProgramFn;
|
|
52
|
+
listRequestValue: ListRequest;
|
|
53
|
+
listColWidths: number[];
|
|
54
|
+
listSortParams: AXDGSortParam[];
|
|
55
|
+
// ... 기타 영속화할 상태들
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 4. States 정의
|
|
59
|
+
interface States extends MetaData {
|
|
60
|
+
loadedStore: boolean;
|
|
61
|
+
routePath?: string;
|
|
62
|
+
listSpinning: boolean;
|
|
63
|
+
listData: AXDGDataItem<DtoItem>[];
|
|
64
|
+
listPage: AXDGPage;
|
|
65
|
+
// ... 기타 임시 상태들
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 5. Actions 정의
|
|
69
|
+
interface Actions extends PageStoreActions<States> {
|
|
70
|
+
setListRequestValue: (requestValue: ListRequest) => void;
|
|
71
|
+
callListApi: (request?: ListRequest) => Promise<void>;
|
|
72
|
+
// ... 기타 액션들
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 6. 초기 상태
|
|
76
|
+
const createState: States = {
|
|
77
|
+
// 초기값 설정
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// 7. Actions 구현
|
|
81
|
+
const createActions: StoreActions<States & Actions, Actions> = (set, get) => ({
|
|
82
|
+
// 액션 구현
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// 8. Store 생성 및 구독
|
|
86
|
+
export const useYourStore = create(
|
|
87
|
+
subscribeWithSelector<YourStore>((set, get) => ({
|
|
88
|
+
...createState,
|
|
89
|
+
...createActions(set, get),
|
|
90
|
+
}))
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// 9. 메타데이터 구독 설정
|
|
94
|
+
useYourStore.subscribe(
|
|
95
|
+
(s): Record<keyof MetaData, any> => ({
|
|
96
|
+
// 영속화할 상태들만 선택
|
|
97
|
+
}),
|
|
98
|
+
getTabStoreListener<MetaData>(createState.routePath),
|
|
99
|
+
{ equalityFn: shallow }
|
|
100
|
+
);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## 케이스별 Store 패턴
|
|
104
|
+
|
|
105
|
+
### 1. 기본 목록 관리 Store (Basic List Pattern)
|
|
106
|
+
|
|
107
|
+
**사용 케이스**: 단순한 목록 조회 및 페이징
|
|
108
|
+
**예시**: `useProductMasterStore.ts`
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
// 기본 목록 관리 패턴
|
|
112
|
+
interface ListRequest extends PostProductGroupMasterListRequest {}
|
|
113
|
+
interface DtoItem extends ProdgrpMst {}
|
|
114
|
+
|
|
115
|
+
interface States extends MetaData {
|
|
116
|
+
listSpinning: boolean;
|
|
117
|
+
listData: AXDGDataItem<DtoItem>[];
|
|
118
|
+
listPage: AXDGPage;
|
|
119
|
+
listSelectedRowKey?: React.Key;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface Actions {
|
|
123
|
+
callListApi: (request?: ListRequest) => Promise<void>;
|
|
124
|
+
changeListPage: (currentPage: number, pageSize?: number) => Promise<void>;
|
|
125
|
+
setListSelectedRowKey: (key?: React.Key) => void;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const createActions = (set, get) => ({
|
|
129
|
+
callListApi: async (request) => {
|
|
130
|
+
if (get().listSpinning) return;
|
|
131
|
+
set({ listSpinning: true });
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const apiParam = { ...get().listRequestValue, ...request };
|
|
135
|
+
const response = await ServiceClass.callApi(apiParam);
|
|
136
|
+
|
|
137
|
+
set({
|
|
138
|
+
listRequestValue: apiParam,
|
|
139
|
+
listData: response.ds.map((values) => ({ values })),
|
|
140
|
+
listPage: {
|
|
141
|
+
currentPage: response.page.pageNumber ?? 1,
|
|
142
|
+
pageSize: response.page.pageSize ?? 0,
|
|
143
|
+
totalPages: response.page.pageCount ?? 0,
|
|
144
|
+
totalElements: response.page?.totalCount,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
} finally {
|
|
148
|
+
set({ listSpinning: false });
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
changeListPage: async (pageNumber, pageSize) => {
|
|
153
|
+
await get().callListApi({ pageNumber, pageSize });
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
setListSelectedRowKey: (key) => {
|
|
157
|
+
set({ listSelectedRowKey: key });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**특징**:
|
|
163
|
+
- 페이징 지원
|
|
164
|
+
- 로딩 상태 관리
|
|
165
|
+
- 선택된 행 추적
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
### 2. 상세 정보 포함 Store (Master-Detail Pattern)
|
|
170
|
+
|
|
171
|
+
**사용 케이스**: 목록에서 선택한 항목의 상세 정보 표시
|
|
172
|
+
**예시**: `useMemberListStore.ts`
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
interface States extends MetaData {
|
|
176
|
+
listData: AXDGDataItem<DtoItem>[];
|
|
177
|
+
selectedItem?: DtoItem;
|
|
178
|
+
detailLoading: boolean;
|
|
179
|
+
saveRequestValue: SaveRequest;
|
|
180
|
+
checkedRowKeys: React.Key[];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
interface Actions {
|
|
184
|
+
setSelectedItem: (selectedItem?: DtoItem) => Promise<void>;
|
|
185
|
+
setSaveRequestValue: (saveRequestValue: SaveRequest) => void;
|
|
186
|
+
setCheckedRowKeys: (checkedRowKeys: React.Key[]) => void;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const createActions = (set, get) => ({
|
|
190
|
+
setSelectedItem: async (selectedItem) => {
|
|
191
|
+
set({ selectedItem, detailLoading: true });
|
|
192
|
+
|
|
193
|
+
if (!selectedItem) {
|
|
194
|
+
set({ saveRequestValue: {} });
|
|
195
|
+
} else {
|
|
196
|
+
// 상세 정보 로딩 로직
|
|
197
|
+
const detailData = await loadDetailData(selectedItem.id);
|
|
198
|
+
set({ saveRequestValue: detailData });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await delay(300); // UX 개선을 위한 지연
|
|
202
|
+
set({ detailLoading: false });
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
setSaveRequestValue: (saveRequestValue) => {
|
|
206
|
+
set({ saveRequestValue });
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
setCheckedRowKeys: (checkedRowKeys) => {
|
|
210
|
+
set({ checkedRowKeys });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**특징**:
|
|
216
|
+
- 선택 항목 상세 로딩
|
|
217
|
+
- 저장용 데이터 관리
|
|
218
|
+
- 다중 선택 지원
|
|
219
|
+
- 로딩 상태 분리
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
### 3. 날짜 범위 검색 Store (Date Range Search Pattern)
|
|
224
|
+
|
|
225
|
+
**사용 케이스**: 날짜 범위 기반 검색 기능
|
|
226
|
+
**예시**: `useBenefitCouponStore.ts`, `useCsFaqStore.ts`
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
interface ListRequest extends ServiceRequest {
|
|
230
|
+
dateRange?: [string, string];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const createState: States = {
|
|
234
|
+
listRequestValue: {
|
|
235
|
+
pageNumber: 1,
|
|
236
|
+
pageSize: 100,
|
|
237
|
+
dateRange: [
|
|
238
|
+
dayjs().add(-1, "month").format("YYYY-MM-DD"),
|
|
239
|
+
dayjs().format("YYYY-MM-DD")
|
|
240
|
+
],
|
|
241
|
+
},
|
|
242
|
+
// ... 기타 초기값
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const createActions = (set, get) => ({
|
|
246
|
+
callListApi: async (request) => {
|
|
247
|
+
const apiParam = { ...get().listRequestValue, ...request };
|
|
248
|
+
|
|
249
|
+
// 날짜 범위를 API 형식으로 변환
|
|
250
|
+
const response = await ServiceClass.callApi(
|
|
251
|
+
deleteEmptyValue({
|
|
252
|
+
...apiParam,
|
|
253
|
+
...dateRangeToDts(apiParam.dateRange, {
|
|
254
|
+
startDateTime: DT_FORMAT.DATE + " 00:00:00",
|
|
255
|
+
endDateTime: DT_FORMAT.DATE + " 23:59:59",
|
|
256
|
+
}),
|
|
257
|
+
})
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
set({
|
|
261
|
+
listRequestValue: apiParam,
|
|
262
|
+
listData: response.ds.map((values) => ({ values })),
|
|
263
|
+
listPage: {
|
|
264
|
+
currentPage: response.page.pageNumber ?? 1,
|
|
265
|
+
pageSize: response.page.pageSize ?? 0,
|
|
266
|
+
totalPages: response.page.pageCount ?? 0,
|
|
267
|
+
totalElements: response.page?.totalCount,
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**특징**:
|
|
275
|
+
- 날짜 범위 기본값 설정
|
|
276
|
+
- 자동 날짜 포맷 변환
|
|
277
|
+
- 시작/종료 시간 자동 설정
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
### 4. 복합 조건 검색 Store (Complex Search Pattern)
|
|
282
|
+
|
|
283
|
+
**사용 케이스**: 다중 조건 검색 및 필터링
|
|
284
|
+
**예시**: `useMemberListStore.ts`
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
interface ListRequest extends PostMemberListMemberRequest {
|
|
288
|
+
visitCount?: string[]; // 방문 횟수 범위
|
|
289
|
+
dateRange?: [string, string]; // 날짜 범위
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const createActions = (set, get) => ({
|
|
293
|
+
callListApi: async (request) => {
|
|
294
|
+
const apiParam: ListRequest = { ...get().listRequestValue, ...request };
|
|
295
|
+
|
|
296
|
+
// 방문 횟수 범위 처리
|
|
297
|
+
if (isEmpty(apiParam.visitCount)) {
|
|
298
|
+
apiParam.mummTmnum = undefined;
|
|
299
|
+
apiParam.mxmmTmnum = undefined;
|
|
300
|
+
} else {
|
|
301
|
+
apiParam.mummTmnum = parseInt(apiParam.visitCount?.[0] ?? "0");
|
|
302
|
+
apiParam.mxmmTmnum = parseInt(apiParam.visitCount?.[1] ?? "99999999");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const response = await MemberService.postMemberListMember(
|
|
306
|
+
deleteEmptyValue({
|
|
307
|
+
...apiParam,
|
|
308
|
+
...dateRangeToDts(apiParam.dateRange, {
|
|
309
|
+
bgnDtm: "YYYY-MM-DD 00:00:00",
|
|
310
|
+
endDtm: "YYYY-MM-DD 23:59:59",
|
|
311
|
+
}),
|
|
312
|
+
})
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
set({
|
|
316
|
+
listRequestValue: apiParam,
|
|
317
|
+
listData: response.ds.map((values) => ({ values })),
|
|
318
|
+
listPage: {
|
|
319
|
+
currentPage: response.page.pageNumber ?? 1,
|
|
320
|
+
pageSize: response.page.pageSize ?? 0,
|
|
321
|
+
totalPages: response.page.pageCount ?? 0,
|
|
322
|
+
totalElements: response.page?.totalCount,
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
**특징**:
|
|
330
|
+
- 복합 검색 조건 처리
|
|
331
|
+
- 조건별 데이터 변환
|
|
332
|
+
- 빈 값 필터링
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
### 5. 삭제 기능 포함 Store (Bulk Delete Pattern)
|
|
337
|
+
|
|
338
|
+
**사용 케이스**: 체크박스 선택 후 일괄 삭제
|
|
339
|
+
**예시**: `useSystemSeoStore.ts`
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
interface States extends MetaData {
|
|
343
|
+
listCheckedIndexes?: number[];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
interface Actions {
|
|
347
|
+
setListCheckedIndexes: (indexes?: number[]) => void;
|
|
348
|
+
callDeleteApi: (indexes: number[]) => Promise<void>;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const createActions = (set, get) => ({
|
|
352
|
+
setListCheckedIndexes: (indexes) => {
|
|
353
|
+
set({ listCheckedIndexes: indexes });
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
callDeleteApi: async (indexes) => {
|
|
357
|
+
const deleteListData = get().listData.filter((r, index) => {
|
|
358
|
+
return indexes.includes(index);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// 순차적 삭제 처리
|
|
362
|
+
for await (const _r of deleteListData) {
|
|
363
|
+
try {
|
|
364
|
+
await SsSystemService.postSsSystemDeleteSeo({
|
|
365
|
+
seoNo: _r.values.seoNo,
|
|
366
|
+
});
|
|
367
|
+
} catch (e) {
|
|
368
|
+
await errorHandling(e);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
set({ listCheckedIndexes: [] }); // 선택 초기화
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
**특징**:
|
|
378
|
+
- 체크된 인덱스 관리
|
|
379
|
+
- 순차적 삭제 처리
|
|
380
|
+
- 에러 핸들링
|
|
381
|
+
- 삭제 후 선택 초기화
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
### 6. 동적 탭 Store (Dynamic Tab Pattern)
|
|
386
|
+
|
|
387
|
+
**사용 케이스**: 탭에 따른 동적 데이터 로딩
|
|
388
|
+
**예시**: `useDisplayBannerMenuStore.ts`
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
interface MetaData {
|
|
392
|
+
bannerLocTp: BannerLocTp[]; // 탭 목록
|
|
393
|
+
activeBannerLocTp?: string; // 활성 탭
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
interface Actions {
|
|
397
|
+
setActiveBannerLocTp: (bannerLocTpcd?: string) => void;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const createActions = (set, get) => ({
|
|
401
|
+
onMountApp: async () => {
|
|
402
|
+
// 탭 목록 로딩
|
|
403
|
+
const { ds } = await BannerService.postBannerListBannerLocTp();
|
|
404
|
+
set({ bannerLocTp: ds });
|
|
405
|
+
|
|
406
|
+
// 기본 탭 설정
|
|
407
|
+
if (!get().activeBannerLocTp || !ds.find(n => n.bannerLocTpcd === get().activeBannerLocTp)) {
|
|
408
|
+
set({ activeBannerLocTp: ds[0].bannerLocTpcd });
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
setActiveBannerLocTp: (bannerLocTpcd) => {
|
|
413
|
+
set({ activeBannerLocTp: bannerLocTpcd });
|
|
414
|
+
},
|
|
415
|
+
|
|
416
|
+
callListApi: async (request) => {
|
|
417
|
+
const bannerLocTpcd = get().activeBannerLocTp;
|
|
418
|
+
const apiParam = { ...get().listRequestValue, ...request, bannerLocTpcd };
|
|
419
|
+
|
|
420
|
+
// 탭별 조건부 로직
|
|
421
|
+
if (apiParam.bannerLocTpcd === "1" && !apiParam.ctgrNo) {
|
|
422
|
+
set({ listData: [] });
|
|
423
|
+
throw new Error("카테고리를 선택해주세요.");
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const response = await BannerService.postBannerListBanner(apiParam);
|
|
427
|
+
|
|
428
|
+
set({
|
|
429
|
+
listRequestValue: apiParam,
|
|
430
|
+
listData: response.ds.map((values) => ({ values })),
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
**특징**:
|
|
437
|
+
- 동적 탭 목록 로딩
|
|
438
|
+
- 탭별 조건부 로직
|
|
439
|
+
- 기본 탭 자동 설정
|
|
440
|
+
- 탭 변경 시 데이터 갱신
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## 공통 인터페이스
|
|
445
|
+
|
|
446
|
+
### 1. PageStoreActions
|
|
447
|
+
|
|
448
|
+
모든 Store에서 공통으로 사용하는 액션들:
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
interface PageStoreActions<T> {
|
|
452
|
+
syncMetadata: (metadata?: T) => void;
|
|
453
|
+
onMountApp: () => Promise<void>;
|
|
454
|
+
resetStore: () => void;
|
|
455
|
+
setRouterPath: (path: string) => void;
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### 2. StoreActions 타입
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
type StoreActions<T, A> = (
|
|
463
|
+
set: (partial: T | Partial<T> | ((state: T) => T | Partial<T>)) => void,
|
|
464
|
+
get: () => T
|
|
465
|
+
) => A;
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### 3. MetaData 패턴
|
|
469
|
+
|
|
470
|
+
영속화할 데이터를 정의:
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
interface MetaData {
|
|
474
|
+
programFn?: ProgramFn; // 프로그램 권한 정보
|
|
475
|
+
listRequestValue: ListRequest; // 검색 조건
|
|
476
|
+
listColWidths: number[]; // 컬럼 너비
|
|
477
|
+
listSortParams: AXDGSortParam[]; // 정렬 정보
|
|
478
|
+
// ... 기타 영속화 필요한 상태들
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
## 메타데이터 관리
|
|
483
|
+
|
|
484
|
+
### 1. 탭 스토어 리스너
|
|
485
|
+
|
|
486
|
+
```typescript
|
|
487
|
+
useYourStore.subscribe(
|
|
488
|
+
(s): Record<keyof MetaData, any> => ({
|
|
489
|
+
programFn: s.programFn,
|
|
490
|
+
listSortParams: s.listSortParams,
|
|
491
|
+
listRequestValue: s.listRequestValue,
|
|
492
|
+
listColWidths: s.listColWidths,
|
|
493
|
+
// 영속화할 상태만 선택
|
|
494
|
+
}),
|
|
495
|
+
getTabStoreListener<MetaData>(createState.routePath),
|
|
496
|
+
{ equalityFn: shallow }
|
|
497
|
+
);
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### 2. 메타데이터 동기화
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
const createActions = (set, get) => ({
|
|
504
|
+
syncMetadata: (s = createState) => {
|
|
505
|
+
set(s); // 저장된 메타데이터로 상태 복원
|
|
506
|
+
},
|
|
507
|
+
|
|
508
|
+
resetStore: () => {
|
|
509
|
+
set(createState); // 초기 상태로 리셋
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
## 실제 구현 예제
|
|
515
|
+
|
|
516
|
+
### 1. 회원 관리 Store 완전 구현
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
// useMemberManagementStore.ts
|
|
520
|
+
import { AXDGDataItem, AXDGPage, AXDGSortParam } from "@axboot/datagrid";
|
|
521
|
+
import { pageStoreActions } from "@core/stores/pageStoreActions";
|
|
522
|
+
import { PageStoreActions, StoreActions } from "@core/stores/types";
|
|
523
|
+
import { delay } from "@core/utils";
|
|
524
|
+
import { deleteEmptyValue } from "@core/utils/object";
|
|
525
|
+
import { ProgramFn } from "@types";
|
|
526
|
+
import { MemberRes, MemberService, PostMemberListRequest } from "services";
|
|
527
|
+
import { getTabStoreListener } from "stores";
|
|
528
|
+
import { create } from "zustand";
|
|
529
|
+
import { subscribeWithSelector } from "zustand/middleware";
|
|
530
|
+
import { shallow } from "zustand/shallow";
|
|
531
|
+
|
|
532
|
+
// 1. Request/Response 인터페이스
|
|
533
|
+
interface ListRequest extends PostMemberListRequest {
|
|
534
|
+
dateRange?: [string, string];
|
|
535
|
+
statusFilter?: string[];
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
interface DtoItem extends MemberRes {}
|
|
539
|
+
|
|
540
|
+
interface SaveRequest {
|
|
541
|
+
memberNo?: number;
|
|
542
|
+
memberName?: string;
|
|
543
|
+
email?: string;
|
|
544
|
+
status?: string;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// 2. MetaData 정의
|
|
548
|
+
interface MetaData {
|
|
549
|
+
programFn?: ProgramFn;
|
|
550
|
+
listRequestValue: ListRequest;
|
|
551
|
+
listColWidths: number[];
|
|
552
|
+
listSortParams: AXDGSortParam[];
|
|
553
|
+
saveRequestValue: SaveRequest;
|
|
554
|
+
activeTab: string;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// 3. States 정의
|
|
558
|
+
interface States extends MetaData {
|
|
559
|
+
routePath?: string;
|
|
560
|
+
listSpinning: boolean;
|
|
561
|
+
listData: AXDGDataItem<DtoItem>[];
|
|
562
|
+
listPage: AXDGPage;
|
|
563
|
+
selectedItem?: DtoItem;
|
|
564
|
+
detailLoading: boolean;
|
|
565
|
+
checkedRowKeys: React.Key[];
|
|
566
|
+
saveSpinning: boolean;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// 4. Actions 정의
|
|
570
|
+
interface Actions extends PageStoreActions<States> {
|
|
571
|
+
// 검색 관련
|
|
572
|
+
setListRequestValue: (requestValue: ListRequest) => void;
|
|
573
|
+
callListApi: (request?: ListRequest) => Promise<void>;
|
|
574
|
+
changeListPage: (currentPage: number, pageSize?: number) => Promise<void>;
|
|
575
|
+
|
|
576
|
+
// 선택 관련
|
|
577
|
+
setSelectedItem: (selectedItem?: DtoItem) => Promise<void>;
|
|
578
|
+
setCheckedRowKeys: (checkedRowKeys: React.Key[]) => void;
|
|
579
|
+
|
|
580
|
+
// 저장 관련
|
|
581
|
+
setSaveRequestValue: (saveRequestValue: SaveRequest) => void;
|
|
582
|
+
callSaveApi: () => Promise<void>;
|
|
583
|
+
callDeleteApi: (memberNos: number[]) => Promise<void>;
|
|
584
|
+
|
|
585
|
+
// UI 관련
|
|
586
|
+
setListColWidths: (colWidths: number[]) => void;
|
|
587
|
+
setListSortParams: (sortParams: AXDGSortParam[]) => void;
|
|
588
|
+
setActiveTab: (tab: string) => void;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// 5. 초기 상태
|
|
592
|
+
const createState: States = {
|
|
593
|
+
routePath: "/member/management",
|
|
594
|
+
listRequestValue: {
|
|
595
|
+
pageNumber: 1,
|
|
596
|
+
pageSize: 100,
|
|
597
|
+
searchType: "NAME",
|
|
598
|
+
dateRange: [
|
|
599
|
+
dayjs().add(-1, "month").format("YYYY-MM-DD"),
|
|
600
|
+
dayjs().format("YYYY-MM-DD")
|
|
601
|
+
],
|
|
602
|
+
statusFilter: ["ACTIVE"]
|
|
603
|
+
},
|
|
604
|
+
listColWidths: [],
|
|
605
|
+
listSpinning: false,
|
|
606
|
+
listData: [],
|
|
607
|
+
listPage: {
|
|
608
|
+
currentPage: 0,
|
|
609
|
+
totalPages: 0,
|
|
610
|
+
},
|
|
611
|
+
listSortParams: [],
|
|
612
|
+
saveRequestValue: {},
|
|
613
|
+
detailLoading: false,
|
|
614
|
+
checkedRowKeys: [],
|
|
615
|
+
saveSpinning: false,
|
|
616
|
+
activeTab: "basic"
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
// 6. Actions 구현
|
|
620
|
+
const createActions: StoreActions<States & Actions, Actions> = (set, get) => ({
|
|
621
|
+
// 공통 액션 포함
|
|
622
|
+
...pageStoreActions(set, get, { createState }),
|
|
623
|
+
|
|
624
|
+
syncMetadata: (s = createState) => set(s),
|
|
625
|
+
|
|
626
|
+
onMountApp: async () => {
|
|
627
|
+
// 초기화 로직
|
|
628
|
+
await get().callListApi();
|
|
629
|
+
},
|
|
630
|
+
|
|
631
|
+
// 검색 관련 액션
|
|
632
|
+
setListRequestValue: (requestValues) => {
|
|
633
|
+
set({ listRequestValue: requestValues });
|
|
634
|
+
},
|
|
635
|
+
|
|
636
|
+
callListApi: async (request) => {
|
|
637
|
+
if (get().listSpinning) return;
|
|
638
|
+
set({ listSpinning: true });
|
|
639
|
+
|
|
640
|
+
try {
|
|
641
|
+
const apiParam: ListRequest = { ...get().listRequestValue, ...request };
|
|
642
|
+
|
|
643
|
+
// 상태 필터 처리
|
|
644
|
+
const statusCondition = apiParam.statusFilter?.join(",") || "";
|
|
645
|
+
|
|
646
|
+
const response = await MemberService.postMemberList(
|
|
647
|
+
deleteEmptyValue({
|
|
648
|
+
...apiParam,
|
|
649
|
+
status: statusCondition,
|
|
650
|
+
...dateRangeToDts(apiParam.dateRange, {
|
|
651
|
+
startDate: "YYYY-MM-DD",
|
|
652
|
+
endDate: "YYYY-MM-DD",
|
|
653
|
+
}),
|
|
654
|
+
})
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
set({
|
|
658
|
+
listRequestValue: apiParam,
|
|
659
|
+
listData: response.ds.map((values) => ({ values })),
|
|
660
|
+
listPage: {
|
|
661
|
+
currentPage: response.page.pageNumber ?? 1,
|
|
662
|
+
pageSize: response.page.pageSize ?? 0,
|
|
663
|
+
totalPages: response.page.pageCount ?? 0,
|
|
664
|
+
totalElements: response.page?.totalCount,
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
} catch (error) {
|
|
668
|
+
console.error("목록 조회 실패:", error);
|
|
669
|
+
} finally {
|
|
670
|
+
set({ listSpinning: false });
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
|
|
674
|
+
changeListPage: async (pageNumber, pageSize) => {
|
|
675
|
+
await get().callListApi({ pageNumber, pageSize });
|
|
676
|
+
},
|
|
677
|
+
|
|
678
|
+
// 선택 관련 액션
|
|
679
|
+
setSelectedItem: async (selectedItem) => {
|
|
680
|
+
set({ selectedItem, detailLoading: true });
|
|
681
|
+
|
|
682
|
+
if (!selectedItem) {
|
|
683
|
+
set({ saveRequestValue: {} });
|
|
684
|
+
} else {
|
|
685
|
+
// 상세 정보 로딩
|
|
686
|
+
try {
|
|
687
|
+
const detailResponse = await MemberService.getMemberDetail({
|
|
688
|
+
memberNo: selectedItem.memberNo
|
|
689
|
+
});
|
|
690
|
+
set({ saveRequestValue: detailResponse });
|
|
691
|
+
} catch (error) {
|
|
692
|
+
console.error("상세 조회 실패:", error);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
await delay(300);
|
|
697
|
+
set({ detailLoading: false });
|
|
698
|
+
},
|
|
699
|
+
|
|
700
|
+
setCheckedRowKeys: (checkedRowKeys) => {
|
|
701
|
+
set({ checkedRowKeys });
|
|
702
|
+
},
|
|
703
|
+
|
|
704
|
+
// 저장 관련 액션
|
|
705
|
+
setSaveRequestValue: (saveRequestValue) => {
|
|
706
|
+
set({ saveRequestValue });
|
|
707
|
+
},
|
|
708
|
+
|
|
709
|
+
callSaveApi: async () => {
|
|
710
|
+
const { saveRequestValue, selectedItem } = get();
|
|
711
|
+
set({ saveSpinning: true });
|
|
712
|
+
|
|
713
|
+
try {
|
|
714
|
+
if (selectedItem?.memberNo) {
|
|
715
|
+
// 수정
|
|
716
|
+
await MemberService.putMemberUpdate({
|
|
717
|
+
...saveRequestValue,
|
|
718
|
+
memberNo: selectedItem.memberNo
|
|
719
|
+
});
|
|
720
|
+
} else {
|
|
721
|
+
// 신규 등록
|
|
722
|
+
await MemberService.postMemberCreate(saveRequestValue);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// 목록 새로고침
|
|
726
|
+
await get().callListApi();
|
|
727
|
+
set({ selectedItem: undefined, saveRequestValue: {} });
|
|
728
|
+
|
|
729
|
+
} catch (error) {
|
|
730
|
+
console.error("저장 실패:", error);
|
|
731
|
+
throw error;
|
|
732
|
+
} finally {
|
|
733
|
+
set({ saveSpinning: false });
|
|
734
|
+
}
|
|
735
|
+
},
|
|
736
|
+
|
|
737
|
+
callDeleteApi: async (memberNos) => {
|
|
738
|
+
try {
|
|
739
|
+
for (const memberNo of memberNos) {
|
|
740
|
+
await MemberService.deleteMember({ memberNo });
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// 목록 새로고침
|
|
744
|
+
await get().callListApi();
|
|
745
|
+
set({ checkedRowKeys: [] });
|
|
746
|
+
|
|
747
|
+
} catch (error) {
|
|
748
|
+
console.error("삭제 실패:", error);
|
|
749
|
+
throw error;
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
|
|
753
|
+
// UI 관련 액션
|
|
754
|
+
setListColWidths: (colWidths) => {
|
|
755
|
+
set({ listColWidths: colWidths });
|
|
756
|
+
},
|
|
757
|
+
|
|
758
|
+
setListSortParams: (sortParams) => {
|
|
759
|
+
set({ listSortParams: sortParams });
|
|
760
|
+
},
|
|
761
|
+
|
|
762
|
+
setActiveTab: (activeTab) => {
|
|
763
|
+
set({ activeTab });
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
// 7. Store 생성
|
|
768
|
+
export interface memberManagementStore extends States, Actions, PageStoreActions<States> {}
|
|
769
|
+
|
|
770
|
+
export const useMemberManagementStore = create(
|
|
771
|
+
subscribeWithSelector<memberManagementStore>((set, get) => ({
|
|
772
|
+
...createState,
|
|
773
|
+
...createActions(set, get),
|
|
774
|
+
}))
|
|
775
|
+
);
|
|
776
|
+
|
|
777
|
+
// 8. 메타데이터 구독 설정
|
|
778
|
+
useMemberManagementStore.subscribe(
|
|
779
|
+
(s): Record<keyof MetaData, any> => ({
|
|
780
|
+
programFn: s.programFn,
|
|
781
|
+
listSortParams: s.listSortParams,
|
|
782
|
+
listRequestValue: s.listRequestValue,
|
|
783
|
+
listColWidths: s.listColWidths,
|
|
784
|
+
saveRequestValue: s.saveRequestValue,
|
|
785
|
+
activeTab: s.activeTab
|
|
786
|
+
}),
|
|
787
|
+
getTabStoreListener<MetaData>(createState.routePath),
|
|
788
|
+
{ equalityFn: shallow }
|
|
789
|
+
);
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
### 2. Store 사용법 (컴포넌트에서)
|
|
793
|
+
|
|
794
|
+
```tsx
|
|
795
|
+
// MemberManagementPage.tsx
|
|
796
|
+
import React from 'react';
|
|
797
|
+
import { useMemberManagementStore } from './useMemberManagementStore';
|
|
798
|
+
|
|
799
|
+
export function MemberManagementPage() {
|
|
800
|
+
// Store 상태 및 액션 구독
|
|
801
|
+
const listData = useMemberManagementStore(s => s.listData);
|
|
802
|
+
const listSpinning = useMemberManagementStore(s => s.listSpinning);
|
|
803
|
+
const selectedItem = useMemberManagementStore(s => s.selectedItem);
|
|
804
|
+
const listRequestValue = useMemberManagementStore(s => s.listRequestValue);
|
|
805
|
+
|
|
806
|
+
const callListApi = useMemberManagementStore(s => s.callListApi);
|
|
807
|
+
const setListRequestValue = useMemberManagementStore(s => s.setListRequestValue);
|
|
808
|
+
const setSelectedItem = useMemberManagementStore(s => s.setSelectedItem);
|
|
809
|
+
|
|
810
|
+
// 컴포넌트 마운트 시 초기화
|
|
811
|
+
React.useEffect(() => {
|
|
812
|
+
callListApi();
|
|
813
|
+
}, [callListApi]);
|
|
814
|
+
|
|
815
|
+
// 검색 핸들러
|
|
816
|
+
const handleSearch = (searchParams: any) => {
|
|
817
|
+
setListRequestValue({
|
|
818
|
+
...listRequestValue,
|
|
819
|
+
...searchParams,
|
|
820
|
+
pageNumber: 1 // 검색 시 첫 페이지로
|
|
821
|
+
});
|
|
822
|
+
callListApi({
|
|
823
|
+
...searchParams,
|
|
824
|
+
pageNumber: 1
|
|
825
|
+
});
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
// 행 선택 핸들러
|
|
829
|
+
const handleRowClick = (record: any) => {
|
|
830
|
+
setSelectedItem(record.values);
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
return (
|
|
834
|
+
<div>
|
|
835
|
+
{/* 검색 폼 */}
|
|
836
|
+
<SearchForm
|
|
837
|
+
onSearch={handleSearch}
|
|
838
|
+
initialValues={listRequestValue}
|
|
839
|
+
/>
|
|
840
|
+
|
|
841
|
+
{/* 데이터 그리드 */}
|
|
842
|
+
<DataGrid
|
|
843
|
+
loading={listSpinning}
|
|
844
|
+
data={listData}
|
|
845
|
+
onRowClick={handleRowClick}
|
|
846
|
+
selectedItem={selectedItem}
|
|
847
|
+
/>
|
|
848
|
+
|
|
849
|
+
{/* 상세 정보 */}
|
|
850
|
+
{selectedItem && (
|
|
851
|
+
<DetailPanel member={selectedItem} />
|
|
852
|
+
)}
|
|
853
|
+
</div>
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
## 고급 패턴
|
|
859
|
+
|
|
860
|
+
### 1. 조건부 Store 액션
|
|
861
|
+
|
|
862
|
+
```typescript
|
|
863
|
+
const createActions = (set, get) => ({
|
|
864
|
+
callListApi: async (request, options = {}) => {
|
|
865
|
+
const { skipLoading = false, resetPage = false } = options;
|
|
866
|
+
|
|
867
|
+
if (!skipLoading && get().listSpinning) return;
|
|
868
|
+
|
|
869
|
+
if (!skipLoading) {
|
|
870
|
+
set({ listSpinning: true });
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
try {
|
|
874
|
+
let apiParam = { ...get().listRequestValue, ...request };
|
|
875
|
+
|
|
876
|
+
if (resetPage) {
|
|
877
|
+
apiParam.pageNumber = 1;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const response = await ServiceClass.callApi(apiParam);
|
|
881
|
+
|
|
882
|
+
set({
|
|
883
|
+
listRequestValue: apiParam,
|
|
884
|
+
listData: response.ds.map(values => ({ values })),
|
|
885
|
+
listPage: response.page,
|
|
886
|
+
});
|
|
887
|
+
} finally {
|
|
888
|
+
if (!skipLoading) {
|
|
889
|
+
set({ listSpinning: false });
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
### 2. 캐싱 패턴
|
|
897
|
+
|
|
898
|
+
```typescript
|
|
899
|
+
interface States {
|
|
900
|
+
cache: Map<string, any>;
|
|
901
|
+
cacheExpiry: Map<string, number>;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const createActions = (set, get) => ({
|
|
905
|
+
getCachedData: async (key: string, fetcher: () => Promise<any>, ttl = 5 * 60 * 1000) => {
|
|
906
|
+
const cache = get().cache;
|
|
907
|
+
const cacheExpiry = get().cacheExpiry;
|
|
908
|
+
const now = Date.now();
|
|
909
|
+
|
|
910
|
+
// 캐시 확인
|
|
911
|
+
if (cache.has(key) && cacheExpiry.get(key)! > now) {
|
|
912
|
+
return cache.get(key);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// 데이터 페치
|
|
916
|
+
const data = await fetcher();
|
|
917
|
+
|
|
918
|
+
// 캐시 저장
|
|
919
|
+
cache.set(key, data);
|
|
920
|
+
cacheExpiry.set(key, now + ttl);
|
|
921
|
+
|
|
922
|
+
set({ cache, cacheExpiry });
|
|
923
|
+
return data;
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
### 3. 낙관적 업데이트 패턴
|
|
929
|
+
|
|
930
|
+
```typescript
|
|
931
|
+
const createActions = (set, get) => ({
|
|
932
|
+
updateItemOptimistic: async (itemId: string, updates: Partial<DtoItem>) => {
|
|
933
|
+
const { listData } = get();
|
|
934
|
+
|
|
935
|
+
// 즉시 UI 업데이트
|
|
936
|
+
const optimisticData = listData.map(item =>
|
|
937
|
+
item.values.id === itemId
|
|
938
|
+
? { ...item, values: { ...item.values, ...updates } }
|
|
939
|
+
: item
|
|
940
|
+
);
|
|
941
|
+
|
|
942
|
+
set({ listData: optimisticData });
|
|
943
|
+
|
|
944
|
+
try {
|
|
945
|
+
// 서버 업데이트
|
|
946
|
+
await ServiceClass.updateItem({ id: itemId, ...updates });
|
|
947
|
+
} catch (error) {
|
|
948
|
+
// 실패 시 롤백
|
|
949
|
+
set({ listData });
|
|
950
|
+
throw error;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
### 4. 무한 스크롤 패턴
|
|
957
|
+
|
|
958
|
+
```typescript
|
|
959
|
+
interface States {
|
|
960
|
+
hasMore: boolean;
|
|
961
|
+
loadingMore: boolean;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const createActions = (set, get) => ({
|
|
965
|
+
loadMore: async () => {
|
|
966
|
+
const { listRequestValue, listData, hasMore, loadingMore } = get();
|
|
967
|
+
|
|
968
|
+
if (!hasMore || loadingMore) return;
|
|
969
|
+
|
|
970
|
+
set({ loadingMore: true });
|
|
971
|
+
|
|
972
|
+
try {
|
|
973
|
+
const nextPage = listRequestValue.pageNumber + 1;
|
|
974
|
+
const response = await ServiceClass.callApi({
|
|
975
|
+
...listRequestValue,
|
|
976
|
+
pageNumber: nextPage
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
set({
|
|
980
|
+
listRequestValue: { ...listRequestValue, pageNumber: nextPage },
|
|
981
|
+
listData: [...listData, ...response.ds.map(values => ({ values }))],
|
|
982
|
+
hasMore: response.ds.length === listRequestValue.pageSize,
|
|
983
|
+
loadingMore: false
|
|
984
|
+
});
|
|
985
|
+
} catch (error) {
|
|
986
|
+
set({ loadingMore: false });
|
|
987
|
+
throw error;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
## 성능 최적화
|
|
994
|
+
|
|
995
|
+
### 1. 선택적 구독
|
|
996
|
+
|
|
997
|
+
```typescript
|
|
998
|
+
// 전체 상태 구독 (지양)
|
|
999
|
+
const store = useMemberManagementStore();
|
|
1000
|
+
|
|
1001
|
+
// 필요한 상태만 구독 (권장)
|
|
1002
|
+
const listData = useMemberManagementStore(s => s.listData);
|
|
1003
|
+
const listSpinning = useMemberManagementStore(s => s.listSpinning);
|
|
1004
|
+
|
|
1005
|
+
// 복수 상태를 하나의 selector로
|
|
1006
|
+
const { listData, listSpinning } = useMemberManagementStore(
|
|
1007
|
+
s => ({ listData: s.listData, listSpinning: s.listSpinning }),
|
|
1008
|
+
shallow // 얕은 비교로 불필요한 렌더링 방지
|
|
1009
|
+
);
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
### 2. 메모이제이션
|
|
1013
|
+
|
|
1014
|
+
```typescript
|
|
1015
|
+
// computed 상태 메모이제이션
|
|
1016
|
+
const filteredData = useMemberManagementStore(
|
|
1017
|
+
s => s.listData.filter(item =>
|
|
1018
|
+
s.listRequestValue.statusFilter?.includes(item.values.status)
|
|
1019
|
+
),
|
|
1020
|
+
shallow
|
|
1021
|
+
);
|
|
1022
|
+
|
|
1023
|
+
// 액션 메모이제이션
|
|
1024
|
+
const actions = useMemberManagementStore(
|
|
1025
|
+
s => ({
|
|
1026
|
+
callListApi: s.callListApi,
|
|
1027
|
+
setSelectedItem: s.setSelectedItem,
|
|
1028
|
+
setListRequestValue: s.setListRequestValue
|
|
1029
|
+
}),
|
|
1030
|
+
shallow
|
|
1031
|
+
);
|
|
1032
|
+
```
|
|
1033
|
+
|
|
1034
|
+
### 3. 배치 업데이트
|
|
1035
|
+
|
|
1036
|
+
```typescript
|
|
1037
|
+
const createActions = (set, get) => ({
|
|
1038
|
+
batchUpdate: (updates: Array<{ key: string, value: any }>) => {
|
|
1039
|
+
// 여러 상태를 한 번에 업데이트
|
|
1040
|
+
const batchedUpdates = updates.reduce((acc, { key, value }) => {
|
|
1041
|
+
acc[key] = value;
|
|
1042
|
+
return acc;
|
|
1043
|
+
}, {} as any);
|
|
1044
|
+
|
|
1045
|
+
set(batchedUpdates);
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
```
|
|
1049
|
+
|
|
1050
|
+
### 4. 디바운싱
|
|
1051
|
+
|
|
1052
|
+
```typescript
|
|
1053
|
+
import { debounce } from 'lodash';
|
|
1054
|
+
|
|
1055
|
+
const createActions = (set, get) => {
|
|
1056
|
+
const debouncedSearch = debounce(async (searchTerm: string) => {
|
|
1057
|
+
await get().callListApi({ searchTerm });
|
|
1058
|
+
}, 300);
|
|
1059
|
+
|
|
1060
|
+
return {
|
|
1061
|
+
setSearchTerm: (searchTerm: string) => {
|
|
1062
|
+
set({ searchTerm });
|
|
1063
|
+
debouncedSearch(searchTerm);
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
};
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
## 에러 처리 및 디버깅
|
|
1070
|
+
|
|
1071
|
+
### 1. 에러 처리 패턴
|
|
1072
|
+
|
|
1073
|
+
```typescript
|
|
1074
|
+
const createActions = (set, get) => ({
|
|
1075
|
+
callListApi: async (request) => {
|
|
1076
|
+
set({ listSpinning: true, error: null });
|
|
1077
|
+
|
|
1078
|
+
try {
|
|
1079
|
+
const response = await ServiceClass.callApi(request);
|
|
1080
|
+
set({
|
|
1081
|
+
listData: response.ds.map(values => ({ values })),
|
|
1082
|
+
listPage: response.page,
|
|
1083
|
+
});
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
console.error('API 호출 실패:', error);
|
|
1086
|
+
set({
|
|
1087
|
+
error: error.message,
|
|
1088
|
+
listData: [],
|
|
1089
|
+
listPage: { currentPage: 0, totalPages: 0 }
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
// 에러 처리 서비스 호출
|
|
1093
|
+
await errorHandling(error);
|
|
1094
|
+
} finally {
|
|
1095
|
+
set({ listSpinning: false });
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
```
|
|
1100
|
+
|
|
1101
|
+
### 2. 개발 모드 디버깅
|
|
1102
|
+
|
|
1103
|
+
```typescript
|
|
1104
|
+
// 개발 환경에서만 로깅
|
|
1105
|
+
const createActions = (set, get) => ({
|
|
1106
|
+
callListApi: async (request) => {
|
|
1107
|
+
if (process.env.NODE_ENV === 'development') {
|
|
1108
|
+
console.group('🔍 Store Debug: callListApi');
|
|
1109
|
+
console.log('Request:', request);
|
|
1110
|
+
console.log('Current State:', get());
|
|
1111
|
+
console.groupEnd();
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// API 호출 로직...
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
// Zustand devtools 연동 (개발 환경)
|
|
1119
|
+
export const useMemberManagementStore = create<memberManagementStore>()(
|
|
1120
|
+
process.env.NODE_ENV === 'development'
|
|
1121
|
+
? devtools(
|
|
1122
|
+
subscribeWithSelector((set, get) => ({
|
|
1123
|
+
...createState,
|
|
1124
|
+
...createActions(set, get),
|
|
1125
|
+
})),
|
|
1126
|
+
{ name: 'MemberManagementStore' }
|
|
1127
|
+
)
|
|
1128
|
+
: subscribeWithSelector((set, get) => ({
|
|
1129
|
+
...createState,
|
|
1130
|
+
...createActions(set, get),
|
|
1131
|
+
}))
|
|
1132
|
+
);
|
|
1133
|
+
```
|
|
1134
|
+
|
|
1135
|
+
이 가이드를 통해 AXBoot의 Zustand Store 패턴을 효과적으로 활용하여 일관성 있고 유지보수 가능한 상태 관리를 구현할 수 있습니다.
|