daiso 1.2.7 → 1.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +17 -17
  2. package/dist/api/actionsProxy.js +0 -10
  3. package/dist/api/cgvHandlers.js +4 -3
  4. package/dist/api/configStatus.js +7 -2
  5. package/dist/api/gs25Handlers.js +4 -3
  6. package/dist/api/handlers.js +11 -7
  7. package/dist/api/healthCheckDefinitions.js +0 -9
  8. package/dist/api/healthChecks.js +0 -1
  9. package/dist/api/lottecinemaHandlers.js +4 -3
  10. package/dist/api/lottemartHandlers.js +5 -142
  11. package/dist/api/megaboxHandlers.js +4 -3
  12. package/dist/api/opinetHandlers.js +2 -1
  13. package/dist/api/routes/cgvRoutes.js +3 -3
  14. package/dist/api/routes/cuRoutes.js +2 -2
  15. package/dist/api/routes/gs25Routes.js +3 -3
  16. package/dist/api/routes/lottecinemaRoutes.js +3 -3
  17. package/dist/api/routes/lottemartRoutes.js +4 -16
  18. package/dist/api/routes/megaboxRoutes.js +3 -3
  19. package/dist/api/routes/opinetRoutes.js +4 -4
  20. package/dist/cli.js +5 -5
  21. package/dist/cliHelp.js +0 -3
  22. package/dist/cliHelpDefinitions.js +2 -19
  23. package/dist/index.js +10 -11
  24. package/dist/middleware/dailyRateLimit.js +0 -1
  25. package/dist/pages/openapiFullSpec.js +0 -2
  26. package/dist/pages/openapiSpecActionParameters.js +4 -4
  27. package/dist/pages/openapiSpecComponents.js +0 -2
  28. package/dist/pages/openapiSpecPathsMegabox.js +1 -1
  29. package/dist/pages/promptReferenceText.js +0 -5
  30. package/dist/pages/promptRetailCinemaText.js +0 -26
  31. package/dist/services/cgv/index.js +3 -6
  32. package/dist/services/cgv/location.js +38 -125
  33. package/dist/services/cgv/tools/findTheaters.js +5 -4
  34. package/dist/services/cgv/tools/getTimetable.js +5 -4
  35. package/dist/services/cgv/tools/searchMovies.js +5 -4
  36. package/dist/services/cu/client.js +3 -35
  37. package/dist/services/cu/index.js +1 -0
  38. package/dist/services/cu/tools/checkInventory.js +3 -1
  39. package/dist/services/gs25/client.js +3 -33
  40. package/dist/services/gs25/index.js +2 -2
  41. package/dist/services/gs25/tools/checkInventory.js +3 -1
  42. package/dist/services/gs25/tools/findNearbyStores.js +3 -1
  43. package/dist/services/lottecinema/index.js +3 -6
  44. package/dist/services/lottecinema/location.js +13 -100
  45. package/dist/services/lottecinema/tools/findNearbyTheaters.js +7 -6
  46. package/dist/services/lottecinema/tools/getRemainingSeats.js +5 -4
  47. package/dist/services/lottecinema/tools/listNowShowing.js +5 -4
  48. package/dist/services/lottemart/client.js +5 -32
  49. package/dist/services/lottemart/geocode.js +3 -46
  50. package/dist/services/megabox/index.js +9 -8
  51. package/dist/services/megabox/location.js +19 -84
  52. package/dist/services/megabox/tools/findNearbyTheaters.js +7 -7
  53. package/dist/services/megabox/tools/getRemainingSeats.js +5 -5
  54. package/dist/services/megabox/tools/listNowShowing.js +5 -5
  55. package/dist/services/oliveyoung/tools/checkInventory.js +5 -5
  56. package/dist/services/oliveyoung/tools/findNearbyStores.js +3 -3
  57. package/dist/services/oliveyoung/tools/searchProducts.js +3 -3
  58. package/dist/services/oliveyoung/transport.js +49 -33
  59. package/dist/services/opinet/client.js +1 -1
  60. package/dist/services/opinet/index.js +1 -1
  61. package/dist/services/opinet/location.js +5 -56
  62. package/dist/services/opinet/tools.js +2 -2
  63. package/dist/utils/geocode.js +155 -0
  64. package/dist/utils/http.js +2 -2
  65. package/dist/utils/relayQuota.js +4 -2
  66. package/openapi.json +4 -183
  67. package/openapi.yaml +4 -131
  68. package/package.json +1 -2
  69. package/dist/pages/openapiSpecComponentsLotteMart.js +0 -85
  70. package/dist/pages/openapiSpecPathsLotteMart.js +0 -157
@@ -7,16 +7,16 @@ export function registerMegaboxRoutes(app) {
7
7
  app.get('/api/megabox/theaters', async (c) => withEdgeCache(c.req.url, {
8
8
  ttlSeconds: 60 * 60 * 24,
9
9
  staleWhileRevalidateSeconds: 60 * 5,
10
- keyPrefix: 'megabox-theaters-v1',
10
+ keyPrefix: 'megabox-theaters-v2',
11
11
  }, () => handleMegaboxFindNearbyTheaters(c)));
12
12
  app.get('/api/megabox/movies', async (c) => withEdgeCache(c.req.url, {
13
13
  ttlSeconds: 60 * 10,
14
14
  staleWhileRevalidateSeconds: 60,
15
- keyPrefix: 'megabox-movies-v1',
15
+ keyPrefix: 'megabox-movies-v2',
16
16
  }, () => handleMegaboxListNowShowing(c)));
17
17
  app.get('/api/megabox/seats', async (c) => withEdgeCache(c.req.url, {
18
18
  ttlSeconds: 60 * 3,
19
19
  staleWhileRevalidateSeconds: 30,
20
- keyPrefix: 'megabox-seats-v1',
20
+ keyPrefix: 'megabox-seats-v2',
21
21
  }, () => handleMegaboxGetRemainingSeats(c)));
22
22
  }
@@ -4,21 +4,21 @@ export function registerOpinetRoutes(app) {
4
4
  app.get('/api/opinet/average', async (c) => withEdgeCache(c.req.url, {
5
5
  ttlSeconds: 60 * 60,
6
6
  staleWhileRevalidateSeconds: 60 * 30,
7
- keyPrefix: 'opinet-average-v1',
7
+ keyPrefix: 'opinet-average-v2',
8
8
  }, () => handleOpinetAveragePrices(c)));
9
9
  app.get('/api/opinet/lowest', async (c) => withEdgeCache(c.req.url, {
10
10
  ttlSeconds: 60 * 30,
11
11
  staleWhileRevalidateSeconds: 60 * 10,
12
- keyPrefix: 'opinet-lowest-v1',
12
+ keyPrefix: 'opinet-lowest-v2',
13
13
  }, () => handleOpinetLowestStations(c)));
14
14
  app.get('/api/opinet/stations/around', async (c) => withEdgeCache(c.req.url, {
15
15
  ttlSeconds: 60 * 20,
16
16
  staleWhileRevalidateSeconds: 60 * 5,
17
- keyPrefix: 'opinet-around-v1',
17
+ keyPrefix: 'opinet-around-v2',
18
18
  }, () => handleOpinetStationsAround(c)));
19
19
  app.get('/api/opinet/station', async (c) => withEdgeCache(c.req.url, {
20
20
  ttlSeconds: 60 * 60,
21
21
  staleWhileRevalidateSeconds: 60 * 10,
22
- keyPrefix: 'opinet-station-v1',
22
+ keyPrefix: 'opinet-station-v2',
23
23
  }, () => handleOpinetStationDetail(c)));
24
24
  }
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, handleCompareProducts, handlePlaces, handleGs25Stores, handleGs25Products, handleGs25Inventory, handleSevenElevenProducts, handleSevenElevenStores, handleSevenElevenInventory, handleSevenElevenPopwords, handleSevenElevenCatalog, } from './cli/commands/convenience.js';
15
+ import { handleCuStores, handleCuInventory, handleEmart24Stores, handleEmart24Products, handleEmart24Inventory, handleCompareProducts, handlePlaces, handleGs25Stores, handleGs25Products, handleGs25Inventory, handleSevenElevenProducts, handleSevenElevenStores, handleSevenElevenInventory, handleSevenElevenPopwords, handleSevenElevenCatalog, } from './cli/commands/convenience.js';
16
16
  import { handleCgvTheaters, handleCgvMovies, handleCgvTimetable, handleLottecinemaTheaters, handleLottecinemaMovies, handleLottecinemaSeats, handleMegaboxTheaters, handleMegaboxMovies, handleMegaboxSeats, } from './cli/commands/cinema.js';
17
17
  function rejectTopLevelUnknownOptions(options, command, deps) {
18
18
  const parsed = parseCliArgs(options);
@@ -101,6 +101,10 @@ export async function runCli(argv, deps) {
101
101
  resolvedDeps.writeOut(`claude ${cliArgs.join(' ')}`);
102
102
  return 0;
103
103
  }
104
+ if (command === 'lottemart-stores' || command === 'lottemart-products') {
105
+ resolvedDeps.writeErr('롯데마트 서비스 지원이 종료되었습니다.');
106
+ return 1;
107
+ }
104
108
  if (command === 'get')
105
109
  return await handleGet(options, resolvedDeps);
106
110
  if (command === 'products')
@@ -145,10 +149,6 @@ export async function runCli(argv, deps) {
145
149
  return await handleEmart24Products(options, resolvedDeps);
146
150
  if (command === 'emart24-inventory')
147
151
  return await handleEmart24Inventory(options, resolvedDeps);
148
- if (command === 'lottemart-stores')
149
- return await handleLotteMartStores(options, resolvedDeps);
150
- if (command === 'lottemart-products')
151
- return await handleLotteMartProducts(options, resolvedDeps);
152
152
  if (command === 'gs25-stores')
153
153
  return await handleGs25Stores(options, resolvedDeps);
154
154
  if (command === 'gs25-products')
package/dist/cliHelp.js CHANGED
@@ -33,8 +33,6 @@ export function printHelp(writeOut) {
33
33
  writeOut(' npx daiso cgv-theaters 강남 --limit 5');
34
34
  writeOut(' npx daiso cgv-timetable --playDate 20260307 --theaterCode 0056');
35
35
  writeOut(' npx daiso lottecinema-theaters --keyword "안산 중앙역" --limit 5');
36
- writeOut(' npx daiso lottemart-stores 잠실 --area 서울 --limit 10');
37
- writeOut(' npx daiso lottemart-products 콜라 --storeName 강변점 --area 서울');
38
36
  writeOut(' npx daiso emart24-products 두바이 --pageSize 20');
39
37
  writeOut(' npx daiso gs25-products 오감자');
40
38
  writeOut(' npx daiso gs25-inventory 오감자 --storeKeyword 강남');
@@ -48,7 +46,6 @@ export function printHelp(writeOut) {
48
46
  writeOut(' 제품명을 먼저 검색해 productId를 확인: npx daiso products 수납박스');
49
47
  writeOut(' productId로 재고 확인: npx daiso inventory <productId> --keyword 강남역');
50
48
  writeOut(' 매장을 먼저 검색해 storeName 또는 storeCode를 확인: npx daiso stores 강남역');
51
- writeOut(' 롯데마트 매장 확인: npx daiso lottemart-stores 잠실 --area 서울');
52
49
  writeOut('');
53
50
  writeOut('상세 도움말:');
54
51
  writeOut(' npx daiso help <command>');
@@ -26,8 +26,6 @@ export const COMMAND_LIST = [
26
26
  'megabox-theaters',
27
27
  'megabox-movies',
28
28
  'megabox-seats',
29
- 'lottemart-stores',
30
- 'lottemart-products',
31
29
  'emart24-stores',
32
30
  'emart24-products',
33
31
  'emart24-inventory',
@@ -262,23 +260,8 @@ export const COMMAND_DETAIL = {
262
260
  '예시: daiso megabox-seats 강남 --playDate 20260310 --movieId 23816',
263
261
  '예시: daiso megabox-seats --playDate 20260310 --keyword "코엑스" --movieId 23816',
264
262
  ],
265
- 'lottemart-stores': [
266
- '명령: lottemart-stores',
267
- '설명: 롯데마트 계열 매장 검색 API를 호출합니다.',
268
- '사용법: daiso lottemart-stores [keyword] [--area 값] [--brandVariant 값] [--lat 값] [--lng 값] [--limit N] [--json]',
269
- '옵션: --keyword, --area, --brandVariant, --lat, --lng, --limit, --json',
270
- '예시: daiso lottemart-stores 잠실 --area 서울',
271
- '예시: daiso lottemart-stores --area 경기 --brandVariant lottemart --limit 10',
272
- ],
273
- 'lottemart-products': [
274
- '명령: lottemart-products',
275
- '설명: 롯데마트 매장 기준 상품 검색 API를 호출합니다.',
276
- '사용법: daiso lottemart-products <keyword> [--storeCode 값 | --storeName 값] [--area 값] [--pageLimit N] [--json]',
277
- '필수: <keyword>, --storeCode 또는 --storeName',
278
- '옵션: --storeCode, --storeName, --area, --pageLimit, --json',
279
- '예시: daiso lottemart-products 콜라 --storeName 강변점 --area 서울',
280
- '예시: daiso lottemart-products 우유 --storeCode 2301 --pageLimit 2',
281
- ],
263
+ 'lottemart-stores': ['롯데마트 서비스 지원이 종료되었습니다.'],
264
+ 'lottemart-products': ['롯데마트 서비스 지원이 종료되었습니다.'],
282
265
  'emart24-stores': [
283
266
  '명령: emart24-stores',
284
267
  '설명: 이마트24 매장 검색 API를 호출합니다.',
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { handleLotteMartFindStores } from './api/lottemartHandlers.js';
2
+ import { geocodeBindings } from './utils/geocode.js';
1
3
  /**
2
4
  * 다중 서비스 MCP 서버
3
5
  *
@@ -17,7 +19,6 @@ import { createDtryxService } from './services/dtryx/index.js';
17
19
  import { createMegaboxService } from './services/megabox/index.js';
18
20
  import { createCgvService } from './services/cgv/index.js';
19
21
  import { createLotteCinemaService } from './services/lottecinema/index.js';
20
- import { createLotteMartService } from './services/lottemart/index.js';
21
22
  import { createCuService } from './services/cu/index.js';
22
23
  import { createEmart24Service } from './services/emart24/index.js';
23
24
  import { createGs25Service } from './services/gs25/index.js';
@@ -50,7 +51,7 @@ const createRegistry = (bindings) => {
50
51
  registry.registerAll([
51
52
  createDaisoService,
52
53
  () => createGs25Service({
53
- googleMapsApiKey: bindings?.GOOGLE_MAPS_API_KEY,
54
+ ...geocodeBindings(bindings),
54
55
  zyteApiKey: bindings?.ZYTE_API_KEY,
55
56
  apiKey: bindings?.GS25_API_KEY,
56
57
  }),
@@ -66,25 +67,21 @@ const createRegistry = (bindings) => {
66
67
  }),
67
68
  () => createOpinetService({
68
69
  apiKey: bindings?.OPINET_API_KEY,
69
- googleMapsApiKey: bindings?.GOOGLE_MAPS_API_KEY,
70
+ ...geocodeBindings(bindings),
70
71
  }),
71
72
  () => createCuService({
72
73
  zyteApiKey: bindings?.ZYTE_API_KEY,
73
- googleMapsApiKey: bindings?.GOOGLE_MAPS_API_KEY,
74
+ ...geocodeBindings(bindings),
74
75
  }),
75
76
  createEmart24Service,
76
- () => createLotteMartService({
77
- googleMapsApiKey: bindings?.GOOGLE_MAPS_API_KEY,
78
- zyteApiKey: bindings?.ZYTE_API_KEY,
79
- }),
80
- createMegaboxService,
77
+ () => createMegaboxService(geocodeBindings(bindings)),
81
78
  () => createDtryxService(dtryxTransportFromBindings(bindings)),
82
79
  () => createLotteCinemaService({
83
- googleMapsApiKey: bindings?.GOOGLE_MAPS_API_KEY,
80
+ ...geocodeBindings(bindings),
84
81
  }),
85
82
  () => createCgvService({
86
83
  zyteApiKey: bindings?.ZYTE_API_KEY,
87
- googleMapsApiKey: bindings?.GOOGLE_MAPS_API_KEY,
84
+ ...geocodeBindings(bindings),
88
85
  }),
89
86
  () => createOliveyoungService({
90
87
  relayUrl: bindings?.OY_RELAY_URL,
@@ -303,6 +300,8 @@ app.get('/privacy', (c) => {
303
300
  });
304
301
  // GET API 엔드포인트 (MCP 미지원 에이전트용)
305
302
  app.get('/api/actions/query', async (c) => {
303
+ if (['lottemartFindStores', 'lottemartSearchProducts'].includes(c.req.query('action') || ''))
304
+ return handleLotteMartFindStores(c);
306
305
  try {
307
306
  const targetUrl = buildActionQueryTargetUrl(c.req.url);
308
307
  const headers = new Headers(c.req.raw.headers);
@@ -9,7 +9,6 @@ const PROTECTED_SERVICES = [
9
9
  { prefix: '/api/cgv/', service: 'cgv' },
10
10
  { prefix: '/api/cu/', service: 'cu' },
11
11
  { prefix: '/api/gs25/', service: 'gs25' },
12
- { prefix: '/api/lottemart/', service: 'lottemart' },
13
12
  ];
14
13
  const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
15
14
  // Cloudflare가 교차 존 Worker subrequest에 사용하는 공통 client IP
@@ -4,7 +4,6 @@
4
4
  import { OPENAPI_PATHS_DAISO_OLIVEYOUNG } from './openapiSpecPathsDaisoOliveyoung.js';
5
5
  import { OPENAPI_PATHS_CU } from './openapiSpecPathsCu.js';
6
6
  import { OPENAPI_PATHS_EMART24 } from './openapiSpecPathsEmart24.js';
7
- import { OPENAPI_PATHS_LOTTEMART } from './openapiSpecPathsLotteMart.js';
8
7
  import { OPENAPI_PATHS_MEGABOX } from './openapiSpecPathsMegabox.js';
9
8
  import { OPENAPI_PATHS_CGV } from './openapiSpecPathsCgv.js';
10
9
  import { OPENAPI_PATHS_LOTTECINEMA } from './openapiSpecPathsLottecinema.js';
@@ -54,7 +53,6 @@ export function generateFullOpenApiSpec(baseUrl) {
54
53
  ...OPENAPI_PATHS_DAISO_OLIVEYOUNG,
55
54
  ...OPENAPI_PATHS_CU,
56
55
  ...OPENAPI_PATHS_EMART24,
57
- ...OPENAPI_PATHS_LOTTEMART,
58
56
  ...OPENAPI_PATHS_GS25,
59
57
  ...OPENAPI_PATHS_SEVENELEVEN,
60
58
  ...OPENAPI_PATHS_COMPARE,
@@ -192,7 +192,7 @@ export const ACTION_QUERY_PARAMETERS = [
192
192
  name: 'area',
193
193
  in: 'query',
194
194
  required: false,
195
- description: '롯데마트 지역',
195
+ description: '지역',
196
196
  schema: { type: 'string' },
197
197
  },
198
198
  {
@@ -220,7 +220,7 @@ export const ACTION_QUERY_PARAMETERS = [
220
220
  name: 'brandVariant',
221
221
  in: 'query',
222
222
  required: false,
223
- description: '롯데마트 브랜드 변형',
223
+ description: '브랜드 변형',
224
224
  schema: { type: 'string' },
225
225
  },
226
226
  {
@@ -255,7 +255,7 @@ export const ACTION_QUERY_PARAMETERS = [
255
255
  name: 'pageLimit',
256
256
  in: 'query',
257
257
  required: false,
258
- description: '롯데마트 추가 페이지 수',
258
+ description: '추가 페이지 수',
259
259
  schema: { type: 'integer' },
260
260
  },
261
261
  {
@@ -304,7 +304,7 @@ export const ACTION_QUERY_PARAMETERS = [
304
304
  name: 'storeName',
305
305
  in: 'query',
306
306
  required: false,
307
- description: '매장명. 롯데마트 상품 조회에서 storeCode가 없을 때 사용하며, 모르면 먼저 lottemartFindNearbyStores 또는 /api/lottemart/stores로 확인하세요.',
307
+ description: '매장명',
308
308
  schema: { type: 'string' },
309
309
  },
310
310
  {
@@ -7,7 +7,6 @@ import { OPENAPI_DAISO_OLIVEYOUNG_COMPONENT_SCHEMAS } from './openapiSpecCompone
7
7
  import { OPENAPI_EMART24_COMPONENT_SCHEMAS } from './openapiSpecComponentsEmart24.js';
8
8
  import { OPENAPI_GS25_COMPONENT_SCHEMAS } from './openapiSpecComponentsGs25.js';
9
9
  import { OPENAPI_LOTTECINEMA_COMPONENT_SCHEMAS } from './openapiSpecComponentsLottecinema.js';
10
- import { OPENAPI_LOTTEMART_COMPONENT_SCHEMAS } from './openapiSpecComponentsLotteMart.js';
11
10
  import { OPENAPI_MEGABOX_COMPONENT_SCHEMAS } from './openapiSpecComponentsMegabox.js';
12
11
  import { OPENAPI_SEVENELEVEN_COMPONENT_SCHEMAS } from './openapiSpecComponentsSeveneleven.js';
13
12
  export const OPENAPI_COMPONENTS = {
@@ -16,7 +15,6 @@ export const OPENAPI_COMPONENTS = {
16
15
  ...OPENAPI_MEGABOX_COMPONENT_SCHEMAS,
17
16
  ...OPENAPI_CU_COMPONENT_SCHEMAS,
18
17
  ...OPENAPI_EMART24_COMPONENT_SCHEMAS,
19
- ...OPENAPI_LOTTEMART_COMPONENT_SCHEMAS,
20
18
  ...OPENAPI_GS25_COMPONENT_SCHEMAS,
21
19
  ...OPENAPI_SEVENELEVEN_COMPONENT_SCHEMAS,
22
20
  ...OPENAPI_LOTTECINEMA_COMPONENT_SCHEMAS,
@@ -6,7 +6,7 @@ export const OPENAPI_PATHS_MEGABOX = {
6
6
  get: {
7
7
  operationId: 'megaboxFindNearbyTheaters',
8
8
  summary: '메가박스 주변 지점 조회',
9
- description: '좌표 또는 위치 키워드 기준으로 메가박스 지점을 거리순으로 조회합니다. lat/lng가 없으면 keyword를 구글 지오코드로 보강할 수 있습니다.',
9
+ description: '좌표 또는 위치 키워드 기준으로 메가박스 지점을 거리순으로 조회합니다. lat/lng가 없으면 keyword를 카카오 무료 위치 검색(장소명은 네이버 지역 검색으로 보완)로 보강할 수 있습니다.',
10
10
  parameters: [
11
11
  {
12
12
  name: 'keyword',
@@ -54,8 +54,6 @@ export function buildPromptReferenceText(baseUrl) {
54
54
  | GS25_STORE_SEARCH_FAILED | GS25 매장 조회 실패 |
55
55
  | GS25_PRODUCT_SEARCH_FAILED | GS25 상품 조회 실패 |
56
56
  | GS25_INVENTORY_CHECK_FAILED | GS25 재고 조회 실패 |
57
- | LOTTEMART_STORE_SEARCH_FAILED | 롯데마트 매장 조회 실패 |
58
- | LOTTEMART_PRODUCT_SEARCH_FAILED | 롯데마트 상품 조회 실패 |
59
57
  | SEVENELEVEN_INVENTORY_CHECK_FAILED | 세븐일레븐 재고 조회 실패 |
60
58
  | PLACES_SEARCH_FAILED | 주변 장소 검색 실패 |
61
59
  | DEVELOPER_REQUEST_SUBMIT_FAILED | 개발자 요청 저장 실패 |
@@ -107,7 +105,6 @@ export function buildPromptReferenceText(baseUrl) {
107
105
  - 너무 넓은 축약어로만 매칭된 경우에는 확정 상품처럼 단정하지 말고, 추정 매칭임을 짧게 밝힌 뒤 재고를 안내합니다.
108
106
  - 위치/브랜드/상품명은 한 인자에 섞지 말고 가능한 한 분리해서 도구에 전달합니다.
109
107
  8. **위치 기반 재고**: lat, lng 파라미터로 가까운 매장 우선 조회
110
- 9. **롯데마트 상품 조회**: /api/lottemart/products는 keyword와 함께 storeCode 또는 storeName이 필요합니다.
111
108
  10. **세븐일레븐 재고 조회**: /api/seveneleven/inventory에 keyword + storeKeyword를 함께 주면 매장별 수량을 바로 확인할 수 있습니다.
112
109
  11. **이마트24 재고 조회**: /api/emart24/inventory는 pluCd + storeKeyword 조합도 지원하므로, 상품 선택 뒤 매장 코드를 다시 모으지 않아도 됩니다.
113
110
  12. **올리브영 재고 해석**: inventory.products[].storeInventory.stores[]가 있으면 그 매장별 stockLabel과 remainQuantity를 우선 사용하고, inStock는 그 주변 매장 기준 결과로 해석합니다.
@@ -128,8 +125,6 @@ MCP 연결 정보: ${baseUrl}/mcp
128
125
  - daiso_check_inventory: 재고 확인
129
126
  - daiso_get_price_info: 가격 정보 조회
130
127
  - daiso_get_display_location: 진열 위치 조회
131
- - lottemart_find_nearby_stores: 롯데마트 주변 매장 탐색
132
- - lottemart_search_products: 롯데마트 상품 검색
133
128
  - gs25_find_nearby_stores: GS25 주변 매장 탐색
134
129
  - gs25_search_products: GS25 상품 검색
135
130
  - gs25_check_inventory: GS25 재고 조회
@@ -48,32 +48,6 @@ export function buildPromptRetailCinemaText(baseUrl) {
48
48
 
49
49
  ---
50
50
 
51
- ### 6-2. 롯데마트 매장/상품 조회
52
-
53
- **설명**: 롯데마트 계열 매장 검색과 특정 매장 기준 상품 가격/재고 조회를 제공합니다.
54
-
55
- **URL**:
56
- - ${baseUrl}/api/lottemart/stores?keyword={키워드}
57
- - ${baseUrl}/api/lottemart/products?keyword={검색어}&storeName={매장명}
58
-
59
- **선택 파라미터**:
60
- - area: 지역 (예: 서울, 경기, 제주)
61
- - brandVariant: lottemart, toysrus, max, bottlebunker, mealguru, grandgrocery
62
- - lat: 위도 (선택)
63
- - lng: 경도 (선택)
64
- - limit: 최대 결과 수 (기본값: 20)
65
- - storeCode 또는 storeName: 상품 검색 대상 매장
66
- - pageLimit: 추가 조회할 최대 페이지 수 (기본값: 3)
67
- - source: 상품 검색 경로 (auto, legacy, zetta). 빠른 검색이나 헬스체크에는 zetta 권장
68
-
69
- **예시**:
70
- - ${baseUrl}/api/lottemart/stores?keyword=잠실&area=서울&limit=10
71
- - ${baseUrl}/api/lottemart/stores?area=경기&brandVariant=lottemart&limit=10
72
- - ${baseUrl}/api/lottemart/products?keyword=콜라&storeName=강변점&area=서울
73
- - ${baseUrl}/api/lottemart/products?keyword=우유&storeCode=2301&pageLimit=2&source=zetta
74
-
75
- ---
76
-
77
51
  ### 6-3. GS25 매장/상품/재고 조회
78
52
 
79
53
  **설명**: GS25 매장 탐색, 상품 키워드 검색, 재고 조회를 제공합니다.
@@ -1,6 +1,3 @@
1
- /**
2
- * CGV 서비스 프로바이더
3
- */
4
1
  import { createFindTheatersTool } from './tools/findTheaters.js';
5
2
  import { createSearchMoviesTool } from './tools/searchMovies.js';
6
3
  import { createGetTimetableTool } from './tools/getTimetable.js';
@@ -18,9 +15,9 @@ class CgvService {
18
15
  metadata = CGV_METADATA;
19
16
  getTools() {
20
17
  return [
21
- createFindTheatersTool(this.options.zyteApiKey, this.options.googleMapsApiKey),
22
- createSearchMoviesTool(this.options.zyteApiKey, this.options.googleMapsApiKey),
23
- createGetTimetableTool(this.options.zyteApiKey, this.options.googleMapsApiKey),
18
+ createFindTheatersTool(this.options.zyteApiKey, this.options.googleMapsApiKey, this.options),
19
+ createSearchMoviesTool(this.options.zyteApiKey, this.options.googleMapsApiKey, this.options),
20
+ createGetTimetableTool(this.options.zyteApiKey, this.options.googleMapsApiKey, this.options),
24
21
  ];
25
22
  }
26
23
  }
@@ -1,32 +1,11 @@
1
1
  /**
2
2
  * CGV 위치 해석 및 근처 극장 조회 보조 모듈
3
3
  */
4
- import { fetchJson } from '../../utils/http.js';
4
+ import { geocodeLocation, readGeocodeEnvironment, searchKakaoPlaces, validKoreanCoordinates, } from '../../utils/geocode.js';
5
5
  import { fetchCgvTheaters } from './client.js';
6
6
  const DEFAULT_TIMEOUT_MS = 15000;
7
- const USER_GEOCODE_CACHE_TTL_MS = 60 * 60 * 1000;
8
- const THEATER_GEOCODE_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
9
- const MAX_CANDIDATE_THEATERS = 12;
10
- const LOCATION_STOPWORDS = new Set([
11
- 'cgv',
12
- '극장',
13
- '영화',
14
- '주변',
15
- '근처',
16
- '오늘',
17
- '상영',
18
- '시간표',
19
- '좌석',
20
- '잔여',
21
- '찾아',
22
- '주세요',
23
- '주세요요',
24
- '역',
25
- ]);
26
7
  const BRAND_PATTERN = /\b(?:cgv|씨지브이)\b/giu;
27
8
  const TRAILING_INTENT_PATTERN = /\s+(?:극장|영화관|영화|상영작|시간표|좌석|잔여좌석|남은좌석|목록|찾고|찾아|찾아서|알려|보여|추천|조회|확인|해주세요|해줘).*/u;
28
- const userGeocodeCache = new Map();
29
- const theaterGeocodeCache = new Map();
30
9
  function calculateDistanceKm(lat1, lng1, lat2, lng2) {
31
10
  const toRad = (deg) => (deg * Math.PI) / 180;
32
11
  const dLat = toRad(lat2 - lat1);
@@ -60,61 +39,12 @@ function sanitizeLocationKeyword(keyword) {
60
39
  }
61
40
  return sanitized || trimmed;
62
41
  }
63
- function extractLocationTokens(...values) {
64
- const tokens = new Set();
65
- for (const value of values) {
66
- const matches = (value || '').match(/[0-9A-Za-z가-힣]+/g) || [];
67
- for (const match of matches) {
68
- const normalized = normalizeText(match);
69
- if (normalized.length < 2 || LOCATION_STOPWORDS.has(normalized)) {
70
- continue;
71
- }
72
- tokens.add(normalized);
73
- }
74
- }
75
- return [...tokens];
76
- }
77
- function scoreTheaterName(theaterName, tokens) {
78
- const normalized = normalizeText(theaterName);
79
- return tokens.reduce((score, token) => (normalized.includes(token) ? score + 1 : score), 0);
80
- }
81
- async function geocodeAddress(query, options, cacheKey, ttlMs, cache) {
82
- const apiKey = (options.googleMapsApiKey || '').trim();
83
- const trimmedQuery = query.trim();
84
- if (apiKey.length === 0 || trimmedQuery.length === 0) {
85
- return null;
86
- }
87
- const cached = cache.get(cacheKey);
88
- if (cached && cached.expiresAt > Date.now()) {
89
- return cached.value;
90
- }
91
- const endpoint = new URL('https://maps.googleapis.com/maps/api/geocode/json');
92
- endpoint.searchParams.set('address', trimmedQuery);
93
- endpoint.searchParams.set('key', apiKey);
94
- const body = await fetchJson(endpoint.toString(), {
95
- method: 'GET',
96
- timeout: options.timeout || DEFAULT_TIMEOUT_MS,
97
- headers: { Accept: 'application/json' },
98
- });
99
- const firstResult = body.status === 'OK' ? body.results?.[0] : null;
100
- const latitude = firstResult?.geometry?.location?.lat;
101
- const longitude = firstResult?.geometry?.location?.lng;
102
- const value = typeof latitude === 'number' && typeof longitude === 'number'
103
- ? {
104
- latitude,
105
- longitude,
106
- formattedAddress: firstResult?.formatted_address || null,
107
- }
108
- : null;
109
- cache.set(cacheKey, {
110
- expiresAt: Date.now() + ttlMs,
111
- value,
112
- });
113
- return value;
114
- }
115
42
  export async function resolveCgvLocation(params, options = {}) {
116
43
  const keyword = sanitizeLocationKeyword((params.keyword || '').trim());
117
44
  const hasCoordinates = isValidCoordinate(params.latitude) && isValidCoordinate(params.longitude);
45
+ if (hasCoordinates &&
46
+ !validKoreanCoordinates(params.latitude, params.longitude))
47
+ throw new Error('유효한 위도/경도를 입력해주세요.');
118
48
  if (hasCoordinates) {
119
49
  return {
120
50
  keyword: keyword.length > 0 ? keyword : null,
@@ -133,7 +63,7 @@ export async function resolveCgvLocation(params, options = {}) {
133
63
  formattedAddress: null,
134
64
  };
135
65
  }
136
- const geocoded = await geocodeAddress(keyword, options, `user:${keyword}`, USER_GEOCODE_CACHE_TTL_MS, userGeocodeCache);
66
+ const geocoded = await geocodeLocation(keyword, options);
137
67
  return {
138
68
  keyword,
139
69
  latitude: geocoded?.latitude ?? null,
@@ -142,28 +72,6 @@ export async function resolveCgvLocation(params, options = {}) {
142
72
  formattedAddress: geocoded?.formattedAddress ?? null,
143
73
  };
144
74
  }
145
- async function geocodeCgvTheater(theater, options) {
146
- const cleanName = theater.theaterName.replace(/^CGV\s*/i, '').trim();
147
- const geocoded = await geocodeAddress(`대한민국 CGV ${cleanName}`, options, `theater:${theater.theaterCode}:${cleanName}`, THEATER_GEOCODE_CACHE_TTL_MS, theaterGeocodeCache);
148
- return geocoded
149
- ? {
150
- latitude: geocoded.latitude,
151
- longitude: geocoded.longitude,
152
- address: geocoded.formattedAddress,
153
- }
154
- : null;
155
- }
156
- function pickCandidateTheaters(theaters, tokens) {
157
- if (tokens.length === 0) {
158
- return theaters.slice(0, MAX_CANDIDATE_THEATERS);
159
- }
160
- const scored = theaters
161
- .map((theater) => ({ theater, score: scoreTheaterName(theater.theaterName, tokens) }))
162
- .filter((item) => item.score > 0)
163
- .sort((left, right) => right.score - left.score || left.theater.theaterName.localeCompare(right.theater.theaterName))
164
- .map((item) => item.theater);
165
- return scored.slice(0, MAX_CANDIDATE_THEATERS);
166
- }
167
75
  export async function fetchCgvNearbyTheaters(params, options = {}) {
168
76
  const limit = Math.max(1, params.limit || 10);
169
77
  const timeout = params.timeout || DEFAULT_TIMEOUT_MS;
@@ -174,34 +82,40 @@ export async function fetchCgvNearbyTheaters(params, options = {}) {
174
82
  zyteApiKey: options.zyteApiKey,
175
83
  });
176
84
  const location = await resolveCgvLocation(params, options);
177
- const tokens = extractLocationTokens(location.keyword, location.formattedAddress);
178
- const hasGoogleKey = (options.googleMapsApiKey || '').trim().length > 0;
179
- if (tokens.length === 0 && !hasGoogleKey) {
180
- return {
181
- ...location,
182
- playDate: params.playDate,
183
- regionCode: params.regionCode || null,
184
- count: 0,
185
- theaters: [],
186
- };
85
+ if (location.keyword && location.latitude === null)
86
+ throw new Error(`위치를 좌표로 변환하지 못했습니다: ${location.keyword}`);
87
+ const geocodeOptions = { ...readGeocodeEnvironment(), ...options };
88
+ let resolvedCandidates;
89
+ if (location.latitude !== null && location.longitude !== null) {
90
+ // 주변 장소를 공식 CGV 목록에 연결하여 전국 앞부분만 검색하는 오류를 막습니다.
91
+ const places = await searchKakaoPlaces('CGV', geocodeOptions, {
92
+ latitude: location.latitude,
93
+ longitude: location.longitude,
94
+ });
95
+ resolvedCandidates = places.flatMap((place) => {
96
+ const cleanName = normalizeText((place.place_name || '').replace(/^CGV\s*/iu, ''));
97
+ const theater = theaters.find((item) => normalizeText(item.theaterName.replace(/^CGV\s*/iu, '')) === cleanName);
98
+ const latitude = Number(place.y);
99
+ const longitude = Number(place.x);
100
+ if (!theater || !validKoreanCoordinates(latitude, longitude))
101
+ return [];
102
+ return [
103
+ {
104
+ ...theater,
105
+ latitude,
106
+ longitude,
107
+ address: place.road_address_name || place.address_name || null,
108
+ distanceKm: Number(calculateDistanceKm(location.latitude, location.longitude, latitude, longitude).toFixed(2)),
109
+ },
110
+ ];
111
+ });
112
+ }
113
+ else {
114
+ resolvedCandidates = [];
187
115
  }
188
- const candidates = pickCandidateTheaters(theaters, tokens);
189
- const resolvedCandidates = await Promise.all(candidates.map(async (theater) => {
190
- const geocoded = await geocodeCgvTheater(theater, options);
191
- const distanceKm = geocoded && location.latitude !== null && location.longitude !== null
192
- ? Number(calculateDistanceKm(location.latitude, location.longitude, geocoded.latitude, geocoded.longitude).toFixed(2))
193
- : null;
194
- return {
195
- ...theater,
196
- latitude: geocoded?.latitude ?? null,
197
- longitude: geocoded?.longitude ?? null,
198
- distanceKm,
199
- address: geocoded?.address ?? null,
200
- };
201
- }));
202
116
  const sorted = resolvedCandidates.sort((left, right) => {
203
- const leftDistance = left.distanceKm ?? Number.POSITIVE_INFINITY;
204
- const rightDistance = right.distanceKm ?? Number.POSITIVE_INFINITY;
117
+ const leftDistance = left.distanceKm;
118
+ const rightDistance = right.distanceKm;
205
119
  return leftDistance - rightDistance || left.theaterName.localeCompare(right.theaterName);
206
120
  });
207
121
  return {
@@ -230,6 +144,5 @@ export async function resolveCgvNearestTheater(params, options = {}) {
230
144
  };
231
145
  }
232
146
  export function __testOnlyClearCgvLocationCaches() {
233
- userGeocodeCache.clear();
234
- theaterGeocodeCache.clear();
147
+ // 캐시를 유지하지 않으므로 초기화할 상태가 없습니다.
235
148
  }
@@ -4,7 +4,7 @@
4
4
  import * as z from 'zod';
5
5
  import { fetchCgvTheaters, toYyyymmdd } from '../client.js';
6
6
  import { fetchCgvNearbyTheaters } from '../location.js';
7
- async function findTheaters(args, apiKey, googleMapsApiKey) {
7
+ async function findTheaters(args, apiKey, googleMapsApiKey, geocodeOptions = {}) {
8
8
  const { playDate = toYyyymmdd(), regionCode, keyword, latitude, longitude, limit = 30, timeoutMs = 15000, } = args;
9
9
  if (keyword || typeof latitude === 'number' || typeof longitude === 'number') {
10
10
  const result = await fetchCgvNearbyTheaters({
@@ -18,7 +18,8 @@ async function findTheaters(args, apiKey, googleMapsApiKey) {
18
18
  }, {
19
19
  timeout: timeoutMs,
20
20
  zyteApiKey: apiKey,
21
- googleMapsApiKey: googleMapsApiKey || process.env.GOOGLE_MAPS_API_KEY,
21
+ ...geocodeOptions,
22
+ googleMapsApiKey,
22
23
  });
23
24
  return {
24
25
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
@@ -47,7 +48,7 @@ async function findTheaters(args, apiKey, googleMapsApiKey) {
47
48
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
48
49
  };
49
50
  }
50
- export function createFindTheatersTool(apiKey, googleMapsApiKey) {
51
+ export function createFindTheatersTool(apiKey, googleMapsApiKey, geocodeOptions = {}) {
51
52
  return {
52
53
  name: 'cgv_find_theaters',
53
54
  metadata: {
@@ -63,6 +64,6 @@ export function createFindTheatersTool(apiKey, googleMapsApiKey) {
63
64
  timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
64
65
  },
65
66
  },
66
- handler: ((args) => findTheaters(args, apiKey, googleMapsApiKey)),
67
+ handler: ((args) => findTheaters(args, apiKey, googleMapsApiKey, geocodeOptions)),
67
68
  };
68
69
  }
@@ -5,7 +5,7 @@ import * as z from 'zod';
5
5
  import { fetchCgvTimetable, toYyyymmdd } from '../client.js';
6
6
  import { resolveCgvNearestTheater } from '../location.js';
7
7
  import { filterAndSortTimetable } from '../timetable.js';
8
- async function getTimetable(args, apiKey, googleMapsApiKey) {
8
+ async function getTimetable(args, apiKey, googleMapsApiKey, geocodeOptions = {}) {
9
9
  const { playDate = toYyyymmdd(), theaterCode, movieCode, keyword, latitude, longitude, limit = 50, timeoutMs = 15000, } = args;
10
10
  let resolvedTheater = null;
11
11
  let targetTheaterCode = theaterCode;
@@ -19,7 +19,8 @@ async function getTimetable(args, apiKey, googleMapsApiKey) {
19
19
  }, {
20
20
  timeout: timeoutMs,
21
21
  zyteApiKey: apiKey,
22
- googleMapsApiKey: googleMapsApiKey || process.env.GOOGLE_MAPS_API_KEY,
22
+ ...geocodeOptions,
23
+ googleMapsApiKey,
23
24
  });
24
25
  resolvedTheater = resolved.theater;
25
26
  targetTheaterCode = resolved.theater?.theaterCode;
@@ -53,7 +54,7 @@ async function getTimetable(args, apiKey, googleMapsApiKey) {
53
54
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
54
55
  };
55
56
  }
56
- export function createGetTimetableTool(apiKey, googleMapsApiKey) {
57
+ export function createGetTimetableTool(apiKey, googleMapsApiKey, geocodeOptions = {}) {
57
58
  return {
58
59
  name: 'cgv_get_timetable',
59
60
  metadata: {
@@ -70,6 +71,6 @@ export function createGetTimetableTool(apiKey, googleMapsApiKey) {
70
71
  timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
71
72
  },
72
73
  },
73
- handler: ((args) => getTimetable(args, apiKey, googleMapsApiKey)),
74
+ handler: ((args) => getTimetable(args, apiKey, googleMapsApiKey, geocodeOptions)),
74
75
  };
75
76
  }