daiso 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +106 -7
  2. package/dist/api/actionsProxy.js +10 -0
  3. package/dist/api/compareHandlers.js +26 -0
  4. package/dist/api/configStatus.js +8 -0
  5. package/dist/api/feedbackHandlers.js +60 -0
  6. package/dist/api/healthCheckDefinitions.js +170 -0
  7. package/dist/api/healthCheckShape.js +106 -0
  8. package/dist/api/healthCheckTypes.js +4 -0
  9. package/dist/api/healthChecks.js +3 -271
  10. package/dist/api/opinetHandlers.js +91 -0
  11. package/dist/api/routes/compareRoutes.js +9 -0
  12. package/dist/api/routes/feedbackRoutes.js +5 -0
  13. package/dist/api/routes/healthRoutes.js +17 -1
  14. package/dist/api/routes/opinetRoutes.js +24 -0
  15. package/dist/bin.js +0 -0
  16. package/dist/cli/commandOptions.js +24 -2
  17. package/dist/cli/commands/apiCommand.js +24 -0
  18. package/dist/cli/commands/cinema.js +65 -0
  19. package/dist/cli/commands/convenience.js +75 -100
  20. package/dist/cli/http.js +1 -1
  21. package/dist/cli.js +12 -1
  22. package/dist/cliHelp.js +55 -8
  23. package/dist/cliRenderer.js +15 -0
  24. package/dist/core/outputSchema.js +141 -0
  25. package/dist/core/registry.js +1 -126
  26. package/dist/index.js +96 -28
  27. package/dist/pages/openapiFullSpec.js +8 -0
  28. package/dist/pages/openapiSpecActions.js +70 -0
  29. package/dist/pages/openapiSpecPathsCompare.js +59 -0
  30. package/dist/pages/openapiSpecPathsFeedback.js +95 -0
  31. package/dist/pages/openapiSpecPathsOpinet.js +81 -0
  32. package/dist/pages/promptReferenceText.js +18 -1
  33. package/dist/pages/promptRetailCinemaText.js +21 -4
  34. package/dist/services/compare/client.js +133 -0
  35. package/dist/services/compare/index.js +17 -0
  36. package/dist/services/compare/tools/compareProducts.js +45 -0
  37. package/dist/services/compare/types.js +1 -0
  38. package/dist/services/feedback/client.js +109 -0
  39. package/dist/services/feedback/index.js +22 -0
  40. package/dist/services/feedback/tools/submitDeveloperRequest.js +54 -0
  41. package/dist/services/feedback/types.js +3 -0
  42. package/dist/services/gs25/client.js +1 -84
  43. package/dist/services/gs25/productSearch.js +97 -0
  44. package/dist/services/oliveyoung/client.js +1 -55
  45. package/dist/services/oliveyoung/normalize.js +58 -0
  46. package/dist/services/opinet/client.js +231 -0
  47. package/dist/services/opinet/index.js +26 -0
  48. package/dist/services/opinet/location.js +119 -0
  49. package/dist/services/opinet/tools.js +80 -0
  50. package/dist/services/opinet/types.js +1 -0
  51. package/openapi.json +92 -0
  52. package/openapi.yaml +62 -0
  53. package/package.json +18 -17
@@ -3,133 +3,8 @@
3
3
  *
4
4
  * 서비스 프로바이더를 등록하고 MCP 서버에 도구를 연결합니다.
5
5
  */
6
- import * as z from 'zod';
7
6
  import { getErrorMessage, toStandardErrorDiagnostics } from './errors.js';
8
- const DEFAULT_OUTPUT_SCHEMA = z.object({}).loose().describe('도구 실행 결과(JSON 객체)');
9
- function createFallbackOutputSchema(toolName) {
10
- if (toolName.includes('search_products') ||
11
- toolName.includes('searchProducts') ||
12
- toolName.endsWith('_products')) {
13
- return {
14
- keyword: z.string().optional(),
15
- query: z.string().optional(),
16
- page: z.number().optional(),
17
- pageSize: z.number().optional(),
18
- totalCount: z.number().optional(),
19
- count: z.number().optional(),
20
- products: z.array(z.unknown()).optional(),
21
- note: z.string().optional(),
22
- };
23
- }
24
- if (toolName.includes('find_stores') ||
25
- toolName.includes('nearby_stores') ||
26
- toolName.includes('search_stores')) {
27
- return {
28
- keyword: z.string().optional(),
29
- searchParams: z.unknown().optional(),
30
- location: z.unknown().optional(),
31
- totalCount: z.number().optional(),
32
- filteredCount: z.number().optional(),
33
- count: z.number().optional(),
34
- stores: z.array(z.unknown()).optional(),
35
- };
36
- }
37
- if (toolName.includes('places')) {
38
- return {
39
- provider: z.string().optional(),
40
- searchMode: z.string().optional(),
41
- query: z.string().optional(),
42
- category: z.string().optional(),
43
- location: z.string().optional(),
44
- totalCount: z.number().optional(),
45
- count: z.number().optional(),
46
- places: z.array(z.unknown()).optional(),
47
- };
48
- }
49
- if (toolName.includes('inventory')) {
50
- return {
51
- productId: z.string().optional(),
52
- keyword: z.string().optional(),
53
- itemCode: z.string().nullable().optional(),
54
- location: z.unknown().optional(),
55
- product: z.unknown().optional(),
56
- onlineStock: z.number().optional(),
57
- inventory: z.unknown().optional(),
58
- storeInventory: z.unknown().optional(),
59
- };
60
- }
61
- if (toolName.includes('display_location')) {
62
- return {
63
- productId: z.string().optional(),
64
- storeCode: z.string().optional(),
65
- displayLocation: z.unknown().optional(),
66
- locations: z.array(z.unknown()).optional(),
67
- };
68
- }
69
- if (toolName.includes('price_info')) {
70
- return {
71
- productId: z.string().optional(),
72
- productName: z.string().optional(),
73
- product: z.unknown().optional(),
74
- price: z.number().optional(),
75
- };
76
- }
77
- if (toolName.includes('theater')) {
78
- return {
79
- keyword: z.string().optional(),
80
- location: z.unknown().optional(),
81
- count: z.number().optional(),
82
- theaters: z.array(z.unknown()).optional(),
83
- };
84
- }
85
- if (toolName.includes('movie') || toolName.includes('now_showing')) {
86
- return {
87
- keyword: z.string().optional(),
88
- playDate: z.string().optional(),
89
- theaterCode: z.string().optional(),
90
- count: z.number().optional(),
91
- movies: z.array(z.unknown()).optional(),
92
- };
93
- }
94
- if (toolName.includes('timetable') || toolName.includes('remaining_seats')) {
95
- return {
96
- playDate: z.string().optional(),
97
- theaterCode: z.string().optional(),
98
- movieCode: z.string().optional(),
99
- count: z.number().optional(),
100
- showtimes: z.array(z.unknown()).optional(),
101
- seats: z.array(z.unknown()).optional(),
102
- };
103
- }
104
- if (toolName.includes('popwords')) {
105
- return {
106
- label: z.string().optional(),
107
- keywords: z.array(z.string()).optional(),
108
- count: z.number().optional(),
109
- };
110
- }
111
- if (toolName.includes('catalog')) {
112
- return {
113
- pages: z.array(z.unknown()).optional(),
114
- issues: z.array(z.unknown()).optional(),
115
- exhibitions: z.array(z.unknown()).optional(),
116
- count: z.number().optional(),
117
- };
118
- }
119
- return {};
120
- }
121
- function createToolOutputSchema(toolName, outputSchema) {
122
- const schema = outputSchema || createFallbackOutputSchema(toolName);
123
- if (Object.keys(schema).length === 0) {
124
- return DEFAULT_OUTPUT_SCHEMA;
125
- }
126
- return z
127
- .object({
128
- ...schema,
129
- standard: z.unknown().optional().describe('서비스 공통 상품/매장/영화관 정규화 결과'),
130
- })
131
- .loose();
132
- }
7
+ import { createToolOutputSchema } from './outputSchema.js';
133
8
  function parseJsonText(text) {
134
9
  try {
135
10
  const parsed = JSON.parse(text);
package/dist/index.js CHANGED
@@ -20,6 +20,9 @@ import { createEmart24Service } from './services/emart24/index.js';
20
20
  import { createGs25Service } from './services/gs25/index.js';
21
21
  import { createSevenElevenService } from './services/seveneleven/index.js';
22
22
  import { createPlacesService } from './services/places/index.js';
23
+ import { createOpinetService } from './services/opinet/index.js';
24
+ import { createCompareService } from './services/compare/index.js';
25
+ import { createFeedbackService } from './services/feedback/index.js';
23
26
  import { createPromptResponse } from './pages/prompt.js';
24
27
  import { createFullOpenApiJsonResponse, createFullOpenApiYamlResponse, createOpenApiJsonResponse, createOpenApiYamlResponse, } from './pages/openapi.js';
25
28
  import { createPrivacyResponse } from './pages/privacy.js';
@@ -35,6 +38,9 @@ import { registerEmart24Routes } from './api/routes/emart24Routes.js';
35
38
  import { registerGs25Routes } from './api/routes/gs25Routes.js';
36
39
  import { registerSevenElevenRoutes } from './api/routes/sevenelevenRoutes.js';
37
40
  import { registerPlacesRoutes } from './api/routes/placesRoutes.js';
41
+ import { registerOpinetRoutes } from './api/routes/opinetRoutes.js';
42
+ import { registerCompareRoutes } from './api/routes/compareRoutes.js';
43
+ import { registerFeedbackRoutes } from './api/routes/feedbackRoutes.js';
38
44
  import { registerHealthRoutes } from './api/routes/healthRoutes.js';
39
45
  import { buildConfigStatus } from './api/configStatus.js';
40
46
  // 서버 메타데이터
@@ -55,10 +61,19 @@ const createRegistry = (bindings) => {
55
61
  zyteApiKey: bindings?.ZYTE_API_KEY,
56
62
  }),
57
63
  createSevenElevenService,
64
+ createCompareService,
65
+ () => createFeedbackService({
66
+ supabaseUrl: bindings?.SUPABASE_URL,
67
+ supabaseServiceRoleKey: bindings?.SUPABASE_SERVICE_ROLE_KEY,
68
+ }),
58
69
  () => createPlacesService({
59
70
  naverClientId: bindings?.NAVER_CLIENT_ID,
60
71
  naverClientSecret: bindings?.NAVER_CLIENT_SECRET,
61
72
  }),
73
+ () => createOpinetService({
74
+ apiKey: bindings?.OPINET_API_KEY,
75
+ googleMapsApiKey: bindings?.GOOGLE_MAPS_API_KEY,
76
+ }),
62
77
  createCuService,
63
78
  createEmart24Service,
64
79
  () => createLotteMartService({
@@ -91,6 +106,53 @@ const createMcpServer = (bindings) => {
91
106
  registry.applyToServer(server);
92
107
  return server;
93
108
  };
109
+ function buildRootInfo() {
110
+ const registry = createRegistry();
111
+ const services = registry.getServicesInfo();
112
+ const allTools = registry.getAllToolNames();
113
+ return {
114
+ name: SERVER_NAME,
115
+ version: SERVER_VERSION,
116
+ description: 'Multi-Service MCP Server for Cloudflare Workers',
117
+ endpoints: {
118
+ mcp: '/ 또는 /mcp (POST) - MCP 프로토콜 엔드포인트',
119
+ health: '/health (GET) - 헬스 체크',
120
+ healthChecks: '/api/health/checks (GET) - 서비스별 상세 헬스 체크',
121
+ openapi: '/openapi.json (GET) - OpenAI Actions용 축약 OpenAPI',
122
+ openapiFull: '/openapi-full.json (GET) - 전체 OpenAPI',
123
+ actionsQuery: '/api/actions/query (GET) - 기존 GET API 통합 facade',
124
+ compareProducts: '/api/compare/products (GET) - 키 없는 통합 상품 가격 후보 비교',
125
+ opinetAverage: '/api/opinet/average (GET) - 오피넷 전국 평균 유가',
126
+ opinetLowest: '/api/opinet/lowest (GET) - 오피넷 최저가 주유소',
127
+ developerRequests: '/api/feedback/requests (POST/GET) - 개발자 요청 제출',
128
+ },
129
+ services,
130
+ tools: allTools,
131
+ totalServices: services.length,
132
+ totalTools: allTools.length,
133
+ };
134
+ }
135
+ const ROOT_INFO_JSON = JSON.stringify(buildRootInfo());
136
+ const ROOT_INFO_BODY = new TextEncoder().encode(ROOT_INFO_JSON);
137
+ const ROOT_INFO_HEADERS = {
138
+ 'Content-Type': 'application/json; charset=utf-8',
139
+ 'Content-Length': String(ROOT_INFO_BODY.byteLength),
140
+ 'Access-Control-Allow-Origin': '*',
141
+ 'Cache-Control': 'public, max-age=300, s-maxage=3600, stale-while-revalidate=86400',
142
+ };
143
+ function isRootInfoRequest(request) {
144
+ if (request.method !== 'GET' && request.method !== 'HEAD') {
145
+ return false;
146
+ }
147
+ const url = new URL(request.url);
148
+ return url.pathname === '/' && url.search === '';
149
+ }
150
+ function createRootInfoResponse(method) {
151
+ return new Response(method === 'HEAD' ? null : ROOT_INFO_BODY, {
152
+ status: 200,
153
+ headers: ROOT_INFO_HEADERS,
154
+ });
155
+ }
94
156
  /**
95
157
  * initialize 요청 여부를 확인합니다.
96
158
  */
@@ -163,6 +225,24 @@ const handleMcpRequest = async (c) => {
163
225
  message: `세션이 없습니다. 먼저 POST /mcp initialize 요청 후 ${SESSION_HEADER} 헤더를 사용해주세요.`,
164
226
  }, 400);
165
227
  };
228
+ const handleRootMcpRequest = async (c) => {
229
+ if (c.req.header(SESSION_HEADER) || c.req.method === 'DELETE') {
230
+ return handleMcpRequest(c);
231
+ }
232
+ const parsedBody = await c.req.raw
233
+ .clone()
234
+ .json()
235
+ .catch(() => undefined);
236
+ if (isInitializeRequest(parsedBody)) {
237
+ const { server, transport } = createSessionTransport(c.env);
238
+ await server.connect(transport);
239
+ return transport.handleRequest(c.req.raw, { parsedBody });
240
+ }
241
+ const transport = new WebStandardStreamableHTTPServerTransport();
242
+ const server = createMcpServer(c.env);
243
+ await server.connect(transport);
244
+ return transport.handleRequest(c.req.raw, { parsedBody });
245
+ };
166
246
  // Hono 앱 생성
167
247
  const app = new Hono();
168
248
  // CORS 설정
@@ -181,34 +261,10 @@ app.use('*', cors({
181
261
  }));
182
262
  // 기본 정보 엔드포인트 (GET 요청만)
183
263
  app.get('/', (c) => {
184
- const registry = createRegistry(c.env);
185
- const services = registry.getServicesInfo();
186
- const allTools = registry.getAllToolNames();
187
- return c.json({
188
- name: SERVER_NAME,
189
- version: SERVER_VERSION,
190
- description: 'Multi-Service MCP Server for Cloudflare Workers',
191
- endpoints: {
192
- mcp: '/ 또는 /mcp (POST) - MCP 프로토콜 엔드포인트',
193
- health: '/health (GET) - 헬스 체크',
194
- healthChecks: '/api/health/checks (GET) - 서비스별 상세 헬스 체크',
195
- openapi: '/openapi.json (GET) - OpenAI Actions용 축약 OpenAPI',
196
- openapiFull: '/openapi-full.json (GET) - 전체 OpenAPI',
197
- actionsQuery: '/api/actions/query (GET) - 기존 GET API 통합 facade',
198
- },
199
- services,
200
- tools: allTools,
201
- totalServices: services.length,
202
- totalTools: allTools.length,
203
- });
204
- });
205
- // 루트 경로에서 MCP 요청 처리 (POST, DELETE, OPTIONS)
206
- app.on(['POST', 'DELETE', 'OPTIONS'], '/', async (c) => {
207
- const transport = new WebStandardStreamableHTTPServerTransport();
208
- const server = createMcpServer(c.env);
209
- await server.connect(transport);
210
- return transport.handleRequest(c.req.raw);
264
+ return c.body(ROOT_INFO_JSON, 200, ROOT_INFO_HEADERS);
211
265
  });
266
+ // 루트 경로에서 MCP 요청 처리 (POST, DELETE)
267
+ app.on(['POST', 'DELETE'], '/', handleRootMcpRequest);
212
268
  // 헬스 체크 엔드포인트
213
269
  app.get('/health', (c) => c.json({ status: 'ok', config: buildConfigStatus(c.env) }));
214
270
  registerHealthRoutes(app);
@@ -243,7 +299,10 @@ app.get('/privacy', (c) => {
243
299
  registerDaisoRoutes(app);
244
300
  registerGs25Routes(app);
245
301
  registerSevenElevenRoutes(app);
302
+ registerCompareRoutes(app);
303
+ registerFeedbackRoutes(app);
246
304
  registerPlacesRoutes(app);
305
+ registerOpinetRoutes(app);
247
306
  registerCuRoutes(app);
248
307
  registerEmart24Routes(app);
249
308
  registerLotteMartRoutes(app);
@@ -274,4 +333,13 @@ app.get('/api/actions/query', async (c) => {
274
333
  });
275
334
  // MCP 엔드포인트
276
335
  app.all('/mcp', handleMcpRequest);
277
- export default app;
336
+ const worker = {
337
+ fetch(request, env, executionCtx) {
338
+ if (isRootInfoRequest(request)) {
339
+ return createRootInfoResponse(request.method);
340
+ }
341
+ return app.fetch(request, env, executionCtx);
342
+ },
343
+ request: app.request.bind(app),
344
+ };
345
+ export default worker;
@@ -11,6 +11,9 @@ import { OPENAPI_PATHS_LOTTECINEMA } from './openapiSpecPathsLottecinema.js';
11
11
  import { OPENAPI_PATHS_GS25 } from './openapiSpecPathsGs25.js';
12
12
  import { OPENAPI_PATHS_SEVENELEVEN } from './openapiSpecPathsSeveneleven.js';
13
13
  import { OPENAPI_PATHS_PLACES } from './openapiSpecPathsPlaces.js';
14
+ import { OPENAPI_PATHS_OPINET } from './openapiSpecPathsOpinet.js';
15
+ import { OPENAPI_PATHS_COMPARE } from './openapiSpecPathsCompare.js';
16
+ import { OPENAPI_PATHS_FEEDBACK } from './openapiSpecPathsFeedback.js';
14
17
  import { OPENAPI_COMPONENTS } from './openapiSpecComponents.js';
15
18
  export function generateFullOpenApiSpec(baseUrl) {
16
19
  return {
@@ -26,11 +29,13 @@ export function generateFullOpenApiSpec(baseUrl) {
26
29
  - 🧭 **진열 위치 조회**: 매장 내 상품 구역/층 정보 조회
27
30
  - 🏪 **GS25 매장/상품/재고 조회**: GS25 매장 탐색, 상품 검색, 재고 조회
28
31
  - 7️⃣ **세븐일레븐 상품/매장/재고 조회**: 상품 검색, 매장 검색, 실재고 조회
32
+ - 🧾 **통합 가격 후보 비교**: 새 외부 키 없이 상품 검색 결과를 묶어 가격 비교
29
33
  - 🏪 **CU 매장/재고 조회**: CU 매장 탐색 및 상품 재고 검색
30
34
  - 🎬 **메가박스 지점/영화 조회**: 주변 지점, 상영 목록, 잔여 좌석 조회
31
35
  - 🎞️ **롯데시네마 지점/영화 조회**: 주변 지점, 상영 목록, 잔여 좌석 조회
32
36
  - 🎥 **CGV 지점/영화 조회**: 극장 목록, 영화 목록, 시간표 조회
33
37
  - 🍽️ **주변 음식점/카페 검색**: 네이버 지역 검색 기반 장소 조회
38
+ - ⛽ **오피넷 유가 정보**: 전국 평균 유가, 최저가 주유소, 반경 내 주유소 조회
34
39
 
35
40
  ## 사용 팁
36
41
  1. 한글 검색어는 URL 인코딩이 자동 처리됩니다
@@ -51,7 +56,10 @@ export function generateFullOpenApiSpec(baseUrl) {
51
56
  ...OPENAPI_PATHS_LOTTEMART,
52
57
  ...OPENAPI_PATHS_GS25,
53
58
  ...OPENAPI_PATHS_SEVENELEVEN,
59
+ ...OPENAPI_PATHS_COMPARE,
60
+ ...OPENAPI_PATHS_FEEDBACK,
54
61
  ...OPENAPI_PATHS_PLACES,
62
+ ...OPENAPI_PATHS_OPINET,
55
63
  ...OPENAPI_PATHS_MEGABOX,
56
64
  ...OPENAPI_PATHS_LOTTECINEMA,
57
65
  ...OPENAPI_PATHS_CGV,
@@ -63,6 +63,76 @@ const ACTION_QUERY_PARAMETERS = [
63
63
  description: '주변 장소 카테고리(restaurant, cafe, food, dessert, all)',
64
64
  schema: { type: 'string' },
65
65
  },
66
+ {
67
+ name: 'services',
68
+ in: 'query',
69
+ required: false,
70
+ description: '통합 비교 서비스 목록(daiso,gs25,seveneleven,emart24)',
71
+ schema: { type: 'string' },
72
+ },
73
+ {
74
+ name: 'type',
75
+ in: 'query',
76
+ required: false,
77
+ description: '개발자 요청 유형(bug, improvement, feature, docs, other)',
78
+ schema: { type: 'string' },
79
+ },
80
+ {
81
+ name: 'severity',
82
+ in: 'query',
83
+ required: false,
84
+ description: '개발자 요청 영향도(low, medium, high, critical)',
85
+ schema: { type: 'string' },
86
+ },
87
+ {
88
+ name: 'title',
89
+ in: 'query',
90
+ required: false,
91
+ description: '개발자 요청 제목',
92
+ schema: { type: 'string' },
93
+ },
94
+ {
95
+ name: 'description',
96
+ in: 'query',
97
+ required: false,
98
+ description: '개발자 요청 상세 설명',
99
+ schema: { type: 'string' },
100
+ },
101
+ {
102
+ name: 'toolName',
103
+ in: 'query',
104
+ required: false,
105
+ description: '문제가 발생했거나 개선이 필요한 MCP 도구 이름',
106
+ schema: { type: 'string' },
107
+ },
108
+ {
109
+ name: 'reproduction',
110
+ in: 'query',
111
+ required: false,
112
+ description: '오류 재현 방법',
113
+ schema: { type: 'string' },
114
+ },
115
+ {
116
+ name: 'expected',
117
+ in: 'query',
118
+ required: false,
119
+ description: '기대 결과',
120
+ schema: { type: 'string' },
121
+ },
122
+ {
123
+ name: 'actual',
124
+ in: 'query',
125
+ required: false,
126
+ description: '실제 결과',
127
+ schema: { type: 'string' },
128
+ },
129
+ {
130
+ name: 'source',
131
+ in: 'query',
132
+ required: false,
133
+ description: '요청 출처(mcp, chatgpt, claude, cli, api, other)',
134
+ schema: { type: 'string' },
135
+ },
66
136
  {
67
137
  name: 'sido',
68
138
  in: 'query',
@@ -0,0 +1,59 @@
1
+ export const OPENAPI_PATHS_COMPARE = {
2
+ '/api/compare/products': {
3
+ get: {
4
+ operationId: 'compareProducts',
5
+ summary: '키 없는 통합 상품 가격 후보 비교',
6
+ description: '새 외부 키 없이 다이소, GS25, 세븐일레븐, 이마트24 상품 검색을 묶어 가격 후보를 비교합니다.',
7
+ parameters: [
8
+ {
9
+ name: 'keyword',
10
+ in: 'query',
11
+ required: true,
12
+ description: '비교할 상품 검색어',
13
+ schema: { type: 'string' },
14
+ },
15
+ {
16
+ name: 'services',
17
+ in: 'query',
18
+ required: false,
19
+ description: '쉼표로 구분한 서비스 목록(daiso,gs25,seveneleven,emart24)',
20
+ schema: { type: 'string' },
21
+ },
22
+ {
23
+ name: 'limit',
24
+ in: 'query',
25
+ required: false,
26
+ description: '서비스별 최대 결과 수',
27
+ schema: { type: 'integer', default: 5, minimum: 1, maximum: 20 },
28
+ },
29
+ ],
30
+ responses: {
31
+ '200': {
32
+ description: '통합 상품 가격 후보 비교 결과',
33
+ content: {
34
+ 'application/json': {
35
+ schema: {
36
+ type: 'object',
37
+ properties: {
38
+ success: { type: 'boolean' },
39
+ data: {
40
+ type: 'object',
41
+ properties: {
42
+ keyword: { type: 'string' },
43
+ serviceCount: { type: 'integer' },
44
+ resultCount: { type: 'integer' },
45
+ bestPrice: { type: ['object', 'null'] },
46
+ results: { type: 'array', items: { type: 'object' } },
47
+ errors: { type: 'array', items: { type: 'object' } },
48
+ note: { type: 'string' },
49
+ },
50
+ },
51
+ },
52
+ },
53
+ },
54
+ },
55
+ },
56
+ },
57
+ },
58
+ },
59
+ };
@@ -0,0 +1,95 @@
1
+ const REQUEST_PROPERTIES = {
2
+ type: {
3
+ type: 'string',
4
+ enum: ['bug', 'improvement', 'feature', 'docs', 'other'],
5
+ default: 'other',
6
+ },
7
+ severity: {
8
+ type: 'string',
9
+ enum: ['low', 'medium', 'high', 'critical'],
10
+ default: 'medium',
11
+ },
12
+ title: { type: 'string' },
13
+ description: { type: 'string' },
14
+ service: { type: 'string' },
15
+ toolName: { type: 'string' },
16
+ reproduction: { type: 'string' },
17
+ expected: { type: 'string' },
18
+ actual: { type: 'string' },
19
+ source: {
20
+ type: 'string',
21
+ enum: ['mcp', 'chatgpt', 'claude', 'cli', 'api', 'other'],
22
+ },
23
+ };
24
+ const RESPONSE_SCHEMA = {
25
+ type: 'object',
26
+ properties: {
27
+ success: { type: 'boolean' },
28
+ data: {
29
+ type: 'object',
30
+ properties: {
31
+ id: { type: 'string' },
32
+ type: { type: 'string' },
33
+ severity: { type: 'string' },
34
+ title: { type: 'string' },
35
+ service: { type: 'string' },
36
+ toolName: { type: 'string' },
37
+ source: { type: 'string' },
38
+ status: { type: 'string' },
39
+ createdAt: { type: 'string' },
40
+ },
41
+ },
42
+ },
43
+ };
44
+ export const OPENAPI_PATHS_FEEDBACK = {
45
+ '/api/feedback/requests': {
46
+ post: {
47
+ operationId: 'submitDeveloperRequest',
48
+ summary: '개발자 요청 제출',
49
+ description: 'MCP 기능 오류, 개선 요청, 신규 기능 요청, 문서 문제를 개발자에게 전달하고 Supabase에 저장합니다.',
50
+ requestBody: {
51
+ required: true,
52
+ content: {
53
+ 'application/json': {
54
+ schema: {
55
+ type: 'object',
56
+ required: ['title', 'description'],
57
+ properties: REQUEST_PROPERTIES,
58
+ },
59
+ },
60
+ },
61
+ },
62
+ responses: {
63
+ '200': {
64
+ description: '개발자 요청 저장 결과',
65
+ content: {
66
+ 'application/json': {
67
+ schema: RESPONSE_SCHEMA,
68
+ },
69
+ },
70
+ },
71
+ },
72
+ },
73
+ get: {
74
+ operationId: 'submitDeveloperRequestViaQuery',
75
+ summary: '개발자 요청 제출(GET)',
76
+ description: 'OpenAI Actions facade 등 GET만 가능한 환경에서 개발자 요청을 제출합니다.',
77
+ parameters: Object.entries(REQUEST_PROPERTIES).map(([name, schema]) => ({
78
+ name,
79
+ in: 'query',
80
+ required: name === 'title' || name === 'description',
81
+ schema,
82
+ })),
83
+ responses: {
84
+ '200': {
85
+ description: '개발자 요청 저장 결과',
86
+ content: {
87
+ 'application/json': {
88
+ schema: RESPONSE_SCHEMA,
89
+ },
90
+ },
91
+ },
92
+ },
93
+ },
94
+ },
95
+ };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * OpenAPI 경로 정의 (오피넷)
3
+ */
4
+ const fuelCodeSchema = {
5
+ type: 'string',
6
+ enum: ['B027', 'D047', 'B034', 'C004', 'K015'],
7
+ default: 'B027',
8
+ };
9
+ const successResponse = {
10
+ description: '조회 성공',
11
+ content: {
12
+ 'application/json': {
13
+ schema: {
14
+ type: 'object',
15
+ properties: {
16
+ success: { type: 'boolean' },
17
+ data: { type: 'object' },
18
+ },
19
+ },
20
+ },
21
+ },
22
+ };
23
+ export const OPENAPI_PATHS_OPINET = {
24
+ '/api/opinet/average': {
25
+ get: {
26
+ operationId: 'opinetGetAveragePrices',
27
+ summary: '오피넷 전국 평균 유가',
28
+ description: '한국석유공사 오피넷 전국 평균 유가를 조회합니다.',
29
+ parameters: [],
30
+ responses: {
31
+ '200': successResponse,
32
+ '500': {
33
+ description: '오피넷 키 누락 또는 API 호출 실패',
34
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } },
35
+ },
36
+ },
37
+ },
38
+ },
39
+ '/api/opinet/lowest': {
40
+ get: {
41
+ operationId: 'opinetGetLowestPriceStations',
42
+ summary: '오피넷 최저가 주유소',
43
+ description: '전국 또는 지역별 최저가 주유소를 유종별로 조회합니다.',
44
+ parameters: [
45
+ { name: 'fuelCode', in: 'query', required: false, description: '유종 코드', schema: fuelCodeSchema },
46
+ { name: 'areaCode', in: 'query', required: false, description: '오피넷 지역 코드', schema: { type: 'string' } },
47
+ { name: 'count', in: 'query', required: false, description: '결과 수(1~20)', schema: { type: 'integer', minimum: 1, maximum: 20, default: 10 } },
48
+ ],
49
+ responses: { '200': successResponse },
50
+ },
51
+ },
52
+ '/api/opinet/stations/around': {
53
+ get: {
54
+ operationId: 'opinetSearchStationsAround',
55
+ summary: '오피넷 반경 내 주유소',
56
+ description: 'KATEC x/y, WGS84 위경도, 또는 location 키워드 기준 반경 내 주유소를 조회합니다.',
57
+ parameters: [
58
+ { name: 'x', in: 'query', required: false, description: '기준 위치 X좌표(KATEC)', schema: { type: 'number' } },
59
+ { name: 'y', in: 'query', required: false, description: '기준 위치 Y좌표(KATEC)', schema: { type: 'number' } },
60
+ { name: 'lat', in: 'query', required: false, description: '위도(WGS84)', schema: { type: 'number' } },
61
+ { name: 'lng', in: 'query', required: false, description: '경도(WGS84)', schema: { type: 'number' } },
62
+ { name: 'location', in: 'query', required: false, description: '장소/주소 키워드. 예: 강남역', schema: { type: 'string' } },
63
+ { name: 'radiusMeters', in: 'query', required: false, description: '검색 반경 m(100~5000)', schema: { type: 'integer', minimum: 100, maximum: 5000, default: 3000 } },
64
+ { name: 'fuelCode', in: 'query', required: false, description: '유종 코드', schema: fuelCodeSchema },
65
+ { name: 'sort', in: 'query', required: false, description: '정렬 기준', schema: { type: 'string', enum: ['price', 'distance'], default: 'price' } },
66
+ ],
67
+ responses: { '200': successResponse },
68
+ },
69
+ },
70
+ '/api/opinet/station': {
71
+ get: {
72
+ operationId: 'opinetGetStationDetail',
73
+ summary: '오피넷 주유소 상세정보',
74
+ description: '오피넷 주유소 ID로 상세 정보, 부가시설, 유종별 가격을 조회합니다.',
75
+ parameters: [
76
+ { name: 'id', in: 'query', required: true, description: '오피넷 주유소 ID', schema: { type: 'string' } },
77
+ ],
78
+ responses: { '200': successResponse },
79
+ },
80
+ },
81
+ };