@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,2226 @@
|
|
|
1
|
+
# React Hook 자동 생성 기능 계획서
|
|
2
|
+
|
|
3
|
+
## 개요
|
|
4
|
+
|
|
5
|
+
리포지토리 인터페이스를 분석하여 리액트 훅 파일을 자동 생성하는 MCP 툴을 추가합니다.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. 기능 목표
|
|
10
|
+
|
|
11
|
+
### 1.0 선행 작업: 인터페이스 타입 분석 (최우선)
|
|
12
|
+
|
|
13
|
+
훅 생성을 위해 **반드시** 인터페이스 파일을 분석하여 요청/응답 타입을 확인해야 합니다.
|
|
14
|
+
|
|
15
|
+
**분석 순서:**
|
|
16
|
+
1. Repository 파일에서 메서드 추출
|
|
17
|
+
2. 각 메서드의 요청 타입(`RequestType`) 추출
|
|
18
|
+
3. 각 메서드의 응답 타입(`ResponseType`) 추출
|
|
19
|
+
4. 응답 타입에서 DTO 타입 추출 (`ds: Type[]` → `Type`)
|
|
20
|
+
5. 페이징 여부 확인 (`page:` 필드 존재 여부)
|
|
21
|
+
6. dateRange 패턴 확인 (`bgnDtm/endDtm` 쌍)
|
|
22
|
+
|
|
23
|
+
**타입 추출 예시:**
|
|
24
|
+
```typescript
|
|
25
|
+
// Repository 파일
|
|
26
|
+
async postMemberListMember(params: PostMemberListMemberRequest): Promise<PostMemberListMemberResponse>
|
|
27
|
+
|
|
28
|
+
// 분석 결과
|
|
29
|
+
{
|
|
30
|
+
methodName: "postMemberListMember",
|
|
31
|
+
requestType: "PostMemberListMemberRequest",
|
|
32
|
+
responseType: "PostMemberListMemberResponse",
|
|
33
|
+
dtoType: "MemberRes", // Response에서 ds: MemberRes[] 추출
|
|
34
|
+
hasPage: true, // Response에 page: 필드 있음
|
|
35
|
+
hasDateRange: true, // Request에 bgnDtm/endDtm 쌍 있음
|
|
36
|
+
dateRangeFields: { startField: "bgnDtm", endField: "endDtm" }
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 1.1 대상 API 필터링
|
|
41
|
+
|
|
42
|
+
| 포함 키워드 | 제외 키워드 |
|
|
43
|
+
|-------------|-------------|
|
|
44
|
+
| 조회, 목록 | 저장, 삭제, 수정, 엑셀, 업로드, 다운로드 |
|
|
45
|
+
|
|
46
|
+
**예시:**
|
|
47
|
+
```typescript
|
|
48
|
+
/* 회원 목록 */ // ✅ 포함
|
|
49
|
+
async postMemberListMember() // ✅ 포함
|
|
50
|
+
/* 회원 저장 */ // ❌ 제외
|
|
51
|
+
async postMemberSave() // ❌ 제외
|
|
52
|
+
/* 엑셀 다운로드 */ // ❌ 제외
|
|
53
|
+
async postMemberExcel() // ❌ 제외
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 1.2 생성되는 훅 구조
|
|
57
|
+
|
|
58
|
+
**핵심 원칙:** 각 메서드의 응답 타입을 분석하여 `hasPage` 여부에 따라 **가변적으로** `page`, `setPage`를 생성합니다.
|
|
59
|
+
|
|
60
|
+
#### Case 1: 페이징 있는 경우
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
export function useMemberService(params?: ListRequest) {
|
|
64
|
+
// 상태
|
|
65
|
+
const [list, setList] = useState<MemberRes[]>([]);
|
|
66
|
+
const [listSpinning, setListSpinning] = useState(false);
|
|
67
|
+
|
|
68
|
+
// ✅ 페이징 있는 경우 (hasPage: true)
|
|
69
|
+
const [page, setPage] = useState<AXDGPage>({
|
|
70
|
+
totalPages: 0,
|
|
71
|
+
totalElements: 0,
|
|
72
|
+
currentPage: 0,
|
|
73
|
+
pageSize: 0,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// dateRange 있는 경우
|
|
77
|
+
interface ListRequest extends Partial<PostMemberListMemberRequest> {
|
|
78
|
+
dateRange?: [string, string];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const getList = useCallback(async (listParams: ListRequest) => {
|
|
82
|
+
setListSpinning(true);
|
|
83
|
+
try {
|
|
84
|
+
const data = await MemberService.postMemberListMember(deleteEmptyValue(listParams));
|
|
85
|
+
setList(data.ds);
|
|
86
|
+
|
|
87
|
+
// ✅ hasPage: true이면 setPage 호출
|
|
88
|
+
setPage({
|
|
89
|
+
totalPages: data.page.pageCount,
|
|
90
|
+
totalElements: data.page.totalCount,
|
|
91
|
+
currentPage: data.page.pageNumber,
|
|
92
|
+
pageSize: data.page.pageSize,
|
|
93
|
+
});
|
|
94
|
+
} catch (err) {
|
|
95
|
+
await errorHandling(err);
|
|
96
|
+
} finally {
|
|
97
|
+
setListSpinning(false);
|
|
98
|
+
}
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
// ✅ hasPage: true이면 return에 page 포함
|
|
102
|
+
return {
|
|
103
|
+
list,
|
|
104
|
+
page, // ✅ 포함
|
|
105
|
+
listSpinning,
|
|
106
|
+
getList,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### Case 2: 페이징 없는 경우
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
export function useCodeService() {
|
|
115
|
+
const [list, setList] = useState<Code[]>([]);
|
|
116
|
+
const [listSpinning, setListSpinning] = useState(false);
|
|
117
|
+
|
|
118
|
+
// ❌ 페이징 없으면 page, setPage 생성 안 함
|
|
119
|
+
|
|
120
|
+
const getList = useCallback(async (params?: GetCodeListRequest) => {
|
|
121
|
+
setListSpinning(true);
|
|
122
|
+
try {
|
|
123
|
+
const data = await CodeService.getCodeList(deleteEmptyValue(params));
|
|
124
|
+
setList(data.ds);
|
|
125
|
+
|
|
126
|
+
// ❌ hasPage: false이면 setPage 호출 안 함
|
|
127
|
+
} catch (err) {
|
|
128
|
+
await errorHandling(err);
|
|
129
|
+
} finally {
|
|
130
|
+
setListSpinning(false);
|
|
131
|
+
}
|
|
132
|
+
}, []);
|
|
133
|
+
|
|
134
|
+
// ❌ hasPage: false이면 return에 page 미포함
|
|
135
|
+
return {
|
|
136
|
+
list,
|
|
137
|
+
// page, // ❌ 미포함
|
|
138
|
+
listSpinning,
|
|
139
|
+
getList,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
#### Case 3: 여러 API - 페이징 섞인 경우
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
export function useMemberService() {
|
|
148
|
+
// 첫 번째: 페이징 있음
|
|
149
|
+
const [list, setList] = useState<MemberRes[]>([]);
|
|
150
|
+
const [listSpinning, setListSpinning] = useState(false);
|
|
151
|
+
const [page, setPage] = useState<AXDGPage>({...}); // ✅ 있음
|
|
152
|
+
|
|
153
|
+
// 두 번째: 페이징 없음
|
|
154
|
+
const [adminList, setAdminList] = useState<AdminRes[]>([]);
|
|
155
|
+
const [adminSpinning, setAdminSpinning] = useState(false);
|
|
156
|
+
// ❌ adminPage, setAdminPage 없음
|
|
157
|
+
|
|
158
|
+
const getList = useCallback(async (params?: ListRequest) => {
|
|
159
|
+
setListSpinning(true);
|
|
160
|
+
try {
|
|
161
|
+
const data = await MemberService.postMemberListMember(deleteEmptyValue(params));
|
|
162
|
+
setList(data.ds);
|
|
163
|
+
setPage({ ... }); // ✅ setPage 호출
|
|
164
|
+
} catch (err) {
|
|
165
|
+
await errorHandling(err);
|
|
166
|
+
} finally {
|
|
167
|
+
setListSpinning(false);
|
|
168
|
+
}
|
|
169
|
+
}, []);
|
|
170
|
+
|
|
171
|
+
const getAdminList = useCallback(async (params?: AdminListRequest) => {
|
|
172
|
+
setAdminSpinning(true);
|
|
173
|
+
try {
|
|
174
|
+
const data = await MemberService.postMemberListAdmin(deleteEmptyValue(params));
|
|
175
|
+
setAdminList(data.ds);
|
|
176
|
+
// ❌ setPage 없음
|
|
177
|
+
} catch (err) {
|
|
178
|
+
await errorHandling(err);
|
|
179
|
+
} finally {
|
|
180
|
+
setAdminSpinning(false);
|
|
181
|
+
}
|
|
182
|
+
}, []);
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
list,
|
|
186
|
+
page, // ✅ 첫 번째는 포함
|
|
187
|
+
listSpinning,
|
|
188
|
+
getList,
|
|
189
|
+
adminList,
|
|
190
|
+
// adminPage, // ❌ 두 번째는 미포함
|
|
191
|
+
adminSpinning,
|
|
192
|
+
getAdminList,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## 2. 여러 목록 API 처리
|
|
200
|
+
|
|
201
|
+
### 2.1 네이밍 규칙 (옵션 1: 메서드명 기반 자동 네이밍)
|
|
202
|
+
|
|
203
|
+
| 메서드명 | 리스트 변수명 | 함수명 |
|
|
204
|
+
|----------|---------------|--------|
|
|
205
|
+
| `postMemberListMember` | `list` (기본) | `getList` |
|
|
206
|
+
| `postMemberListAdmin` | `adminList` | `getAdminList` |
|
|
207
|
+
| `postMemberListActive` | `activeList` | `getActiveList` |
|
|
208
|
+
| `postMemberListByStatus` | `byStatusList` | `getByStatusList` |
|
|
209
|
+
| `postCategoryList` | `categoryList` | `getCategoryList` |
|
|
210
|
+
|
|
211
|
+
**규칙:**
|
|
212
|
+
- `post{Entity}List{Suffix}` → `{suffix}List` (camelCase)
|
|
213
|
+
- `post{Entity}List` (접미사 없음) → `list` (기본)
|
|
214
|
+
|
|
215
|
+
### 2.2 여러 API가 있는 경우 생성 예시
|
|
216
|
+
|
|
217
|
+
**중요:** 각 메서드별로 `hasPage` 여부를 확인하여 **가변적으로** `page`, `setPage`를 생성합니다.
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
export function useMemberService() {
|
|
221
|
+
// 첫 번째 목록 (기본) - 페이징 있음 (hasPage: true)
|
|
222
|
+
const [list, setList] = useState<MemberRes[]>([]);
|
|
223
|
+
const [listSpinning, setListSpinning] = useState(false);
|
|
224
|
+
const [page, setPage] = useState<AXDGPage>({...}); // ✅ 있음
|
|
225
|
+
|
|
226
|
+
// 두 번째 목록 - 페이징 없음 (hasPage: false)
|
|
227
|
+
const [adminList, setAdminList] = useState<AdminRes[]>([]);
|
|
228
|
+
const [adminSpinning, setAdminSpinning] = useState(false);
|
|
229
|
+
// ❌ adminPage, setAdminPage 없음
|
|
230
|
+
|
|
231
|
+
// 세 번째 목록 - 페이징 있음 (hasPage: true)
|
|
232
|
+
const [activeList, setActiveList] = useState<ActiveRes[]>([]);
|
|
233
|
+
const [activeSpinning, setActiveSpinning] = useState(false);
|
|
234
|
+
const [activePage, setActivePage] = useState<AXDGPage>({...}); // ✅ 있음
|
|
235
|
+
|
|
236
|
+
// 각각의 getList 함수
|
|
237
|
+
const getList = useCallback(async (params?: ListRequest) => {
|
|
238
|
+
setListSpinning(true);
|
|
239
|
+
try {
|
|
240
|
+
const data = await MemberService.postMemberListMember(deleteEmptyValue(params));
|
|
241
|
+
setList(data.ds);
|
|
242
|
+
setPage({ // ✅ hasPage: true이면 호출
|
|
243
|
+
totalPages: data.page.pageCount,
|
|
244
|
+
totalElements: data.page.totalCount,
|
|
245
|
+
currentPage: data.page.pageNumber,
|
|
246
|
+
pageSize: data.page.pageSize,
|
|
247
|
+
});
|
|
248
|
+
} catch (err) {
|
|
249
|
+
await errorHandling(err);
|
|
250
|
+
} finally {
|
|
251
|
+
setListSpinning(false);
|
|
252
|
+
}
|
|
253
|
+
}, []);
|
|
254
|
+
|
|
255
|
+
const getAdminList = useCallback(async (params?: AdminListRequest) => {
|
|
256
|
+
setAdminSpinning(true);
|
|
257
|
+
try {
|
|
258
|
+
const data = await MemberService.postMemberListAdmin(deleteEmptyValue(params));
|
|
259
|
+
setAdminList(data.ds);
|
|
260
|
+
// ❌ hasPage: false이면 setPage 없음
|
|
261
|
+
} catch (err) {
|
|
262
|
+
await errorHandling(err);
|
|
263
|
+
} finally {
|
|
264
|
+
setAdminSpinning(false);
|
|
265
|
+
}
|
|
266
|
+
}, []);
|
|
267
|
+
|
|
268
|
+
const getActiveList = useCallback(async (params?: ActiveListRequest) => {
|
|
269
|
+
setActiveSpinning(true);
|
|
270
|
+
try {
|
|
271
|
+
const data = await MemberService.postMemberListActive(deleteEmptyValue(params));
|
|
272
|
+
setActiveList(data.ds);
|
|
273
|
+
setActivePage({ // ✅ hasPage: true이면 호출
|
|
274
|
+
totalPages: data.page.pageCount,
|
|
275
|
+
totalElements: data.page.totalCount,
|
|
276
|
+
currentPage: data.page.pageNumber,
|
|
277
|
+
pageSize: data.page.pageSize,
|
|
278
|
+
});
|
|
279
|
+
} catch (err) {
|
|
280
|
+
await errorHandling(err);
|
|
281
|
+
} finally {
|
|
282
|
+
setActiveSpinning(false);
|
|
283
|
+
}
|
|
284
|
+
}, []);
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
list,
|
|
288
|
+
page, // ✅ hasPage: true
|
|
289
|
+
listSpinning,
|
|
290
|
+
getList,
|
|
291
|
+
adminList,
|
|
292
|
+
// adminPage, // ❌ hasPage: false
|
|
293
|
+
adminSpinning,
|
|
294
|
+
getAdminList,
|
|
295
|
+
activeList,
|
|
296
|
+
activePage, // ✅ hasPage: true
|
|
297
|
+
activeSpinning,
|
|
298
|
+
getActiveList,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**페이징 처리 규칙:**
|
|
304
|
+
| 메서드 | hasPage | 상태 생성 | setPage 호출 | return 포함 |
|
|
305
|
+
|--------|---------|-----------|--------------|-------------|
|
|
306
|
+
| getList | true | ✅ | ✅ | ✅ |
|
|
307
|
+
| getAdminList | false | ❌ | ❌ | ❌ |
|
|
308
|
+
| getActiveList | true | ✅ | ✅ | ✅ |
|
|
309
|
+
|
|
310
|
+
### 2.3 단건(Detail) API 네이밍 규칙
|
|
311
|
+
|
|
312
|
+
**메서드 패턴 분류:**
|
|
313
|
+
|
|
314
|
+
| 구분 | 메서드 패턴 | 예시 | 응답 구조 |
|
|
315
|
+
|------|-------------|------|-----------|
|
|
316
|
+
| 목록 | `post{Entity}List{Suffix}` | `postMemberListMember` | `ds: Type[]` |
|
|
317
|
+
| 단건 | `post{Entity}{Suffix}` | `postMemberDetail`, `postMemberInfo` | `rs: Type` |
|
|
318
|
+
| 단건 | `get{Entity}{Suffix}` | `getMemberDetail`, `getMemberInfo` | `rs: Type` |
|
|
319
|
+
| 단건 | `post{Entity}` | `postMember` | `rs: Type` |
|
|
320
|
+
| 단건 | `get{Entity}` | `getMember` | `rs: Type` |
|
|
321
|
+
|
|
322
|
+
**단건 네이밍 규칙:**
|
|
323
|
+
|
|
324
|
+
| 메서드명 | 변수명 | 함수명 | Spinning |
|
|
325
|
+
|----------|--------|--------|----------|
|
|
326
|
+
| `postMemberDetail` | `detail` | `getDetail` | `detailSpinning` |
|
|
327
|
+
| `postMemberInfo` | `info` | `getInfo` | `infoSpinning` |
|
|
328
|
+
| `postMemberProfile` | `profile` | `getProfile` | `profileSpinning` |
|
|
329
|
+
| `postMember` | `member` | `getMember` | `memberSpinning` |
|
|
330
|
+
| `getMemberDetail` | `detail` | `getDetail` | `detailSpinning` |
|
|
331
|
+
| `getMember` | `member` | `getMember` | `memberSpinning` |
|
|
332
|
+
|
|
333
|
+
**단건 네이밍 추출 로직:**
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
/**
|
|
337
|
+
* 메서드명에서 단건 변수명 추출
|
|
338
|
+
* postMemberDetail → detail
|
|
339
|
+
* postMemberInfo → info
|
|
340
|
+
* postMemberProfile → profile
|
|
341
|
+
* getMember → member
|
|
342
|
+
*/
|
|
343
|
+
function extractDetailName(methodName: string, entityName: string): string {
|
|
344
|
+
// post{Entity}{Suffix} 패턴
|
|
345
|
+
const postPattern = new RegExp(`post${entityName}(.*)`);
|
|
346
|
+
const postMatch = methodName.match(postPattern);
|
|
347
|
+
|
|
348
|
+
if (postMatch) {
|
|
349
|
+
const suffix = postMatch[1];
|
|
350
|
+
if (!suffix) return entityName.toLowerCase(); // postMember → member
|
|
351
|
+
|
|
352
|
+
// 첫 글자 소문자로 변환 (camelCase)
|
|
353
|
+
return suffix.charAt(0).toLowerCase() + suffix.slice(1);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// get{Entity}{Suffix} 패턴
|
|
357
|
+
const getPattern = new RegExp(`get${entityName}(.*)`);
|
|
358
|
+
const getMatch = methodName.match(getPattern);
|
|
359
|
+
|
|
360
|
+
if (getMatch) {
|
|
361
|
+
const suffix = getMatch[1];
|
|
362
|
+
if (!suffix) return entityName.toLowerCase(); // getMember → member
|
|
363
|
+
|
|
364
|
+
return suffix.charAt(0).toLowerCase() + suffix.slice(1);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return 'detail'; // 기본값
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* 메서드명에서 단건 함수명 추출
|
|
372
|
+
* postMemberDetail → getDetail
|
|
373
|
+
* postMemberInfo → getInfo
|
|
374
|
+
* postMemberProfile → getProfile
|
|
375
|
+
* getMember → getMember
|
|
376
|
+
*/
|
|
377
|
+
function extractDetailFunctionName(methodName: string, entityName: string): string {
|
|
378
|
+
const detailName = extractDetailName(methodName, entityName);
|
|
379
|
+
|
|
380
|
+
// detail → getDetail, info → getInfo, member → getMember
|
|
381
|
+
return 'get' + detailName.charAt(0).toUpperCase() + detailName.slice(1);
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### 2.4 중복 처리 규칙 (옵션 C: 중복 시만 구분)
|
|
386
|
+
|
|
387
|
+
**중복 발생 예시:**
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
// 중복 가능성
|
|
391
|
+
async postMemberDetail(...) // → detail, getDetail
|
|
392
|
+
async getMemberDetail(...) // → detail, getDetail (중복!)
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
**중복 처리 로직:**
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
/**
|
|
399
|
+
* 중복 확인 후 접두사 추가
|
|
400
|
+
* 1. 기본 네이밍으로 모든 메서드의 이름을 생성
|
|
401
|
+
* 2. 중복이 있는 메서드에만 접두사 추가 (post → post, get → get)
|
|
402
|
+
*/
|
|
403
|
+
function resolveDuplicates(methods: Method[]): Method[] {
|
|
404
|
+
const nameMap = new Map<string, Method[]>();
|
|
405
|
+
|
|
406
|
+
// 1. 기본 네이밍으로 이름 생성
|
|
407
|
+
methods.forEach(method => {
|
|
408
|
+
const baseName = extractDetailName(method.name, entityName);
|
|
409
|
+
if (!nameMap.has(baseName)) {
|
|
410
|
+
nameMap.set(baseName, []);
|
|
411
|
+
}
|
|
412
|
+
nameMap.get(baseName)!.push(method);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// 2. 중복이 있는 경우 접두사 추가
|
|
416
|
+
nameMap.forEach((duplicates, baseName) => {
|
|
417
|
+
if (duplicates.length > 1) {
|
|
418
|
+
// 중복이 있으면 접두사 추가
|
|
419
|
+
duplicates.forEach(method => {
|
|
420
|
+
const prefix = method.name.startsWith('post') ? 'post' : 'get';
|
|
421
|
+
method.variableName = prefix + baseName.charAt(0).toUpperCase() + baseName.slice(1);
|
|
422
|
+
method.functionName = 'get' + method.variableName.charAt(0).toUpperCase() + method.variableName.slice(1);
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
return methods;
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
**중복 처리 결과:**
|
|
432
|
+
|
|
433
|
+
| 메서드명 | 기본 네이밍 | 중복处理后 |
|
|
434
|
+
|----------|-------------|-----------|
|
|
435
|
+
| `postMemberDetail` | `detail`, `getDetail` | `postDetail`, `getPostDetail` |
|
|
436
|
+
| `getMemberDetail` | `detail`, `getDetail` | `detail`, `getDetail` |
|
|
437
|
+
| `postMemberInfo` | `info`, `getInfo` | `info`, `getInfo` (중복 없음) |
|
|
438
|
+
|
|
439
|
+
### 2.5 목록과 단건 혼합 예시
|
|
440
|
+
|
|
441
|
+
**입력:**
|
|
442
|
+
```typescript
|
|
443
|
+
// MemberRepository.ts
|
|
444
|
+
|
|
445
|
+
/* 목록 API */
|
|
446
|
+
async postMemberListMember(...) : Promise<PostMemberListMemberResponse>
|
|
447
|
+
// Response: { ds: MemberRes[]; page: {...} } → 목록, hasPage: true
|
|
448
|
+
|
|
449
|
+
async postMemberListAdmin(...) : Promise<PostMemberListAdminResponse>
|
|
450
|
+
// Response: { ds: AdminRes[]; } → 목록, hasPage: false
|
|
451
|
+
|
|
452
|
+
/* 단건 API */
|
|
453
|
+
async postMemberDetail(...) : Promise<PostMemberDetailResponse>
|
|
454
|
+
// Response: { rs: MemberDetailRes; } → 단건
|
|
455
|
+
|
|
456
|
+
async postMemberInfo(...) : Promise<PostMemberInfoResponse>
|
|
457
|
+
// Response: { rs: MemberInfoRes; } → 단건
|
|
458
|
+
|
|
459
|
+
async postMemberProfile(...) : Promise<PostMemberProfileResponse>
|
|
460
|
+
// Response: { rs: MemberProfileRes; } → 단건
|
|
461
|
+
|
|
462
|
+
async getMember(...) : Promise<GetMemberResponse>
|
|
463
|
+
// Response: { rs: MemberRes; } → 단건
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**생성되는 훅:**
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
export function useMemberService() {
|
|
470
|
+
// ===== 목록 (List) =====
|
|
471
|
+
const [list, setList] = useState<MemberRes[]>([]);
|
|
472
|
+
const [listSpinning, setListSpinning] = useState(false);
|
|
473
|
+
const [page, setPage] = useState<AXDGPage>({...}); // ✅ hasPage: true
|
|
474
|
+
|
|
475
|
+
const [adminList, setAdminList] = useState<AdminRes[]>([]);
|
|
476
|
+
const [adminSpinning, setAdminSpinning] = useState(false);
|
|
477
|
+
// ❌ adminPage 없음 (hasPage: false)
|
|
478
|
+
|
|
479
|
+
// ===== 단건 (Detail) =====
|
|
480
|
+
const [detail, setDetail] = useState<MemberDetailRes>();
|
|
481
|
+
const [detailSpinning, setDetailSpinning] = useState(false);
|
|
482
|
+
|
|
483
|
+
const [info, setInfo] = useState<MemberInfoRes>();
|
|
484
|
+
const [infoSpinning, setInfoSpinning] = useState(false);
|
|
485
|
+
|
|
486
|
+
const [profile, setProfile] = useState<MemberProfileRes>();
|
|
487
|
+
const [profileSpinning, setProfileSpinning] = useState(false);
|
|
488
|
+
|
|
489
|
+
const [member, setMember] = useState<MemberRes>();
|
|
490
|
+
const [memberSpinning, setMemberSpinning] = useState(false);
|
|
491
|
+
|
|
492
|
+
// ===== 목록 함수 =====
|
|
493
|
+
const getList = useCallback(async (params?: ListRequest) => {
|
|
494
|
+
setListSpinning(true);
|
|
495
|
+
try {
|
|
496
|
+
const data = await MemberService.postMemberListMember(deleteEmptyValue(params));
|
|
497
|
+
setList(data.ds);
|
|
498
|
+
setPage({ ... }); // ✅ hasPage: true
|
|
499
|
+
} catch (err) {
|
|
500
|
+
await errorHandling(err);
|
|
501
|
+
} finally {
|
|
502
|
+
setListSpinning(false);
|
|
503
|
+
}
|
|
504
|
+
}, []);
|
|
505
|
+
|
|
506
|
+
const getAdminList = useCallback(async (params?: AdminListRequest) => {
|
|
507
|
+
setAdminSpinning(true);
|
|
508
|
+
try {
|
|
509
|
+
const data = await MemberService.postMemberListAdmin(deleteEmptyValue(params));
|
|
510
|
+
setAdminList(data.ds);
|
|
511
|
+
} catch (err) {
|
|
512
|
+
await errorHandling(err);
|
|
513
|
+
} finally {
|
|
514
|
+
setAdminSpinning(false);
|
|
515
|
+
}
|
|
516
|
+
}, []);
|
|
517
|
+
|
|
518
|
+
// ===== 단건 함수 =====
|
|
519
|
+
const getDetail = useCallback(async (params?: DetailRequest) => {
|
|
520
|
+
setDetailSpinning(true);
|
|
521
|
+
try {
|
|
522
|
+
const data = await MemberService.postMemberDetail(deleteEmptyValue(params));
|
|
523
|
+
setDetail(data.rs);
|
|
524
|
+
} catch (err) {
|
|
525
|
+
await errorHandling(err);
|
|
526
|
+
} finally {
|
|
527
|
+
setDetailSpinning(false);
|
|
528
|
+
}
|
|
529
|
+
}, []);
|
|
530
|
+
|
|
531
|
+
const getInfo = useCallback(async (params?: InfoRequest) => {
|
|
532
|
+
setInfoSpinning(true);
|
|
533
|
+
try {
|
|
534
|
+
const data = await MemberService.postMemberInfo(deleteEmptyValue(params));
|
|
535
|
+
setInfo(data.rs);
|
|
536
|
+
} catch (err) {
|
|
537
|
+
await errorHandling(err);
|
|
538
|
+
} finally {
|
|
539
|
+
setInfoSpinning(false);
|
|
540
|
+
}
|
|
541
|
+
}, []);
|
|
542
|
+
|
|
543
|
+
const getProfile = useCallback(async (params?: ProfileRequest) => {
|
|
544
|
+
setProfileSpinning(true);
|
|
545
|
+
try {
|
|
546
|
+
const data = await MemberService.postMemberProfile(deleteEmptyValue(params));
|
|
547
|
+
setProfile(data.rs);
|
|
548
|
+
} catch (err) {
|
|
549
|
+
await errorHandling(err);
|
|
550
|
+
} finally {
|
|
551
|
+
setProfileSpinning(false);
|
|
552
|
+
}
|
|
553
|
+
}, []);
|
|
554
|
+
|
|
555
|
+
const getMember = useCallback(async (params?: MemberRequest) => {
|
|
556
|
+
setMemberSpinning(true);
|
|
557
|
+
try {
|
|
558
|
+
const data = await MemberService.getMember(deleteEmptyValue(params));
|
|
559
|
+
setMember(data.rs);
|
|
560
|
+
} catch (err) {
|
|
561
|
+
await errorHandling(err);
|
|
562
|
+
} finally {
|
|
563
|
+
setMemberSpinning(false);
|
|
564
|
+
}
|
|
565
|
+
}, []);
|
|
566
|
+
|
|
567
|
+
return {
|
|
568
|
+
// 목록
|
|
569
|
+
list,
|
|
570
|
+
page, // ✅ hasPage: true
|
|
571
|
+
listSpinning,
|
|
572
|
+
getList,
|
|
573
|
+
adminList,
|
|
574
|
+
// adminPage, // ❌ hasPage: false
|
|
575
|
+
adminSpinning,
|
|
576
|
+
getAdminList,
|
|
577
|
+
// 단건
|
|
578
|
+
detail,
|
|
579
|
+
detailSpinning,
|
|
580
|
+
getDetail,
|
|
581
|
+
info,
|
|
582
|
+
infoSpinning,
|
|
583
|
+
getInfo,
|
|
584
|
+
profile,
|
|
585
|
+
profileSpinning,
|
|
586
|
+
getProfile,
|
|
587
|
+
member,
|
|
588
|
+
memberSpinning,
|
|
589
|
+
getMember,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
**목록 vs 단건 차이점:**
|
|
595
|
+
|
|
596
|
+
| 구분 | 상태 타입 | 응답 필드 | 페이징 |
|
|
597
|
+
|------|-----------|-----------|--------|
|
|
598
|
+
| 목록 | `Type[]` | `data.ds` | `page` 상태 생성 |
|
|
599
|
+
| 단건 | `Type \| undefined` | `data.rs` | 페이지 없음 |
|
|
600
|
+
|
|
601
|
+
---
|
|
602
|
+
|
|
603
|
+
## 3. Spinning 상태 관리 (옵션 B: 개별 spinning)
|
|
604
|
+
|
|
605
|
+
각 목록마다 개별 spinning 상태를 관리합니다.
|
|
606
|
+
|
|
607
|
+
```typescript
|
|
608
|
+
const [listSpinning, setListSpinning] = useState(false);
|
|
609
|
+
const [adminSpinning, setAdminSpinning] = useState(false);
|
|
610
|
+
const [activeSpinning, setActiveSpinning] = useState(false);
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
**장점:**
|
|
614
|
+
- 각 API의 로딩 상태를 독립적으로 관리
|
|
615
|
+
- UI에서 정확한 로딩 표시 가능
|
|
616
|
+
|
|
617
|
+
---
|
|
618
|
+
|
|
619
|
+
## 4. DateRange 처리
|
|
620
|
+
|
|
621
|
+
요청 타입에 `bgnDtm/endDtm` 또는 `startDateTime/endDateTime` 쌍이 있는 경우 자동으로 `dateRange` 필드를 추가합니다.
|
|
622
|
+
|
|
623
|
+
```typescript
|
|
624
|
+
interface ListRequest extends Partial<PostMemberListMemberRequest> {
|
|
625
|
+
dateRange?: [string, string];
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const getList = useCallback(async (listParams: ListRequest) => {
|
|
629
|
+
setListSpinning(true);
|
|
630
|
+
try {
|
|
631
|
+
const data = await MemberService.postMemberListMember(
|
|
632
|
+
deleteEmptyValue({
|
|
633
|
+
...listParams,
|
|
634
|
+
...dateRangeToDts(listParams.dateRange, {
|
|
635
|
+
bgnDtm: DT_FORMAT.DATETIME_HHMMSS,
|
|
636
|
+
endDtm: DT_FORMAT.DATETIME_HHMMSS,
|
|
637
|
+
}),
|
|
638
|
+
}),
|
|
639
|
+
);
|
|
640
|
+
setList(data.ds);
|
|
641
|
+
// ...
|
|
642
|
+
} catch (err) {
|
|
643
|
+
await errorHandling(err);
|
|
644
|
+
} finally {
|
|
645
|
+
setListSpinning(false);
|
|
646
|
+
}
|
|
647
|
+
}, []);
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
---
|
|
651
|
+
|
|
652
|
+
## 5. 출력 경로 처리
|
|
653
|
+
|
|
654
|
+
### 5.1 기본 경로
|
|
655
|
+
|
|
656
|
+
출력 경로를 지정하지 않은 경우 `src/hooks`에 생성합니다.
|
|
657
|
+
|
|
658
|
+
```
|
|
659
|
+
src/hooks/useMemberService.ts
|
|
660
|
+
src/hooks/useCategoryService.ts
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
### 5.2 사용자 지정 경로
|
|
664
|
+
|
|
665
|
+
출력 경로를 지정한 경우 해당 폴더에 생성합니다.
|
|
666
|
+
|
|
667
|
+
```
|
|
668
|
+
src/hooks/member/useMemberService.ts
|
|
669
|
+
src/hooks/product/useProductService.ts
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
### 5.3 index.ts 자동 생성
|
|
673
|
+
|
|
674
|
+
대상 폴더에 `index.ts`가 없으면 자동으로 생성하고 export를 추가합니다.
|
|
675
|
+
|
|
676
|
+
```typescript
|
|
677
|
+
// src/hooks/index.ts
|
|
678
|
+
export * from './useMemberService';
|
|
679
|
+
export * from './useCategoryService';
|
|
680
|
+
export * from './useProductService';
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
### 5.4 기존 파일 처리
|
|
684
|
+
|
|
685
|
+
기존 파일이 있으면 덮어씁니다.
|
|
686
|
+
|
|
687
|
+
---
|
|
688
|
+
|
|
689
|
+
## 6. 구현 파일 구조
|
|
690
|
+
|
|
691
|
+
```
|
|
692
|
+
src/
|
|
693
|
+
├── handlers/
|
|
694
|
+
│ └── generate-hook.ts # 새로 추가
|
|
695
|
+
├── templates/
|
|
696
|
+
│ └── hook-template.ts # 새로 추가
|
|
697
|
+
└── index.ts # 도구 등록
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
---
|
|
701
|
+
|
|
702
|
+
## 7. MCP 도구 사양
|
|
703
|
+
|
|
704
|
+
### 7.1 도구 이름
|
|
705
|
+
|
|
706
|
+
`generate-hook`
|
|
707
|
+
|
|
708
|
+
### 7.2 파라미터
|
|
709
|
+
|
|
710
|
+
| 파라미터 | 타입 | 필수 | 설명 | 기본값 |
|
|
711
|
+
|----------|------|------|------|--------|
|
|
712
|
+
| `interfacePath` | string | ✅ | 인터페이스 Repository 파일 경로 (절대 경로) | - |
|
|
713
|
+
| `outputPath` | string | ❌ | 생성할 훅 파일 경로 (절대 경로) | `src/hooks/use{Entity}Service.ts` |
|
|
714
|
+
|
|
715
|
+
### 7.3 사용 예시
|
|
716
|
+
|
|
717
|
+
```typescript
|
|
718
|
+
// 기본 경로 (src/hooks)
|
|
719
|
+
{
|
|
720
|
+
"interfacePath": "/path/to/repository/MemberRepository.ts"
|
|
721
|
+
}
|
|
722
|
+
// → src/hooks/useMemberService.ts 생성
|
|
723
|
+
|
|
724
|
+
// 사용자 지정 경로
|
|
725
|
+
{
|
|
726
|
+
"interfacePath": "/path/to/repository/MemberRepository.ts",
|
|
727
|
+
"outputPath": "/path/to/src/hooks/member/useMemberService.ts"
|
|
728
|
+
}
|
|
729
|
+
// → 지정한 경로에 생성
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
---
|
|
733
|
+
|
|
734
|
+
## 8. 네이밍 추출 로직
|
|
735
|
+
|
|
736
|
+
### 8.1 리스트 변수명 추출
|
|
737
|
+
|
|
738
|
+
```typescript
|
|
739
|
+
/**
|
|
740
|
+
* 메서드명에서 리스트 변수명 추출
|
|
741
|
+
* postMemberListMember → memberList (접미사가 Entity면 기본값 list)
|
|
742
|
+
* postMemberListAdmin → adminList
|
|
743
|
+
* postMemberList → list (접미사 없으면 기본값)
|
|
744
|
+
*/
|
|
745
|
+
function extractListName(methodName: string, entityName: string): string {
|
|
746
|
+
const pattern = new RegExp(`post${entityName}List(.*)`);
|
|
747
|
+
const match = methodName.match(pattern);
|
|
748
|
+
|
|
749
|
+
if (match) {
|
|
750
|
+
const suffix = match[1];
|
|
751
|
+
if (!suffix) return 'list'; // 접미사 없으면 기본값
|
|
752
|
+
|
|
753
|
+
// 접미사가 Entity명과 같으면 기본값 (postMemberListMember → list)
|
|
754
|
+
if (suffix.toLowerCase() === entityName.toLowerCase()) {
|
|
755
|
+
return 'list';
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// 첫 글자 소문자로 변환 (camelCase)
|
|
759
|
+
return suffix.charAt(0).toLowerCase() + suffix.slice(1) + 'List';
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return 'list';
|
|
763
|
+
}
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
### 8.2 함수명 추출
|
|
767
|
+
|
|
768
|
+
```typescript
|
|
769
|
+
/**
|
|
770
|
+
* 메서드명에서 함수명 추출
|
|
771
|
+
* postMemberListMember → getList (접미사가 Entity면 getList)
|
|
772
|
+
* postMemberListAdmin → getAdminList
|
|
773
|
+
* postMemberList → getList
|
|
774
|
+
*/
|
|
775
|
+
function extractFunctionName(methodName: string, entityName: string): string {
|
|
776
|
+
const pattern = new RegExp(`post${entityName}List(.*)`);
|
|
777
|
+
const match = methodName.match(pattern);
|
|
778
|
+
|
|
779
|
+
if (match) {
|
|
780
|
+
const suffix = match[1];
|
|
781
|
+
if (!suffix) return 'getList';
|
|
782
|
+
|
|
783
|
+
// 접미사가 Entity명과 같으면 getList
|
|
784
|
+
if (suffix.toLowerCase() === entityName.toLowerCase()) {
|
|
785
|
+
return 'getList';
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// 첫 글자 대문자로 변환 (PascalCase)
|
|
789
|
+
return 'get' + suffix.charAt(0).toUpperCase() + suffix.slice(1);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
return 'getList';
|
|
793
|
+
}
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
### 8.2 단건(Detail) 변수명 추출
|
|
797
|
+
|
|
798
|
+
```typescript
|
|
799
|
+
/**
|
|
800
|
+
* 메서드명에서 단건 변수명 추출
|
|
801
|
+
* postMemberDetail → detail
|
|
802
|
+
* postMemberInfo → info
|
|
803
|
+
* postMemberProfile → profile
|
|
804
|
+
* postMember → member
|
|
805
|
+
* getMemberDetail → detail
|
|
806
|
+
* getMember → member
|
|
807
|
+
*/
|
|
808
|
+
function extractDetailName(methodName: string, entityName: string): string {
|
|
809
|
+
// post{Entity}{Suffix} 패턴
|
|
810
|
+
const postPattern = new RegExp(`post${entityName}(.*)`);
|
|
811
|
+
const postMatch = methodName.match(postPattern);
|
|
812
|
+
|
|
813
|
+
if (postMatch) {
|
|
814
|
+
const suffix = postMatch[1];
|
|
815
|
+
if (!suffix) return entityName.toLowerCase(); // postMember → member
|
|
816
|
+
|
|
817
|
+
// 첫 글자 소문자로 변환 (camelCase)
|
|
818
|
+
return suffix.charAt(0).toLowerCase() + suffix.slice(1);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// get{Entity}{Suffix} 패턴
|
|
822
|
+
const getPattern = new RegExp(`get${entityName}(.*)`);
|
|
823
|
+
const getMatch = methodName.match(getPattern);
|
|
824
|
+
|
|
825
|
+
if (getMatch) {
|
|
826
|
+
const suffix = getMatch[1];
|
|
827
|
+
if (!suffix) return entityName.toLowerCase(); // getMember → member
|
|
828
|
+
|
|
829
|
+
return suffix.charAt(0).toLowerCase() + suffix.slice(1);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
return 'detail'; // 기본값
|
|
833
|
+
}
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
### 8.3 단건(Detail) 함수명 추출
|
|
837
|
+
|
|
838
|
+
```typescript
|
|
839
|
+
/**
|
|
840
|
+
* 메서드명에서 단건 함수명 추출
|
|
841
|
+
* postMemberDetail → getDetail
|
|
842
|
+
* postMemberInfo → getInfo
|
|
843
|
+
* postMember → getMember
|
|
844
|
+
* getMemberDetail → getDetail
|
|
845
|
+
*/
|
|
846
|
+
function extractDetailFunctionName(methodName: string, entityName: string): string {
|
|
847
|
+
const detailName = extractDetailName(methodName, entityName);
|
|
848
|
+
|
|
849
|
+
// detail → getDetail, info → getInfo, member → getMember
|
|
850
|
+
return 'get' + detailName.charAt(0).toUpperCase() + detailName.slice(1);
|
|
851
|
+
}
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
### 8.4 중복 처리 로직
|
|
855
|
+
|
|
856
|
+
```typescript
|
|
857
|
+
/**
|
|
858
|
+
* 중복 확인 후 접두사 추가
|
|
859
|
+
* 1. 기본 네이밍으로 모든 메서드의 이름을 생성
|
|
860
|
+
* 2. 중복이 있는 메서드에만 접두사 추가
|
|
861
|
+
*/
|
|
862
|
+
function resolveDuplicateNames(methods: MethodInfo[]): MethodInfo[] {
|
|
863
|
+
const nameMap = new Map<string, MethodInfo[]>();
|
|
864
|
+
|
|
865
|
+
// 1. 기본 네이밍으로 이름 생성 후 그룹화
|
|
866
|
+
methods.forEach(method => {
|
|
867
|
+
const baseName = method.isListMethod
|
|
868
|
+
? extractListName(method.name, entityName)
|
|
869
|
+
: extractDetailName(method.name, entityName);
|
|
870
|
+
|
|
871
|
+
if (!nameMap.has(baseName)) {
|
|
872
|
+
nameMap.set(baseName, []);
|
|
873
|
+
}
|
|
874
|
+
nameMap.get(baseName)!.push(method);
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
// 2. 중복이 있는 경우 접두사 추가
|
|
878
|
+
nameMap.forEach((duplicates, baseName) => {
|
|
879
|
+
if (duplicates.length > 1) {
|
|
880
|
+
// 중복이 있으면 접두사 추가 (post/get 또는 List/Detail 구분)
|
|
881
|
+
duplicates.forEach((method, index) => {
|
|
882
|
+
if (method.isListMethod) {
|
|
883
|
+
// 목록은 접미사로 구분
|
|
884
|
+
method.listName = baseName + (index + 1);
|
|
885
|
+
method.functionName = 'get' + baseName.charAt(0).toUpperCase() + baseName.slice(1) + (index + 1);
|
|
886
|
+
} else {
|
|
887
|
+
// 단건은 post/get 접두사로 구분
|
|
888
|
+
const prefix = method.name.startsWith('post') ? 'post' : 'get';
|
|
889
|
+
method.detailName = prefix + baseName.charAt(0).toUpperCase() + baseName.slice(1);
|
|
890
|
+
method.functionName = 'get' + method.detailName.charAt(0).toUpperCase() + method.detailName.slice(1);
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
return methods;
|
|
897
|
+
}
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
---
|
|
901
|
+
|
|
902
|
+
## 9. 템플릿 변수
|
|
903
|
+
|
|
904
|
+
```typescript
|
|
905
|
+
interface HookTemplateVariables {
|
|
906
|
+
// 서비스 정보
|
|
907
|
+
serviceName: string; // MemberService
|
|
908
|
+
entityName: string; // Member
|
|
909
|
+
hookName: string; // useMemberService
|
|
910
|
+
|
|
911
|
+
// 메서드 정보 (목록 + 단건)
|
|
912
|
+
methods: Array<{
|
|
913
|
+
// 공통
|
|
914
|
+
name: string; // postMemberListMember, postMemberDetail
|
|
915
|
+
functionName: string; // getList, getDetail
|
|
916
|
+
requestType: string; // PostMemberListMemberRequest, PostMemberDetailRequest
|
|
917
|
+
responseType: string; // PostMemberListMemberResponse, PostMemberDetailResponse
|
|
918
|
+
dtoType: string; // MemberRes, MemberDetailRes
|
|
919
|
+
|
|
920
|
+
// 목록(List) 전용
|
|
921
|
+
isListMethod: boolean; // true: 목록, false: 단건
|
|
922
|
+
listName?: string; // list, adminList (목록인 경우)
|
|
923
|
+
hasPage: boolean; // 페이징 여부 (가변 처리 결정)
|
|
924
|
+
|
|
925
|
+
// 단건(Detail) 전용
|
|
926
|
+
detailName?: string; // detail, info, member (단건인 경우)
|
|
927
|
+
|
|
928
|
+
// dateRange 정보
|
|
929
|
+
hasDateRange: boolean;
|
|
930
|
+
dateRangeStartField?: string; // bgnDtm
|
|
931
|
+
dateRangeEndField?: string; // endDtm
|
|
932
|
+
}>;
|
|
933
|
+
|
|
934
|
+
// 분류된 메서드
|
|
935
|
+
listMethods: typeof methods; // isListMethod: true인 메서드들
|
|
936
|
+
detailMethods: typeof methods; // isListMethod: false인 메서드들
|
|
937
|
+
}
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
### 9.1 목록/단건에 따른 가변 처리 로직
|
|
941
|
+
|
|
942
|
+
```typescript
|
|
943
|
+
// 템플릿 내부 처리 로직 (pseudo code)
|
|
944
|
+
|
|
945
|
+
// ===== 목록(List) 메서드 처리 =====
|
|
946
|
+
listMethods.forEach(method => {
|
|
947
|
+
// 1. 상태 선언
|
|
948
|
+
const [listName, setListName] = useState<DtoType[]>([]);
|
|
949
|
+
const [listNameSpinning, setListNameSpinning] = useState(false);
|
|
950
|
+
|
|
951
|
+
// 2. 페이징 상태 (hasPage: true인 경우만)
|
|
952
|
+
if (method.hasPage) {
|
|
953
|
+
const [page, setPage] = useState<AXDGPage>({...});
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// 3. useCallback
|
|
957
|
+
const callback = useCallback(async (params) => {
|
|
958
|
+
setListNameSpinning(true);
|
|
959
|
+
try {
|
|
960
|
+
const data = await Service.method(deleteEmptyValue(params));
|
|
961
|
+
setListName(data.ds); // 목록은 data.ds 사용
|
|
962
|
+
|
|
963
|
+
if (method.hasPage) {
|
|
964
|
+
setPage({ ... }); // 가변 처리
|
|
965
|
+
}
|
|
966
|
+
} catch (err) {
|
|
967
|
+
await errorHandling(err);
|
|
968
|
+
} finally {
|
|
969
|
+
setListNameSpinning(false);
|
|
970
|
+
}
|
|
971
|
+
}, []);
|
|
972
|
+
|
|
973
|
+
// 4. return 값
|
|
974
|
+
if (method.hasPage) {
|
|
975
|
+
return { listName, page, listNameSpinning, functionName };
|
|
976
|
+
} else {
|
|
977
|
+
return { listName, listNameSpinning, functionName };
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
// ===== 단건(Detail) 메서드 처리 =====
|
|
982
|
+
detailMethods.forEach(method => {
|
|
983
|
+
// 1. 상태 선언
|
|
984
|
+
const [detailName, setDetailName] = useState<DtoType>();
|
|
985
|
+
const [detailNameSpinning, setDetailNameSpinning] = useState(false);
|
|
986
|
+
|
|
987
|
+
// 2. useCallback
|
|
988
|
+
const callback = useCallback(async (params) => {
|
|
989
|
+
setDetailNameSpinning(true);
|
|
990
|
+
try {
|
|
991
|
+
const data = await Service.method(deleteEmptyValue(params));
|
|
992
|
+
setDetailName(data.rs); // 단건은 data.rs 사용
|
|
993
|
+
} catch (err) {
|
|
994
|
+
await errorHandling(err);
|
|
995
|
+
} finally {
|
|
996
|
+
setDetailNameSpinning(false);
|
|
997
|
+
}
|
|
998
|
+
}, []);
|
|
999
|
+
|
|
1000
|
+
// 3. return 값 (단건은 페이징 없음)
|
|
1001
|
+
return { detailName, detailNameSpinning, functionName };
|
|
1002
|
+
});
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
### 9.2 응답 필드에 따른 분류
|
|
1006
|
+
|
|
1007
|
+
```typescript
|
|
1008
|
+
/**
|
|
1009
|
+
* 응답 인터페이스에서 필드 구조 분석
|
|
1010
|
+
* data.ds[] → 목록 (isListMethod: true)
|
|
1011
|
+
* data.rs → 단건 (isListMethod: false)
|
|
1012
|
+
*/
|
|
1013
|
+
function classifyMethodByResponse(responseType: string): 'list' | 'detail' {
|
|
1014
|
+
const responseInterface = findInterface(content, responseType);
|
|
1015
|
+
|
|
1016
|
+
if (responseInterface) {
|
|
1017
|
+
// ds: Type[] 형태 → 목록
|
|
1018
|
+
if (/ds:\s*\w+\[\]/.test(responseInterface)) {
|
|
1019
|
+
return 'list';
|
|
1020
|
+
}
|
|
1021
|
+
// rs: Type 형태 → 단건
|
|
1022
|
+
if (/rs:\s*\w+/.test(responseInterface)) {
|
|
1023
|
+
return 'detail';
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
return 'list'; // 기본값
|
|
1028
|
+
}
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
---
|
|
1032
|
+
|
|
1033
|
+
## 10. 구현 우선순위
|
|
1034
|
+
|
|
1035
|
+
### 최우선: 인터페이스 타입 분석
|
|
1036
|
+
|
|
1037
|
+
**모든 작업의 선행 조건:**
|
|
1038
|
+
1. ✅ 인터페이스 Repository 파일 파싱
|
|
1039
|
+
2. ✅ 메서드 추출 (주석, 시그니처)
|
|
1040
|
+
3. ✅ 요청 타입(`RequestType`) 추출
|
|
1041
|
+
4. ✅ 응답 타입(`ResponseType`) 추출
|
|
1042
|
+
5. ✅ 응답 타입에서 DTO 타입 추출 (`ds: Type[]` → `Type`)
|
|
1043
|
+
6. ✅ 페이징 여부 확인 (`page:` 필드)
|
|
1044
|
+
7. ✅ dateRange 패턴 확인 (`bgnDtm/endDtm` 쌍)
|
|
1045
|
+
|
|
1046
|
+
### 이후 작업 순서
|
|
1047
|
+
|
|
1048
|
+
1. ✅ 기본 훅 템플릿 생성 (단일 API)
|
|
1049
|
+
2. ✅ 여러 API 네이밍 로직 구현
|
|
1050
|
+
3. ✅ 개별 spinning 상태 추가
|
|
1051
|
+
4. ✅ **hasPage에 따른 가변 처리** (page, setPage)
|
|
1052
|
+
5. ✅ dateRange 처리
|
|
1053
|
+
6. ✅ 출력 경로 처리 (기본/사용자 지정)
|
|
1054
|
+
7. ✅ index.ts 자동 생성 및 export
|
|
1055
|
+
8. ✅ 제외 키워드 필터링
|
|
1056
|
+
9. ✅ ESLint, Prettier 자동 적용
|
|
1057
|
+
|
|
1058
|
+
---
|
|
1059
|
+
|
|
1060
|
+
## 11. 테스트 시나리오
|
|
1061
|
+
|
|
1062
|
+
### 11.1 페이징 유무에 따른 가변 처리 확인
|
|
1063
|
+
|
|
1064
|
+
**입력 (페이징 있음):**
|
|
1065
|
+
```typescript
|
|
1066
|
+
// MemberRepository.ts
|
|
1067
|
+
/* 회원 목록 */
|
|
1068
|
+
async postMemberListMember(params: PostMemberListMemberRequest): Promise<PostMemberListMemberResponse>
|
|
1069
|
+
|
|
1070
|
+
// Response 인터페이스
|
|
1071
|
+
export interface PostMemberListMemberResponse extends DataGridPageResponse {
|
|
1072
|
+
ds: MemberRes[];
|
|
1073
|
+
page: { pageCount: number; totalCount: number; pageNumber: number; pageSize: number; };
|
|
1074
|
+
}
|
|
1075
|
+
```
|
|
1076
|
+
|
|
1077
|
+
**분석 결과:**
|
|
1078
|
+
```typescript
|
|
1079
|
+
{
|
|
1080
|
+
methodName: "postMemberListMember",
|
|
1081
|
+
hasPage: true, // ✅ page: 필드 있음
|
|
1082
|
+
}
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
**출력 (페이징 있음):**
|
|
1086
|
+
```typescript
|
|
1087
|
+
// useMemberService.ts
|
|
1088
|
+
export function useMemberService(params?: ListRequest) {
|
|
1089
|
+
const [list, setList] = useState<MemberRes[]>([]);
|
|
1090
|
+
const [listSpinning, setListSpinning] = useState(false);
|
|
1091
|
+
const [page, setPage] = useState<AXDGPage>({...}); // ✅ 있음
|
|
1092
|
+
|
|
1093
|
+
const getList = useCallback(async (listParams: ListRequest) => {
|
|
1094
|
+
setListSpinning(true);
|
|
1095
|
+
try {
|
|
1096
|
+
const data = await MemberService.postMemberListMember(deleteEmptyValue(listParams));
|
|
1097
|
+
setList(data.ds);
|
|
1098
|
+
setPage({ ... }); // ✅ 호출
|
|
1099
|
+
} catch (err) {
|
|
1100
|
+
await errorHandling(err);
|
|
1101
|
+
} finally {
|
|
1102
|
+
setListSpinning(false);
|
|
1103
|
+
}
|
|
1104
|
+
}, []);
|
|
1105
|
+
|
|
1106
|
+
return { list, page, listSpinning, getList }; // ✅ page 포함
|
|
1107
|
+
}
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
---
|
|
1111
|
+
|
|
1112
|
+
**입력 (페이징 없음):**
|
|
1113
|
+
```typescript
|
|
1114
|
+
// CodeRepository.ts
|
|
1115
|
+
/* 코드 목록 */
|
|
1116
|
+
async getCodeList(params: GetCodeListRequest): Promise<GetCodeListResponse>
|
|
1117
|
+
|
|
1118
|
+
// Response 인터페이스
|
|
1119
|
+
export interface GetCodeListResponse {
|
|
1120
|
+
ds: Code[];
|
|
1121
|
+
// page: 필드 없음
|
|
1122
|
+
}
|
|
1123
|
+
```
|
|
1124
|
+
|
|
1125
|
+
**분석 결과:**
|
|
1126
|
+
```typescript
|
|
1127
|
+
{
|
|
1128
|
+
methodName: "getCodeList",
|
|
1129
|
+
hasPage: false, // ❌ page: 필드 없음
|
|
1130
|
+
}
|
|
1131
|
+
```
|
|
1132
|
+
|
|
1133
|
+
**출력 (페이징 없음):**
|
|
1134
|
+
```typescript
|
|
1135
|
+
// useCodeService.ts
|
|
1136
|
+
export function useCodeService() {
|
|
1137
|
+
const [list, setList] = useState<Code[]>([]);
|
|
1138
|
+
const [listSpinning, setListSpinning] = useState(false);
|
|
1139
|
+
// ❌ page, setPage 없음
|
|
1140
|
+
|
|
1141
|
+
const getList = useCallback(async (params?: GetCodeListRequest) => {
|
|
1142
|
+
setListSpinning(true);
|
|
1143
|
+
try {
|
|
1144
|
+
const data = await CodeService.getCodeList(deleteEmptyValue(params));
|
|
1145
|
+
setList(data.ds);
|
|
1146
|
+
// ❌ setPage 없음
|
|
1147
|
+
} catch (err) {
|
|
1148
|
+
await errorHandling(err);
|
|
1149
|
+
} finally {
|
|
1150
|
+
setListSpinning(false);
|
|
1151
|
+
}
|
|
1152
|
+
}, []);
|
|
1153
|
+
|
|
1154
|
+
return { list, listSpinning, getList }; // ❌ page 미포함
|
|
1155
|
+
}
|
|
1156
|
+
```
|
|
1157
|
+
|
|
1158
|
+
### 11.2 여러 목록 API (페이징 섞인 경우)
|
|
1159
|
+
|
|
1160
|
+
**입력:**
|
|
1161
|
+
```typescript
|
|
1162
|
+
// MemberRepository.ts
|
|
1163
|
+
/* 회원 목록 */
|
|
1164
|
+
async postMemberListMember(params: PostMemberListMemberRequest): Promise<PostMemberListMemberResponse> { ... }
|
|
1165
|
+
// Response: { ds: MemberRes[]; page: {...} } → hasPage: true
|
|
1166
|
+
|
|
1167
|
+
/* 관리자 목록 */
|
|
1168
|
+
async postMemberListAdmin(params: PostMemberListAdminRequest): Promise<PostMemberListAdminResponse> { ... }
|
|
1169
|
+
// Response: { ds: AdminRes[]; } → hasPage: false
|
|
1170
|
+
|
|
1171
|
+
/* 활성 회원 목록 */
|
|
1172
|
+
async postMemberListActive(params: PostMemberListActiveRequest): Promise<PostMemberListActiveResponse> { ... }
|
|
1173
|
+
// Response: { ds: ActiveRes[]; page: {...} } → hasPage: true
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
**출력 (가변 처리 적용):**
|
|
1177
|
+
```typescript
|
|
1178
|
+
// useMemberService.ts
|
|
1179
|
+
export function useMemberService() {
|
|
1180
|
+
const [list, setList] = useState<MemberRes[]>([]);
|
|
1181
|
+
const [listSpinning, setListSpinning] = useState(false);
|
|
1182
|
+
const [page, setPage] = useState<AXDGPage>({...}); // ✅ 있음 (hasPage: true)
|
|
1183
|
+
|
|
1184
|
+
const [adminList, setAdminList] = useState<AdminRes[]>([]);
|
|
1185
|
+
const [adminSpinning, setAdminSpinning] = useState(false);
|
|
1186
|
+
// ❌ adminPage, setAdminPage 없음 (hasPage: false)
|
|
1187
|
+
|
|
1188
|
+
const [activeList, setActiveList] = useState<ActiveRes[]>([]);
|
|
1189
|
+
const [activeSpinning, setActiveSpinning] = useState(false);
|
|
1190
|
+
const [activePage, setActivePage] = useState<AXDGPage>({...}); // ✅ 있음 (hasPage: true)
|
|
1191
|
+
|
|
1192
|
+
const getList = useCallback(async (params?: ListRequest) => {
|
|
1193
|
+
setListSpinning(true);
|
|
1194
|
+
try {
|
|
1195
|
+
const data = await MemberService.postMemberListMember(deleteEmptyValue(params));
|
|
1196
|
+
setList(data.ds);
|
|
1197
|
+
setPage({ ... }); // ✅ 호출 (hasPage: true)
|
|
1198
|
+
} catch (err) {
|
|
1199
|
+
await errorHandling(err);
|
|
1200
|
+
} finally {
|
|
1201
|
+
setListSpinning(false);
|
|
1202
|
+
}
|
|
1203
|
+
}, []);
|
|
1204
|
+
|
|
1205
|
+
const getAdminList = useCallback(async (params?: AdminListRequest) => {
|
|
1206
|
+
setAdminSpinning(true);
|
|
1207
|
+
try {
|
|
1208
|
+
const data = await MemberService.postMemberListAdmin(deleteEmptyValue(params));
|
|
1209
|
+
setAdminList(data.ds);
|
|
1210
|
+
// ❌ setPage 없음 (hasPage: false)
|
|
1211
|
+
} catch (err) {
|
|
1212
|
+
await errorHandling(err);
|
|
1213
|
+
} finally {
|
|
1214
|
+
setAdminSpinning(false);
|
|
1215
|
+
}
|
|
1216
|
+
}, []);
|
|
1217
|
+
|
|
1218
|
+
const getActiveList = useCallback(async (params?: ActiveListRequest) => {
|
|
1219
|
+
setActiveSpinning(true);
|
|
1220
|
+
try {
|
|
1221
|
+
const data = await MemberService.postMemberListActive(deleteEmptyValue(params));
|
|
1222
|
+
setActiveList(data.ds);
|
|
1223
|
+
setActivePage({ ... }); // ✅ 호출 (hasPage: true)
|
|
1224
|
+
} catch (err) {
|
|
1225
|
+
await errorHandling(err);
|
|
1226
|
+
} finally {
|
|
1227
|
+
setActiveSpinning(false);
|
|
1228
|
+
}
|
|
1229
|
+
}, []);
|
|
1230
|
+
|
|
1231
|
+
return {
|
|
1232
|
+
list,
|
|
1233
|
+
page, // ✅ 포함 (hasPage: true)
|
|
1234
|
+
listSpinning,
|
|
1235
|
+
getList,
|
|
1236
|
+
adminList,
|
|
1237
|
+
// adminPage, // ❌ 미포함 (hasPage: false)
|
|
1238
|
+
adminSpinning,
|
|
1239
|
+
getAdminList,
|
|
1240
|
+
activeList,
|
|
1241
|
+
activePage, // ✅ 포함 (hasPage: true)
|
|
1242
|
+
activeSpinning,
|
|
1243
|
+
getActiveList,
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
```
|
|
1247
|
+
|
|
1248
|
+
**페이징 처리 규칙:**
|
|
1249
|
+
| 메서드 | hasPage | 상태 생성 | setPage 호출 | return 포함 |
|
|
1250
|
+
|--------|---------|-----------|--------------|-------------|
|
|
1251
|
+
| getList | true | ✅ | ✅ | ✅ |
|
|
1252
|
+
| getAdminList | false | ❌ | ❌ | ❌ |
|
|
1253
|
+
| getActiveList | true | ✅ | ✅ | ✅ |
|
|
1254
|
+
|
|
1255
|
+
### 11.3 제외 키워드가 포함된 API
|
|
1256
|
+
|
|
1257
|
+
**입력:**
|
|
1258
|
+
```typescript
|
|
1259
|
+
// MemberRepository.ts
|
|
1260
|
+
/* 회원 목록 */
|
|
1261
|
+
async postMemberListMember(params: PostMemberListMemberRequest): Promise<PostMemberListMemberResponse> { ... }
|
|
1262
|
+
// Response: { ds: MemberRes[]; page: {...} } → hasPage: true
|
|
1263
|
+
|
|
1264
|
+
/* 회원 저장 */ // ❌ "저장" 키워드로 제외
|
|
1265
|
+
async postMemberSave(params: PostMemberSaveRequest) { ... }
|
|
1266
|
+
|
|
1267
|
+
/* 회원 삭제 */ // ❌ "삭제" 키워드로 제외
|
|
1268
|
+
async postMemberDelete(params: PostMemberDeleteRequest) { ... }
|
|
1269
|
+
|
|
1270
|
+
/* 엑셀 다운로드 */ // ❌ "엑셀", "다운로드" 키워드로 제외
|
|
1271
|
+
async postMemberExcel(params: PostMemberExcelRequest) { ... }
|
|
1272
|
+
```
|
|
1273
|
+
|
|
1274
|
+
**분석 결과:**
|
|
1275
|
+
```typescript
|
|
1276
|
+
// 포함 API만 필터링
|
|
1277
|
+
{
|
|
1278
|
+
includedMethods: [
|
|
1279
|
+
{ name: "postMemberListMember", hasPage: true }
|
|
1280
|
+
],
|
|
1281
|
+
excludedMethods: [
|
|
1282
|
+
{ name: "postMemberSave", reason: "저장" },
|
|
1283
|
+
{ name: "postMemberDelete", reason: "삭제" },
|
|
1284
|
+
{ name: "postMemberExcel", reason: "엑셀, 다운로드" }
|
|
1285
|
+
]
|
|
1286
|
+
}
|
|
1287
|
+
```
|
|
1288
|
+
|
|
1289
|
+
**출력 (목록/조회 API만 포함):**
|
|
1290
|
+
```typescript
|
|
1291
|
+
// useMemberService.ts
|
|
1292
|
+
export function useMemberService(params?: ListRequest) {
|
|
1293
|
+
const [list, setList] = useState<MemberRes[]>([]);
|
|
1294
|
+
const [listSpinning, setListSpinning] = useState(false);
|
|
1295
|
+
const [page, setPage] = useState<AXDGPage>({...}); // ✅ hasPage: true
|
|
1296
|
+
|
|
1297
|
+
const getList = useCallback(async (listParams: ListRequest) => {
|
|
1298
|
+
setListSpinning(true);
|
|
1299
|
+
try {
|
|
1300
|
+
const data = await MemberService.postMemberListMember(deleteEmptyValue(listParams));
|
|
1301
|
+
setList(data.ds);
|
|
1302
|
+
setPage({ ... }); // ✅ hasPage: true
|
|
1303
|
+
} catch (err) {
|
|
1304
|
+
await errorHandling(err);
|
|
1305
|
+
} finally {
|
|
1306
|
+
setListSpinning(false);
|
|
1307
|
+
}
|
|
1308
|
+
}, []);
|
|
1309
|
+
|
|
1310
|
+
return { list, page, listSpinning, getList };
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// ❌ 저장, 삭제, 엑셀 API는 훅에 포함되지 않음
|
|
1314
|
+
```
|
|
1315
|
+
|
|
1316
|
+
### 11.4 목록과 단건 혼합
|
|
1317
|
+
|
|
1318
|
+
**입력:**
|
|
1319
|
+
```typescript
|
|
1320
|
+
// MemberRepository.ts
|
|
1321
|
+
|
|
1322
|
+
/* 목록 API */
|
|
1323
|
+
/* 회원 목록 */
|
|
1324
|
+
async postMemberListMember(params: PostMemberListMemberRequest): Promise<PostMemberListMemberResponse>
|
|
1325
|
+
// Response: { ds: MemberRes[]; page: {...} } → 목록, hasPage: true
|
|
1326
|
+
|
|
1327
|
+
/* 관리자 목록 */
|
|
1328
|
+
async postMemberListAdmin(params: PostMemberListAdminRequest): Promise<PostMemberListAdminResponse>
|
|
1329
|
+
// Response: { ds: AdminRes[]; } → 목록, hasPage: false
|
|
1330
|
+
|
|
1331
|
+
/* 단건 API */
|
|
1332
|
+
/* 회원 상세 */
|
|
1333
|
+
async postMemberDetail(params: PostMemberDetailRequest): Promise<PostMemberDetailResponse>
|
|
1334
|
+
// Response: { rs: MemberDetailRes; } → 단건
|
|
1335
|
+
|
|
1336
|
+
/* 회원 정보 */
|
|
1337
|
+
async postMemberInfo(params: PostMemberInfoRequest): Promise<PostMemberInfoResponse>
|
|
1338
|
+
// Response: { rs: MemberInfoRes; } → 단건
|
|
1339
|
+
|
|
1340
|
+
/* 회원 프로필 */
|
|
1341
|
+
async postMemberProfile(params: PostMemberProfileRequest): Promise<PostMemberProfileResponse>
|
|
1342
|
+
// Response: { rs: MemberProfileRes; } → 단건
|
|
1343
|
+
|
|
1344
|
+
/* 회원 조회 (단건) */
|
|
1345
|
+
async getMember(params: GetMemberRequest): Promise<GetMemberResponse>
|
|
1346
|
+
// Response: { rs: MemberRes; } → 단건
|
|
1347
|
+
```
|
|
1348
|
+
|
|
1349
|
+
**분석 결과:**
|
|
1350
|
+
```typescript
|
|
1351
|
+
{
|
|
1352
|
+
listMethods: [
|
|
1353
|
+
{ name: "postMemberListMember", hasPage: true, listName: "list", functionName: "getList" },
|
|
1354
|
+
{ name: "postMemberListAdmin", hasPage: false, listName: "adminList", functionName: "getAdminList" }
|
|
1355
|
+
],
|
|
1356
|
+
detailMethods: [
|
|
1357
|
+
{ name: "postMemberDetail", detailName: "detail", functionName: "getDetail" },
|
|
1358
|
+
{ name: "postMemberInfo", detailName: "info", functionName: "getInfo" },
|
|
1359
|
+
{ name: "postMemberProfile", detailName: "profile", functionName: "getProfile" },
|
|
1360
|
+
{ name: "getMember", detailName: "member", functionName: "getMember" }
|
|
1361
|
+
]
|
|
1362
|
+
}
|
|
1363
|
+
```
|
|
1364
|
+
|
|
1365
|
+
**출력 (목록 + 단건 혼합):**
|
|
1366
|
+
```typescript
|
|
1367
|
+
// useMemberService.ts
|
|
1368
|
+
export function useMemberService() {
|
|
1369
|
+
// ===== 목록 (List) =====
|
|
1370
|
+
const [list, setList] = useState<MemberRes[]>([]);
|
|
1371
|
+
const [listSpinning, setListSpinning] = useState(false);
|
|
1372
|
+
const [page, setPage] = useState<AXDGPage>({...}); // ✅ hasPage: true
|
|
1373
|
+
|
|
1374
|
+
const [adminList, setAdminList] = useState<AdminRes[]>([]);
|
|
1375
|
+
const [adminSpinning, setAdminSpinning] = useState(false);
|
|
1376
|
+
// ❌ adminPage 없음 (hasPage: false)
|
|
1377
|
+
|
|
1378
|
+
// ===== 단건 (Detail) =====
|
|
1379
|
+
const [detail, setDetail] = useState<MemberDetailRes>();
|
|
1380
|
+
const [detailSpinning, setDetailSpinning] = useState(false);
|
|
1381
|
+
|
|
1382
|
+
const [info, setInfo] = useState<MemberInfoRes>();
|
|
1383
|
+
const [infoSpinning, setInfoSpinning] = useState(false);
|
|
1384
|
+
|
|
1385
|
+
const [profile, setProfile] = useState<MemberProfileRes>();
|
|
1386
|
+
const [profileSpinning, setProfileSpinning] = useState(false);
|
|
1387
|
+
|
|
1388
|
+
const [member, setMember] = useState<MemberRes>();
|
|
1389
|
+
const [memberSpinning, setMemberSpinning] = useState(false);
|
|
1390
|
+
|
|
1391
|
+
// ===== 목록 함수 =====
|
|
1392
|
+
const getList = useCallback(async (params?: ListRequest) => {
|
|
1393
|
+
setListSpinning(true);
|
|
1394
|
+
try {
|
|
1395
|
+
const data = await MemberService.postMemberListMember(deleteEmptyValue(params));
|
|
1396
|
+
setList(data.ds); // 목록은 data.ds
|
|
1397
|
+
setPage({ ... }); // ✅ hasPage: true
|
|
1398
|
+
} catch (err) {
|
|
1399
|
+
await errorHandling(err);
|
|
1400
|
+
} finally {
|
|
1401
|
+
setListSpinning(false);
|
|
1402
|
+
}
|
|
1403
|
+
}, []);
|
|
1404
|
+
|
|
1405
|
+
const getAdminList = useCallback(async (params?: AdminListRequest) => {
|
|
1406
|
+
setAdminSpinning(true);
|
|
1407
|
+
try {
|
|
1408
|
+
const data = await MemberService.postMemberListAdmin(deleteEmptyValue(params));
|
|
1409
|
+
setAdminList(data.ds); // 목록은 data.ds
|
|
1410
|
+
} catch (err) {
|
|
1411
|
+
await errorHandling(err);
|
|
1412
|
+
} finally {
|
|
1413
|
+
setAdminSpinning(false);
|
|
1414
|
+
}
|
|
1415
|
+
}, []);
|
|
1416
|
+
|
|
1417
|
+
// ===== 단건 함수 =====
|
|
1418
|
+
const getDetail = useCallback(async (params?: DetailRequest) => {
|
|
1419
|
+
setDetailSpinning(true);
|
|
1420
|
+
try {
|
|
1421
|
+
const data = await MemberService.postMemberDetail(deleteEmptyValue(params));
|
|
1422
|
+
setDetail(data.rs); // 단건은 data.rs
|
|
1423
|
+
} catch (err) {
|
|
1424
|
+
await errorHandling(err);
|
|
1425
|
+
} finally {
|
|
1426
|
+
setDetailSpinning(false);
|
|
1427
|
+
}
|
|
1428
|
+
}, []);
|
|
1429
|
+
|
|
1430
|
+
const getInfo = useCallback(async (params?: InfoRequest) => {
|
|
1431
|
+
setInfoSpinning(true);
|
|
1432
|
+
try {
|
|
1433
|
+
const data = await MemberService.postMemberInfo(deleteEmptyValue(params));
|
|
1434
|
+
setInfo(data.rs);
|
|
1435
|
+
} catch (err) {
|
|
1436
|
+
await errorHandling(err);
|
|
1437
|
+
} finally {
|
|
1438
|
+
setInfoSpinning(false);
|
|
1439
|
+
}
|
|
1440
|
+
}, []);
|
|
1441
|
+
|
|
1442
|
+
const getProfile = useCallback(async (params?: ProfileRequest) => {
|
|
1443
|
+
setProfileSpinning(true);
|
|
1444
|
+
try {
|
|
1445
|
+
const data = await MemberService.postMemberProfile(deleteEmptyValue(params));
|
|
1446
|
+
setProfile(data.rs);
|
|
1447
|
+
} catch (err) {
|
|
1448
|
+
await errorHandling(err);
|
|
1449
|
+
} finally {
|
|
1450
|
+
setProfileSpinning(false);
|
|
1451
|
+
}
|
|
1452
|
+
}, []);
|
|
1453
|
+
|
|
1454
|
+
const getMember = useCallback(async (params?: MemberRequest) => {
|
|
1455
|
+
setMemberSpinning(true);
|
|
1456
|
+
try {
|
|
1457
|
+
const data = await MemberService.getMember(deleteEmptyValue(params));
|
|
1458
|
+
setMember(data.rs);
|
|
1459
|
+
} catch (err) {
|
|
1460
|
+
await errorHandling(err);
|
|
1461
|
+
} finally {
|
|
1462
|
+
setMemberSpinning(false);
|
|
1463
|
+
}
|
|
1464
|
+
}, []);
|
|
1465
|
+
|
|
1466
|
+
return {
|
|
1467
|
+
// 목록
|
|
1468
|
+
list,
|
|
1469
|
+
page, // ✅ hasPage: true
|
|
1470
|
+
listSpinning,
|
|
1471
|
+
getList,
|
|
1472
|
+
adminList,
|
|
1473
|
+
// adminPage, // ❌ hasPage: false
|
|
1474
|
+
adminSpinning,
|
|
1475
|
+
getAdminList,
|
|
1476
|
+
// 단건
|
|
1477
|
+
detail,
|
|
1478
|
+
detailSpinning,
|
|
1479
|
+
getDetail,
|
|
1480
|
+
info,
|
|
1481
|
+
infoSpinning,
|
|
1482
|
+
getInfo,
|
|
1483
|
+
profile,
|
|
1484
|
+
profileSpinning,
|
|
1485
|
+
getProfile,
|
|
1486
|
+
member,
|
|
1487
|
+
memberSpinning,
|
|
1488
|
+
getMember,
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
```
|
|
1492
|
+
|
|
1493
|
+
### 11.5 중복 발생 시 처리 예시
|
|
1494
|
+
|
|
1495
|
+
**입력 (중복 발생):**
|
|
1496
|
+
```typescript
|
|
1497
|
+
// MemberRepository.ts
|
|
1498
|
+
/* 회원 상세 (post) */
|
|
1499
|
+
async postMemberDetail(...) : Promise<PostMemberDetailResponse>
|
|
1500
|
+
|
|
1501
|
+
/* 회원 상세 (get) */
|
|
1502
|
+
async getMemberDetail(...) : Promise<GetMemberDetailResponse>
|
|
1503
|
+
```
|
|
1504
|
+
|
|
1505
|
+
**분석 결과 (중복 감지):**
|
|
1506
|
+
```typescript
|
|
1507
|
+
{
|
|
1508
|
+
duplicates: [
|
|
1509
|
+
{ name: "postMemberDetail", baseName: "detail", functionName: "getDetail" },
|
|
1510
|
+
{ name: "getMemberDetail", baseName: "detail", functionName: "getDetail" }
|
|
1511
|
+
]
|
|
1512
|
+
}
|
|
1513
|
+
```
|
|
1514
|
+
|
|
1515
|
+
**출력 (중복 처리 - 옵션 C: 중복 시만 구분):**
|
|
1516
|
+
```typescript
|
|
1517
|
+
// useMemberService.ts
|
|
1518
|
+
export function useMemberService() {
|
|
1519
|
+
// 중복 처리: post 접두사 추가
|
|
1520
|
+
const [postDetail, setPostDetail] = useState<PostMemberDetailRes>();
|
|
1521
|
+
const [postDetailSpinning, setPostDetailSpinning] = useState(false);
|
|
1522
|
+
|
|
1523
|
+
// 중복 없음: 기본 네이밍
|
|
1524
|
+
const [detail, setDetail] = useState<GetMemberDetailRes>();
|
|
1525
|
+
const [detailSpinning, setDetailSpinning] = useState(false);
|
|
1526
|
+
|
|
1527
|
+
const getPostDetail = useCallback(async (params?: PostDetailRequest) => {
|
|
1528
|
+
setPostDetailSpinning(true);
|
|
1529
|
+
try {
|
|
1530
|
+
const data = await MemberService.postMemberDetail(deleteEmptyValue(params));
|
|
1531
|
+
setPostDetail(data.rs);
|
|
1532
|
+
} catch (err) {
|
|
1533
|
+
await errorHandling(err);
|
|
1534
|
+
} finally {
|
|
1535
|
+
setPostDetailSpinning(false);
|
|
1536
|
+
}
|
|
1537
|
+
}, []);
|
|
1538
|
+
|
|
1539
|
+
const getDetail = useCallback(async (params?: DetailRequest) => {
|
|
1540
|
+
setDetailSpinning(true);
|
|
1541
|
+
try {
|
|
1542
|
+
const data = await MemberService.getMemberDetail(deleteEmptyValue(params));
|
|
1543
|
+
setDetail(data.rs);
|
|
1544
|
+
} catch (err) {
|
|
1545
|
+
await errorHandling(err);
|
|
1546
|
+
} finally {
|
|
1547
|
+
setDetailSpinning(false);
|
|
1548
|
+
}
|
|
1549
|
+
}, []);
|
|
1550
|
+
|
|
1551
|
+
return {
|
|
1552
|
+
postDetail,
|
|
1553
|
+
postDetailSpinning,
|
|
1554
|
+
getPostDetail, // 중복 처리: getPostDetail
|
|
1555
|
+
detail,
|
|
1556
|
+
detailSpinning,
|
|
1557
|
+
getDetail, // 기본 유지
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
```
|
|
1561
|
+
|
|
1562
|
+
---
|
|
1563
|
+
|
|
1564
|
+
## 12. 공통 Import
|
|
1565
|
+
|
|
1566
|
+
```typescript
|
|
1567
|
+
import { AXDGPage } from "@axboot/datagrid";
|
|
1568
|
+
import { useCallback, useState } from "react";
|
|
1569
|
+
import { {Service}, {ResponseType}, {RequestType} } from "../services";
|
|
1570
|
+
import { dateRangeToDts, errorHandling } from "../utils";
|
|
1571
|
+
import { deleteEmptyValue } from "../@core/utils/object";
|
|
1572
|
+
import { DT_FORMAT } from "../@types";
|
|
1573
|
+
```
|
|
1574
|
+
|
|
1575
|
+
---
|
|
1576
|
+
|
|
1577
|
+
## 13. 추가 고려사항
|
|
1578
|
+
|
|
1579
|
+
### 13.1 주석 처리된 옵션 기능 (작업자 판단 후 사용)
|
|
1580
|
+
|
|
1581
|
+
**원칙:** 생성 시 아래 내용을 **무조건 주석 처리**하여 포함하고, 작업자가 필요시 주석을 해제하여 사용합니다.
|
|
1582
|
+
|
|
1583
|
+
#### 13.1.1 자동 초기화 (주석 처리)
|
|
1584
|
+
|
|
1585
|
+
훅 생성 시 마운트 시 자동으로 데이터를 조회하는 코드를 주석 처리하여 포함합니다.
|
|
1586
|
+
|
|
1587
|
+
```typescript
|
|
1588
|
+
export function useMemberService(params?: ListRequest) {
|
|
1589
|
+
// ... 상태 선언, useCallback 등
|
|
1590
|
+
|
|
1591
|
+
// ===== 옵션: 자동 초기화 (필요시 주석 해제) =====
|
|
1592
|
+
// React.useEffect(() => {
|
|
1593
|
+
// (async () => {
|
|
1594
|
+
// await getList(params);
|
|
1595
|
+
// })();
|
|
1596
|
+
// }, [getList, params]);
|
|
1597
|
+
// ===============================================
|
|
1598
|
+
|
|
1599
|
+
return {
|
|
1600
|
+
list,
|
|
1601
|
+
page,
|
|
1602
|
+
listSpinning,
|
|
1603
|
+
getList,
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
```
|
|
1607
|
+
|
|
1608
|
+
**작업자 가이드:**
|
|
1609
|
+
- 컴포넌트 마운트 시 자동으로 데이터를 조회하려면 주석 해제
|
|
1610
|
+
- `params`가 변경될 때마다 재조회됨
|
|
1611
|
+
|
|
1612
|
+
#### 13.1.2 i18n 사용 (주석 처리)
|
|
1613
|
+
|
|
1614
|
+
훅 생성 시 i18n 사용 코드를 주석 처리하여 포함합니다.
|
|
1615
|
+
|
|
1616
|
+
```typescript
|
|
1617
|
+
export function useMemberService(params?: ListRequest) {
|
|
1618
|
+
// ===== 옵션: i18n 사용 (필요시 주석 해제) =====
|
|
1619
|
+
// const { t } = useI18n();
|
|
1620
|
+
// ==============================================
|
|
1621
|
+
|
|
1622
|
+
// ... 상태 선언, useCallback 등
|
|
1623
|
+
|
|
1624
|
+
return {
|
|
1625
|
+
list,
|
|
1626
|
+
page,
|
|
1627
|
+
listSpinning,
|
|
1628
|
+
getList,
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
```
|
|
1632
|
+
|
|
1633
|
+
**작업자 가이드:**
|
|
1634
|
+
- 다국어 처리가 필요한 경우 주석 해제 후 `t()` 함수 사용
|
|
1635
|
+
|
|
1636
|
+
#### 13.1.3 전체 옵션 포함 예시
|
|
1637
|
+
|
|
1638
|
+
```typescript
|
|
1639
|
+
export function useMemberService(params?: ListRequest) {
|
|
1640
|
+
// ===== 옵션: i18n 사용 (필요시 주석 해제) =====
|
|
1641
|
+
// const { t } = useI18n();
|
|
1642
|
+
// ==============================================
|
|
1643
|
+
|
|
1644
|
+
const [list, setList] = useState<MemberRes[]>([]);
|
|
1645
|
+
const [listSpinning, setListSpinning] = useState(false);
|
|
1646
|
+
const [page, setPage] = useState<AXDGPage>({...});
|
|
1647
|
+
|
|
1648
|
+
const getList = useCallback(async (listParams: ListRequest) => {
|
|
1649
|
+
setListSpinning(true);
|
|
1650
|
+
try {
|
|
1651
|
+
const data = await MemberService.postMemberListMember(deleteEmptyValue(listParams));
|
|
1652
|
+
setList(data.ds);
|
|
1653
|
+
setPage({ ... });
|
|
1654
|
+
} catch (err) {
|
|
1655
|
+
await errorHandling(err);
|
|
1656
|
+
} finally {
|
|
1657
|
+
setListSpinning(false);
|
|
1658
|
+
}
|
|
1659
|
+
}, []);
|
|
1660
|
+
|
|
1661
|
+
// ===== 옵션: 자동 초기화 (필요시 주석 해제) =====
|
|
1662
|
+
// React.useEffect(() => {
|
|
1663
|
+
// (async () => {
|
|
1664
|
+
// await getList(params);
|
|
1665
|
+
// })();
|
|
1666
|
+
// }, [getList, params]);
|
|
1667
|
+
// ===============================================
|
|
1668
|
+
|
|
1669
|
+
return {
|
|
1670
|
+
list,
|
|
1671
|
+
page,
|
|
1672
|
+
listSpinning,
|
|
1673
|
+
getList,
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
```
|
|
1677
|
+
|
|
1678
|
+
### 13.2 옵션 기능 포함 여부 결정 기준
|
|
1679
|
+
|
|
1680
|
+
| 옵션 | 포함 여부 | 주석 처리 | 비고 |
|
|
1681
|
+
|------|-----------|-----------|------|
|
|
1682
|
+
| 자동 초기화 | ✅ 항상 포함 | ✅ 주석 처리 | 작업자가 필요 시 해제 |
|
|
1683
|
+
| i18n 사용 | ✅ 항상 포함 | ✅ 주석 처리 | 작업자가 필요 시 해제 |
|
|
1684
|
+
| 페이징 | ✅ hasPage에 따라 가변 | ❌ 주석 없음 | 자동 생성 |
|
|
1685
|
+
| dateRange | ✅ 패턴에 따라 가변 | ❌ 주석 없음 | 자동 생성 |
|
|
1686
|
+
|
|
1687
|
+
---
|
|
1688
|
+
|
|
1689
|
+
## 14. 타입 체크 및 자동 수정
|
|
1690
|
+
|
|
1691
|
+
### 14.1 타입 체크 흐름
|
|
1692
|
+
|
|
1693
|
+
```
|
|
1694
|
+
1. 훅 파일 생성
|
|
1695
|
+
2. 프로젝트 구조 자동 분석 (tsconfig.json, package.json)
|
|
1696
|
+
3. 적절한 명령으로 타입 체크 실행 (npm run type-check 또는 npx tsc --noEmit)
|
|
1697
|
+
4. 타입 오류가 있으면 AI가 1차 자동 수정 시도
|
|
1698
|
+
5. 수정된 코드로 재시도
|
|
1699
|
+
6. 그래도 실패하면 사용자에게 위임 (결과에 타입 오류 포함)
|
|
1700
|
+
7. 사용자가 판단 후 수정 요청
|
|
1701
|
+
```
|
|
1702
|
+
|
|
1703
|
+
### 14.2 재시도 로직
|
|
1704
|
+
|
|
1705
|
+
| 시도 | 동작 | 실패 시 처리 |
|
|
1706
|
+
|------|------|-------------|
|
|
1707
|
+
| 1회차 | 기본 생성 후 타입 체크 | AI 자동 수정 |
|
|
1708
|
+
| 2회차 | AI 수정 후 타입 체크 | 사용자에게 위임 |
|
|
1709
|
+
| 3회차 | (옵션) 재시도 | 사용자에게 위임 |
|
|
1710
|
+
|
|
1711
|
+
### 14.2 프로젝트 구조 자동 분석
|
|
1712
|
+
|
|
1713
|
+
```typescript
|
|
1714
|
+
/**
|
|
1715
|
+
* 프로젝트 루트 찾기 (tsconfig.json이 있는 위치)
|
|
1716
|
+
*/
|
|
1717
|
+
async function findProjectRoot(startPath: string): Promise<string> {
|
|
1718
|
+
let currentDir = path.dirname(startPath);
|
|
1719
|
+
|
|
1720
|
+
// 최대 10단계 상위 폴더로 탐색
|
|
1721
|
+
for (let i = 0; i < 10; i++) {
|
|
1722
|
+
const tsconfigPath = path.join(currentDir, 'tsconfig.json');
|
|
1723
|
+
|
|
1724
|
+
try {
|
|
1725
|
+
await fs.access(tsconfigPath);
|
|
1726
|
+
return currentDir; // tsconfig.json을 찾으면 해당 경로가 프로젝트 루트
|
|
1727
|
+
} catch {
|
|
1728
|
+
// 상위 폴더로 이동
|
|
1729
|
+
currentDir = path.dirname(currentDir);
|
|
1730
|
+
if (currentDir === path.dirname(currentDir)) break;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
return process.cwd(); // 기본값
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
/**
|
|
1738
|
+
* package.json 확인하여 type-check 스크립트 확인
|
|
1739
|
+
*/
|
|
1740
|
+
async function hasTypeCheckScript(projectRoot: string): Promise<boolean> {
|
|
1741
|
+
try {
|
|
1742
|
+
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
1743
|
+
const content = await fs.readFile(packageJsonPath, 'utf-8');
|
|
1744
|
+
const packageJson = JSON.parse(content);
|
|
1745
|
+
|
|
1746
|
+
return !!(packageJson.scripts && packageJson.scripts['type-check']);
|
|
1747
|
+
} catch {
|
|
1748
|
+
return false;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
```
|
|
1752
|
+
|
|
1753
|
+
### 14.3 타입 체크 실행
|
|
1754
|
+
|
|
1755
|
+
```typescript
|
|
1756
|
+
/**
|
|
1757
|
+
* 타입 체크 실행 (프로젝트 구조 자동 분석)
|
|
1758
|
+
*/
|
|
1759
|
+
async function typeCheck(filePath: string): Promise<{ success: boolean; errors?: string[] }> {
|
|
1760
|
+
try {
|
|
1761
|
+
// 1. 프로젝트 루트 찾기
|
|
1762
|
+
const projectRoot = await findProjectRoot(filePath);
|
|
1763
|
+
|
|
1764
|
+
// 2. package.json의 type-check 스크립트 확인
|
|
1765
|
+
const hasTypeCheck = await hasTypeCheckScript(projectRoot);
|
|
1766
|
+
|
|
1767
|
+
let command: string;
|
|
1768
|
+
let args: string[];
|
|
1769
|
+
|
|
1770
|
+
if (hasTypeCheck) {
|
|
1771
|
+
// package.json에 type-check 스크립트가 있으면 그것 사용
|
|
1772
|
+
command = 'npm';
|
|
1773
|
+
args = ['run', 'type-check'];
|
|
1774
|
+
} else {
|
|
1775
|
+
// 없으면 tsc 직접 실행
|
|
1776
|
+
command = 'npx';
|
|
1777
|
+
args = ['tsc', '--noEmit', filePath];
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// 3. 명령 실행 (백그라운드)
|
|
1781
|
+
execSync(
|
|
1782
|
+
`${command} ${args.join(' ')}`,
|
|
1783
|
+
{
|
|
1784
|
+
encoding: 'utf-8',
|
|
1785
|
+
cwd: projectRoot,
|
|
1786
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
1787
|
+
timeout: 30000 // 30초 타임아웃
|
|
1788
|
+
}
|
|
1789
|
+
);
|
|
1790
|
+
|
|
1791
|
+
return { success: true };
|
|
1792
|
+
} catch (error: any) {
|
|
1793
|
+
const stderr = error.stderr || error.stdout || '';
|
|
1794
|
+
const errors = stderr
|
|
1795
|
+
.split('\n')
|
|
1796
|
+
.filter((line: string) => line.includes('error TS'))
|
|
1797
|
+
.map((line: string) => line.trim())
|
|
1798
|
+
.filter((line: string) => line.length > 0);
|
|
1799
|
+
return { success: false, errors };
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
```
|
|
1803
|
+
|
|
1804
|
+
### 14.4 결과 반환 (AI 1차 수정 후 사용자 위임)
|
|
1805
|
+
|
|
1806
|
+
```typescript
|
|
1807
|
+
/**
|
|
1808
|
+
* AI 기반 타입 오류 수정
|
|
1809
|
+
* @param code 현재 코드
|
|
1810
|
+
* @param errors 타입 오류 목록
|
|
1811
|
+
* @param interfacePath 인터페이스 경로 (컨텍스트)
|
|
1812
|
+
* @returns 수정된 코드
|
|
1813
|
+
*/
|
|
1814
|
+
async function fixTypeErrors(
|
|
1815
|
+
code: string,
|
|
1816
|
+
errors: string[],
|
|
1817
|
+
interfacePath: string
|
|
1818
|
+
): Promise<string> {
|
|
1819
|
+
// TODO: AI 기반 수정 로직 구현
|
|
1820
|
+
// - 타입 오류 분석
|
|
1821
|
+
// - import 추가
|
|
1822
|
+
// - 타입 이름 수정
|
|
1823
|
+
// - 코드 재생성
|
|
1824
|
+
|
|
1825
|
+
// 현재는 기본 코드 반환 (추후 구현)
|
|
1826
|
+
return code;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
/**
|
|
1830
|
+
* 훅 생성 (타입 체크 + AI 1차 수정)
|
|
1831
|
+
*/
|
|
1832
|
+
async function generateHookWithCheck(args: { interfacePath: string; outputPath?: string }) {
|
|
1833
|
+
const { interfacePath, outputPath } = args;
|
|
1834
|
+
|
|
1835
|
+
// 1. 기본 훅 생성
|
|
1836
|
+
let hookCode = await generateHookCode(interfacePath, outputPath);
|
|
1837
|
+
const finalPath = outputPath || getDefaultPath(interfacePath);
|
|
1838
|
+
await fs.writeFile(finalPath, hookCode);
|
|
1839
|
+
|
|
1840
|
+
// 2. 타입 체크 및 수정 (최대 3회 시도)
|
|
1841
|
+
const maxRetries = 3;
|
|
1842
|
+
let typeErrors: string[] = [];
|
|
1843
|
+
|
|
1844
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
1845
|
+
const result = await typeCheck(finalPath);
|
|
1846
|
+
|
|
1847
|
+
if (result.success) {
|
|
1848
|
+
break; // 타입 체크 통과
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
// 오류 수집
|
|
1852
|
+
typeErrors = result.errors || [];
|
|
1853
|
+
|
|
1854
|
+
// 마지막 시도이면 종료 (사용자에게 위임)
|
|
1855
|
+
if (i === maxRetries - 1) {
|
|
1856
|
+
break;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
// 1차 시도: AI 자동 수정
|
|
1860
|
+
if (i === 0) {
|
|
1861
|
+
console.log(`[AI 자동 수정] 타입 오류 ${typeErrors.length}건`);
|
|
1862
|
+
hookCode = await fixTypeErrors(hookCode, typeErrors, interfacePath);
|
|
1863
|
+
await fs.writeFile(finalPath, hookCode);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
// 3. Lint 및 Prettier 실행
|
|
1868
|
+
await runLintAndPrettier(finalPath);
|
|
1869
|
+
|
|
1870
|
+
// 4. 결과 반환
|
|
1871
|
+
return {
|
|
1872
|
+
success: true,
|
|
1873
|
+
message: typeErrors.length > 0
|
|
1874
|
+
? `훅 파일 생성 완료 (타입 오류 ${typeErrors.length}건, AI 수정 후 실패)`
|
|
1875
|
+
: '훅 파일 생성 완료 (타입 체크 통과)',
|
|
1876
|
+
path: finalPath,
|
|
1877
|
+
typeErrors: typeErrors.length > 0 ? typeErrors : undefined, // 사용자에게 위임
|
|
1878
|
+
aiFixed: typeErrors.length > 0, // AI 시도 표시
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
```
|
|
1882
|
+
|
|
1883
|
+
### 14.5 사용자 경험
|
|
1884
|
+
|
|
1885
|
+
**타입 체크 통과:**
|
|
1886
|
+
```
|
|
1887
|
+
Store 파일 생성 완료 (타입 체크 통과)
|
|
1888
|
+
|
|
1889
|
+
생성된 파일: src/stores/useMemberListStore.ts
|
|
1890
|
+
```
|
|
1891
|
+
|
|
1892
|
+
**AI 수정 후 통과:**
|
|
1893
|
+
```
|
|
1894
|
+
Store 파일 생성 완료 (AI 수정 후 타입 체크 통과)
|
|
1895
|
+
|
|
1896
|
+
생성된 파일: src/stores/useMemberListStore.ts
|
|
1897
|
+
```
|
|
1898
|
+
|
|
1899
|
+
**AI 수정 후 실패 (결과만 표시, 질문 없음):**
|
|
1900
|
+
```
|
|
1901
|
+
Store 파일 생성 완료 (타입 오류 3건)
|
|
1902
|
+
|
|
1903
|
+
생성된 파일: src/stores/useMemberListStore.ts
|
|
1904
|
+
|
|
1905
|
+
타입 오류:
|
|
1906
|
+
- error TS2304: Cannot find name 'MemberRes'.
|
|
1907
|
+
- error TS2339: Property 'ds' does not exist on type '...'.
|
|
1908
|
+
- error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
|
|
1909
|
+
```
|
|
1910
|
+
|
|
1911
|
+
### 14.6 전체 흐름도
|
|
1912
|
+
|
|
1913
|
+
```
|
|
1914
|
+
┌─────────────────┐
|
|
1915
|
+
│ 훅 파일 생성 │
|
|
1916
|
+
└────────┬────────┘
|
|
1917
|
+
│
|
|
1918
|
+
▼
|
|
1919
|
+
┌─────────────────┐
|
|
1920
|
+
│ 타입 체크 실행 │
|
|
1921
|
+
└────────┬────────┘
|
|
1922
|
+
│
|
|
1923
|
+
┌────┴────┐
|
|
1924
|
+
│ 통과? │
|
|
1925
|
+
└────┬────┘
|
|
1926
|
+
│
|
|
1927
|
+
┌────┴─────────┐
|
|
1928
|
+
│ │
|
|
1929
|
+
YES NO
|
|
1930
|
+
│ │
|
|
1931
|
+
▼ ▼
|
|
1932
|
+
┌────────┐ ┌──────────┐
|
|
1933
|
+
│ 완료 │ │ AI 자동 │
|
|
1934
|
+
└────────┘ │ 수정 시도 │
|
|
1935
|
+
└─────┬────┘
|
|
1936
|
+
│
|
|
1937
|
+
▼
|
|
1938
|
+
┌──────────┐
|
|
1939
|
+
│ 타입 체크 │
|
|
1940
|
+
└─────┬────┘
|
|
1941
|
+
│
|
|
1942
|
+
┌────┴────┐
|
|
1943
|
+
│ 통과? │
|
|
1944
|
+
└────┬────┘
|
|
1945
|
+
│
|
|
1946
|
+
┌────┴─────┐
|
|
1947
|
+
│ │
|
|
1948
|
+
YES NO
|
|
1949
|
+
│ │
|
|
1950
|
+
▼ ▼
|
|
1951
|
+
┌────────┐ ┌──────────┐
|
|
1952
|
+
│ 완료 │ │ 결과 반환 │
|
|
1953
|
+
└────────┘ │ (오류만) │
|
|
1954
|
+
└──────────┘
|
|
1955
|
+
}
|
|
1956
|
+
```
|
|
1957
|
+
|
|
1958
|
+
### 14.5 사용자 경험
|
|
1959
|
+
|
|
1960
|
+
**타입 체크 통과:**
|
|
1961
|
+
```
|
|
1962
|
+
훅 파일 생성 완료 (타입 체크 통과)
|
|
1963
|
+
|
|
1964
|
+
생성된 파일: src/hooks/useMemberService.ts
|
|
1965
|
+
```
|
|
1966
|
+
|
|
1967
|
+
**타입 오류 있음:**
|
|
1968
|
+
```
|
|
1969
|
+
훅 파일 생성 완료 (타입 오류 3건)
|
|
1970
|
+
|
|
1971
|
+
생성된 파일: src/hooks/useMemberService.ts
|
|
1972
|
+
|
|
1973
|
+
타입 오류:
|
|
1974
|
+
- error TS2304: Cannot find name 'MemberRes'.
|
|
1975
|
+
- error TS2339: Property 'ds' does not exist on type '...'.
|
|
1976
|
+
- error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
|
|
1977
|
+
|
|
1978
|
+
수정하시겠습니까?
|
|
1979
|
+
```
|
|
1980
|
+
|
|
1981
|
+
### 14.6 타입 오류 수정 예시
|
|
1982
|
+
|
|
1983
|
+
**생성된 코드 (타입 오류):**
|
|
1984
|
+
```typescript
|
|
1985
|
+
// ❌ MemberRes를 찾을 수 없음
|
|
1986
|
+
const [list, setList] = useState<MemberRes[]>([]);
|
|
1987
|
+
```
|
|
1988
|
+
|
|
1989
|
+
**수정된 코드:**
|
|
1990
|
+
```typescript
|
|
1991
|
+
// ✅ 올바른 타입으로 수정
|
|
1992
|
+
const [list, setList] = useState<Member[]>([]);
|
|
1993
|
+
```
|
|
1994
|
+
|
|
1995
|
+
### 14.7 타입 체크 명령어
|
|
1996
|
+
|
|
1997
|
+
```bash
|
|
1998
|
+
# 프로젝트에 type-check 스크립트가 있으면
|
|
1999
|
+
npm run type-check
|
|
2000
|
+
|
|
2001
|
+
# 없으면 직접 실행
|
|
2002
|
+
npx tsc --noEmit src/hooks/useMemberService.ts
|
|
2003
|
+
```
|
|
2004
|
+
|
|
2005
|
+
### 14.8 타입 오류 패턴별 수정 로직
|
|
2006
|
+
|
|
2007
|
+
| 오류 패턴 | 수정 방법 |
|
|
2008
|
+
|----------|----------|
|
|
2009
|
+
| `Could not find namespace 'XXX'` | import 추가 또는 타입 경로 수정 |
|
|
2010
|
+
| `Property 'xxx' does not exist on type 'XXX'` | 타입 정의 수정 |
|
|
2011
|
+
| `Type 'XXX' is not assignable to type 'YYY'` | 타입 호환성 수정 |
|
|
2012
|
+
| `Module '"XXX"' has no exported member 'YYY'` | import 경로 또는 이름 수정 |
|
|
2013
|
+
|
|
2014
|
+
---
|
|
2015
|
+
| 2회차 | 오류 수정 후 재생성 |
|
|
2016
|
+
| 3회차 | 마지막 시도 후 결과 반환 |
|
|
2017
|
+
|
|
2018
|
+
### 14.5 타입 체크 명령어
|
|
2019
|
+
|
|
2020
|
+
```bash
|
|
2021
|
+
# 특정 파일 타입 체크
|
|
2022
|
+
npx tsc --noEmit src/hooks/useMemberService.ts
|
|
2023
|
+
|
|
2024
|
+
# 프로젝트 전체 타입 체크
|
|
2025
|
+
npx tsc --noEmit
|
|
2026
|
+
|
|
2027
|
+
# 또는
|
|
2028
|
+
npm run type-check # package.json에 설정된 경우
|
|
2029
|
+
```
|
|
2030
|
+
|
|
2031
|
+
### 14.6 타입 오류 패턴별 수정 로직
|
|
2032
|
+
|
|
2033
|
+
| 오류 패턴 | 수정 방법 |
|
|
2034
|
+
|----------|----------|
|
|
2035
|
+
| `Could not find namespace 'XXX'` | import 추가 또는 타입 경로 수정 |
|
|
2036
|
+
| `Property 'xxx' does not exist on type 'XXX'` | 타입 정의 수정 |
|
|
2037
|
+
| `Type 'XXX' is not assignable to type 'YYY'` | 타입 호환성 수정 |
|
|
2038
|
+
| `Module '"XXX"' has no exported member 'YYY'` | import 경로 또는 이름 수정 |
|
|
2039
|
+
|
|
2040
|
+
---
|
|
2041
|
+
|
|
2042
|
+
## 15. Store와 Hook 동시 생성 (결과 요약)
|
|
2043
|
+
|
|
2044
|
+
### 15.1 사용자 시나리오
|
|
2045
|
+
|
|
2046
|
+
사용자가 단일 요청으로 Store와 Hook을 동시에 생성할 때, 각각의 개별 결과가 아닌 **요약된 결과**를 보여줍니다.
|
|
2047
|
+
|
|
2048
|
+
```typescript
|
|
2049
|
+
// 사용자 요청
|
|
2050
|
+
generateStoreAndHook({ interfacePath: "/path/to/MemberRepository.ts" })
|
|
2051
|
+
```
|
|
2052
|
+
|
|
2053
|
+
### 15.2 결과 요약 형식
|
|
2054
|
+
|
|
2055
|
+
**성공 시:**
|
|
2056
|
+
```
|
|
2057
|
+
✅ Store 및 Hook 생성 완료
|
|
2058
|
+
|
|
2059
|
+
생성된 파일:
|
|
2060
|
+
- src/stores/useMemberListStore.ts (Store)
|
|
2061
|
+
- src/hooks/useMemberService.ts (Hook)
|
|
2062
|
+
|
|
2063
|
+
Store: 타입 체크 통과
|
|
2064
|
+
Hook: 타입 체크 통과
|
|
2065
|
+
```
|
|
2066
|
+
|
|
2067
|
+
**부분 실패 시 (Hook 타입 오류):**
|
|
2068
|
+
```
|
|
2069
|
+
⚠️ Store 및 Hook 생성 완료 (일부 타입 오류)
|
|
2070
|
+
|
|
2071
|
+
생성된 파일:
|
|
2072
|
+
- src/stores/useMemberListStore.ts (Store)
|
|
2073
|
+
- src/hooks/useMemberService.ts (Hook)
|
|
2074
|
+
|
|
2075
|
+
Store: 타입 체크 통과
|
|
2076
|
+
Hook: 타입 오류 2건
|
|
2077
|
+
|
|
2078
|
+
Hook 타입 오류:
|
|
2079
|
+
- error TS2304: Cannot find name 'MemberRes'.
|
|
2080
|
+
- error TS2339: Property 'ds' does not exist on type '...'.
|
|
2081
|
+
```
|
|
2082
|
+
|
|
2083
|
+
### 15.3 MCP 도구 설계
|
|
2084
|
+
|
|
2085
|
+
#### 옵션 A: 통합 도구
|
|
2086
|
+
|
|
2087
|
+
```typescript
|
|
2088
|
+
// 도구: generate-store-and-hook
|
|
2089
|
+
{
|
|
2090
|
+
"name": "generate-store-and-hook",
|
|
2091
|
+
"description": "Store와 Hook을 동시에 생성합니다",
|
|
2092
|
+
"inputSchema": {
|
|
2093
|
+
"type": "object",
|
|
2094
|
+
"properties": {
|
|
2095
|
+
"interfacePath": { "type": "string" },
|
|
2096
|
+
"storeOutputPath": { "type": "string" },
|
|
2097
|
+
"hookOutputPath": { "type": "string" },
|
|
2098
|
+
"storeType": { "type": "number" }
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
```
|
|
2103
|
+
|
|
2104
|
+
#### 옵션 B: 개별 도구 (현재 방식)
|
|
2105
|
+
|
|
2106
|
+
```typescript
|
|
2107
|
+
// 사용자가 각각 호출
|
|
2108
|
+
generateStore({ interfacePath, outputPath: "stores/useMemberListStore.ts" })
|
|
2109
|
+
generateHook({ interfacePath, outputPath: "hooks/useMemberService.ts" })
|
|
2110
|
+
```
|
|
2111
|
+
|
|
2112
|
+
### 15.4 결과 요약 로직
|
|
2113
|
+
|
|
2114
|
+
```typescript
|
|
2115
|
+
/**
|
|
2116
|
+
* Store와 Hook 동시 생성 (결과 요약)
|
|
2117
|
+
*/
|
|
2118
|
+
async function generateStoreAndHook(args: {
|
|
2119
|
+
interfacePath: string;
|
|
2120
|
+
storeOutputPath?: string;
|
|
2121
|
+
hookOutputPath?: string;
|
|
2122
|
+
storeType?: number;
|
|
2123
|
+
}) {
|
|
2124
|
+
const { interfacePath, storeOutputPath, hookOutputPath, storeType = 1 } = args;
|
|
2125
|
+
|
|
2126
|
+
// 1. Store 생성
|
|
2127
|
+
const storeResult = await generateStore({
|
|
2128
|
+
interfacePath,
|
|
2129
|
+
outputPath: storeOutputPath || getDefaultStorePath(interfacePath),
|
|
2130
|
+
storeType,
|
|
2131
|
+
});
|
|
2132
|
+
|
|
2133
|
+
// 2. Hook 생성
|
|
2134
|
+
const hookResult = await generateHook({
|
|
2135
|
+
interfacePath,
|
|
2136
|
+
outputPath: hookOutputPath || getDefaultHookPath(interfacePath),
|
|
2137
|
+
});
|
|
2138
|
+
|
|
2139
|
+
// 3. 결과 요약
|
|
2140
|
+
const storeData = JSON.parse(storeResult.content[0].text);
|
|
2141
|
+
const hookData = JSON.parse(hookResult.content[0].text);
|
|
2142
|
+
|
|
2143
|
+
// 4. 요약된 결과 반환
|
|
2144
|
+
return {
|
|
2145
|
+
content: [{
|
|
2146
|
+
type: 'text',
|
|
2147
|
+
text: formatSummary(storeData, hookData),
|
|
2148
|
+
}],
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
/**
|
|
2153
|
+
* 결과 요약 포맷팅
|
|
2154
|
+
*/
|
|
2155
|
+
function formatSummary(storeData: any, hookData: any): string {
|
|
2156
|
+
const lines: string[] = [];
|
|
2157
|
+
|
|
2158
|
+
// 헤더
|
|
2159
|
+
const hasErrors = storeData.typeErrors || hookData.typeErrors;
|
|
2160
|
+
const icon = hasErrors ? '⚠️' : '✅';
|
|
2161
|
+
const title = hasErrors ? 'Store 및 Hook 생성 완료 (일부 타입 오류)' : 'Store 및 Hook 생성 완료';
|
|
2162
|
+
|
|
2163
|
+
lines.push(`${icon} ${title}\n`);
|
|
2164
|
+
|
|
2165
|
+
// 생성된 파일
|
|
2166
|
+
lines.push('생성된 파일:');
|
|
2167
|
+
lines.push(`- ${storeData.outputPath} (Store)`);
|
|
2168
|
+
lines.push(`- ${hookData.path} (Hook)\n`);
|
|
2169
|
+
|
|
2170
|
+
// Store 상태
|
|
2171
|
+
const storeStatus = storeData.typeErrors
|
|
2172
|
+
? `Store: 타입 오류 ${storeData.typeErrors.length}건`
|
|
2173
|
+
: 'Store: 타입 체크 통과';
|
|
2174
|
+
lines.push(storeStatus);
|
|
2175
|
+
|
|
2176
|
+
// Hook 상태
|
|
2177
|
+
const hookStatus = hookData.typeErrors
|
|
2178
|
+
? `Hook: 타입 오류 ${hookData.typeErrors.length}건`
|
|
2179
|
+
: 'Hook: 타입 체크 통과';
|
|
2180
|
+
lines.push(hookStatus);
|
|
2181
|
+
|
|
2182
|
+
// 타입 오류가 있으면 추가
|
|
2183
|
+
const allErrors = [
|
|
2184
|
+
...(storeData.typeErrors || []).map((e: string) => `[Store] ${e}`),
|
|
2185
|
+
...(hookData.typeErrors || []).map((e: string) => `[Hook] ${e}`),
|
|
2186
|
+
];
|
|
2187
|
+
|
|
2188
|
+
if (allErrors.length > 0) {
|
|
2189
|
+
lines.push('\n타입 오류:');
|
|
2190
|
+
allErrors.forEach(error => lines.push(`- ${error}`));
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
return lines.join('\n');
|
|
2194
|
+
}
|
|
2195
|
+
```
|
|
2196
|
+
|
|
2197
|
+
### 15.5 사용자 경험 비교
|
|
2198
|
+
|
|
2199
|
+
| 방식 | 장점 | 단점 |
|
|
2200
|
+
|------|------|------|
|
|
2201
|
+
| **개별 결과** | 각 파일의 상세 결과 확인 | 결과가 길어짐, 전체 현황 파악 어려움 |
|
|
2202
|
+
| **요약 결과** | 전체 현황 한눈에 파악 | 개별 상세 정보는 별도 확인 필요 |
|
|
2203
|
+
|
|
2204
|
+
### 15.6 구현 우선순위
|
|
2205
|
+
|
|
2206
|
+
1. ✅ Store 생성 결과에 `typeErrors` 필드 추가
|
|
2207
|
+
2. ✅ Hook 생성 결과에 `typeErrors` 필드 추가
|
|
2208
|
+
3. ⏳ 통합 도구 `generate-store-and-hook` 구현
|
|
2209
|
+
4. ⏳ 결과 요약 함수 `formatSummary` 구현
|
|
2210
|
+
|
|
2211
|
+
---
|
|
2212
|
+
|
|
2213
|
+
## 16. 향후 확장 가능성
|
|
2214
|
+
|
|
2215
|
+
1. **커스텀 훅 옵션**: 사용자가 네이밍 규칙, spinning 관리 방식 등을 선택 가능
|
|
2216
|
+
2. **타입스크립트 엄격 모드**: 더 엄격한 타입 검증
|
|
2217
|
+
3. **테스트 코드 자동 생성**: 훅에 대한 테스트 코드 자동 생성
|
|
2218
|
+
4. **문서 자동 생성**: 훅 사용법에 대한 문서 자동 생성
|
|
2219
|
+
|
|
2220
|
+
---
|
|
2221
|
+
|
|
2222
|
+
## 17. 참고 자료
|
|
2223
|
+
|
|
2224
|
+
- NH-FE-B 리액트 훅 패턴 분석 결과
|
|
2225
|
+
- 기존 Store 생성 템플릿
|
|
2226
|
+
- React Hooks 공식 문서
|