daiso 1.0.6 → 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 +83 -4
- package/dist/api/actionsProxy.js +116 -21
- package/dist/api/compareHandlers.js +26 -0
- package/dist/api/configStatus.js +5 -1
- package/dist/api/gs25Handlers.js +45 -10
- package/dist/api/healthChecks.js +50 -9
- package/dist/api/placesHandlers.js +44 -0
- package/dist/api/routes/compareRoutes.js +9 -0
- package/dist/api/routes/healthRoutes.js +1 -1
- package/dist/api/routes/placesRoutes.js +9 -0
- package/dist/cli/commandOptions.js +65 -6
- package/dist/cli/commands/convenience.js +38 -0
- package/dist/cli.js +5 -1
- package/dist/cliHelp.js +25 -6
- package/dist/cliRenderer.js +24 -2
- package/dist/core/registry.js +33 -2
- package/dist/index.js +22 -3
- package/dist/pages/openapiFullSpec.js +6 -0
- package/dist/pages/openapiSpecActions.js +315 -42
- package/dist/pages/openapiSpecPathsCompare.js +59 -0
- package/dist/pages/openapiSpecPathsPlaces.js +85 -0
- package/dist/pages/promptReferenceText.js +5 -0
- package/dist/pages/promptRetailCinemaText.js +41 -3
- 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/gs25/api.js +3 -0
- package/dist/services/gs25/client.js +211 -35
- package/dist/services/gs25/index.js +11 -3
- package/dist/services/gs25/tools/checkInventory.js +39 -12
- package/dist/services/gs25/tools/findNearbyStores.js +19 -5
- package/dist/services/gs25/tools/searchProducts.js +11 -4
- package/dist/services/oliveyoung/client.js +90 -41
- package/dist/services/places/client.js +116 -0
- package/dist/services/places/index.js +23 -0
- package/dist/services/places/tools/searchNearby.js +56 -0
- package/dist/services/places/types.js +1 -0
- package/dist/utils/zyte.js +45 -18
- package/openapi.json +29 -0
- package/openapi.yaml +20 -0
- package/package.json +3 -2
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 장소 검색 GET API 핸들러
|
|
3
|
+
*/
|
|
4
|
+
import { errorResponse, successResponse } from './response.js';
|
|
5
|
+
import { searchNaverLocalPlaces } from '../services/places/client.js';
|
|
6
|
+
import { getErrorMessage } from '../core/errors.js';
|
|
7
|
+
const PLACE_CATEGORIES = new Set(['restaurant', 'cafe', 'food', 'dessert', 'all']);
|
|
8
|
+
function parseCategory(value) {
|
|
9
|
+
if (value && PLACE_CATEGORIES.has(value)) {
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
return 'all';
|
|
13
|
+
}
|
|
14
|
+
export async function handlePlacesSearch(c) {
|
|
15
|
+
const location = c.req.query('location') || '';
|
|
16
|
+
const keyword = c.req.query('keyword') || '';
|
|
17
|
+
const limit = Number.parseInt(c.req.query('limit') || '5', 10);
|
|
18
|
+
const start = Number.parseInt(c.req.query('start') || '1', 10);
|
|
19
|
+
const sort = c.req.query('sort') === 'comment' ? 'comment' : 'random';
|
|
20
|
+
const category = parseCategory(c.req.query('category'));
|
|
21
|
+
if (location.trim().length === 0 && keyword.trim().length === 0) {
|
|
22
|
+
return errorResponse(c, 'MISSING_QUERY', '위치(location) 또는 검색어(keyword)를 입력해주세요.');
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const result = await searchNaverLocalPlaces({
|
|
26
|
+
naverClientId: c.env?.NAVER_CLIENT_ID,
|
|
27
|
+
naverClientSecret: c.env?.NAVER_CLIENT_SECRET,
|
|
28
|
+
location,
|
|
29
|
+
keyword,
|
|
30
|
+
category,
|
|
31
|
+
limit,
|
|
32
|
+
start,
|
|
33
|
+
sort,
|
|
34
|
+
});
|
|
35
|
+
return successResponse(c, result, {
|
|
36
|
+
total: result.totalCount,
|
|
37
|
+
page: start,
|
|
38
|
+
pageSize: result.count,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
return errorResponse(c, 'PLACES_SEARCH_FAILED', getErrorMessage(error), 500);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -41,7 +41,7 @@ export function registerHealthRoutes(app) {
|
|
|
41
41
|
const baseUrl = c.req.query('baseUrl')?.trim() || c.env?.HEALTH_CHECK_BASE_URL?.trim() || new URL(c.req.url).origin;
|
|
42
42
|
const transport = c.req.query('transport') || c.env?.HEALTH_CHECK_TRANSPORT || 'internal';
|
|
43
43
|
const cacheBust = parseBoolean(c.req.query('cacheBust')) || transport === 'network';
|
|
44
|
-
const fetchImpl =
|
|
44
|
+
const fetchImpl = transport !== 'network'
|
|
45
45
|
? async (input, init) => app.fetch(new Request(input, init), c.env)
|
|
46
46
|
: undefined;
|
|
47
47
|
const result = await runHealthChecks({
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { withEdgeCache } from '../../utils/cache.js';
|
|
2
|
+
import { handlePlacesSearch } from '../placesHandlers.js';
|
|
3
|
+
export function registerPlacesRoutes(app) {
|
|
4
|
+
app.get('/api/places/search', async (c) => withEdgeCache(c.req.url, {
|
|
5
|
+
ttlSeconds: 60 * 60,
|
|
6
|
+
staleWhileRevalidateSeconds: 60 * 10,
|
|
7
|
+
keyPrefix: 'places-search-v1',
|
|
8
|
+
}, () => handlePlacesSearch(c)));
|
|
9
|
+
}
|
|
@@ -12,16 +12,75 @@ 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'],
|
|
16
|
+
places: ['help', 'json', 'location', 'category', 'keyword', 'limit', 'start', 'sort'],
|
|
15
17
|
'cu-stores': ['help', 'json', 'keyword', 'lat', 'lng', 'limit'],
|
|
16
|
-
'cu-inventory': [
|
|
18
|
+
'cu-inventory': [
|
|
19
|
+
'help',
|
|
20
|
+
'json',
|
|
21
|
+
'storeKeyword',
|
|
22
|
+
'lat',
|
|
23
|
+
'lng',
|
|
24
|
+
'size',
|
|
25
|
+
'offset',
|
|
26
|
+
'searchSort',
|
|
27
|
+
'storeLimit',
|
|
28
|
+
],
|
|
17
29
|
'lottecinema-theaters': ['help', 'json', 'keyword', 'lat', 'lng', 'playDate', 'limit'],
|
|
18
|
-
'lottecinema-movies': [
|
|
19
|
-
|
|
20
|
-
|
|
30
|
+
'lottecinema-movies': [
|
|
31
|
+
'help',
|
|
32
|
+
'json',
|
|
33
|
+
'keyword',
|
|
34
|
+
'playDate',
|
|
35
|
+
'theaterId',
|
|
36
|
+
'movieId',
|
|
37
|
+
'lat',
|
|
38
|
+
'lng',
|
|
39
|
+
],
|
|
40
|
+
'lottecinema-seats': [
|
|
41
|
+
'help',
|
|
42
|
+
'json',
|
|
43
|
+
'keyword',
|
|
44
|
+
'playDate',
|
|
45
|
+
'theaterId',
|
|
46
|
+
'movieId',
|
|
47
|
+
'lat',
|
|
48
|
+
'lng',
|
|
49
|
+
'limit',
|
|
50
|
+
],
|
|
51
|
+
'emart24-stores': [
|
|
52
|
+
'help',
|
|
53
|
+
'json',
|
|
54
|
+
'keyword',
|
|
55
|
+
'area1',
|
|
56
|
+
'area2',
|
|
57
|
+
'lat',
|
|
58
|
+
'lng',
|
|
59
|
+
'service24h',
|
|
60
|
+
'limit',
|
|
61
|
+
],
|
|
21
62
|
'emart24-products': ['help', 'json', 'page', 'pageSize'],
|
|
22
|
-
'emart24-inventory': [
|
|
63
|
+
'emart24-inventory': [
|
|
64
|
+
'help',
|
|
65
|
+
'json',
|
|
66
|
+
'bizNoArr',
|
|
67
|
+
'storeKeyword',
|
|
68
|
+
'area1',
|
|
69
|
+
'area2',
|
|
70
|
+
'lat',
|
|
71
|
+
'lng',
|
|
72
|
+
'storeLimit',
|
|
73
|
+
],
|
|
23
74
|
'lottemart-stores': ['help', 'json', 'keyword', 'area', 'brandVariant', 'lat', 'lng', 'limit'],
|
|
24
|
-
'lottemart-products': [
|
|
75
|
+
'lottemart-products': [
|
|
76
|
+
'help',
|
|
77
|
+
'json',
|
|
78
|
+
'storeCode',
|
|
79
|
+
'storeName',
|
|
80
|
+
'area',
|
|
81
|
+
'brandVariant',
|
|
82
|
+
'pageLimit',
|
|
83
|
+
],
|
|
25
84
|
'gs25-stores': ['help', 'json', 'keyword', 'lat', 'lng', 'serviceCode', 'limit'],
|
|
26
85
|
'gs25-products': ['help', 'json', 'serviceCode', 'limit'],
|
|
27
86
|
'gs25-inventory': ['help', 'json', 'storeKeyword', 'lat', 'lng', 'serviceCode', 'storeLimit'],
|
|
@@ -6,6 +6,44 @@ 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
|
+
}
|
|
27
|
+
export async function handlePlaces(options, deps) {
|
|
28
|
+
const parsed = parseCliArgs(options);
|
|
29
|
+
if (parsed.options.help === 'true') {
|
|
30
|
+
return printCommandHelp('places', deps.writeOut, deps.writeErr);
|
|
31
|
+
}
|
|
32
|
+
const location = parsed.positionals[0];
|
|
33
|
+
if (location) {
|
|
34
|
+
parsed.options = { location, ...parsed.options };
|
|
35
|
+
}
|
|
36
|
+
if (validateCommandOptions('places', parsed.options, deps.writeErr)) {
|
|
37
|
+
return 1;
|
|
38
|
+
}
|
|
39
|
+
if (!parsed.options.location && !parsed.options.keyword) {
|
|
40
|
+
deps.writeErr('places 명령은 위치나 검색어가 필요합니다. 예: daiso places 강남역 --category cafe');
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
const targetUrl = toUrl('/api/places/search');
|
|
44
|
+
applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
|
|
45
|
+
return await requestAndPrintResponse(deps.fetchImpl, deps.writeOut, deps.writeErr, targetUrl, 'places', parsed.options.json === 'true');
|
|
46
|
+
}
|
|
9
47
|
export async function handleCuStores(options, deps) {
|
|
10
48
|
const parsed = parseCliArgs(options);
|
|
11
49
|
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, 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,10 @@ 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);
|
|
117
|
+
if (command === 'places')
|
|
118
|
+
return await handlePlaces(options, resolvedDeps);
|
|
115
119
|
if (command === 'cu-stores')
|
|
116
120
|
return await handleCuStores(options, resolvedDeps);
|
|
117
121
|
if (command === 'cu-inventory')
|
package/dist/cliHelp.js
CHANGED
|
@@ -13,6 +13,8 @@ export const COMMAND_LIST = [
|
|
|
13
13
|
'stores',
|
|
14
14
|
'inventory',
|
|
15
15
|
'display-location',
|
|
16
|
+
'compare',
|
|
17
|
+
'places',
|
|
16
18
|
'cu-stores',
|
|
17
19
|
'cu-inventory',
|
|
18
20
|
'lottecinema-theaters',
|
|
@@ -43,6 +45,8 @@ const COMMAND_SUMMARY = {
|
|
|
43
45
|
stores: '다이소 매장 검색',
|
|
44
46
|
inventory: '다이소 재고 조회',
|
|
45
47
|
'display-location': '다이소 진열 위치 조회',
|
|
48
|
+
compare: '키 없는 통합 상품 가격 후보 비교',
|
|
49
|
+
places: '음식점/카페 등 주변 장소 검색',
|
|
46
50
|
'cu-stores': 'CU 매장 검색',
|
|
47
51
|
'cu-inventory': 'CU 재고 조회',
|
|
48
52
|
'lottecinema-theaters': '롯데시네마 주변 지점 조회',
|
|
@@ -68,11 +72,7 @@ const COMMAND_DETAIL = {
|
|
|
68
72
|
'사용법: daiso help [command]',
|
|
69
73
|
'예시: daiso help products',
|
|
70
74
|
],
|
|
71
|
-
version: [
|
|
72
|
-
'명령: version',
|
|
73
|
-
'설명: 설치된 daiso CLI 버전을 출력합니다.',
|
|
74
|
-
'사용법: daiso version',
|
|
75
|
-
],
|
|
75
|
+
version: ['명령: version', '설명: 설치된 daiso CLI 버전을 출력합니다.', '사용법: daiso version'],
|
|
76
76
|
url: [
|
|
77
77
|
'명령: url',
|
|
78
78
|
'설명: MCP 엔드포인트 URL을 출력합니다.',
|
|
@@ -141,6 +141,24 @@ const COMMAND_DETAIL = {
|
|
|
141
141
|
'옵션: --json',
|
|
142
142
|
'예시: daiso display-location 1034604 04515',
|
|
143
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
|
+
],
|
|
153
|
+
places: [
|
|
154
|
+
'명령: places',
|
|
155
|
+
'설명: 네이버 지역 검색으로 음식점, 카페, 디저트 가게 등 주변 장소를 검색합니다.',
|
|
156
|
+
'사용법: daiso places <location> [--category cafe|restaurant|food|dessert|all] [--keyword 검색어] [--limit N] [--json]',
|
|
157
|
+
'필수: <location> 또는 --keyword',
|
|
158
|
+
'옵션: --category, --keyword, --limit, --start, --sort, --json',
|
|
159
|
+
'예시: daiso places 강남역 --category cafe --limit 5',
|
|
160
|
+
'예시: daiso places 성수동 --keyword 브런치 --limit 5',
|
|
161
|
+
],
|
|
144
162
|
'cu-stores': [
|
|
145
163
|
'명령: cu-stores',
|
|
146
164
|
'설명: CU 매장 검색 API를 호출합니다.',
|
|
@@ -304,6 +322,7 @@ export function printHelp(writeOut) {
|
|
|
304
322
|
writeOut(' npx daiso stores 강남역');
|
|
305
323
|
writeOut(' npx daiso inventory 1034604 --keyword 강남역');
|
|
306
324
|
writeOut(' npx daiso display-location 1034604 04515');
|
|
325
|
+
writeOut(' npx daiso places 강남역 --category cafe --limit 5');
|
|
307
326
|
writeOut(' npx daiso cu-stores 강남');
|
|
308
327
|
writeOut(' npx daiso cu-inventory 과자 --storeKeyword 강남');
|
|
309
328
|
writeOut(' npx daiso lottecinema-theaters --keyword "안산 중앙역" --limit 5');
|
|
@@ -335,7 +354,7 @@ export function printHelp(writeOut) {
|
|
|
335
354
|
export function printCommandHelp(command, writeOut, writeErr) {
|
|
336
355
|
if (!Object.hasOwn(COMMAND_DETAIL, command)) {
|
|
337
356
|
writeErr(`도움말을 찾을 수 없는 명령어: ${command}`);
|
|
338
|
-
writeErr('사용 가능한 명령어: help, version, url, health, claude, get, products, product, stores, inventory, display-location, cu-stores, cu-inventory, lottecinema-theaters, lottecinema-movies, lottecinema-seats, lottemart-stores, lottemart-products, emart24-stores, emart24-products, emart24-inventory, gs25-stores, gs25-products, gs25-inventory, seveneleven-products, seveneleven-stores, seveneleven-popwords, seveneleven-catalog');
|
|
357
|
+
writeErr('사용 가능한 명령어: help, version, url, health, claude, get, products, product, stores, inventory, display-location, places, cu-stores, cu-inventory, lottecinema-theaters, lottecinema-movies, lottecinema-seats, lottemart-stores, lottemart-products, emart24-stores, emart24-products, emart24-inventory, gs25-stores, gs25-products, gs25-inventory, seveneleven-products, seveneleven-stores, seveneleven-popwords, seveneleven-catalog');
|
|
339
358
|
return 1;
|
|
340
359
|
}
|
|
341
360
|
const detail = COMMAND_DETAIL[command];
|
package/dist/cliRenderer.js
CHANGED
|
@@ -69,8 +69,14 @@ function formatCollection(title, items) {
|
|
|
69
69
|
entry.storeCode ??
|
|
70
70
|
entry.bizNo ??
|
|
71
71
|
entry.theaterId ??
|
|
72
|
-
entry.movieId
|
|
73
|
-
|
|
72
|
+
entry.movieId ??
|
|
73
|
+
entry.link;
|
|
74
|
+
const price = entry.price ??
|
|
75
|
+
entry.prc ??
|
|
76
|
+
entry.PD_PRC ??
|
|
77
|
+
entry.viewPrice ??
|
|
78
|
+
entry.priceToPay ??
|
|
79
|
+
entry.searchItemSellPrice;
|
|
74
80
|
let detail = `- ${name}`;
|
|
75
81
|
if (id !== undefined) {
|
|
76
82
|
detail += ` [${toText(id)}]`;
|
|
@@ -168,6 +174,18 @@ function formatInventory(data) {
|
|
|
168
174
|
}
|
|
169
175
|
return lines;
|
|
170
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
|
+
}
|
|
171
189
|
export function renderApiEnvelope(command, url, payload) {
|
|
172
190
|
if (!isRecord(payload)) {
|
|
173
191
|
return `요청 성공: ${url.pathname}\n${toText(payload)}\n\n원본 JSON은 --json 옵션으로 확인하세요.`;
|
|
@@ -193,6 +211,9 @@ export function renderApiEnvelope(command, url, payload) {
|
|
|
193
211
|
else if (command === 'display-location') {
|
|
194
212
|
lines.push(...formatDisplayLocation(data));
|
|
195
213
|
}
|
|
214
|
+
else if (command === 'compare') {
|
|
215
|
+
lines.push(...formatCompare(data));
|
|
216
|
+
}
|
|
196
217
|
else if (command === 'cu-inventory') {
|
|
197
218
|
lines.push(...formatCollection('매장 목록', isRecord(data.nearbyStores) ? data.nearbyStores.stores : undefined));
|
|
198
219
|
lines.push(...formatCollection('재고 항목', isRecord(data.inventory) ? data.inventory.items : undefined));
|
|
@@ -204,6 +225,7 @@ export function renderApiEnvelope(command, url, payload) {
|
|
|
204
225
|
lines.push(...formatCollection('제품 목록', data.products));
|
|
205
226
|
lines.push(...formatCollection('매장 목록', data.stores));
|
|
206
227
|
lines.push(...formatCollection('극장 목록', data.theaters));
|
|
228
|
+
lines.push(...formatCollection('장소 목록', data.places));
|
|
207
229
|
lines.push(...formatCollection('영화 목록', data.movies));
|
|
208
230
|
lines.push(...formatCollection('시간표', data.timetable));
|
|
209
231
|
lines.push(...formatCollection('좌석 목록', data.seats));
|
package/dist/core/registry.js
CHANGED
|
@@ -7,7 +7,21 @@ 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('
|
|
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
|
+
}
|
|
22
|
+
if (toolName.includes('search_products') ||
|
|
23
|
+
toolName.includes('searchProducts') ||
|
|
24
|
+
toolName.endsWith('_products')) {
|
|
11
25
|
return {
|
|
12
26
|
keyword: z.string().optional(),
|
|
13
27
|
query: z.string().optional(),
|
|
@@ -19,7 +33,9 @@ function createFallbackOutputSchema(toolName) {
|
|
|
19
33
|
note: z.string().optional(),
|
|
20
34
|
};
|
|
21
35
|
}
|
|
22
|
-
if (toolName.includes('find_stores') ||
|
|
36
|
+
if (toolName.includes('find_stores') ||
|
|
37
|
+
toolName.includes('nearby_stores') ||
|
|
38
|
+
toolName.includes('search_stores')) {
|
|
23
39
|
return {
|
|
24
40
|
keyword: z.string().optional(),
|
|
25
41
|
searchParams: z.unknown().optional(),
|
|
@@ -30,6 +46,18 @@ function createFallbackOutputSchema(toolName) {
|
|
|
30
46
|
stores: z.array(z.unknown()).optional(),
|
|
31
47
|
};
|
|
32
48
|
}
|
|
49
|
+
if (toolName.includes('places')) {
|
|
50
|
+
return {
|
|
51
|
+
provider: z.string().optional(),
|
|
52
|
+
searchMode: z.string().optional(),
|
|
53
|
+
query: z.string().optional(),
|
|
54
|
+
category: z.string().optional(),
|
|
55
|
+
location: z.string().optional(),
|
|
56
|
+
totalCount: z.number().optional(),
|
|
57
|
+
count: z.number().optional(),
|
|
58
|
+
places: z.array(z.unknown()).optional(),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
33
61
|
if (toolName.includes('inventory')) {
|
|
34
62
|
return {
|
|
35
63
|
productId: z.string().optional(),
|
|
@@ -178,6 +206,9 @@ function withStandardCollections(payload) {
|
|
|
178
206
|
if (Array.isArray(payload.stores)) {
|
|
179
207
|
standard.stores = normalizeStores(payload.stores);
|
|
180
208
|
}
|
|
209
|
+
if (Array.isArray(payload.places)) {
|
|
210
|
+
standard.places = normalizeStores(payload.places);
|
|
211
|
+
}
|
|
181
212
|
if (Array.isArray(payload.theaters)) {
|
|
182
213
|
standard.theaters = normalizeStores(payload.theaters);
|
|
183
214
|
}
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,8 @@ import { createCuService } from './services/cu/index.js';
|
|
|
19
19
|
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
|
+
import { createPlacesService } from './services/places/index.js';
|
|
23
|
+
import { createCompareService } from './services/compare/index.js';
|
|
22
24
|
import { createPromptResponse } from './pages/prompt.js';
|
|
23
25
|
import { createFullOpenApiJsonResponse, createFullOpenApiYamlResponse, createOpenApiJsonResponse, createOpenApiYamlResponse, } from './pages/openapi.js';
|
|
24
26
|
import { createPrivacyResponse } from './pages/privacy.js';
|
|
@@ -33,6 +35,8 @@ import { registerCuRoutes } from './api/routes/cuRoutes.js';
|
|
|
33
35
|
import { registerEmart24Routes } from './api/routes/emart24Routes.js';
|
|
34
36
|
import { registerGs25Routes } from './api/routes/gs25Routes.js';
|
|
35
37
|
import { registerSevenElevenRoutes } from './api/routes/sevenelevenRoutes.js';
|
|
38
|
+
import { registerPlacesRoutes } from './api/routes/placesRoutes.js';
|
|
39
|
+
import { registerCompareRoutes } from './api/routes/compareRoutes.js';
|
|
36
40
|
import { registerHealthRoutes } from './api/routes/healthRoutes.js';
|
|
37
41
|
import { buildConfigStatus } from './api/configStatus.js';
|
|
38
42
|
// 서버 메타데이터
|
|
@@ -48,8 +52,16 @@ const createRegistry = (bindings) => {
|
|
|
48
52
|
const registry = new ServiceRegistry();
|
|
49
53
|
registry.registerAll([
|
|
50
54
|
createDaisoService,
|
|
51
|
-
createGs25Service
|
|
55
|
+
() => createGs25Service({
|
|
56
|
+
googleMapsApiKey: bindings?.GOOGLE_MAPS_API_KEY,
|
|
57
|
+
zyteApiKey: bindings?.ZYTE_API_KEY,
|
|
58
|
+
}),
|
|
52
59
|
createSevenElevenService,
|
|
60
|
+
createCompareService,
|
|
61
|
+
() => createPlacesService({
|
|
62
|
+
naverClientId: bindings?.NAVER_CLIENT_ID,
|
|
63
|
+
naverClientSecret: bindings?.NAVER_CLIENT_SECRET,
|
|
64
|
+
}),
|
|
53
65
|
createCuService,
|
|
54
66
|
createEmart24Service,
|
|
55
67
|
() => createLotteMartService({
|
|
@@ -87,9 +99,13 @@ const createMcpServer = (bindings) => {
|
|
|
87
99
|
*/
|
|
88
100
|
const isInitializeRequest = (body) => {
|
|
89
101
|
if (Array.isArray(body)) {
|
|
90
|
-
return body.some((item) => typeof item === 'object' &&
|
|
102
|
+
return body.some((item) => typeof item === 'object' &&
|
|
103
|
+
item !== null &&
|
|
104
|
+
item.method === 'initialize');
|
|
91
105
|
}
|
|
92
|
-
return typeof body === 'object' &&
|
|
106
|
+
return (typeof body === 'object' &&
|
|
107
|
+
body !== null &&
|
|
108
|
+
body.method === 'initialize');
|
|
93
109
|
};
|
|
94
110
|
/**
|
|
95
111
|
* 새 세션 transport/server를 생성합니다.
|
|
@@ -182,6 +198,7 @@ app.get('/', (c) => {
|
|
|
182
198
|
openapi: '/openapi.json (GET) - OpenAI Actions용 축약 OpenAPI',
|
|
183
199
|
openapiFull: '/openapi-full.json (GET) - 전체 OpenAPI',
|
|
184
200
|
actionsQuery: '/api/actions/query (GET) - 기존 GET API 통합 facade',
|
|
201
|
+
compareProducts: '/api/compare/products (GET) - 키 없는 통합 상품 가격 후보 비교',
|
|
185
202
|
},
|
|
186
203
|
services,
|
|
187
204
|
tools: allTools,
|
|
@@ -230,6 +247,8 @@ app.get('/privacy', (c) => {
|
|
|
230
247
|
registerDaisoRoutes(app);
|
|
231
248
|
registerGs25Routes(app);
|
|
232
249
|
registerSevenElevenRoutes(app);
|
|
250
|
+
registerCompareRoutes(app);
|
|
251
|
+
registerPlacesRoutes(app);
|
|
233
252
|
registerCuRoutes(app);
|
|
234
253
|
registerEmart24Routes(app);
|
|
235
254
|
registerLotteMartRoutes(app);
|
|
@@ -10,6 +10,8 @@ import { OPENAPI_PATHS_CGV } from './openapiSpecPathsCgv.js';
|
|
|
10
10
|
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
|
+
import { OPENAPI_PATHS_PLACES } from './openapiSpecPathsPlaces.js';
|
|
14
|
+
import { OPENAPI_PATHS_COMPARE } from './openapiSpecPathsCompare.js';
|
|
13
15
|
import { OPENAPI_COMPONENTS } from './openapiSpecComponents.js';
|
|
14
16
|
export function generateFullOpenApiSpec(baseUrl) {
|
|
15
17
|
return {
|
|
@@ -25,10 +27,12 @@ export function generateFullOpenApiSpec(baseUrl) {
|
|
|
25
27
|
- 🧭 **진열 위치 조회**: 매장 내 상품 구역/층 정보 조회
|
|
26
28
|
- 🏪 **GS25 매장/상품/재고 조회**: GS25 매장 탐색, 상품 검색, 재고 조회
|
|
27
29
|
- 7️⃣ **세븐일레븐 상품/매장/재고 조회**: 상품 검색, 매장 검색, 실재고 조회
|
|
30
|
+
- 🧾 **통합 가격 후보 비교**: 새 외부 키 없이 상품 검색 결과를 묶어 가격 비교
|
|
28
31
|
- 🏪 **CU 매장/재고 조회**: CU 매장 탐색 및 상품 재고 검색
|
|
29
32
|
- 🎬 **메가박스 지점/영화 조회**: 주변 지점, 상영 목록, 잔여 좌석 조회
|
|
30
33
|
- 🎞️ **롯데시네마 지점/영화 조회**: 주변 지점, 상영 목록, 잔여 좌석 조회
|
|
31
34
|
- 🎥 **CGV 지점/영화 조회**: 극장 목록, 영화 목록, 시간표 조회
|
|
35
|
+
- 🍽️ **주변 음식점/카페 검색**: 네이버 지역 검색 기반 장소 조회
|
|
32
36
|
|
|
33
37
|
## 사용 팁
|
|
34
38
|
1. 한글 검색어는 URL 인코딩이 자동 처리됩니다
|
|
@@ -49,6 +53,8 @@ export function generateFullOpenApiSpec(baseUrl) {
|
|
|
49
53
|
...OPENAPI_PATHS_LOTTEMART,
|
|
50
54
|
...OPENAPI_PATHS_GS25,
|
|
51
55
|
...OPENAPI_PATHS_SEVENELEVEN,
|
|
56
|
+
...OPENAPI_PATHS_COMPARE,
|
|
57
|
+
...OPENAPI_PATHS_PLACES,
|
|
52
58
|
...OPENAPI_PATHS_MEGABOX,
|
|
53
59
|
...OPENAPI_PATHS_LOTTECINEMA,
|
|
54
60
|
...OPENAPI_PATHS_CGV,
|