@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.
Files changed (78) hide show
  1. package/CLAUDE.md +119 -0
  2. package/MCP_TOOL_PLAN.md +710 -0
  3. package/MCP_USAGE.md +914 -0
  4. package/README.md +168 -0
  5. package/REPOSITORY_CONVENTIONS.md +250 -0
  6. package/SEARCH_PARAMS_MCP_TOOL_COMPLETE_PLAN.md +646 -0
  7. package/SEARCH_PARAMS_PLAN.md +2570 -0
  8. package/STORE_PATTERNS.md +1178 -0
  9. package/debug-dto.js +72 -0
  10. package/generate-banner-store.js +62 -0
  11. package/generation-plan.json +2176 -0
  12. package/generation-results.json +1817 -0
  13. package/package.json +45 -0
  14. package/scripts/batch-generate-all.js +159 -0
  15. package/scripts/batch-generate-mcp.js +329 -0
  16. package/scripts/batch-generate-stores-v2.js +272 -0
  17. package/scripts/batch-generate-stores.js +179 -0
  18. package/scripts/batch-plan.json +3810 -0
  19. package/scripts/batch-process.py +90 -0
  20. package/scripts/batch-regenerate.js +356 -0
  21. package/scripts/direct-generate.js +227 -0
  22. package/scripts/execute-batches.js +1911 -0
  23. package/scripts/generate-all-stores.js +144 -0
  24. package/scripts/generate-stores-mcp.js +161 -0
  25. package/scripts/generate-stores-v2.js +450 -0
  26. package/scripts/generate-stores-v3.js +412 -0
  27. package/scripts/generate-stores-v4.js +521 -0
  28. package/scripts/generate-stores.js +382 -0
  29. package/scripts/repos-to-process.json +1899 -0
  30. package/src/config/nh-layout-patterns.ts +166 -0
  31. package/src/docs/HOOK_GENERATION_PLAN.md +2226 -0
  32. package/src/docs/NH_STORE_PATTERNS.md +297 -0
  33. package/src/docs/README.md +216 -0
  34. package/src/docs/index.ts +28 -0
  35. package/src/docs/loader.ts +568 -0
  36. package/src/docs/patterns.json +419 -0
  37. package/src/docs/practical-examples.md +732 -0
  38. package/src/docs/quick-start.md +257 -0
  39. package/src/docs/requirements-analysis-guide.md +364 -0
  40. package/src/docs/rules.json +321 -0
  41. package/src/docs/store-pattern-analysis.md +664 -0
  42. package/src/docs/store-patterns-rules.md +1168 -0
  43. package/src/docs/store-patterns-usage-guide.md +1835 -0
  44. package/src/docs/troubleshooting.md +544 -0
  45. package/src/docs/type-selection-guide.md +572 -0
  46. 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
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. 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
  55. 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
  56. 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
  57. package/src/features/store-features.ts +232 -0
  58. package/src/handlers/analyze-requirements.ts +403 -0
  59. package/src/handlers/analyze.ts +1373 -0
  60. package/src/handlers/generate-from-requirements.ts +250 -0
  61. package/src/handlers/generate-hook.ts +950 -0
  62. package/src/handlers/generate-interactive.ts +840 -0
  63. package/src/handlers/generate-listdatagrid.ts +521 -0
  64. package/src/handlers/generate-multi-stores.ts +577 -0
  65. package/src/handlers/generate-requirements-from-layout.ts +160 -0
  66. package/src/handlers/generate-search-params.ts +717 -0
  67. package/src/handlers/generate.ts +911 -0
  68. package/src/handlers/list-templates.ts +104 -0
  69. package/src/handlers/scan-metadata.ts +485 -0
  70. package/src/handlers/suggest-layout.ts +326 -0
  71. package/src/index.ts +959 -0
  72. package/src/prompts/search-params.md +793 -0
  73. package/src/templates/index.ts +107 -0
  74. package/src/templates/unified.ts +462 -0
  75. package/store-generation-error-patterns.md +225 -0
  76. package/test/useAgentStore.ts +136 -0
  77. package/test-server.js +78 -0
  78. 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 패턴 제약 조건과 규칙을 정리한 것입니다.*