daiso 1.0.4 → 1.0.6

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 (166) hide show
  1. package/README.md +249 -533
  2. package/dist/api/actionsProxy.js +82 -0
  3. package/dist/api/cgvHandlers.js +103 -16
  4. package/dist/api/configStatus.js +19 -0
  5. package/dist/api/daisoHandlers.js +127 -0
  6. package/dist/api/emart24Handlers.js +176 -0
  7. package/dist/api/gs25Handlers.js +267 -0
  8. package/dist/api/handlers.js +209 -104
  9. package/dist/api/healthChecks.js +504 -0
  10. package/dist/api/lottecinemaHandlers.js +155 -0
  11. package/dist/api/lottemartHandlers.js +144 -0
  12. package/dist/api/megaboxHandlers.js +81 -50
  13. package/dist/api/response.js +2 -0
  14. package/dist/api/routes/cuRoutes.js +17 -0
  15. package/dist/api/routes/daisoRoutes.js +6 -1
  16. package/dist/api/routes/emart24Routes.js +22 -0
  17. package/dist/api/routes/gs25Routes.js +22 -0
  18. package/dist/api/routes/healthRoutes.js +61 -0
  19. package/dist/api/routes/lottecinemaRoutes.js +22 -0
  20. package/dist/api/routes/lottemartRoutes.js +18 -0
  21. package/dist/api/routes/oliveyoungRoutes.js +6 -1
  22. package/dist/api/routes/sevenelevenRoutes.js +32 -0
  23. package/dist/api/sevenelevenHandlers.js +200 -0
  24. package/dist/bin.js +0 -0
  25. package/dist/cli/args.js +75 -0
  26. package/dist/cli/commandOptions.js +38 -0
  27. package/dist/cli/commands/convenience.js +293 -0
  28. package/dist/cli/commands/daiso.js +112 -0
  29. package/dist/cli/constants.js +5 -0
  30. package/dist/cli/deps.js +48 -0
  31. package/dist/cli/http.js +156 -0
  32. package/dist/cli/interactiveFetch.js +31 -0
  33. package/dist/cli/interactiveItemSearch.js +261 -0
  34. package/dist/cli/interactivePrompt.js +50 -0
  35. package/dist/cli/interactiveTypes.js +4 -0
  36. package/dist/cli/types.js +4 -0
  37. package/dist/cli.js +87 -196
  38. package/dist/cliHelp.js +212 -1
  39. package/dist/cliInteractive.js +243 -0
  40. package/dist/cliPicker.js +57 -0
  41. package/dist/cliRenderer.js +90 -3
  42. package/dist/core/errors.js +66 -0
  43. package/dist/core/registry.js +227 -3
  44. package/dist/index.js +149 -9
  45. package/dist/pages/openapi.js +2 -1
  46. package/dist/pages/openapiFullSpec.js +58 -0
  47. package/dist/pages/openapiResponse.js +28 -0
  48. package/dist/pages/openapiSpec.js +2 -41
  49. package/dist/pages/openapiSpecActions.js +109 -0
  50. package/dist/pages/openapiSpecComponents.js +30 -391
  51. package/dist/pages/openapiSpecComponentsCgv.js +24 -0
  52. package/dist/pages/openapiSpecComponentsCu.js +112 -0
  53. package/dist/pages/openapiSpecComponentsDaisoOliveyoung.js +342 -0
  54. package/dist/pages/openapiSpecComponentsEmart24.js +141 -0
  55. package/dist/pages/openapiSpecComponentsGs25.js +109 -0
  56. package/dist/pages/openapiSpecComponentsLotteMart.js +85 -0
  57. package/dist/pages/openapiSpecComponentsLottecinema.js +163 -0
  58. package/dist/pages/openapiSpecComponentsMegabox.js +144 -0
  59. package/dist/pages/openapiSpecComponentsSeveneleven.js +82 -0
  60. package/dist/pages/openapiSpecPathsCgv.js +66 -3
  61. package/dist/pages/openapiSpecPathsCu.js +145 -0
  62. package/dist/pages/openapiSpecPathsDaiso.js +302 -0
  63. package/dist/pages/openapiSpecPathsDaisoOliveyoung.js +4 -376
  64. package/dist/pages/openapiSpecPathsEmart24.js +223 -0
  65. package/dist/pages/openapiSpecPathsGs25.js +166 -0
  66. package/dist/pages/openapiSpecPathsLotteMart.js +149 -0
  67. package/dist/pages/openapiSpecPathsLottecinema.js +212 -0
  68. package/dist/pages/openapiSpecPathsMegabox.js +52 -3
  69. package/dist/pages/openapiSpecPathsOliveyoung.js +266 -0
  70. package/dist/pages/openapiSpecPathsSeveneleven.js +234 -0
  71. package/dist/pages/prompt.js +8 -402
  72. package/dist/pages/promptCoreText.js +278 -0
  73. package/dist/pages/promptReferenceText.js +137 -0
  74. package/dist/pages/promptRetailCinemaText.js +274 -0
  75. package/dist/services/cgv/client.js +6 -1
  76. package/dist/services/cgv/index.js +3 -3
  77. package/dist/services/cgv/location.js +235 -0
  78. package/dist/services/cgv/tools/findTheaters.js +30 -5
  79. package/dist/services/cgv/tools/getTimetable.js +42 -14
  80. package/dist/services/cgv/tools/searchMovies.js +40 -12
  81. package/dist/services/cgv/transport.js +0 -3
  82. package/dist/services/cu/api.js +11 -0
  83. package/dist/services/cu/client.js +297 -0
  84. package/dist/services/cu/index.js +21 -0
  85. package/dist/services/cu/tools/checkInventory.js +123 -0
  86. package/dist/services/cu/tools/findNearbyStores.js +49 -0
  87. package/dist/services/cu/types.js +4 -0
  88. package/dist/services/daiso/api.js +22 -2
  89. package/dist/services/daiso/client.js +67 -0
  90. package/dist/services/daiso/index.js +4 -0
  91. package/dist/services/daiso/product.js +17 -0
  92. package/dist/services/daiso/tools/checkInventory.js +80 -43
  93. package/dist/services/daiso/tools/findInventoryByName.js +222 -0
  94. package/dist/services/daiso/tools/findStores.js +17 -7
  95. package/dist/services/daiso/tools/getDisplayLocation.js +75 -0
  96. package/dist/services/emart24/api.js +11 -0
  97. package/dist/services/emart24/client.js +182 -0
  98. package/dist/services/emart24/index.js +22 -0
  99. package/dist/services/emart24/inventoryLookup.js +134 -0
  100. package/dist/services/emart24/tools/checkInventory.js +54 -0
  101. package/dist/services/emart24/tools/findNearbyStores.js +72 -0
  102. package/dist/services/emart24/tools/searchProducts.js +73 -0
  103. package/dist/services/emart24/types.js +4 -0
  104. package/dist/services/gs25/api.js +9 -0
  105. package/dist/services/gs25/client.js +208 -0
  106. package/dist/services/gs25/index.js +22 -0
  107. package/dist/services/gs25/storeUtils.js +168 -0
  108. package/dist/services/gs25/tools/checkInventory.js +163 -0
  109. package/dist/services/gs25/tools/findNearbyStores.js +103 -0
  110. package/dist/services/gs25/tools/searchProducts.js +57 -0
  111. package/dist/services/gs25/types.js +4 -0
  112. package/dist/services/lottecinema/api.js +12 -0
  113. package/dist/services/lottecinema/client.js +161 -0
  114. package/dist/services/lottecinema/index.js +30 -0
  115. package/dist/services/lottecinema/location.js +191 -0
  116. package/dist/services/lottecinema/tools/findNearbyTheaters.js +47 -0
  117. package/dist/services/lottecinema/tools/getRemainingSeats.js +85 -0
  118. package/dist/services/lottecinema/tools/listNowShowing.js +79 -0
  119. package/dist/services/lottecinema/types.js +4 -0
  120. package/dist/services/lottemart/api.js +31 -0
  121. package/dist/services/lottemart/client.js +254 -0
  122. package/dist/services/lottemart/clientTypes.js +4 -0
  123. package/dist/services/lottemart/config.js +4 -0
  124. package/dist/services/lottemart/debug.js +92 -0
  125. package/dist/services/lottemart/geocode.js +49 -0
  126. package/dist/services/lottemart/index.js +30 -0
  127. package/dist/services/lottemart/keyword.js +29 -0
  128. package/dist/services/lottemart/parser.js +185 -0
  129. package/dist/services/lottemart/session.js +331 -0
  130. package/dist/services/lottemart/storeSearch.js +52 -0
  131. package/dist/services/lottemart/tools/findNearbyStores.js +67 -0
  132. package/dist/services/lottemart/tools/searchProducts.js +69 -0
  133. package/dist/services/lottemart/types.js +4 -0
  134. package/dist/services/lottemart/zetta.js +105 -0
  135. package/dist/services/megabox/location.js +219 -0
  136. package/dist/services/megabox/tools/findNearbyTheaters.js +13 -44
  137. package/dist/services/megabox/tools/getRemainingSeats.js +31 -3
  138. package/dist/services/megabox/tools/listNowShowing.js +31 -3
  139. package/dist/services/oliveyoung/api.js +2 -0
  140. package/dist/services/oliveyoung/client.js +225 -13
  141. package/dist/services/oliveyoung/index.js +3 -1
  142. package/dist/services/oliveyoung/tools/checkInventory.js +21 -5
  143. package/dist/services/oliveyoung/tools/searchProducts.js +57 -0
  144. package/dist/services/seveneleven/api.js +15 -0
  145. package/dist/services/seveneleven/client.js +286 -0
  146. package/dist/services/seveneleven/index.js +30 -0
  147. package/dist/services/seveneleven/inventory.js +302 -0
  148. package/dist/services/seveneleven/productKeyword.js +141 -0
  149. package/dist/services/seveneleven/tools/checkInventory.js +82 -0
  150. package/dist/services/seveneleven/tools/getCatalogSnapshot.js +55 -0
  151. package/dist/services/seveneleven/tools/getSearchPopwords.js +41 -0
  152. package/dist/services/seveneleven/tools/searchProducts.js +67 -0
  153. package/dist/services/seveneleven/tools/searchStores.js +45 -0
  154. package/dist/services/seveneleven/types.js +4 -0
  155. package/dist/utils/cliInteractiveHelpers.js +63 -0
  156. package/dist/utils/daisoKeyword.js +30 -0
  157. package/dist/utils/http.js +98 -12
  158. package/dist/utils/zyte.js +47 -26
  159. package/openapi.json +1979 -736
  160. package/openapi.yaml +1428 -541
  161. package/package.json +49 -18
  162. package/dist/tools/checkInventory.js +0 -98
  163. package/dist/tools/findStores.js +0 -125
  164. package/dist/tools/getPriceInfo.js +0 -77
  165. package/dist/tools/searchProducts.js +0 -61
  166. package/dist/types/index.js +0 -4
@@ -3,11 +3,12 @@
3
3
  */
4
4
  import { createFindNearbyStoresTool } from './tools/findNearbyStores.js';
5
5
  import { createCheckInventoryTool } from './tools/checkInventory.js';
6
+ import { createSearchProductsTool } from './tools/searchProducts.js';
6
7
  const OLIVEYOUNG_METADATA = {
7
8
  id: 'oliveyoung',
8
9
  name: '올리브영',
9
10
  version: '1.0.0',
10
- description: '올리브영 주변 매장 탐색 및 재고 파악 서비스 (Zyte API 기반)',
11
+ description: '올리브영 상품 검색, 주변 매장 탐색 및 재고 파악 서비스 (Zyte API 기반)',
11
12
  };
12
13
  class OliveyoungService {
13
14
  options;
@@ -17,6 +18,7 @@ class OliveyoungService {
17
18
  }
18
19
  getTools() {
19
20
  return [
21
+ createSearchProductsTool(this.options.zyteApiKey),
20
22
  createFindNearbyStoresTool(this.options.zyteApiKey),
21
23
  createCheckInventoryTool(this.options.zyteApiKey),
22
24
  ];
@@ -2,9 +2,9 @@
2
2
  * 올리브영 재고 파악 도구
3
3
  */
4
4
  import * as z from 'zod';
5
- import { fetchOliveyoungProducts, fetchOliveyoungStores } from '../client.js';
5
+ import { enrichOliveyoungProductsWithNearbyStoreInventory, fetchOliveyoungProducts, fetchOliveyoungStores, } from '../client.js';
6
6
  async function checkInventory(args) {
7
- const { keyword, latitude = 37.5665, longitude = 126.978, storeKeyword = '', page = 1, size = 20, sort = '01', includeSoldOut = false, storeLimit = 10, timeoutMs = 15000, zyteApiKey, } = args;
7
+ const { keyword, latitude = 37.5665, longitude = 126.978, storeKeyword = '', page = 1, size = 20, sort = '01', includeSoldOut = false, storeLimit = 10, stockCheckLimit = 5, timeoutMs = 15000, zyteApiKey, } = args;
8
8
  if (!keyword || keyword.trim().length === 0) {
9
9
  throw new Error('상품 검색어(keyword)를 입력해주세요.');
10
10
  }
@@ -29,7 +29,16 @@ async function checkInventory(args) {
29
29
  apiKey: zyteApiKey,
30
30
  }),
31
31
  ]);
32
- const inStockProducts = productResult.products.filter((product) => product.o2oStockFlag || product.o2oRemainQuantity > 0);
32
+ const enrichedInventory = await enrichOliveyoungProductsWithNearbyStoreInventory(productResult.products, {
33
+ latitude,
34
+ longitude,
35
+ storeKeyword,
36
+ maxProducts: Math.min(productResult.products.length, Math.max(0, stockCheckLimit)),
37
+ }, {
38
+ timeout: timeoutMs,
39
+ apiKey: zyteApiKey,
40
+ });
41
+ const inStockProducts = enrichedInventory.products.filter((product) => product.inStock);
33
42
  const result = {
34
43
  keyword,
35
44
  searchOptions: {
@@ -51,9 +60,11 @@ async function checkInventory(args) {
51
60
  inventory: {
52
61
  totalCount: productResult.totalCount,
53
62
  nextPage: productResult.nextPage,
63
+ stockCheckedCount: enrichedInventory.checkedCount,
64
+ stockUncheckedCount: Math.max(0, productResult.products.length - enrichedInventory.checkedCount),
54
65
  inStockCount: inStockProducts.length,
55
- outOfStockCount: productResult.products.length - inStockProducts.length,
56
- products: productResult.products,
66
+ outOfStockCount: enrichedInventory.products.length - inStockProducts.length,
67
+ products: enrichedInventory.products,
57
68
  },
58
69
  };
59
70
  return {
@@ -79,6 +90,11 @@ export function createCheckInventoryTool(apiKey) {
79
90
  sort: z.string().optional().default('01').describe('정렬 코드 (기본값: 01)'),
80
91
  includeSoldOut: z.boolean().optional().default(false).describe('품절 상품 포함 여부 (기본값: false)'),
81
92
  storeLimit: z.number().optional().default(10).describe('반환할 주변 매장 최대 수 (기본값: 10)'),
93
+ stockCheckLimit: z
94
+ .number()
95
+ .optional()
96
+ .default(5)
97
+ .describe('주변 매장별 재고를 보강할 상품 수 (기본값: 5, 낮출수록 빠름)'),
82
98
  timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
83
99
  },
84
100
  },
@@ -0,0 +1,57 @@
1
+ /**
2
+ * 올리브영 상품 검색 도구
3
+ */
4
+ import * as z from 'zod';
5
+ import { fetchOliveyoungProducts } from '../client.js';
6
+ async function searchProducts(args) {
7
+ const { keyword, page = 1, size = 20, sort = '01', includeSoldOut = false, timeoutMs = 15000, zyteApiKey, } = args;
8
+ if (!keyword || keyword.trim().length === 0) {
9
+ throw new Error('상품 검색어(keyword)를 입력해주세요.');
10
+ }
11
+ const result = await fetchOliveyoungProducts({
12
+ keyword,
13
+ page,
14
+ size,
15
+ sort,
16
+ includeSoldOut,
17
+ }, {
18
+ timeout: timeoutMs,
19
+ apiKey: zyteApiKey,
20
+ });
21
+ return {
22
+ content: [
23
+ {
24
+ type: 'text',
25
+ text: JSON.stringify({
26
+ keyword,
27
+ page,
28
+ size,
29
+ sort,
30
+ includeSoldOut,
31
+ totalCount: result.totalCount,
32
+ nextPage: result.nextPage,
33
+ count: result.products.length,
34
+ products: result.products,
35
+ }, null, 2),
36
+ },
37
+ ],
38
+ };
39
+ }
40
+ export function createSearchProductsTool(apiKey) {
41
+ return {
42
+ name: 'oliveyoung_search_products',
43
+ metadata: {
44
+ title: '올리브영 상품 검색',
45
+ description: '키워드로 올리브영 상품 목록을 조회합니다. "어떤 거 있나요" 같은 질문에는 먼저 이 도구를 사용하세요.',
46
+ inputSchema: {
47
+ keyword: z.string().describe('상품 검색어 (예: 마스크팩, 선크림, 립밤)'),
48
+ page: z.number().optional().default(1).describe('상품 검색 페이지 번호 (기본값: 1)'),
49
+ size: z.number().optional().default(20).describe('페이지당 상품 수 (기본값: 20)'),
50
+ sort: z.string().optional().default('01').describe('정렬 코드 (기본값: 01)'),
51
+ includeSoldOut: z.boolean().optional().default(false).describe('품절 상품 포함 여부 (기본값: false)'),
52
+ timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
53
+ },
54
+ },
55
+ handler: ((args) => searchProducts({ ...args, zyteApiKey: apiKey })),
56
+ };
57
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * 세븐일레븐 API 엔드포인트
3
+ */
4
+ export const SEVENELEVEN_API = {
5
+ BASE_URL: 'https://new.7-elevenapp.co.kr',
6
+ SEARCH_GOODS_PATH: '/api/v1/open/search/goods',
7
+ SEARCH_STORE_PATH: '/api/v1/open/search/store',
8
+ SEARCH_POPWORD_PATH: '/api/v1/open/search/popword',
9
+ PRODUCT_SEARCH_STOCK_PATH: '/api/v1/open/product/search/stock',
10
+ STOCK_SEARCH_STORES_PATH: '/api/v1/open/stock/search/stores',
11
+ REAL_STOCK_MULTI_PATH: '/api/v1/open/real-stock/multi',
12
+ PRODUCT_PAGES_PATH: '/api/v1/product/pages',
13
+ PRODUCT_ISSUES_PATH: '/api/v1/product/issues',
14
+ EXHIBITION_MAIN_PATH: '/api/v1/exhibition/main/list',
15
+ };
@@ -0,0 +1,286 @@
1
+ /**
2
+ * 세븐일레븐 API 클라이언트
3
+ */
4
+ /* c8 ignore start */
5
+ import { fetchJson } from '../../utils/http.js';
6
+ import { SEVENELEVEN_API } from './api.js';
7
+ const SEVENELEVEN_DEFAULT_HEADERS = {
8
+ Accept: 'application/json, text/plain, */*',
9
+ 'Content-Type': 'application/json',
10
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 15)',
11
+ };
12
+ const SEVENELEVEN_DEFAULT_FETCH_OPTIONS = {
13
+ retries: 1,
14
+ retryDelayMs: 250,
15
+ };
16
+ function toNumber(value) {
17
+ if (typeof value === 'number') {
18
+ return Number.isFinite(value) ? value : 0;
19
+ }
20
+ if (typeof value === 'string') {
21
+ const parsed = Number.parseFloat(value);
22
+ return Number.isFinite(parsed) ? parsed : 0;
23
+ }
24
+ return 0;
25
+ }
26
+ function toStringValue(value) {
27
+ return typeof value === 'string' ? value : '';
28
+ }
29
+ function normalizeStoreKeyword(keyword) {
30
+ return keyword
31
+ .trim()
32
+ .replace(/\s+/g, '')
33
+ .replace(/(역|지점|점)$/g, '');
34
+ }
35
+ function toBooleanYn(value) {
36
+ const normalized = toStringValue(value).trim().toUpperCase();
37
+ return normalized === 'Y' || normalized === 'TRUE' || normalized === '1';
38
+ }
39
+ function toRawProduct(input) {
40
+ if (!input || typeof input !== 'object') {
41
+ return {};
42
+ }
43
+ const record = input;
44
+ if (record.field && typeof record.field === 'object') {
45
+ return record.field;
46
+ }
47
+ return record;
48
+ }
49
+ function normalizeProduct(raw) {
50
+ return {
51
+ productNo: toStringValue(raw.prdNo),
52
+ itemCode: toStringValue(raw.itemCd),
53
+ itemName: toStringValue(raw.itemOnm),
54
+ salePrice: toNumber(raw.onlinePrice),
55
+ originalPrice: toNumber(raw.onlineCost),
56
+ imageUrl: toStringValue(raw.repImgUrl),
57
+ eventName: toStringValue(raw.eventGbnNm),
58
+ itemType: toStringValue(raw.itemGbnNm),
59
+ makerName: toStringValue(raw.makerNm),
60
+ reviewScore: raw.avgEvalScore === undefined ? null : toNumber(raw.avgEvalScore),
61
+ reviewCount: toNumber(raw.productReviewCnt),
62
+ };
63
+ }
64
+ function normalizeProducts(items) {
65
+ return items
66
+ .map(toRawProduct)
67
+ .map(normalizeProduct)
68
+ .filter((item) => item.itemCode.length > 0 || item.itemName.length > 0);
69
+ }
70
+ function normalizeStore(raw) {
71
+ const address1 = toStringValue(raw.addr1);
72
+ const address2 = toStringValue(raw.addr2);
73
+ const mergedAddress = `${address1} ${address2}`.trim();
74
+ return {
75
+ storeCode: toStringValue(raw.storeCode || raw.storeCd || raw.strCd || raw.storCd || raw.shopCd),
76
+ storeName: toStringValue(raw.storeName || raw.storeNm || raw.strNm || raw.storNm || raw.shopNm),
77
+ address: toStringValue(raw.address || raw.addr || raw.roadAddr || raw.shopAddr) || mergedAddress,
78
+ latitude: toNumber(raw.latitude || raw.storeLat || raw.lat || raw.yPos || raw.y || raw.yCoord),
79
+ longitude: toNumber(raw.longitude || raw.storeLon || raw.lng || raw.xPos || raw.x || raw.xCoord),
80
+ pickupEnabled: toBooleanYn(raw.pickupEnabled || raw.pickupYn),
81
+ deliveryEnabled: toBooleanYn(raw.deliveryEnabled || raw.deliveryYn || raw.dlvyYn),
82
+ closeYn: toStringValue(raw.closeYn || raw.storeCloseYn || raw.closeYN || raw.clsYn),
83
+ };
84
+ }
85
+ function normalizeStockProductMeta(raw) {
86
+ return {
87
+ productNo: toStringValue(raw.prdNo),
88
+ itemCode: toStringValue(raw.itemCd),
89
+ itemName: toStringValue(raw.itemOnm),
90
+ smCode: toStringValue(raw.smCd),
91
+ stockManagementCode: toStringValue(raw.stokMngCd),
92
+ stockManagementQuantity: toNumber(raw.stokMngQty),
93
+ stockApplicationRate: toStringValue(raw.stockApplicationRate),
94
+ };
95
+ }
96
+ function normalizeStores(items) {
97
+ return items
98
+ .map((item) => {
99
+ if (!item || typeof item !== 'object') {
100
+ return {};
101
+ }
102
+ const record = item;
103
+ if (record.field && typeof record.field === 'object') {
104
+ return record.field;
105
+ }
106
+ return record;
107
+ })
108
+ .map(normalizeStore)
109
+ .filter((store) => store.storeCode.length > 0 || store.storeName.length > 0);
110
+ }
111
+ async function requestSevenElevenJson(path, method, body, options = {}) {
112
+ const { timeout = 15000 } = options;
113
+ const url = `${SEVENELEVEN_API.BASE_URL}${path}`;
114
+ return fetchJson(url, {
115
+ ...SEVENELEVEN_DEFAULT_FETCH_OPTIONS,
116
+ method,
117
+ retryUnsafeMethods: method === 'POST',
118
+ timeout,
119
+ headers: SEVENELEVEN_DEFAULT_HEADERS,
120
+ body: method === 'POST' ? JSON.stringify(body || {}) : undefined,
121
+ });
122
+ }
123
+ export async function searchSevenElevenProducts(params, options = {}) {
124
+ const { query, page = 1, size = 20 } = params;
125
+ const pageNo = Math.max(Math.trunc(page) - 1, 0);
126
+ const pageSize = Math.max(Math.trunc(size), 1);
127
+ const response = await requestSevenElevenJson(SEVENELEVEN_API.SEARCH_GOODS_PATH, 'POST', {
128
+ // 공개 검색 엔드포인트는 pageNo/pageSize만 안정적으로 동작한다.
129
+ // sort를 함께 보내면 빈 결과가 내려오는 케이스가 확인되어 제외한다.
130
+ query,
131
+ pageNo,
132
+ pageSize,
133
+ }, options);
134
+ const data = response.data || {};
135
+ const queryResult = data.SearchQueryResult;
136
+ const collections = queryResult?.Collection || [];
137
+ const collectionIds = collections.map((item) => item.CollectionId || '').filter((id) => id.length > 0);
138
+ let totalCount = 0;
139
+ const allDocuments = [];
140
+ for (const collection of collections) {
141
+ totalCount += toNumber(collection.Documentset?.totalCount);
142
+ allDocuments.push(...(collection.Documentset?.Document || []));
143
+ }
144
+ const collectionProducts = normalizeProducts(allDocuments);
145
+ const contentProducts = normalizeProducts(Array.isArray(data.content) ? data.content : []);
146
+ const products = (collectionProducts.length > 0 ? collectionProducts : contentProducts).slice(0, pageSize);
147
+ return {
148
+ query: queryResult?.query || query,
149
+ totalCount,
150
+ products,
151
+ collectionIds,
152
+ };
153
+ }
154
+ export async function fetchSevenElevenStoresByKeyword(params, options = {}) {
155
+ const { keyword, limit = 20 } = params;
156
+ const query = normalizeStoreKeyword(keyword) || keyword.trim();
157
+ const safeLimit = Number.isFinite(limit) ? Math.trunc(limit) : 20;
158
+ const listCount = Math.max(1, Math.min(safeLimit, 9999));
159
+ const response = await requestSevenElevenJson(SEVENELEVEN_API.SEARCH_STORE_PATH, 'POST', {
160
+ collection: 'store',
161
+ query,
162
+ sort: 'Date/desc',
163
+ listCount,
164
+ }, options);
165
+ const data = response.data || {};
166
+ const queryResult = data.SearchQueryResult;
167
+ const collections = queryResult?.Collection || [];
168
+ let totalCount = 0;
169
+ const allDocuments = [];
170
+ for (const collection of collections) {
171
+ totalCount += toNumber(collection.Documentset?.totalCount);
172
+ allDocuments.push(...(collection.Documentset?.Document || []));
173
+ }
174
+ const stores = normalizeStores(allDocuments);
175
+ return {
176
+ query: queryResult?.query || query,
177
+ totalCount,
178
+ stores,
179
+ };
180
+ }
181
+ export async function fetchSevenElevenSearchPopwords(label = 'home', options = {}) {
182
+ const encodedLabel = encodeURIComponent(label);
183
+ const response = await requestSevenElevenJson(`${SEVENELEVEN_API.SEARCH_POPWORD_PATH}?label=${encodedLabel}`, 'POST', {}, options);
184
+ const { data } = response;
185
+ if (Array.isArray(data)) {
186
+ return data.map((item) => toStringValue(item)).filter((item) => item.length > 0);
187
+ }
188
+ if (!data || typeof data !== 'object') {
189
+ return [];
190
+ }
191
+ const record = data;
192
+ const candidates = [record.keywords, record.popwords, record.wordList, record.list];
193
+ for (const candidate of candidates) {
194
+ if (!Array.isArray(candidate)) {
195
+ continue;
196
+ }
197
+ const values = candidate
198
+ .map((item) => {
199
+ if (typeof item === 'string') {
200
+ return item;
201
+ }
202
+ if (item && typeof item === 'object') {
203
+ const value = item;
204
+ return toStringValue(value.keyword || value.word || value.text || value.name);
205
+ }
206
+ return '';
207
+ })
208
+ .filter((item) => item.length > 0);
209
+ if (values.length > 0) {
210
+ return values;
211
+ }
212
+ }
213
+ return [];
214
+ }
215
+ export async function fetchSevenElevenStockProductMeta(itemCode, options = {}) {
216
+ const { timeout = 15000 } = options;
217
+ const encodedItemCode = encodeURIComponent(itemCode.trim());
218
+ const url = `${SEVENELEVEN_API.BASE_URL}${SEVENELEVEN_API.PRODUCT_SEARCH_STOCK_PATH}?itemCd=${encodedItemCode}`;
219
+ const response = await fetchJson(url, {
220
+ ...SEVENELEVEN_DEFAULT_FETCH_OPTIONS,
221
+ method: 'GET',
222
+ timeout,
223
+ headers: {
224
+ Accept: SEVENELEVEN_DEFAULT_HEADERS.Accept,
225
+ 'User-Agent': SEVENELEVEN_DEFAULT_HEADERS['User-Agent'],
226
+ },
227
+ });
228
+ const productMeta = normalizeStockProductMeta(response);
229
+ if (productMeta.itemCode.length === 0 || productMeta.smCode.length === 0) {
230
+ return null;
231
+ }
232
+ return productMeta;
233
+ }
234
+ function normalizeExhibitions(items) {
235
+ return items
236
+ .map((item) => {
237
+ const value = (item || {});
238
+ const productList = Array.isArray(value.exhibitionProductList) ? value.exhibitionProductList : [];
239
+ return {
240
+ exhibitionIdx: toNumber(value.exhibitionIdx),
241
+ exhibitionName: toStringValue(value.exhibitionName),
242
+ startDate: toStringValue(value.exhibitionStartDate),
243
+ endDate: toStringValue(value.exhibitionEndDate),
244
+ productCount: productList.length,
245
+ };
246
+ })
247
+ .filter((item) => item.exhibitionIdx > 0 || item.exhibitionName.length > 0);
248
+ }
249
+ function extractContentArray(data) {
250
+ if (Array.isArray(data)) {
251
+ return data;
252
+ }
253
+ if (!data || typeof data !== 'object') {
254
+ return [];
255
+ }
256
+ const record = data;
257
+ if (Array.isArray(record.content)) {
258
+ return record.content;
259
+ }
260
+ if (Array.isArray(record.list)) {
261
+ return record.list;
262
+ }
263
+ return [];
264
+ }
265
+ export async function fetchSevenElevenCatalogSnapshot(options = {}) {
266
+ const { includeIssues = true, includeExhibition = true, timeout = 15000 } = options;
267
+ const tasks = [
268
+ requestSevenElevenJson(SEVENELEVEN_API.PRODUCT_PAGES_PATH, 'GET', null, { timeout }),
269
+ includeIssues
270
+ ? requestSevenElevenJson(SEVENELEVEN_API.PRODUCT_ISSUES_PATH, 'GET', null, { timeout })
271
+ : Promise.resolve({ data: { content: [] } }),
272
+ includeExhibition
273
+ ? requestSevenElevenJson(SEVENELEVEN_API.EXHIBITION_MAIN_PATH, 'GET', null, { timeout })
274
+ : Promise.resolve({ data: [] }),
275
+ ];
276
+ const [pagesResult, issuesResult, exhibitionsResult] = await Promise.allSettled(tasks);
277
+ const pagesData = pagesResult.status === 'fulfilled' ? pagesResult.value.data : [];
278
+ const issuesData = issuesResult.status === 'fulfilled' ? issuesResult.value.data : [];
279
+ const exhibitionData = exhibitionsResult.status === 'fulfilled' ? exhibitionsResult.value.data : [];
280
+ return {
281
+ pages: normalizeProducts(extractContentArray(pagesData)),
282
+ issues: normalizeProducts(extractContentArray(issuesData)),
283
+ exhibitions: normalizeExhibitions(extractContentArray(exhibitionData)),
284
+ };
285
+ }
286
+ /* c8 ignore stop */
@@ -0,0 +1,30 @@
1
+ /**
2
+ * 세븐일레븐 서비스 프로바이더
3
+ */
4
+ import { createSearchProductsTool } from './tools/searchProducts.js';
5
+ import { createSearchStoresTool } from './tools/searchStores.js';
6
+ import { createCheckInventoryTool } from './tools/checkInventory.js';
7
+ import { createGetSearchPopwordsTool } from './tools/getSearchPopwords.js';
8
+ import { createGetCatalogSnapshotTool } from './tools/getCatalogSnapshot.js';
9
+ const SEVENELEVEN_METADATA = {
10
+ id: 'seveneleven',
11
+ name: '세븐일레븐',
12
+ version: '1.0.0',
13
+ description: '세븐일레븐 공개 상품/검색/카탈로그 조회 서비스',
14
+ };
15
+ class SevenElevenService {
16
+ metadata = SEVENELEVEN_METADATA;
17
+ getTools() {
18
+ return [
19
+ createSearchProductsTool(),
20
+ createSearchStoresTool(),
21
+ createCheckInventoryTool(),
22
+ createGetSearchPopwordsTool(),
23
+ createGetCatalogSnapshotTool(),
24
+ ];
25
+ }
26
+ }
27
+ export function createSevenElevenService() {
28
+ return new SevenElevenService();
29
+ }
30
+ export * from './types.js';