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.
- package/README.md +106 -7
- package/dist/api/actionsProxy.js +10 -0
- package/dist/api/compareHandlers.js +26 -0
- package/dist/api/configStatus.js +8 -0
- package/dist/api/feedbackHandlers.js +60 -0
- package/dist/api/healthCheckDefinitions.js +170 -0
- package/dist/api/healthCheckShape.js +106 -0
- package/dist/api/healthCheckTypes.js +4 -0
- package/dist/api/healthChecks.js +3 -271
- package/dist/api/opinetHandlers.js +91 -0
- package/dist/api/routes/compareRoutes.js +9 -0
- package/dist/api/routes/feedbackRoutes.js +5 -0
- package/dist/api/routes/healthRoutes.js +17 -1
- package/dist/api/routes/opinetRoutes.js +24 -0
- package/dist/bin.js +0 -0
- package/dist/cli/commandOptions.js +24 -2
- package/dist/cli/commands/apiCommand.js +24 -0
- package/dist/cli/commands/cinema.js +65 -0
- package/dist/cli/commands/convenience.js +75 -100
- package/dist/cli/http.js +1 -1
- package/dist/cli.js +12 -1
- package/dist/cliHelp.js +55 -8
- package/dist/cliRenderer.js +15 -0
- package/dist/core/outputSchema.js +141 -0
- package/dist/core/registry.js +1 -126
- package/dist/index.js +96 -28
- package/dist/pages/openapiFullSpec.js +8 -0
- package/dist/pages/openapiSpecActions.js +70 -0
- package/dist/pages/openapiSpecPathsCompare.js +59 -0
- package/dist/pages/openapiSpecPathsFeedback.js +95 -0
- package/dist/pages/openapiSpecPathsOpinet.js +81 -0
- package/dist/pages/promptReferenceText.js +18 -1
- package/dist/pages/promptRetailCinemaText.js +21 -4
- package/dist/services/compare/client.js +133 -0
- package/dist/services/compare/index.js +17 -0
- package/dist/services/compare/tools/compareProducts.js +45 -0
- package/dist/services/compare/types.js +1 -0
- package/dist/services/feedback/client.js +109 -0
- package/dist/services/feedback/index.js +22 -0
- package/dist/services/feedback/tools/submitDeveloperRequest.js +54 -0
- package/dist/services/feedback/types.js +3 -0
- package/dist/services/gs25/client.js +1 -84
- package/dist/services/gs25/productSearch.js +97 -0
- package/dist/services/oliveyoung/client.js +1 -55
- package/dist/services/oliveyoung/normalize.js +58 -0
- package/dist/services/opinet/client.js +231 -0
- package/dist/services/opinet/index.js +26 -0
- package/dist/services/opinet/location.js +119 -0
- package/dist/services/opinet/tools.js +80 -0
- package/dist/services/opinet/types.js +1 -0
- package/openapi.json +92 -0
- package/openapi.yaml +62 -0
- package/package.json +18 -17
package/dist/core/registry.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|