daiso 1.0.7 → 1.0.8
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 +27 -4
- package/dist/api/actionsProxy.js +5 -0
- package/dist/api/compareHandlers.js +26 -0
- package/dist/api/routes/compareRoutes.js +9 -0
- package/dist/cli/commandOptions.js +1 -0
- package/dist/cli/commands/convenience.js +18 -0
- package/dist/cli.js +3 -1
- package/dist/cliHelp.js +11 -0
- package/dist/cliRenderer.js +15 -0
- package/dist/core/registry.js +12 -0
- package/dist/index.js +5 -0
- package/dist/pages/openapiFullSpec.js +3 -0
- package/dist/pages/openapiSpecActions.js +7 -0
- package/dist/pages/openapiSpecPathsCompare.js +59 -0
- package/dist/pages/promptReferenceText.js +3 -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/openapi.json +10 -0
- package/openapi.yaml +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
# Daiso MCP 및 Skill
|
|
9
9
|
|
|
10
|
-
다이소(제품/매장/재고), 주변 음식점/카페, 롯데마트(매장/상품), GS25(매장/상품/재고), 세븐일레븐(상품/매장/재고/인기검색어/카탈로그), CU(매장/재고), 이마트24(매장/상품/재고), 올리브영(매장/재고), 메가박스(지점/영화/시간표/좌석), 롯데시네마(지점/영화/좌석), CGV(극장/영화/시간표) 조회 기능을 MCP, CLI, Codex Skill로 AI에 연결합니다.
|
|
10
|
+
다이소(제품/매장/재고), 키 없는 통합 가격 후보 비교, 주변 음식점/카페, 롯데마트(매장/상품), GS25(매장/상품/재고), 세븐일레븐(상품/매장/재고/인기검색어/카탈로그), CU(매장/재고), 이마트24(매장/상품/재고), 올리브영(매장/재고), 메가박스(지점/영화/시간표/좌석), 롯데시네마(지점/영화/좌석), CGV(극장/영화/시간표) 조회 기능을 MCP, CLI, Codex Skill로 AI에 연결합니다.
|
|
11
11
|
|
|
12
12
|
<br>
|
|
13
13
|
|
|
@@ -35,6 +35,11 @@
|
|
|
35
35
|
<td>네이버 지역 검색</td>
|
|
36
36
|
<td>음식점, 카페, 디저트, 주변 장소</td>
|
|
37
37
|
</tr>
|
|
38
|
+
<tr>
|
|
39
|
+
<td>비교</td>
|
|
40
|
+
<td>다이소, GS25, 세븐일레븐, 이마트24</td>
|
|
41
|
+
<td>새 외부 키 없는 상품 가격 후보 비교</td>
|
|
42
|
+
</tr>
|
|
38
43
|
<tr>
|
|
39
44
|
<td>리테일</td>
|
|
40
45
|
<td>다이소, 올리브영, 롯데마트</td>
|
|
@@ -65,11 +70,11 @@
|
|
|
65
70
|
**[실시간 서비스 상태 보기](https://aka-page.betteruptime.com/)**
|
|
66
71
|
|
|
67
72
|
<!-- WORKERS_INVOCATIONS_CHART:START -->
|
|
68
|
-
<h3>Cloudflare Workers 호출량 (2026-03-01 ~ 2026-05-
|
|
73
|
+
<h3>Cloudflare Workers 호출량 (2026-03-01 ~ 2026-05-23, 84일)</h3>
|
|
69
74
|
|
|
70
|
-
<img src="./assets/analytics/workers-invocations.png?v=2026-05-
|
|
75
|
+
<img src="./assets/analytics/workers-invocations.png?v=2026-05-23T15:44:29.609Z" alt="Cloudflare Workers 호출량 그래프 (2026-03-01 ~ 2026-05-23)" width="100%">
|
|
71
76
|
|
|
72
|
-
<sub>기준 워커: <code>daiso-mcp</code> · 마지막 갱신: 2026-05-
|
|
77
|
+
<sub>기준 워커: <code>daiso-mcp</code> · 마지막 갱신: 2026-05-24 00:44 KST</sub>
|
|
73
78
|
|
|
74
79
|
<!-- WORKERS_INVOCATIONS_CHART:END -->
|
|
75
80
|
|
|
@@ -105,6 +110,7 @@ ChatGPT, Claude, Grok 같은 AI 앱에서 바로 연결해 사용할 수 있습
|
|
|
105
110
|
|
|
106
111
|
```
|
|
107
112
|
다이소 mcp로 수납박스 검색해줘
|
|
113
|
+
콜라 어디가 싼지 비교해줘
|
|
108
114
|
올리브영 mcp로 명동 근처 매장 찾아줘
|
|
109
115
|
이마트24 mcp로 강남 근처 매장과 두바이 재고 알려줘
|
|
110
116
|
롯데마트 mcp로 잠실 근처 매장 찾아줘
|
|
@@ -254,6 +260,7 @@ npx daiso product 1034604
|
|
|
254
260
|
npx daiso stores 강남역
|
|
255
261
|
npx daiso inventory 1034604 --keyword 강남역
|
|
256
262
|
npx daiso display-location 1034604 04515
|
|
263
|
+
npx daiso compare 콜라 --limit 3
|
|
257
264
|
npx daiso places 강남역 --category cafe --limit 5
|
|
258
265
|
npx daiso places 성수동 --keyword 브런치 --limit 5
|
|
259
266
|
npx daiso cu-stores 강남
|
|
@@ -304,6 +311,22 @@ MCP 도구 응답은 각 서비스의 원본 필드를 유지하면서, 클라
|
|
|
304
311
|
- `standard.theaters`: 영화관 목록을 `code`, `name`, `address`, `distanceMeters`, `raw` 형태로 정규화합니다.
|
|
305
312
|
- `standard.places`: 음식점/카페 같은 장소 목록을 `name`, `address`, `raw` 중심으로 정규화합니다.
|
|
306
313
|
|
|
314
|
+
### 키 없는 통합 가격 후보 비교
|
|
315
|
+
|
|
316
|
+
새 외부 API 키 없이 기존 상품 검색을 묶어 가격 후보를 비교합니다.
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
npx daiso compare 콜라 --limit 3 --json
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
REST:
|
|
323
|
+
|
|
324
|
+
```text
|
|
325
|
+
GET /api/compare/products?keyword=콜라&limit=3
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
지원 서비스는 `daiso`, `gs25`, `seveneleven`, `emart24`입니다. GS25처럼 상품 검색 응답에 가격이 없는 서비스는 후보에는 포함되지만 최저가 계산에서는 제외됩니다.
|
|
329
|
+
|
|
307
330
|
### 운영 헬스 체크
|
|
308
331
|
|
|
309
332
|
공개 상태 페이지: **[Daiso MCP Status](https://aka-page.betteruptime.com/)**
|
package/dist/api/actionsProxy.js
CHANGED
|
@@ -86,6 +86,11 @@ export const ACTION_QUERY_DEFINITIONS = [
|
|
|
86
86
|
summary: '세븐일레븐 카탈로그 조회',
|
|
87
87
|
targetPath: '/api/seveneleven/catalog',
|
|
88
88
|
},
|
|
89
|
+
{
|
|
90
|
+
action: 'compareProducts',
|
|
91
|
+
summary: '키 없는 통합 상품 가격 후보 비교',
|
|
92
|
+
targetPath: '/api/compare/products',
|
|
93
|
+
},
|
|
89
94
|
{
|
|
90
95
|
action: 'placesSearchNearby',
|
|
91
96
|
summary: '음식점/카페 등 주변 장소 검색',
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { compareProducts, parseCompareServices } from '../services/compare/client.js';
|
|
2
|
+
import { errorResponse, successResponse } from './response.js';
|
|
3
|
+
export async function handleCompareProducts(c) {
|
|
4
|
+
const keyword = c.req.query('keyword') || c.req.query('q') || '';
|
|
5
|
+
const limit = parseInt(c.req.query('limit') || '5', 10);
|
|
6
|
+
const services = parseCompareServices(c.req.query('services'));
|
|
7
|
+
if (keyword.trim().length === 0) {
|
|
8
|
+
return errorResponse(c, 'MISSING_KEYWORD', '검색어(keyword)를 입력해주세요.');
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const result = await compareProducts({
|
|
12
|
+
keyword,
|
|
13
|
+
limit,
|
|
14
|
+
services,
|
|
15
|
+
});
|
|
16
|
+
return successResponse(c, result, {
|
|
17
|
+
total: result.resultCount,
|
|
18
|
+
page: 1,
|
|
19
|
+
pageSize: limit,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
|
|
24
|
+
return errorResponse(c, 'COMPARE_PRODUCTS_FAILED', message, 500);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { withEdgeCache } from '../../utils/cache.js';
|
|
2
|
+
import { handleCompareProducts } from '../compareHandlers.js';
|
|
3
|
+
export function registerCompareRoutes(app) {
|
|
4
|
+
app.get('/api/compare/products', async (c) => withEdgeCache(c.req.url, {
|
|
5
|
+
ttlSeconds: 60 * 10,
|
|
6
|
+
staleWhileRevalidateSeconds: 60,
|
|
7
|
+
keyPrefix: 'compare-products-v1',
|
|
8
|
+
}, () => handleCompareProducts(c)));
|
|
9
|
+
}
|
|
@@ -12,6 +12,7 @@ export const COMMAND_OPTION_ALLOWLISTS = {
|
|
|
12
12
|
stores: ['help', 'json', 'keyword', 'sido', 'gugun', 'dong', 'limit'],
|
|
13
13
|
inventory: ['help', 'json', 'keyword', 'lat', 'lng', 'page', 'pageSize'],
|
|
14
14
|
'display-location': ['help', 'json'],
|
|
15
|
+
compare: ['help', 'json', 'keyword', 'q', 'services', 'limit'],
|
|
15
16
|
places: ['help', 'json', 'location', 'category', 'keyword', 'limit', 'start', 'sort'],
|
|
16
17
|
'cu-stores': ['help', 'json', 'keyword', 'lat', 'lng', 'limit'],
|
|
17
18
|
'cu-inventory': [
|
|
@@ -6,6 +6,24 @@ import { printCommandHelp } from '../../cliHelp.js';
|
|
|
6
6
|
import { parseCliArgs, toUrl, applyOptionsToQuery, toQueryOptions } from '../args.js';
|
|
7
7
|
import { validateCommandOptions } from '../commandOptions.js';
|
|
8
8
|
import { requestAndPrintResponse } from '../http.js';
|
|
9
|
+
export async function handleCompareProducts(options, deps) {
|
|
10
|
+
const parsed = parseCliArgs(options);
|
|
11
|
+
if (parsed.options.help === 'true') {
|
|
12
|
+
return printCommandHelp('compare', deps.writeOut, deps.writeErr);
|
|
13
|
+
}
|
|
14
|
+
if (validateCommandOptions('compare', parsed.options, deps.writeErr)) {
|
|
15
|
+
return 1;
|
|
16
|
+
}
|
|
17
|
+
const keyword = parsed.positionals[0] || parsed.options.keyword || parsed.options.q;
|
|
18
|
+
if (!keyword) {
|
|
19
|
+
deps.writeErr('compare 명령은 검색어가 필요합니다. 예: daiso compare 콜라');
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
const targetUrl = toUrl('/api/compare/products');
|
|
23
|
+
targetUrl.searchParams.set('keyword', keyword);
|
|
24
|
+
applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
|
|
25
|
+
return await requestAndPrintResponse(deps.fetchImpl, deps.writeOut, deps.writeErr, targetUrl, 'compare', parsed.options.json === 'true');
|
|
26
|
+
}
|
|
9
27
|
export async function handlePlaces(options, deps) {
|
|
10
28
|
const parsed = parseCliArgs(options);
|
|
11
29
|
if (parsed.options.help === 'true') {
|
package/dist/cli.js
CHANGED
|
@@ -12,7 +12,7 @@ import { DEFAULT_BASE_URL, DEFAULT_MCP_URL } from './cli/constants.js';
|
|
|
12
12
|
import { parseCliArgs } from './cli/args.js';
|
|
13
13
|
import { validateCommandOptions } from './cli/commandOptions.js';
|
|
14
14
|
import { handleGet, handleProducts, handleProduct, handleStores, handleInventory, handleDisplayLocation, } from './cli/commands/daiso.js';
|
|
15
|
-
import { handleCuStores, handleCuInventory, handleEmart24Stores, handleEmart24Products, handleEmart24Inventory, handleLotteMartStores, handleLotteMartProducts, handlePlaces, handleGs25Stores, handleGs25Products, handleGs25Inventory, handleSevenElevenProducts, handleSevenElevenStores, handleSevenElevenPopwords, handleSevenElevenCatalog, handleLottecinemaTheaters, handleLottecinemaMovies, handleLottecinemaSeats, } from './cli/commands/convenience.js';
|
|
15
|
+
import { handleCuStores, handleCuInventory, handleEmart24Stores, handleEmart24Products, handleEmart24Inventory, handleLotteMartStores, handleLotteMartProducts, handleCompareProducts, handlePlaces, handleGs25Stores, handleGs25Products, handleGs25Inventory, handleSevenElevenProducts, handleSevenElevenStores, handleSevenElevenPopwords, handleSevenElevenCatalog, handleLottecinemaTheaters, handleLottecinemaMovies, handleLottecinemaSeats, } from './cli/commands/convenience.js';
|
|
16
16
|
function rejectTopLevelUnknownOptions(options, command, deps) {
|
|
17
17
|
const parsed = parseCliArgs(options);
|
|
18
18
|
return validateCommandOptions(command, parsed.options, deps.writeErr);
|
|
@@ -112,6 +112,8 @@ export async function runCli(argv, deps) {
|
|
|
112
112
|
return await handleInventory(options, resolvedDeps);
|
|
113
113
|
if (command === 'display-location')
|
|
114
114
|
return await handleDisplayLocation(options, resolvedDeps);
|
|
115
|
+
if (command === 'compare')
|
|
116
|
+
return await handleCompareProducts(options, resolvedDeps);
|
|
115
117
|
if (command === 'places')
|
|
116
118
|
return await handlePlaces(options, resolvedDeps);
|
|
117
119
|
if (command === 'cu-stores')
|
package/dist/cliHelp.js
CHANGED
|
@@ -13,6 +13,7 @@ export const COMMAND_LIST = [
|
|
|
13
13
|
'stores',
|
|
14
14
|
'inventory',
|
|
15
15
|
'display-location',
|
|
16
|
+
'compare',
|
|
16
17
|
'places',
|
|
17
18
|
'cu-stores',
|
|
18
19
|
'cu-inventory',
|
|
@@ -44,6 +45,7 @@ const COMMAND_SUMMARY = {
|
|
|
44
45
|
stores: '다이소 매장 검색',
|
|
45
46
|
inventory: '다이소 재고 조회',
|
|
46
47
|
'display-location': '다이소 진열 위치 조회',
|
|
48
|
+
compare: '키 없는 통합 상품 가격 후보 비교',
|
|
47
49
|
places: '음식점/카페 등 주변 장소 검색',
|
|
48
50
|
'cu-stores': 'CU 매장 검색',
|
|
49
51
|
'cu-inventory': 'CU 재고 조회',
|
|
@@ -139,6 +141,15 @@ const COMMAND_DETAIL = {
|
|
|
139
141
|
'옵션: --json',
|
|
140
142
|
'예시: daiso display-location 1034604 04515',
|
|
141
143
|
],
|
|
144
|
+
compare: [
|
|
145
|
+
'명령: compare',
|
|
146
|
+
'설명: 새 외부 키 없이 다이소, GS25, 세븐일레븐, 이마트24 상품 검색을 묶어 가격 후보를 비교합니다.',
|
|
147
|
+
'사용법: daiso compare <keyword> [--services daiso,gs25,seveneleven,emart24] [--limit N] [--json]',
|
|
148
|
+
'필수: <keyword>',
|
|
149
|
+
'옵션: --services, --limit, --json',
|
|
150
|
+
'예시: daiso compare 콜라 --limit 3',
|
|
151
|
+
'예시: daiso compare 컵라면 --services seveneleven,emart24 --json',
|
|
152
|
+
],
|
|
142
153
|
places: [
|
|
143
154
|
'명령: places',
|
|
144
155
|
'설명: 네이버 지역 검색으로 음식점, 카페, 디저트 가게 등 주변 장소를 검색합니다.',
|
package/dist/cliRenderer.js
CHANGED
|
@@ -174,6 +174,18 @@ function formatInventory(data) {
|
|
|
174
174
|
}
|
|
175
175
|
return lines;
|
|
176
176
|
}
|
|
177
|
+
function formatCompare(data) {
|
|
178
|
+
const lines = [];
|
|
179
|
+
if (isRecord(data.bestPrice)) {
|
|
180
|
+
lines.push(`최저가 후보: ${toText(data.bestPrice.serviceName)} / ${toText(data.bestPrice.name)} / ${toText(data.bestPrice.price)}원`);
|
|
181
|
+
}
|
|
182
|
+
lines.push(...formatCollection('가격 후보', data.results));
|
|
183
|
+
lines.push(...formatCollection('오류', data.errors));
|
|
184
|
+
if (typeof data.note === 'string') {
|
|
185
|
+
lines.push(`참고: ${data.note}`);
|
|
186
|
+
}
|
|
187
|
+
return lines;
|
|
188
|
+
}
|
|
177
189
|
export function renderApiEnvelope(command, url, payload) {
|
|
178
190
|
if (!isRecord(payload)) {
|
|
179
191
|
return `요청 성공: ${url.pathname}\n${toText(payload)}\n\n원본 JSON은 --json 옵션으로 확인하세요.`;
|
|
@@ -199,6 +211,9 @@ export function renderApiEnvelope(command, url, payload) {
|
|
|
199
211
|
else if (command === 'display-location') {
|
|
200
212
|
lines.push(...formatDisplayLocation(data));
|
|
201
213
|
}
|
|
214
|
+
else if (command === 'compare') {
|
|
215
|
+
lines.push(...formatCompare(data));
|
|
216
|
+
}
|
|
202
217
|
else if (command === 'cu-inventory') {
|
|
203
218
|
lines.push(...formatCollection('매장 목록', isRecord(data.nearbyStores) ? data.nearbyStores.stores : undefined));
|
|
204
219
|
lines.push(...formatCollection('재고 항목', isRecord(data.inventory) ? data.inventory.items : undefined));
|
package/dist/core/registry.js
CHANGED
|
@@ -7,6 +7,18 @@ import * as z from 'zod';
|
|
|
7
7
|
import { getErrorMessage, toStandardErrorDiagnostics } from './errors.js';
|
|
8
8
|
const DEFAULT_OUTPUT_SCHEMA = z.object({}).loose().describe('도구 실행 결과(JSON 객체)');
|
|
9
9
|
function createFallbackOutputSchema(toolName) {
|
|
10
|
+
if (toolName.includes('compare')) {
|
|
11
|
+
return {
|
|
12
|
+
keyword: z.string().optional(),
|
|
13
|
+
services: z.array(z.string()).optional(),
|
|
14
|
+
serviceCount: z.number().optional(),
|
|
15
|
+
resultCount: z.number().optional(),
|
|
16
|
+
bestPrice: z.unknown().optional(),
|
|
17
|
+
results: z.array(z.unknown()).optional(),
|
|
18
|
+
errors: z.array(z.unknown()).optional(),
|
|
19
|
+
note: z.string().optional(),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
10
22
|
if (toolName.includes('search_products') ||
|
|
11
23
|
toolName.includes('searchProducts') ||
|
|
12
24
|
toolName.endsWith('_products')) {
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ 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 { createCompareService } from './services/compare/index.js';
|
|
23
24
|
import { createPromptResponse } from './pages/prompt.js';
|
|
24
25
|
import { createFullOpenApiJsonResponse, createFullOpenApiYamlResponse, createOpenApiJsonResponse, createOpenApiYamlResponse, } from './pages/openapi.js';
|
|
25
26
|
import { createPrivacyResponse } from './pages/privacy.js';
|
|
@@ -35,6 +36,7 @@ import { registerEmart24Routes } from './api/routes/emart24Routes.js';
|
|
|
35
36
|
import { registerGs25Routes } from './api/routes/gs25Routes.js';
|
|
36
37
|
import { registerSevenElevenRoutes } from './api/routes/sevenelevenRoutes.js';
|
|
37
38
|
import { registerPlacesRoutes } from './api/routes/placesRoutes.js';
|
|
39
|
+
import { registerCompareRoutes } from './api/routes/compareRoutes.js';
|
|
38
40
|
import { registerHealthRoutes } from './api/routes/healthRoutes.js';
|
|
39
41
|
import { buildConfigStatus } from './api/configStatus.js';
|
|
40
42
|
// 서버 메타데이터
|
|
@@ -55,6 +57,7 @@ const createRegistry = (bindings) => {
|
|
|
55
57
|
zyteApiKey: bindings?.ZYTE_API_KEY,
|
|
56
58
|
}),
|
|
57
59
|
createSevenElevenService,
|
|
60
|
+
createCompareService,
|
|
58
61
|
() => createPlacesService({
|
|
59
62
|
naverClientId: bindings?.NAVER_CLIENT_ID,
|
|
60
63
|
naverClientSecret: bindings?.NAVER_CLIENT_SECRET,
|
|
@@ -195,6 +198,7 @@ app.get('/', (c) => {
|
|
|
195
198
|
openapi: '/openapi.json (GET) - OpenAI Actions용 축약 OpenAPI',
|
|
196
199
|
openapiFull: '/openapi-full.json (GET) - 전체 OpenAPI',
|
|
197
200
|
actionsQuery: '/api/actions/query (GET) - 기존 GET API 통합 facade',
|
|
201
|
+
compareProducts: '/api/compare/products (GET) - 키 없는 통합 상품 가격 후보 비교',
|
|
198
202
|
},
|
|
199
203
|
services,
|
|
200
204
|
tools: allTools,
|
|
@@ -243,6 +247,7 @@ app.get('/privacy', (c) => {
|
|
|
243
247
|
registerDaisoRoutes(app);
|
|
244
248
|
registerGs25Routes(app);
|
|
245
249
|
registerSevenElevenRoutes(app);
|
|
250
|
+
registerCompareRoutes(app);
|
|
246
251
|
registerPlacesRoutes(app);
|
|
247
252
|
registerCuRoutes(app);
|
|
248
253
|
registerEmart24Routes(app);
|
|
@@ -11,6 +11,7 @@ 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_COMPARE } from './openapiSpecPathsCompare.js';
|
|
14
15
|
import { OPENAPI_COMPONENTS } from './openapiSpecComponents.js';
|
|
15
16
|
export function generateFullOpenApiSpec(baseUrl) {
|
|
16
17
|
return {
|
|
@@ -26,6 +27,7 @@ export function generateFullOpenApiSpec(baseUrl) {
|
|
|
26
27
|
- 🧭 **진열 위치 조회**: 매장 내 상품 구역/층 정보 조회
|
|
27
28
|
- 🏪 **GS25 매장/상품/재고 조회**: GS25 매장 탐색, 상품 검색, 재고 조회
|
|
28
29
|
- 7️⃣ **세븐일레븐 상품/매장/재고 조회**: 상품 검색, 매장 검색, 실재고 조회
|
|
30
|
+
- 🧾 **통합 가격 후보 비교**: 새 외부 키 없이 상품 검색 결과를 묶어 가격 비교
|
|
29
31
|
- 🏪 **CU 매장/재고 조회**: CU 매장 탐색 및 상품 재고 검색
|
|
30
32
|
- 🎬 **메가박스 지점/영화 조회**: 주변 지점, 상영 목록, 잔여 좌석 조회
|
|
31
33
|
- 🎞️ **롯데시네마 지점/영화 조회**: 주변 지점, 상영 목록, 잔여 좌석 조회
|
|
@@ -51,6 +53,7 @@ export function generateFullOpenApiSpec(baseUrl) {
|
|
|
51
53
|
...OPENAPI_PATHS_LOTTEMART,
|
|
52
54
|
...OPENAPI_PATHS_GS25,
|
|
53
55
|
...OPENAPI_PATHS_SEVENELEVEN,
|
|
56
|
+
...OPENAPI_PATHS_COMPARE,
|
|
54
57
|
...OPENAPI_PATHS_PLACES,
|
|
55
58
|
...OPENAPI_PATHS_MEGABOX,
|
|
56
59
|
...OPENAPI_PATHS_LOTTECINEMA,
|
|
@@ -63,6 +63,13 @@ 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
|
+
},
|
|
66
73
|
{
|
|
67
74
|
name: 'sido',
|
|
68
75
|
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
|
+
};
|
|
@@ -98,7 +98,8 @@ export function buildPromptReferenceText(baseUrl) {
|
|
|
98
98
|
11. **이마트24 재고 조회**: /api/emart24/inventory는 pluCd + storeKeyword 조합도 지원하므로, 상품 선택 뒤 매장 코드를 다시 모으지 않아도 됩니다.
|
|
99
99
|
12. **올리브영 재고 해석**: inventory.products[].storeInventory.stores[]가 있으면 그 매장별 stockLabel과 remainQuantity를 우선 사용하고, inStock는 그 주변 매장 기준 결과로 해석합니다.
|
|
100
100
|
13. **올리브영 상품 이미지 표시**: /api/oliveyoung/products 또는 oliveyoung_search_products 결과에 imageUrl이 있으면 목록형 답변에서도 각 상품 이미지를 생략하지 말고 모두 렌더링합니다. 특히 \`?l=ko\` 같은 query string을 삭제하지 말고 전체 URL 그대로 마크다운 이미지 src에 넣습니다.
|
|
101
|
-
14.
|
|
101
|
+
14. **통합 가격 후보 비교**: 사용자가 브랜드를 특정하지 않고 "어디가 싸?", "비교해줘"처럼 물으면 \`compare_products\` 또는 /api/compare/products를 먼저 사용합니다. 새 외부 키 없이 기존 상품 검색을 묶은 1차 가격 후보이므로 실제 재고/행사가는 각 서비스 상세 조회로 확인해야 합니다.
|
|
102
|
+
15. **주변 음식점/카페 검색**: 음식점이나 카페만 물으면 \`places_search_nearby\` 또는 /api/places/search를 먼저 사용합니다. 이 기능은 네이버 지역 검색 기반 키워드 검색이므로 좌표 반경 검색처럼 단정하지 말고, \`searchMode: "keyword"\`임을 필요할 때 짧게 밝혀주세요.
|
|
102
103
|
|
|
103
104
|
---
|
|
104
105
|
|
|
@@ -123,6 +124,7 @@ MCP 연결 정보: ${baseUrl}/mcp
|
|
|
123
124
|
- seveneleven_check_inventory: 세븐일레븐 재고 조회
|
|
124
125
|
- seveneleven_get_search_popwords: 세븐일레븐 인기 검색어 조회
|
|
125
126
|
- seveneleven_get_catalog_snapshot: 세븐일레븐 카탈로그 조회
|
|
127
|
+
- compare_products: 키 없는 통합 상품 가격 후보 비교
|
|
126
128
|
- places_search_nearby: 음식점/카페 등 주변 장소 검색
|
|
127
129
|
- oliveyoung_search_products: 올리브영 상품 검색
|
|
128
130
|
- oliveyoung_find_nearby_stores: 올리브영 주변 매장 탐색
|
|
@@ -2,7 +2,24 @@
|
|
|
2
2
|
* 프롬프트 리테일/영화 API 섹션
|
|
3
3
|
*/
|
|
4
4
|
export function buildPromptRetailCinemaText(baseUrl) {
|
|
5
|
-
return `### 6-0.
|
|
5
|
+
return `### 6-0. 키 없는 통합 상품 가격 후보 비교
|
|
6
|
+
|
|
7
|
+
**설명**: 새 외부 API 키 없이 기존 다이소, GS25, 세븐일레븐, 이마트24 상품 검색을 묶어 가격 후보를 비교합니다. 실제 매장 재고와 행사가까지 확정하는 기능이 아니라 "어디가 싸 보이는지"를 빠르게 고르는 1차 검색입니다.
|
|
8
|
+
|
|
9
|
+
**URL**:
|
|
10
|
+
- ${baseUrl}/api/compare/products?keyword={검색어}
|
|
11
|
+
|
|
12
|
+
**선택 파라미터**:
|
|
13
|
+
- services: daiso,gs25,seveneleven,emart24 중 쉼표 구분 목록
|
|
14
|
+
- limit: 서비스별 최대 결과 수 (기본값: 5)
|
|
15
|
+
|
|
16
|
+
**예시**:
|
|
17
|
+
- ${baseUrl}/api/compare/products?keyword=콜라&limit=3
|
|
18
|
+
- ${baseUrl}/api/compare/products?keyword=컵라면&services=seveneleven,emart24
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
### 6-1. 주변 음식점/카페 검색
|
|
6
23
|
|
|
7
24
|
**설명**: 네이버 지역 검색으로 특정 지역의 음식점, 카페, 디저트 가게 등 주변 장소를 조회합니다. 좌표 반경 검색이 아니라 \`강남역 카페\` 같은 키워드 기반 검색입니다.
|
|
8
25
|
|
|
@@ -23,7 +40,7 @@ export function buildPromptRetailCinemaText(baseUrl) {
|
|
|
23
40
|
|
|
24
41
|
---
|
|
25
42
|
|
|
26
|
-
### 6-
|
|
43
|
+
### 6-2. 롯데마트 매장/상품 조회
|
|
27
44
|
|
|
28
45
|
**설명**: 롯데마트 계열 매장 검색과 특정 매장 기준 상품 가격/재고 조회를 제공합니다.
|
|
29
46
|
|
|
@@ -49,7 +66,7 @@ export function buildPromptRetailCinemaText(baseUrl) {
|
|
|
49
66
|
|
|
50
67
|
---
|
|
51
68
|
|
|
52
|
-
### 6-
|
|
69
|
+
### 6-3. GS25 매장/상품/재고 조회
|
|
53
70
|
|
|
54
71
|
**설명**: GS25 매장 탐색, 상품 키워드 검색, 재고 조회를 제공합니다.
|
|
55
72
|
|
|
@@ -73,7 +90,7 @@ export function buildPromptRetailCinemaText(baseUrl) {
|
|
|
73
90
|
|
|
74
91
|
---
|
|
75
92
|
|
|
76
|
-
### 6-
|
|
93
|
+
### 6-4. 세븐일레븐 상품/매장/재고/인기검색어/카탈로그 조회
|
|
77
94
|
|
|
78
95
|
**설명**: 세븐일레븐 상품 검색, 매장 검색, 재고 수량 조회, 인기 검색어, 카탈로그 스냅샷을 제공합니다.
|
|
79
96
|
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { fetchProducts as fetchDaisoProducts } from '../daiso/tools/searchProducts.js';
|
|
2
|
+
import { fetchGs25SearchProducts } from '../gs25/client.js';
|
|
3
|
+
import { searchSevenElevenProducts } from '../seveneleven/client.js';
|
|
4
|
+
import { searchEmart24Products } from '../emart24/client.js';
|
|
5
|
+
const DEFAULT_COMPARE_SERVICES = ['daiso', 'gs25', 'seveneleven', 'emart24'];
|
|
6
|
+
const SERVICE_NAMES = {
|
|
7
|
+
daiso: '다이소',
|
|
8
|
+
gs25: 'GS25',
|
|
9
|
+
seveneleven: '세븐일레븐',
|
|
10
|
+
emart24: '이마트24',
|
|
11
|
+
};
|
|
12
|
+
function toErrorMessage(error) {
|
|
13
|
+
return error instanceof Error ? error.message : String(error);
|
|
14
|
+
}
|
|
15
|
+
function parseServices(value) {
|
|
16
|
+
if (!value || value.trim().length === 0) {
|
|
17
|
+
return DEFAULT_COMPARE_SERVICES;
|
|
18
|
+
}
|
|
19
|
+
const allowed = new Set(DEFAULT_COMPARE_SERVICES);
|
|
20
|
+
const services = value
|
|
21
|
+
.split(',')
|
|
22
|
+
.map((item) => item.trim().toLowerCase())
|
|
23
|
+
.filter((item) => allowed.has(item));
|
|
24
|
+
return services.length > 0 ? [...new Set(services)] : DEFAULT_COMPARE_SERVICES;
|
|
25
|
+
}
|
|
26
|
+
function normalizePrice(value) {
|
|
27
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : null;
|
|
28
|
+
}
|
|
29
|
+
function sortComparableProducts(products) {
|
|
30
|
+
const serviceOrder = new Map(DEFAULT_COMPARE_SERVICES.map((service, index) => [service, index]));
|
|
31
|
+
return [...products].sort((a, b) => {
|
|
32
|
+
if (a.price !== null && b.price !== null && a.price !== b.price) {
|
|
33
|
+
return a.price - b.price;
|
|
34
|
+
}
|
|
35
|
+
if (a.price !== null && b.price === null) {
|
|
36
|
+
return -1;
|
|
37
|
+
}
|
|
38
|
+
if (a.price === null && b.price !== null) {
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
return serviceOrder.get(a.service) - serviceOrder.get(b.service);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async function searchService(service, keyword, limit) {
|
|
45
|
+
if (service === 'daiso') {
|
|
46
|
+
const result = await fetchDaisoProducts(keyword, 1, limit);
|
|
47
|
+
return result.products.map((product) => ({
|
|
48
|
+
service,
|
|
49
|
+
serviceName: SERVICE_NAMES[service],
|
|
50
|
+
code: product.id,
|
|
51
|
+
name: product.name,
|
|
52
|
+
price: normalizePrice(product.price),
|
|
53
|
+
imageUrl: product.imageUrl,
|
|
54
|
+
raw: product,
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
if (service === 'gs25') {
|
|
58
|
+
const products = await fetchGs25SearchProducts(keyword, { timeout: 15000 });
|
|
59
|
+
return products.slice(0, limit).map((product) => ({
|
|
60
|
+
service,
|
|
61
|
+
serviceName: SERVICE_NAMES[service],
|
|
62
|
+
code: product.itemCode,
|
|
63
|
+
name: product.itemName || product.shortItemName,
|
|
64
|
+
price: null,
|
|
65
|
+
imageUrl: product.imageUrl,
|
|
66
|
+
stockCheckEnabled: product.stockCheckEnabled,
|
|
67
|
+
raw: product,
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
if (service === 'seveneleven') {
|
|
71
|
+
const result = await searchSevenElevenProducts({ query: keyword, page: 1, size: limit });
|
|
72
|
+
return result.products.map((product) => ({
|
|
73
|
+
service,
|
|
74
|
+
serviceName: SERVICE_NAMES[service],
|
|
75
|
+
code: product.itemCode || product.productNo,
|
|
76
|
+
name: product.itemName,
|
|
77
|
+
price: normalizePrice(product.salePrice),
|
|
78
|
+
originalPrice: normalizePrice(product.originalPrice),
|
|
79
|
+
imageUrl: product.imageUrl,
|
|
80
|
+
raw: product,
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
const result = await searchEmart24Products({
|
|
84
|
+
keyword,
|
|
85
|
+
page: 1,
|
|
86
|
+
pageSize: limit,
|
|
87
|
+
sortType: 'PRICE_ASC',
|
|
88
|
+
saleProductYn: 'N',
|
|
89
|
+
});
|
|
90
|
+
return result.products.map((product) => ({
|
|
91
|
+
service,
|
|
92
|
+
serviceName: SERVICE_NAMES[service],
|
|
93
|
+
code: product.pluCd,
|
|
94
|
+
name: product.goodsName,
|
|
95
|
+
price: normalizePrice(product.viewPrice),
|
|
96
|
+
originalPrice: normalizePrice(product.originPrice),
|
|
97
|
+
raw: product,
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
export function parseCompareServices(value) {
|
|
101
|
+
return parseServices(value);
|
|
102
|
+
}
|
|
103
|
+
export async function compareProducts(params) {
|
|
104
|
+
const keyword = params.keyword.trim();
|
|
105
|
+
const limit = Math.max(1, Math.min(Math.trunc(params.limit || 5), 20));
|
|
106
|
+
const services = params.services && params.services.length > 0 ? params.services : DEFAULT_COMPARE_SERVICES;
|
|
107
|
+
const settled = await Promise.allSettled(services.map(async (service) => ({
|
|
108
|
+
service,
|
|
109
|
+
products: await searchService(service, keyword, limit),
|
|
110
|
+
})));
|
|
111
|
+
const results = [];
|
|
112
|
+
const errors = [];
|
|
113
|
+
for (const item of settled) {
|
|
114
|
+
if (item.status === 'fulfilled') {
|
|
115
|
+
results.push(...item.value.products);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
const service = services[settled.indexOf(item)];
|
|
119
|
+
errors.push({ service, message: toErrorMessage(item.reason) });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const sorted = sortComparableProducts(results);
|
|
123
|
+
return {
|
|
124
|
+
keyword,
|
|
125
|
+
services,
|
|
126
|
+
serviceCount: services.length,
|
|
127
|
+
resultCount: sorted.length,
|
|
128
|
+
bestPrice: sorted.find((product) => product.price !== null) || null,
|
|
129
|
+
results: sorted,
|
|
130
|
+
errors,
|
|
131
|
+
note: '새 외부 키 없이 기존 공개 상품 검색을 묶은 가격 후보입니다. 실제 재고와 행사가는 매장별로 달라질 수 있습니다.',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createCompareProductsTool } from './tools/compareProducts.js';
|
|
2
|
+
const COMPARE_METADATA = {
|
|
3
|
+
id: 'compare',
|
|
4
|
+
name: '통합 비교',
|
|
5
|
+
version: '1.0.0',
|
|
6
|
+
description: '새 외부 키 없이 기존 상품 검색을 묶어 가격 후보를 비교하는 서비스',
|
|
7
|
+
};
|
|
8
|
+
class CompareService {
|
|
9
|
+
metadata = COMPARE_METADATA;
|
|
10
|
+
getTools() {
|
|
11
|
+
return [createCompareProductsTool()];
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function createCompareService() {
|
|
15
|
+
return new CompareService();
|
|
16
|
+
}
|
|
17
|
+
export * from './types.js';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
import { compareProducts, parseCompareServices } from '../client.js';
|
|
3
|
+
async function compareProductsHandler(args) {
|
|
4
|
+
const keyword = args.keyword?.trim();
|
|
5
|
+
if (!keyword) {
|
|
6
|
+
throw new Error('검색어(keyword)를 입력해주세요.');
|
|
7
|
+
}
|
|
8
|
+
const result = await compareProducts({
|
|
9
|
+
keyword,
|
|
10
|
+
limit: args.limit,
|
|
11
|
+
services: parseCompareServices(args.services),
|
|
12
|
+
});
|
|
13
|
+
return {
|
|
14
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
15
|
+
structuredContent: { ...result },
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function createCompareProductsTool() {
|
|
19
|
+
return {
|
|
20
|
+
name: 'compare_products',
|
|
21
|
+
metadata: {
|
|
22
|
+
title: '가격 후보 통합 비교',
|
|
23
|
+
description: '새 외부 키 없이 다이소, GS25, 세븐일레븐, 이마트24 상품 검색을 묶어 가격 후보를 비교합니다.',
|
|
24
|
+
inputSchema: {
|
|
25
|
+
keyword: z.string().describe('비교할 상품 검색어'),
|
|
26
|
+
limit: z.number().optional().default(5).describe('서비스별 최대 결과 수'),
|
|
27
|
+
services: z
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe('쉼표로 구분한 서비스 목록(daiso,gs25,seveneleven,emart24)'),
|
|
31
|
+
},
|
|
32
|
+
outputSchema: {
|
|
33
|
+
keyword: z.string().optional(),
|
|
34
|
+
services: z.array(z.string()).optional(),
|
|
35
|
+
serviceCount: z.number().optional(),
|
|
36
|
+
resultCount: z.number().optional(),
|
|
37
|
+
bestPrice: z.unknown().optional(),
|
|
38
|
+
results: z.array(z.unknown()).optional(),
|
|
39
|
+
errors: z.array(z.unknown()).optional(),
|
|
40
|
+
note: z.string().optional(),
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
handler: compareProductsHandler,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/openapi.json
CHANGED
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"sevenelevenCheckInventory",
|
|
54
54
|
"sevenelevenGetPopwords",
|
|
55
55
|
"sevenelevenGetCatalog",
|
|
56
|
+
"compareProducts",
|
|
56
57
|
"placesSearchNearby",
|
|
57
58
|
"megaboxFindTheaters",
|
|
58
59
|
"megaboxListMovies",
|
|
@@ -129,6 +130,15 @@
|
|
|
129
130
|
"type": "string"
|
|
130
131
|
}
|
|
131
132
|
},
|
|
133
|
+
{
|
|
134
|
+
"name": "services",
|
|
135
|
+
"in": "query",
|
|
136
|
+
"required": false,
|
|
137
|
+
"description": "통합 비교 서비스 목록(daiso,gs25,seveneleven,emart24)",
|
|
138
|
+
"schema": {
|
|
139
|
+
"type": "string"
|
|
140
|
+
}
|
|
141
|
+
},
|
|
132
142
|
{
|
|
133
143
|
"name": "sido",
|
|
134
144
|
"in": "query",
|
package/openapi.yaml
CHANGED
|
@@ -54,6 +54,7 @@ paths:
|
|
|
54
54
|
- sevenelevenCheckInventory
|
|
55
55
|
- sevenelevenGetPopwords
|
|
56
56
|
- sevenelevenGetCatalog
|
|
57
|
+
- compareProducts
|
|
57
58
|
- placesSearchNearby
|
|
58
59
|
- megaboxFindTheaters
|
|
59
60
|
- megaboxListMovies
|
|
@@ -106,6 +107,12 @@ paths:
|
|
|
106
107
|
description: 주변 장소 카테고리(restaurant, cafe, food, dessert, all)
|
|
107
108
|
schema:
|
|
108
109
|
type: string
|
|
110
|
+
- name: services
|
|
111
|
+
in: query
|
|
112
|
+
required: false
|
|
113
|
+
description: 통합 비교 서비스 목록(daiso,gs25,seveneleven,emart24)
|
|
114
|
+
schema:
|
|
115
|
+
type: string
|
|
109
116
|
- name: sido
|
|
110
117
|
in: query
|
|
111
118
|
required: false
|
package/package.json
CHANGED