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.
Files changed (42) hide show
  1. package/README.md +83 -4
  2. package/dist/api/actionsProxy.js +116 -21
  3. package/dist/api/compareHandlers.js +26 -0
  4. package/dist/api/configStatus.js +5 -1
  5. package/dist/api/gs25Handlers.js +45 -10
  6. package/dist/api/healthChecks.js +50 -9
  7. package/dist/api/placesHandlers.js +44 -0
  8. package/dist/api/routes/compareRoutes.js +9 -0
  9. package/dist/api/routes/healthRoutes.js +1 -1
  10. package/dist/api/routes/placesRoutes.js +9 -0
  11. package/dist/cli/commandOptions.js +65 -6
  12. package/dist/cli/commands/convenience.js +38 -0
  13. package/dist/cli.js +5 -1
  14. package/dist/cliHelp.js +25 -6
  15. package/dist/cliRenderer.js +24 -2
  16. package/dist/core/registry.js +33 -2
  17. package/dist/index.js +22 -3
  18. package/dist/pages/openapiFullSpec.js +6 -0
  19. package/dist/pages/openapiSpecActions.js +315 -42
  20. package/dist/pages/openapiSpecPathsCompare.js +59 -0
  21. package/dist/pages/openapiSpecPathsPlaces.js +85 -0
  22. package/dist/pages/promptReferenceText.js +5 -0
  23. package/dist/pages/promptRetailCinemaText.js +41 -3
  24. package/dist/services/compare/client.js +133 -0
  25. package/dist/services/compare/index.js +17 -0
  26. package/dist/services/compare/tools/compareProducts.js +45 -0
  27. package/dist/services/compare/types.js +1 -0
  28. package/dist/services/gs25/api.js +3 -0
  29. package/dist/services/gs25/client.js +211 -35
  30. package/dist/services/gs25/index.js +11 -3
  31. package/dist/services/gs25/tools/checkInventory.js +39 -12
  32. package/dist/services/gs25/tools/findNearbyStores.js +19 -5
  33. package/dist/services/gs25/tools/searchProducts.js +11 -4
  34. package/dist/services/oliveyoung/client.js +90 -41
  35. package/dist/services/places/client.js +116 -0
  36. package/dist/services/places/index.js +23 -0
  37. package/dist/services/places/tools/searchNearby.js +56 -0
  38. package/dist/services/places/types.js +1 -0
  39. package/dist/utils/zyte.js +45 -18
  40. package/openapi.json +29 -0
  41. package/openapi.yaml +20 -0
  42. 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 = c.env?.HEALTH_CHECK_BASE_URL && transport !== 'network'
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': ['help', 'json', 'storeKeyword', 'lat', 'lng', 'size', 'offset', 'searchSort', 'storeLimit'],
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': ['help', 'json', 'keyword', 'playDate', 'theaterId', 'movieId', 'lat', 'lng'],
19
- 'lottecinema-seats': ['help', 'json', 'keyword', 'playDate', 'theaterId', 'movieId', 'lat', 'lng', 'limit'],
20
- 'emart24-stores': ['help', 'json', 'keyword', 'area1', 'area2', 'lat', 'lng', 'service24h', 'limit'],
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': ['help', 'json', 'bizNoArr', 'storeKeyword', 'area1', 'area2', 'lat', 'lng', 'storeLimit'],
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': ['help', 'json', 'storeCode', 'storeName', 'area', 'brandVariant', 'pageLimit'],
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];
@@ -69,8 +69,14 @@ function formatCollection(title, items) {
69
69
  entry.storeCode ??
70
70
  entry.bizNo ??
71
71
  entry.theaterId ??
72
- entry.movieId;
73
- const price = entry.price ?? entry.prc ?? entry.PD_PRC ?? entry.viewPrice ?? entry.priceToPay ?? entry.searchItemSellPrice;
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));
@@ -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('search_products') || toolName.includes('searchProducts') || toolName.endsWith('_products')) {
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') || toolName.includes('nearby_stores') || toolName.includes('search_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' && item !== null && item.method === 'initialize');
102
+ return body.some((item) => typeof item === 'object' &&
103
+ item !== null &&
104
+ item.method === 'initialize');
91
105
  }
92
- return typeof body === 'object' && body !== null && body.method === 'initialize';
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,