@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.
@@ -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: operation.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: operation.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
- filterEndpoints(endpoints, method) {
365
+ filterByMethod(endpoints, method) {
272
366
  if (method === 'ALL') {
273
367
  return endpoints;
274
368
  }
@@ -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">, listSetFilter: import("@reduxjs/toolkit").ActionCreatorWithPayload<HttpMethod | "ALL", "app/listSetFilter">, listSetTag: import("@reduxjs/toolkit").ActionCreatorWithPayload<string | null, "app/listSetTag">, listSetFocus: import("@reduxjs/toolkit").ActionCreatorWithPayload<"LIST" | "TAGS", "app/listSetFocus">, listSelectTag: import("@reduxjs/toolkit").ActionCreatorWithPayload<number, "app/listSelectTag">, 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<{
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("@reduxjs/toolkit").Reducer<AppState>;
13
+ declare const _default: import("redux").Reducer<AppState>;
14
14
  export default _default;
@@ -11,18 +11,21 @@ const initialState = {
11
11
  list: {
12
12
  selectedIndex: 0,
13
13
  filteredEndpoints: [],
14
- activeFilter: 'ALL',
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
- listSetFilter: (state, action) => {
61
- state.list.activeFilter = action.payload;
62
- state.list.selectedIndex = 0;
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, listSetFilter, listSetTag, listSetFocus, listSelectTag, listSetSearch, listSetTagSearch, listScroll, detailSet, detailSetFocus, detailSetParam, detailSetBody, detailSetHeader, detailSetPanel, detailSetRequestTab, detailSetResponseTab, detailSetResponse, reset, } = appSlice.actions;
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;
@@ -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("@reduxjs/toolkit").UnknownAction> & import("@reduxjs/toolkit").Dispatch<import("@reduxjs/toolkit").UnknownAction>>;
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
  }>;
@@ -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("@reduxjs/toolkit").UnknownAction, import("@reduxjs/toolkit").Tuple<[import("@reduxjs/toolkit").StoreEnhancer<{
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("@reduxjs/toolkit").UnknownAction>;
10
- }>, import("@reduxjs/toolkit").StoreEnhancer]>>;
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
- activeFilter: HttpMethod | 'ALL';
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: 'LIST_SET_FILTER';
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;
@@ -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: ResponsesObject;
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
- lines.push(`${indentStr}${refName} []`);
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
- lines.push(`${indentStr}Array<${itemType}>`);
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
- const enumValues = schema.enum.map((v) => `'${v}'`).join(' | ');
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 = 3) {
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;