@gomjellie/lazyapi 0.0.2 → 0.0.3
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/dist/App.js +1 -1
- package/dist/components/common/LoadingSpinner.js +8 -4
- package/dist/components/common/StatusBar.js +3 -0
- package/dist/components/detail-view/DetailView.js +38 -38
- package/dist/components/detail-view/LeftPanel.d.ts +4 -1
- package/dist/components/detail-view/LeftPanel.js +402 -72
- package/dist/components/detail-view/RightPanel.d.ts +3 -2
- package/dist/components/detail-view/RightPanel.js +119 -31
- package/dist/components/list-view/EndpointList.js +16 -11
- package/dist/components/list-view/ListView.js +34 -58
- package/dist/components/list-view/MethodsBar.d.ts +14 -0
- package/dist/components/list-view/MethodsBar.js +36 -0
- package/dist/hooks/useKeyPress.js +66 -23
- package/dist/services/openapi-parser.d.ts +10 -2
- package/dist/services/openapi-parser.js +104 -10
- package/dist/store/appSlice.d.ts +2 -2
- package/dist/store/appSlice.js +43 -5
- package/dist/store/hooks.d.ts +1 -1
- package/dist/store/index.d.ts +3 -3
- package/dist/store/navigationActions.d.ts +9 -0
- package/dist/store/navigationActions.js +129 -0
- package/dist/types/app-state.d.ts +13 -4
- package/dist/types/openapi.d.ts +18 -1
- package/dist/utils/schema-formatter.js +19 -5
- package/dist/utils/schema-scaffolder.d.ts +7 -0
- package/dist/utils/schema-scaffolder.js +61 -0
- package/package.json +5 -1
- package/dist/components/list-view/FilterBar.d.ts +0 -12
- package/dist/components/list-view/FilterBar.js +0 -33
|
@@ -64,7 +64,8 @@ export class OpenAPIParserService {
|
|
|
64
64
|
for (const method of methods) {
|
|
65
65
|
const operation = pathItem[method];
|
|
66
66
|
if (operation) {
|
|
67
|
-
endpoints.push(this.createEndpointV2(path, method.toUpperCase(), operation, pathItem
|
|
67
|
+
endpoints.push(this.createEndpointV2(path, method.toUpperCase(), operation, pathItem, document // document 인자 전달
|
|
68
|
+
));
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
}
|
|
@@ -123,6 +124,24 @@ export class OpenAPIParserService {
|
|
|
123
124
|
requestBody = this.normalizeRequestBodyV3(operation.requestBody);
|
|
124
125
|
}
|
|
125
126
|
}
|
|
127
|
+
// Responses 정규화
|
|
128
|
+
const responses = Object.entries(operation.responses || {})
|
|
129
|
+
.map(([status, response]) => {
|
|
130
|
+
if (!response)
|
|
131
|
+
return null;
|
|
132
|
+
if (isReferenceObject(response)) {
|
|
133
|
+
if (document) {
|
|
134
|
+
const resolved = resolveRef(response.$ref, document);
|
|
135
|
+
if (resolved && !isReferenceObject(resolved)) {
|
|
136
|
+
return this.normalizeResponseV3(status, resolved);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Resolve 실패 시 기본값 (설명 없음)
|
|
140
|
+
return { status, description: 'Reference not found' };
|
|
141
|
+
}
|
|
142
|
+
return this.normalizeResponseV3(status, response);
|
|
143
|
+
})
|
|
144
|
+
.filter((r) => r !== null);
|
|
126
145
|
return {
|
|
127
146
|
id,
|
|
128
147
|
method,
|
|
@@ -132,7 +151,7 @@ export class OpenAPIParserService {
|
|
|
132
151
|
tags: operation.tags || [],
|
|
133
152
|
parameters,
|
|
134
153
|
requestBody,
|
|
135
|
-
responses
|
|
154
|
+
responses,
|
|
136
155
|
operationId: operation.operationId,
|
|
137
156
|
deprecated: operation.deprecated,
|
|
138
157
|
security: operation.security,
|
|
@@ -141,8 +160,9 @@ export class OpenAPIParserService {
|
|
|
141
160
|
/**
|
|
142
161
|
* Swagger 2.0 Operation 객체에서 Endpoint 생성
|
|
143
162
|
*/
|
|
144
|
-
createEndpointV2(path, method, operation, pathItem) {
|
|
163
|
+
createEndpointV2(path, method, operation, pathItem, document) {
|
|
145
164
|
const id = `${method}_${path.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
|
165
|
+
// ... 파라미터 처리 (생략) ...
|
|
146
166
|
// 파라미터 병합 및 정규화
|
|
147
167
|
const pathParameters = pathItem.parameters || [];
|
|
148
168
|
const operationParameters = operation.parameters || [];
|
|
@@ -153,23 +173,36 @@ export class OpenAPIParserService {
|
|
|
153
173
|
if (isReferenceObject(p))
|
|
154
174
|
continue;
|
|
155
175
|
const param = p;
|
|
156
|
-
// Swagger 2.0의 body 파라미터 처리
|
|
157
176
|
if (param.in === 'body') {
|
|
158
177
|
const bodyParam = param;
|
|
159
|
-
// requestBody로도 저장
|
|
160
178
|
requestBody = {
|
|
161
179
|
description: bodyParam.description,
|
|
162
180
|
required: bodyParam.required,
|
|
163
181
|
schema: bodyParam.schema,
|
|
164
182
|
};
|
|
165
|
-
// UI 표시를 위해 parameters에도 포함
|
|
166
183
|
parameters.push(this.normalizeParameterV2(param));
|
|
167
184
|
}
|
|
168
185
|
else {
|
|
169
|
-
// 나머지 파라미터들 (path, query, header, formData)
|
|
170
186
|
parameters.push(this.normalizeParameterV2(param));
|
|
171
187
|
}
|
|
172
188
|
}
|
|
189
|
+
// Responses 정규화
|
|
190
|
+
const responses = Object.entries(operation.responses || {})
|
|
191
|
+
.map(([status, response]) => {
|
|
192
|
+
if (!response)
|
|
193
|
+
return null;
|
|
194
|
+
if (isReferenceObject(response)) {
|
|
195
|
+
if (document) {
|
|
196
|
+
const resolved = resolveRef(response.$ref, document);
|
|
197
|
+
if (resolved && !isReferenceObject(resolved)) {
|
|
198
|
+
return this.normalizeResponseV2(status, resolved);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return { status, description: 'Reference not found' };
|
|
202
|
+
}
|
|
203
|
+
return this.normalizeResponseV2(status, response);
|
|
204
|
+
})
|
|
205
|
+
.filter((r) => r !== null);
|
|
173
206
|
return {
|
|
174
207
|
id,
|
|
175
208
|
method,
|
|
@@ -179,12 +212,73 @@ export class OpenAPIParserService {
|
|
|
179
212
|
tags: operation.tags || [],
|
|
180
213
|
parameters,
|
|
181
214
|
requestBody,
|
|
182
|
-
responses
|
|
215
|
+
responses,
|
|
183
216
|
operationId: operation.operationId,
|
|
184
217
|
deprecated: operation.deprecated,
|
|
185
218
|
security: operation.security,
|
|
186
219
|
};
|
|
187
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* OpenAPI v3 Response 객체 정규화
|
|
223
|
+
*/
|
|
224
|
+
normalizeResponseV3(status, response) {
|
|
225
|
+
const content = response.content
|
|
226
|
+
? Object.entries(response.content).map(([mediaType, mediaObj]) => ({
|
|
227
|
+
mediaType,
|
|
228
|
+
schema: isReferenceObject(mediaObj.schema) ? undefined : mediaObj.schema,
|
|
229
|
+
examples: mediaObj.examples || mediaObj.example,
|
|
230
|
+
}))
|
|
231
|
+
: undefined;
|
|
232
|
+
const headers = response.headers
|
|
233
|
+
? Object.entries(response.headers).map(([name, header]) => {
|
|
234
|
+
if (isReferenceObject(header)) {
|
|
235
|
+
return { name, description: 'Reference header not supported yet' };
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
name,
|
|
239
|
+
description: header.description,
|
|
240
|
+
schema: isReferenceObject(header.schema) ? undefined : header.schema,
|
|
241
|
+
};
|
|
242
|
+
})
|
|
243
|
+
: undefined;
|
|
244
|
+
return {
|
|
245
|
+
status,
|
|
246
|
+
description: response.description,
|
|
247
|
+
content,
|
|
248
|
+
headers,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Swagger 2.0 Response 객체 정규화
|
|
253
|
+
*/
|
|
254
|
+
normalizeResponseV2(status, response) {
|
|
255
|
+
// V2는 content가 없고 schema가 바로 있음. 이를 application/json (또는 produces) content로 변환
|
|
256
|
+
let content;
|
|
257
|
+
if (response.schema) {
|
|
258
|
+
// V2 스키마를 V3 스타일 content로 래핑
|
|
259
|
+
content = [
|
|
260
|
+
{
|
|
261
|
+
mediaType: 'application/json', // 기본값
|
|
262
|
+
schema: response.schema,
|
|
263
|
+
examples: response.examples,
|
|
264
|
+
},
|
|
265
|
+
];
|
|
266
|
+
}
|
|
267
|
+
const headers = response.headers
|
|
268
|
+
? Object.entries(response.headers).map(([name, header]) => ({
|
|
269
|
+
name,
|
|
270
|
+
description: header.description,
|
|
271
|
+
type: header.type,
|
|
272
|
+
// V2 헤더는 schema 대신 type/format 등을 가짐. 이를 schema 형태로 변환 가능하지만 일단 type만 유지
|
|
273
|
+
}))
|
|
274
|
+
: undefined;
|
|
275
|
+
return {
|
|
276
|
+
status,
|
|
277
|
+
description: response.description,
|
|
278
|
+
content,
|
|
279
|
+
headers,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
188
282
|
/**
|
|
189
283
|
* OpenAPI v3 파라미터를 정규화
|
|
190
284
|
*/
|
|
@@ -266,9 +360,9 @@ export class OpenAPIParserService {
|
|
|
266
360
|
};
|
|
267
361
|
}
|
|
268
362
|
/**
|
|
269
|
-
* 엔드포인트 필터링
|
|
363
|
+
* 엔드포인트 메서드별 필터링
|
|
270
364
|
*/
|
|
271
|
-
|
|
365
|
+
filterByMethod(endpoints, method) {
|
|
272
366
|
if (method === 'ALL') {
|
|
273
367
|
return endpoints;
|
|
274
368
|
}
|
package/dist/store/appSlice.d.ts
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { AppState, AppMode, Screen, ApiResponse } from '../types/app-state.js';
|
|
5
5
|
import { Endpoint, HttpMethod, OpenAPIDocument } from '../types/openapi.js';
|
|
6
|
-
export declare const setDocument: import("@reduxjs/toolkit").ActionCreatorWithPayload<OpenAPIDocument, "app/setDocument">, setEndpoints: import("@reduxjs/toolkit").ActionCreatorWithPayload<Endpoint[], "app/setEndpoints">, setScreen: import("@reduxjs/toolkit").ActionCreatorWithPayload<Screen, "app/setScreen">, setMode: import("@reduxjs/toolkit").ActionCreatorWithPayload<AppMode, "app/setMode">, setCommandInput: import("@reduxjs/toolkit").ActionCreatorWithPayload<string, "app/setCommandInput">, setError: import("@reduxjs/toolkit").ActionCreatorWithPayload<string | null, "app/setError">, setLoading: import("@reduxjs/toolkit").ActionCreatorWithPayload<boolean, "app/setLoading">, listSelect: import("@reduxjs/toolkit").ActionCreatorWithPayload<number, "app/listSelect">,
|
|
6
|
+
export declare const setDocument: import("@reduxjs/toolkit").ActionCreatorWithPayload<OpenAPIDocument, "app/setDocument">, setEndpoints: import("@reduxjs/toolkit").ActionCreatorWithPayload<Endpoint[], "app/setEndpoints">, setScreen: import("@reduxjs/toolkit").ActionCreatorWithPayload<Screen, "app/setScreen">, setMode: import("@reduxjs/toolkit").ActionCreatorWithPayload<AppMode, "app/setMode">, setCommandInput: import("@reduxjs/toolkit").ActionCreatorWithPayload<string, "app/setCommandInput">, setNavigationCount: import("@reduxjs/toolkit").ActionCreatorWithPayload<number | null, "app/setNavigationCount">, setError: import("@reduxjs/toolkit").ActionCreatorWithPayload<string | null, "app/setError">, setLoading: import("@reduxjs/toolkit").ActionCreatorWithPayload<boolean, "app/setLoading">, listSelect: import("@reduxjs/toolkit").ActionCreatorWithPayload<number, "app/listSelect">, listSetMethodFilter: import("@reduxjs/toolkit").ActionCreatorWithPayload<HttpMethod | "ALL", "app/listSetMethodFilter">, listSetTag: import("@reduxjs/toolkit").ActionCreatorWithPayload<string | null, "app/listSetTag">, listSetFocus: import("@reduxjs/toolkit").ActionCreatorWithPayload<"LIST" | "TAGS" | "METHODS", "app/listSetFocus">, listSelectTag: import("@reduxjs/toolkit").ActionCreatorWithPayload<number, "app/listSelectTag">, listSelectMethod: import("@reduxjs/toolkit").ActionCreatorWithPayload<number, "app/listSelectMethod">, listSetSearch: import("@reduxjs/toolkit").ActionCreatorWithPayload<string, "app/listSetSearch">, listSetTagSearch: import("@reduxjs/toolkit").ActionCreatorWithPayload<string, "app/listSetTagSearch">, listScroll: import("@reduxjs/toolkit").ActionCreatorWithPayload<number, "app/listScroll">, detailSet: import("@reduxjs/toolkit").ActionCreatorWithPayload<Endpoint, "app/detailSet">, detailSetFocus: import("@reduxjs/toolkit").ActionCreatorWithPayload<number, "app/detailSetFocus">, detailSetParam: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
|
|
7
7
|
key: string;
|
|
8
8
|
value: string;
|
|
9
9
|
}, "app/detailSetParam">, detailSetBody: import("@reduxjs/toolkit").ActionCreatorWithPayload<string, "app/detailSetBody">, detailSetHeader: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
|
|
10
10
|
key: string;
|
|
11
11
|
value: string;
|
|
12
12
|
}, "app/detailSetHeader">, detailSetPanel: import("@reduxjs/toolkit").ActionCreatorWithPayload<"left" | "request" | "response", "app/detailSetPanel">, detailSetRequestTab: import("@reduxjs/toolkit").ActionCreatorWithPayload<"headers" | "query" | "body", "app/detailSetRequestTab">, detailSetResponseTab: import("@reduxjs/toolkit").ActionCreatorWithPayload<"headers" | "body" | "cookies" | "curl", "app/detailSetResponseTab">, detailSetResponse: import("@reduxjs/toolkit").ActionCreatorWithPayload<ApiResponse | null, "app/detailSetResponse">, reset: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"app/reset">;
|
|
13
|
-
declare const _default: import("
|
|
13
|
+
declare const _default: import("redux").Reducer<AppState>;
|
|
14
14
|
export default _default;
|
package/dist/store/appSlice.js
CHANGED
|
@@ -11,18 +11,21 @@ const initialState = {
|
|
|
11
11
|
list: {
|
|
12
12
|
selectedIndex: 0,
|
|
13
13
|
filteredEndpoints: [],
|
|
14
|
-
|
|
14
|
+
activeMethod: 'ALL',
|
|
15
15
|
activeTag: null,
|
|
16
16
|
focus: 'LIST',
|
|
17
17
|
selectedTagIndex: 0,
|
|
18
|
+
focusedMethodIndex: 0,
|
|
18
19
|
searchQuery: '',
|
|
19
20
|
tagSearchQuery: '',
|
|
20
21
|
scrollOffset: 0,
|
|
22
|
+
methodHistory: {},
|
|
21
23
|
},
|
|
22
24
|
detail: null,
|
|
23
25
|
error: null,
|
|
24
26
|
loading: false,
|
|
25
27
|
commandInput: '',
|
|
28
|
+
navigationCount: null,
|
|
26
29
|
};
|
|
27
30
|
const appSlice = createSlice({
|
|
28
31
|
name: 'app',
|
|
@@ -43,10 +46,15 @@ const appSlice = createSlice({
|
|
|
43
46
|
if (action.payload === 'COMMAND') {
|
|
44
47
|
state.commandInput = '';
|
|
45
48
|
}
|
|
49
|
+
// 모드 변경 시 Count 초기화
|
|
50
|
+
state.navigationCount = null;
|
|
46
51
|
},
|
|
47
52
|
setCommandInput: (state, action) => {
|
|
48
53
|
state.commandInput = action.payload;
|
|
49
54
|
},
|
|
55
|
+
setNavigationCount: (state, action) => {
|
|
56
|
+
state.navigationCount = action.payload;
|
|
57
|
+
},
|
|
50
58
|
setError: (state, action) => {
|
|
51
59
|
state.error = action.payload;
|
|
52
60
|
state.loading = false;
|
|
@@ -56,10 +64,32 @@ const appSlice = createSlice({
|
|
|
56
64
|
},
|
|
57
65
|
listSelect: (state, action) => {
|
|
58
66
|
state.list.selectedIndex = Math.max(0, Math.min(action.payload, state.list.filteredEndpoints.length - 1));
|
|
67
|
+
// 히스토리 업데이트
|
|
68
|
+
state.list.methodHistory[state.list.activeMethod] = {
|
|
69
|
+
selectedIndex: state.list.selectedIndex,
|
|
70
|
+
scrollOffset: state.list.scrollOffset,
|
|
71
|
+
};
|
|
59
72
|
},
|
|
60
|
-
|
|
61
|
-
state.list.
|
|
62
|
-
|
|
73
|
+
listSetMethodFilter: (state, action) => {
|
|
74
|
+
if (state.list.activeMethod !== action.payload) {
|
|
75
|
+
// 현재 상태 저장
|
|
76
|
+
state.list.methodHistory[state.list.activeMethod] = {
|
|
77
|
+
selectedIndex: state.list.selectedIndex,
|
|
78
|
+
scrollOffset: state.list.scrollOffset,
|
|
79
|
+
};
|
|
80
|
+
// 메서드 변경
|
|
81
|
+
state.list.activeMethod = action.payload;
|
|
82
|
+
// 히스토리 복원
|
|
83
|
+
const history = state.list.methodHistory[action.payload];
|
|
84
|
+
if (history) {
|
|
85
|
+
state.list.selectedIndex = history.selectedIndex;
|
|
86
|
+
state.list.scrollOffset = history.scrollOffset;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
state.list.selectedIndex = 0;
|
|
90
|
+
state.list.scrollOffset = 0;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
63
93
|
},
|
|
64
94
|
listSetTag: (state, action) => {
|
|
65
95
|
state.list.activeTag = action.payload;
|
|
@@ -71,6 +101,9 @@ const appSlice = createSlice({
|
|
|
71
101
|
listSelectTag: (state, action) => {
|
|
72
102
|
state.list.selectedTagIndex = action.payload;
|
|
73
103
|
},
|
|
104
|
+
listSelectMethod: (state, action) => {
|
|
105
|
+
state.list.focusedMethodIndex = action.payload;
|
|
106
|
+
},
|
|
74
107
|
listSetSearch: (state, action) => {
|
|
75
108
|
state.list.searchQuery = action.payload;
|
|
76
109
|
state.list.selectedIndex = 0;
|
|
@@ -81,6 +114,11 @@ const appSlice = createSlice({
|
|
|
81
114
|
},
|
|
82
115
|
listScroll: (state, action) => {
|
|
83
116
|
state.list.scrollOffset = action.payload;
|
|
117
|
+
// 히스토리 업데이트
|
|
118
|
+
state.list.methodHistory[state.list.activeMethod] = {
|
|
119
|
+
selectedIndex: state.list.selectedIndex,
|
|
120
|
+
scrollOffset: state.list.scrollOffset,
|
|
121
|
+
};
|
|
84
122
|
},
|
|
85
123
|
detailSet: (state, action) => {
|
|
86
124
|
const endpoint = action.payload;
|
|
@@ -140,5 +178,5 @@ const appSlice = createSlice({
|
|
|
140
178
|
reset: () => initialState,
|
|
141
179
|
},
|
|
142
180
|
});
|
|
143
|
-
export const { setDocument, setEndpoints, setScreen, setMode, setCommandInput, setError, setLoading, listSelect,
|
|
181
|
+
export const { setDocument, setEndpoints, setScreen, setMode, setCommandInput, setNavigationCount, setError, setLoading, listSelect, listSetMethodFilter, listSetTag, listSetFocus, listSelectTag, listSelectMethod, listSetSearch, listSetTagSearch, listScroll, detailSet, detailSetFocus, detailSetParam, detailSetBody, detailSetHeader, detailSetPanel, detailSetRequestTab, detailSetResponseTab, detailSetResponse, reset, } = appSlice.actions;
|
|
144
182
|
export default appSlice.reducer;
|
package/dist/store/hooks.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export declare const useAppDispatch: import("react-redux").UseDispatch<import("@reduxjs/toolkit").ThunkDispatch<{
|
|
5
5
|
app: import("../types/app-state.js").AppState;
|
|
6
|
-
}, undefined, import("
|
|
6
|
+
}, undefined, import("redux").UnknownAction> & import("redux").Dispatch<import("redux").UnknownAction>>;
|
|
7
7
|
export declare const useAppSelector: import("react-redux").UseSelector<{
|
|
8
8
|
app: import("../types/app-state.js").AppState;
|
|
9
9
|
}>;
|
package/dist/store/index.d.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export declare const store: import("@reduxjs/toolkit").EnhancedStore<{
|
|
5
5
|
app: import("../types/app-state.js").AppState;
|
|
6
|
-
}, import("
|
|
6
|
+
}, import("redux").UnknownAction, import("@reduxjs/toolkit").Tuple<[import("redux").StoreEnhancer<{
|
|
7
7
|
dispatch: import("@reduxjs/toolkit").ThunkDispatch<{
|
|
8
8
|
app: import("../types/app-state.js").AppState;
|
|
9
|
-
}, undefined, import("
|
|
10
|
-
}>, import("
|
|
9
|
+
}, undefined, import("redux").UnknownAction>;
|
|
10
|
+
}>, import("redux").StoreEnhancer]>>;
|
|
11
11
|
export type RootState = ReturnType<typeof store.getState>;
|
|
12
12
|
export type AppDispatch = typeof store.dispatch;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ThunkAction } from '@reduxjs/toolkit';
|
|
2
|
+
import { AnyAction } from 'redux';
|
|
3
|
+
import { RootState } from './index.js';
|
|
4
|
+
type AppThunk = ThunkAction<void, RootState, unknown, AnyAction>;
|
|
5
|
+
export declare const navigateUp: () => AppThunk;
|
|
6
|
+
export declare const navigateDown: () => AppThunk;
|
|
7
|
+
export declare const navigateLeft: () => AppThunk;
|
|
8
|
+
export declare const navigateRight: () => AppThunk;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { listSetFocus, listSelectMethod, listSetMethodFilter, listSelect, listSelectTag, detailSetFocus, detailSetPanel, } from './appSlice.js';
|
|
2
|
+
const METHODS = ['ALL', 'GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
|
|
3
|
+
// k Key (Up)
|
|
4
|
+
export const navigateUp = () => (dispatch, getState) => {
|
|
5
|
+
const { app } = getState();
|
|
6
|
+
const { screen, list, detail } = app;
|
|
7
|
+
if (screen === 'LIST') {
|
|
8
|
+
if (list.focus === 'TAGS') {
|
|
9
|
+
const prevIndex = Math.max(list.selectedTagIndex - 1, 0);
|
|
10
|
+
dispatch(listSelectTag(prevIndex));
|
|
11
|
+
}
|
|
12
|
+
else if (list.focus === 'METHODS') {
|
|
13
|
+
// 아무 동작 안함
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
// LIST
|
|
17
|
+
if (list.selectedIndex === 0) {
|
|
18
|
+
dispatch(listSetFocus('METHODS'));
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
const prevIndex = Math.max(list.selectedIndex - 1, 0);
|
|
22
|
+
dispatch(listSelect(prevIndex));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else if (screen === 'DETAIL' && detail) {
|
|
27
|
+
if (detail.activePanel === 'left') {
|
|
28
|
+
const prevIndex = Math.max(detail.focusedFieldIndex - 1, 0);
|
|
29
|
+
dispatch(detailSetFocus(prevIndex));
|
|
30
|
+
}
|
|
31
|
+
// RightPanel 스크롤은 컴포넌트 내 Functional Update로 처리되므로 둠
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
// j Key (Down)
|
|
35
|
+
export const navigateDown = () => (dispatch, getState) => {
|
|
36
|
+
const { app } = getState();
|
|
37
|
+
const { screen, list, detail, endpoints } = app;
|
|
38
|
+
if (screen === 'LIST') {
|
|
39
|
+
if (list.focus === 'TAGS') {
|
|
40
|
+
// 태그 목록 계산 (Explorer와 동일한 로직 필요)
|
|
41
|
+
const tags = new Set();
|
|
42
|
+
endpoints.forEach((ep) => ep.tags?.forEach((tag) => tags.add(tag)));
|
|
43
|
+
// Tag Search Query가 있을 경우 필터링된 개수 기준이어야 함
|
|
44
|
+
// 현재는 간단히 최대치로 제한 (ListView에서 계산된 filteredTags.length가 이상적이나 접근 불가)
|
|
45
|
+
// Thunk에서 filteredTags를 직접 계산하거나 상위에서 넘겨줘야 함.
|
|
46
|
+
// 일단 selectedTagIndex 업데이트만 처리.
|
|
47
|
+
dispatch(listSelectTag(list.selectedTagIndex + 1));
|
|
48
|
+
}
|
|
49
|
+
else if (list.focus === 'METHODS') {
|
|
50
|
+
dispatch(listSetFocus('LIST'));
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// LIST
|
|
54
|
+
const nextIndex = Math.min(list.selectedIndex + 1, list.filteredEndpoints.length - 1);
|
|
55
|
+
dispatch(listSelect(nextIndex));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else if (screen === 'DETAIL' && detail) {
|
|
59
|
+
if (detail.activePanel === 'left') {
|
|
60
|
+
// totalFields 계산 로직 포함
|
|
61
|
+
const endpoint = detail.endpoint;
|
|
62
|
+
const pathParams = endpoint.parameters.filter((p) => p.in === 'path') || [];
|
|
63
|
+
const queryParams = endpoint.parameters.filter((p) => p.in === 'query') || [];
|
|
64
|
+
const headerParams = endpoint.parameters.filter((p) => p.in === 'header') || [];
|
|
65
|
+
const bodyParams = endpoint.parameters.filter((p) => p.in === 'body') || [];
|
|
66
|
+
const formDataParams = endpoint.parameters.filter((p) => p.in === 'formData') || [];
|
|
67
|
+
const totalFields = pathParams.length +
|
|
68
|
+
queryParams.length +
|
|
69
|
+
headerParams.length +
|
|
70
|
+
bodyParams.length +
|
|
71
|
+
formDataParams.length +
|
|
72
|
+
(endpoint.requestBody && bodyParams.length === 0 ? 1 : 0) +
|
|
73
|
+
(endpoint.responses?.length || 0);
|
|
74
|
+
const nextIndex = Math.min(detail.focusedFieldIndex + 1, totalFields - 1);
|
|
75
|
+
dispatch(detailSetFocus(nextIndex));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
// h Key (Left)
|
|
80
|
+
export const navigateLeft = () => (dispatch, getState) => {
|
|
81
|
+
const { app } = getState();
|
|
82
|
+
const { screen, list } = app;
|
|
83
|
+
if (screen === 'LIST') {
|
|
84
|
+
if (list.focus === 'LIST') {
|
|
85
|
+
dispatch(listSetFocus('TAGS'));
|
|
86
|
+
}
|
|
87
|
+
else if (list.focus === 'METHODS') {
|
|
88
|
+
if (list.focusedMethodIndex === 0) {
|
|
89
|
+
dispatch(listSetFocus('LIST'));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const prevIndex = Math.max(list.focusedMethodIndex - 1, 0);
|
|
93
|
+
dispatch(listSelectMethod(prevIndex));
|
|
94
|
+
dispatch(listSetMethodFilter(METHODS[prevIndex]));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (screen === 'DETAIL') {
|
|
99
|
+
dispatch(detailSetPanel('left'));
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
// l Key (Right)
|
|
103
|
+
export const navigateRight = () => (dispatch, getState) => {
|
|
104
|
+
const { app } = getState();
|
|
105
|
+
const { screen, list, detail } = app;
|
|
106
|
+
if (screen === 'LIST') {
|
|
107
|
+
if (list.focus === 'TAGS') {
|
|
108
|
+
// TAGS -> LIST (태그 선택은 ListView에서 엔터/l 로직 유지)
|
|
109
|
+
dispatch(listSelectTag(list.selectedTagIndex)); // 현재 태그 선택 유지하며 포커스만 이동
|
|
110
|
+
dispatch(listSetFocus('LIST'));
|
|
111
|
+
}
|
|
112
|
+
else if (list.focus === 'LIST') {
|
|
113
|
+
const nextIndex = (list.focusedMethodIndex + 1) % METHODS.length;
|
|
114
|
+
dispatch(listSetFocus('METHODS'));
|
|
115
|
+
dispatch(listSelectMethod(nextIndex));
|
|
116
|
+
dispatch(listSetMethodFilter(METHODS[nextIndex]));
|
|
117
|
+
}
|
|
118
|
+
else if (list.focus === 'METHODS') {
|
|
119
|
+
const nextIndex = (list.focusedMethodIndex + 1) % METHODS.length;
|
|
120
|
+
dispatch(listSelectMethod(nextIndex));
|
|
121
|
+
dispatch(listSetMethodFilter(METHODS[nextIndex]));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else if (screen === 'DETAIL' && detail) {
|
|
125
|
+
if (detail.activePanel === 'left') {
|
|
126
|
+
dispatch(detailSetPanel('request'));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
@@ -10,6 +10,7 @@ export interface AppState {
|
|
|
10
10
|
screen: Screen;
|
|
11
11
|
mode: AppMode;
|
|
12
12
|
commandInput: string;
|
|
13
|
+
navigationCount: number | null;
|
|
13
14
|
list: ListViewState;
|
|
14
15
|
detail: DetailViewState | null;
|
|
15
16
|
error: string | null;
|
|
@@ -18,13 +19,18 @@ export interface AppState {
|
|
|
18
19
|
export interface ListViewState {
|
|
19
20
|
selectedIndex: number;
|
|
20
21
|
filteredEndpoints: Endpoint[];
|
|
21
|
-
|
|
22
|
+
activeMethod: HttpMethod | 'ALL';
|
|
22
23
|
activeTag: string | null;
|
|
23
|
-
focus: 'TAGS' | 'LIST';
|
|
24
|
+
focus: 'TAGS' | 'LIST' | 'METHODS';
|
|
24
25
|
selectedTagIndex: number;
|
|
26
|
+
focusedMethodIndex: number;
|
|
25
27
|
searchQuery: string;
|
|
26
28
|
tagSearchQuery: string;
|
|
27
29
|
scrollOffset: number;
|
|
30
|
+
methodHistory: Record<string, {
|
|
31
|
+
selectedIndex: number;
|
|
32
|
+
scrollOffset: number;
|
|
33
|
+
}>;
|
|
28
34
|
}
|
|
29
35
|
export interface DetailViewState {
|
|
30
36
|
endpoint: Endpoint;
|
|
@@ -68,17 +74,20 @@ export type AppAction = {
|
|
|
68
74
|
type: 'LIST_SELECT';
|
|
69
75
|
payload: number;
|
|
70
76
|
} | {
|
|
71
|
-
type: '
|
|
77
|
+
type: 'LIST_SET_METHOD_FILTER';
|
|
72
78
|
payload: HttpMethod | 'ALL';
|
|
73
79
|
} | {
|
|
74
80
|
type: 'LIST_SET_TAG';
|
|
75
81
|
payload: string | null;
|
|
76
82
|
} | {
|
|
77
83
|
type: 'LIST_SET_FOCUS';
|
|
78
|
-
payload: 'TAGS' | 'LIST';
|
|
84
|
+
payload: 'TAGS' | 'LIST' | 'METHODS';
|
|
79
85
|
} | {
|
|
80
86
|
type: 'LIST_SELECT_TAG';
|
|
81
87
|
payload: number;
|
|
88
|
+
} | {
|
|
89
|
+
type: 'LIST_SELECT_METHOD';
|
|
90
|
+
payload: number;
|
|
82
91
|
} | {
|
|
83
92
|
type: 'LIST_SET_SEARCH';
|
|
84
93
|
payload: string;
|
package/dist/types/openapi.d.ts
CHANGED
|
@@ -42,6 +42,23 @@ export interface NormalizedParameter {
|
|
|
42
42
|
default?: any;
|
|
43
43
|
example?: any;
|
|
44
44
|
}
|
|
45
|
+
export interface NormalizedHeader {
|
|
46
|
+
name: string;
|
|
47
|
+
description?: string;
|
|
48
|
+
schema?: SchemaObject;
|
|
49
|
+
type?: string;
|
|
50
|
+
}
|
|
51
|
+
export interface NormalizedContent {
|
|
52
|
+
mediaType: string;
|
|
53
|
+
schema?: SchemaObject;
|
|
54
|
+
examples?: Record<string, any>;
|
|
55
|
+
}
|
|
56
|
+
export interface NormalizedResponse {
|
|
57
|
+
status: string;
|
|
58
|
+
description: string;
|
|
59
|
+
content?: NormalizedContent[];
|
|
60
|
+
headers?: NormalizedHeader[];
|
|
61
|
+
}
|
|
45
62
|
export interface NormalizedRequestBody {
|
|
46
63
|
description?: string;
|
|
47
64
|
required?: boolean;
|
|
@@ -56,7 +73,7 @@ export interface Endpoint {
|
|
|
56
73
|
tags: string[];
|
|
57
74
|
parameters: NormalizedParameter[];
|
|
58
75
|
requestBody?: NormalizedRequestBody;
|
|
59
|
-
responses:
|
|
76
|
+
responses: NormalizedResponse[];
|
|
60
77
|
operationId?: string;
|
|
61
78
|
deprecated?: boolean;
|
|
62
79
|
security?: SecurityRequirementObject[];
|
|
@@ -138,12 +138,23 @@ function formatSchemaObject(schema, options) {
|
|
|
138
138
|
}
|
|
139
139
|
else {
|
|
140
140
|
// 단순 타입 배열
|
|
141
|
-
|
|
141
|
+
const itemType = formatType(itemsSchema);
|
|
142
|
+
if ('enum' in itemsSchema && itemsSchema.enum && Array.isArray(itemsSchema.enum)) {
|
|
143
|
+
lines.push(`${indentStr}(${itemType}) []`);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
lines.push(`${indentStr}Array<${itemType}>`);
|
|
147
|
+
}
|
|
142
148
|
}
|
|
143
149
|
}
|
|
144
150
|
else {
|
|
145
151
|
const itemType = formatType(itemsSchema);
|
|
146
|
-
|
|
152
|
+
if ('enum' in itemsSchema && itemsSchema.enum && Array.isArray(itemsSchema.enum)) {
|
|
153
|
+
lines.push(`${indentStr}(${itemType}) []`);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
lines.push(`${indentStr}Array<${itemType}>`);
|
|
157
|
+
}
|
|
147
158
|
}
|
|
148
159
|
return lines;
|
|
149
160
|
}
|
|
@@ -158,8 +169,7 @@ function formatSchemaObject(schema, options) {
|
|
|
158
169
|
function formatType(schema) {
|
|
159
170
|
// Enum 타입
|
|
160
171
|
if (schema.enum && Array.isArray(schema.enum)) {
|
|
161
|
-
|
|
162
|
-
return `Enum(${enumValues})`;
|
|
172
|
+
return schema.enum.map((v) => `'${v}'`).join(' | ');
|
|
163
173
|
}
|
|
164
174
|
// Array 타입
|
|
165
175
|
if (schema.type === 'array' && schema.items) {
|
|
@@ -168,6 +178,10 @@ function formatType(schema) {
|
|
|
168
178
|
return `Array<${refName}>`;
|
|
169
179
|
}
|
|
170
180
|
const itemType = formatType(schema.items);
|
|
181
|
+
// Enum 배열인 경우 괄호 추가
|
|
182
|
+
if ('enum' in schema.items && schema.items.enum && Array.isArray(schema.items.enum)) {
|
|
183
|
+
return `(${itemType}) []`;
|
|
184
|
+
}
|
|
171
185
|
return `Array<${itemType}>`;
|
|
172
186
|
}
|
|
173
187
|
// 기본 타입
|
|
@@ -232,7 +246,7 @@ function formatComplexType(schema, options) {
|
|
|
232
246
|
/**
|
|
233
247
|
* Schema 전체를 포맷팅 (최상위 레벨)
|
|
234
248
|
*/
|
|
235
|
-
export function formatSchemaFull(schema, document, maxDepth =
|
|
249
|
+
export function formatSchemaFull(schema, document, maxDepth = 10) {
|
|
236
250
|
const lines = [];
|
|
237
251
|
// $ref인 경우 먼저 resolve
|
|
238
252
|
let actualSchema;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { SchemaObject, ReferenceObject, OpenAPIDocument } from '../types/openapi.js';
|
|
2
|
+
/**
|
|
3
|
+
* OpenAPI 스키마를 기반으로 기본값 JSON 객체를 생성합니다.
|
|
4
|
+
* @param schema 스키마 객체
|
|
5
|
+
* @param document 참조 해결을 위한 OpenAPI 문서 (선택)
|
|
6
|
+
*/
|
|
7
|
+
export declare const scaffoldSchema: (schema: SchemaObject | ReferenceObject | undefined, document?: OpenAPIDocument) => string | number | boolean | object | null;
|