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
@@ -0,0 +1,302 @@
1
+ /**
2
+ * 세븐일레븐 재고 조회 클라이언트
3
+ *
4
+ * 상품 검색 + 매장 검색 + 재고 API를 조합하여 재고 정보를 제공합니다.
5
+ * 재고 API가 암호화로 차단된 경우에도 상품/매장 정보를 graceful하게 반환합니다.
6
+ */
7
+ /* c8 ignore start */
8
+ import { fetchJson, HttpError } from '../../utils/http.js';
9
+ import { SEVENELEVEN_API } from './api.js';
10
+ import { fetchSevenElevenStockProductMeta, fetchSevenElevenStoresByKeyword, } from './client.js';
11
+ import { pickBestSevenElevenProduct, searchSevenElevenProductsWithVariants } from './productKeyword.js';
12
+ const SEVENELEVEN_DEFAULT_HEADERS = {
13
+ Accept: 'application/json, text/plain, */*',
14
+ 'Content-Type': 'application/json',
15
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 15)',
16
+ };
17
+ function toNumber(value) {
18
+ if (typeof value === 'number') {
19
+ return Number.isFinite(value) ? value : 0;
20
+ }
21
+ if (typeof value === 'string') {
22
+ const parsed = Number.parseFloat(value);
23
+ return Number.isFinite(parsed) ? parsed : 0;
24
+ }
25
+ return 0;
26
+ }
27
+ function toStringValue(value) {
28
+ return typeof value === 'string' ? value : '';
29
+ }
30
+ function normalizeStockStore(raw) {
31
+ return {
32
+ storeCode: toStringValue(raw.storeCd),
33
+ storeName: '',
34
+ address: '',
35
+ latitude: 0,
36
+ longitude: 0,
37
+ stockQuantity: toNumber(raw.stock),
38
+ isSoldOut: toNumber(raw.stock) <= 0,
39
+ distanceM: null,
40
+ };
41
+ }
42
+ function tryParseJsonObject(text) {
43
+ if (text.trim().length === 0) {
44
+ return null;
45
+ }
46
+ try {
47
+ const parsed = JSON.parse(text);
48
+ return typeof parsed === 'object' && parsed !== null ? parsed : null;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ function toNullableNumber(value) {
55
+ if (typeof value === 'number' && Number.isFinite(value)) {
56
+ return value;
57
+ }
58
+ if (typeof value === 'string' && value.trim().length > 0) {
59
+ const parsed = Number.parseFloat(value);
60
+ return Number.isFinite(parsed) ? parsed : null;
61
+ }
62
+ return null;
63
+ }
64
+ function normalizeEnvelopeError(response) {
65
+ const hasErrorFlag = response.success === false;
66
+ const hasErrorCode = typeof response.code === 'number' && (response.code < 200 || response.code >= 300);
67
+ const hasErrorMessage = typeof response.message === 'string' && response.message.trim().length > 0;
68
+ if (response.success === true && !hasErrorCode) {
69
+ return null;
70
+ }
71
+ if (!hasErrorFlag && !hasErrorCode && !hasErrorMessage) {
72
+ return null;
73
+ }
74
+ return {
75
+ cause: 'api',
76
+ httpStatus: null,
77
+ code: toNullableNumber(response.code),
78
+ message: typeof response.message === 'string' ? response.message : '재고 API가 오류를 반환했습니다.',
79
+ raw: JSON.stringify(response),
80
+ };
81
+ }
82
+ function normalizeStockError(error) {
83
+ if (error instanceof HttpError) {
84
+ const parsedBody = tryParseJsonObject(error.bodyText);
85
+ return {
86
+ cause: 'api',
87
+ httpStatus: error.status,
88
+ code: toNullableNumber(parsedBody?.code),
89
+ message: typeof parsedBody?.message === 'string' && parsedBody.message.trim().length > 0
90
+ ? parsedBody.message
91
+ : error.message,
92
+ raw: error.bodyText.trim().length > 0 ? error.bodyText : null,
93
+ };
94
+ }
95
+ if (error instanceof Error) {
96
+ return {
97
+ cause: 'network',
98
+ httpStatus: null,
99
+ code: null,
100
+ message: error.message,
101
+ raw: null,
102
+ };
103
+ }
104
+ return {
105
+ cause: 'unknown',
106
+ httpStatus: null,
107
+ code: null,
108
+ message: '알 수 없는 재고 API 오류',
109
+ raw: null,
110
+ };
111
+ }
112
+ function haversineDistanceM(lat1, lon1, lat2, lon2) {
113
+ const R = 6371000;
114
+ const toRad = (deg) => (deg * Math.PI) / 180;
115
+ const dLat = toRad(lat2 - lat1);
116
+ const dLon = toRad(lon2 - lon1);
117
+ const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
118
+ Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
119
+ return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
120
+ }
121
+ function attachDistanceToStockStores(stores, refLat, refLon) {
122
+ return stores.map((store) => {
123
+ if (store.latitude === 0 && store.longitude === 0) {
124
+ return store;
125
+ }
126
+ return {
127
+ ...store,
128
+ distanceM: Math.round(haversineDistanceM(refLat, refLon, store.latitude, store.longitude)),
129
+ };
130
+ });
131
+ }
132
+ function normalizeStoreKeyword(keyword) {
133
+ return keyword
134
+ .trim()
135
+ .replace(/\s+/g, '')
136
+ .replace(/(역|지점|점)$/g, '')
137
+ .toLowerCase();
138
+ }
139
+ function tokenizeStoreKeyword(keyword) {
140
+ return keyword
141
+ .trim()
142
+ .split(/\s+/)
143
+ .map((token) => normalizeStoreKeyword(token))
144
+ .filter((token) => token.length > 0);
145
+ }
146
+ function filterStoresByKeyword(stores, keyword) {
147
+ if (!keyword || keyword.trim().length === 0) {
148
+ return stores;
149
+ }
150
+ const normalized = normalizeStoreKeyword(keyword);
151
+ const exactMatched = stores.filter((store) => {
152
+ const nameNorm = store.storeName.replace(/\s+/g, '').toLowerCase();
153
+ const addrNorm = store.address.replace(/\s+/g, '').toLowerCase();
154
+ return nameNorm.includes(normalized) || addrNorm.includes(normalized);
155
+ });
156
+ if (exactMatched.length > 0) {
157
+ return exactMatched;
158
+ }
159
+ const tokens = tokenizeStoreKeyword(keyword).filter((token) => token.length >= 2);
160
+ if (tokens.length === 0) {
161
+ return stores;
162
+ }
163
+ const tokenMatched = stores.filter((store) => {
164
+ const combined = `${store.storeName} ${store.address}`.replace(/\s+/g, '').toLowerCase();
165
+ return tokens.every((token) => combined.includes(token));
166
+ });
167
+ return tokenMatched.length > 0 ? tokenMatched : stores;
168
+ }
169
+ async function tryStockApi(stockProduct, stores, options = {}) {
170
+ const { timeout = 15000 } = options;
171
+ const url = `${SEVENELEVEN_API.BASE_URL}${SEVENELEVEN_API.REAL_STOCK_MULTI_PATH}/01/stocks`;
172
+ const payload = {
173
+ smCd: stockProduct.smCode,
174
+ stokMngCd: stockProduct.stockManagementCode,
175
+ stokMngQty: stockProduct.stockManagementQuantity,
176
+ stockApplicationRate: stockProduct.stockApplicationRate,
177
+ storeList: stores.map((store) => store.storeCode).filter((storeCode) => storeCode.length > 0),
178
+ };
179
+ if (payload.smCd.length === 0 || payload.stokMngCd.length === 0 || payload.storeList.length === 0) {
180
+ return { stores: null, error: null };
181
+ }
182
+ try {
183
+ const response = await fetchJson(url, {
184
+ method: 'POST',
185
+ timeout,
186
+ headers: SEVENELEVEN_DEFAULT_HEADERS,
187
+ body: JSON.stringify(payload),
188
+ });
189
+ const responseError = normalizeEnvelopeError(response);
190
+ if (responseError) {
191
+ return { stores: null, error: responseError };
192
+ }
193
+ const data = response.data || {};
194
+ const rawStores = Array.isArray(data.storeList) ? data.storeList : [];
195
+ return {
196
+ stores: rawStores
197
+ .map((item) => normalizeStockStore(item))
198
+ .filter((store) => store.storeCode.length > 0),
199
+ error: null,
200
+ };
201
+ }
202
+ catch (error) {
203
+ return {
204
+ stores: null,
205
+ error: normalizeStockError(error),
206
+ };
207
+ }
208
+ }
209
+ export async function checkSevenElevenInventory(params, options = {}) {
210
+ const { productKeyword, storeKeyword, storeLimit = 20 } = params;
211
+ // 1) 상품 검색
212
+ const productResult = await searchSevenElevenProductsWithVariants(productKeyword, options);
213
+ const firstProduct = pickBestSevenElevenProduct(productResult.products, productKeyword) ||
214
+ productResult.products.find((p) => p.itemCode.length > 0) ||
215
+ productResult.products[0] ||
216
+ null;
217
+ // 2) 매장 검색
218
+ const storeResult = await fetchSevenElevenStoresByKeyword({ keyword: storeKeyword, limit: 100 }, options);
219
+ const matchedStores = filterStoresByKeyword(storeResult.stores, storeKeyword);
220
+ // 3) 재고 API 시도 (실패 시 graceful fallback)
221
+ let stockStores = null;
222
+ let stockError = null;
223
+ if (firstProduct && firstProduct.itemCode.length > 0) {
224
+ try {
225
+ const stockProduct = await fetchSevenElevenStockProductMeta(firstProduct.itemCode, options);
226
+ if (stockProduct) {
227
+ const stockAttempt = await tryStockApi(stockProduct, matchedStores, options);
228
+ stockStores = stockAttempt.stores;
229
+ stockError = stockAttempt.error;
230
+ }
231
+ }
232
+ catch (error) {
233
+ stockError = normalizeStockError(error);
234
+ }
235
+ }
236
+ // 4) 결과 조합
237
+ let resultStores;
238
+ let stockAvailable;
239
+ if (stockStores !== null && stockStores.length > 0) {
240
+ stockAvailable = true;
241
+ const stockMap = new Map(stockStores.map((store) => [store.storeCode, store]));
242
+ resultStores = matchedStores.map((store) => {
243
+ const stockStore = stockMap.get(store.storeCode);
244
+ return {
245
+ storeCode: store.storeCode,
246
+ storeName: store.storeName,
247
+ address: store.address,
248
+ latitude: store.latitude,
249
+ longitude: store.longitude,
250
+ stockQuantity: stockStore?.stockQuantity ?? -1,
251
+ isSoldOut: stockStore ? stockStore.isSoldOut : false,
252
+ distanceM: null,
253
+ };
254
+ });
255
+ }
256
+ else {
257
+ stockAvailable = false;
258
+ resultStores = matchedStores.map((store) => ({
259
+ storeCode: store.storeCode,
260
+ storeName: store.storeName,
261
+ address: store.address,
262
+ latitude: store.latitude,
263
+ longitude: store.longitude,
264
+ stockQuantity: -1,
265
+ isSoldOut: false,
266
+ distanceM: null,
267
+ }));
268
+ }
269
+ // 거리 계산: 첫 번째 매장 좌표를 기준으로 정렬
270
+ const refStore = matchedStores.find((s) => s.latitude !== 0 && s.longitude !== 0);
271
+ if (refStore) {
272
+ resultStores = attachDistanceToStockStores(resultStores, refStore.latitude, refStore.longitude);
273
+ resultStores.sort((a, b) => {
274
+ if (a.distanceM === null && b.distanceM === null)
275
+ return 0;
276
+ if (a.distanceM === null)
277
+ return 1;
278
+ if (b.distanceM === null)
279
+ return -1;
280
+ return a.distanceM - b.distanceM;
281
+ });
282
+ }
283
+ const limited = resultStores.slice(0, storeLimit);
284
+ const inStockCount = stockAvailable ? limited.filter((s) => s.stockQuantity > 0).length : 0;
285
+ return {
286
+ productKeyword,
287
+ product: firstProduct
288
+ ? {
289
+ itemCode: firstProduct.itemCode,
290
+ itemName: firstProduct.itemName,
291
+ salePrice: firstProduct.salePrice,
292
+ imageUrl: firstProduct.imageUrl,
293
+ }
294
+ : null,
295
+ stockAvailable,
296
+ stockError,
297
+ totalStoreCount: resultStores.length,
298
+ inStockStoreCount: inStockCount,
299
+ stores: limited,
300
+ };
301
+ }
302
+ /* c8 ignore stop */
@@ -0,0 +1,141 @@
1
+ /**
2
+ * 세븐일레븐 상품 검색어 보정
3
+ */
4
+ import { searchSevenElevenProducts } from './client.js';
5
+ const SANDWICH_SUFFIX_FAMILY = ['샌드위치', '샌드', '산도'];
6
+ function normalizeProductText(value) {
7
+ return value.replace(/[^0-9A-Za-z가-힣]+/g, '').toLowerCase();
8
+ }
9
+ function removeSuffixFamily(value) {
10
+ for (const suffix of SANDWICH_SUFFIX_FAMILY) {
11
+ if (value.endsWith(suffix)) {
12
+ return value.slice(0, -suffix.length);
13
+ }
14
+ }
15
+ return value;
16
+ }
17
+ /**
18
+ * 세븐일레븐 상품 검색용 보정 질의를 생성합니다.
19
+ */
20
+ export function buildSevenElevenProductKeywordVariants(rawKeyword) {
21
+ const base = rawKeyword.trim();
22
+ if (!base) {
23
+ return [];
24
+ }
25
+ const compact = base.replace(/\s+/g, '');
26
+ const stem = removeSuffixFamily(compact);
27
+ const candidates = [base, compact];
28
+ if (stem && stem !== compact) {
29
+ candidates.push(stem);
30
+ for (const suffix of SANDWICH_SUFFIX_FAMILY) {
31
+ candidates.push(`${stem}${suffix}`);
32
+ }
33
+ candidates.push('샌드');
34
+ }
35
+ return [...new Set(candidates.map((value) => value.trim()).filter((value) => value.length > 0))];
36
+ }
37
+ /**
38
+ * 원본 검색어 기준으로 가장 그럴듯한 상품을 고릅니다.
39
+ */
40
+ export function pickBestSevenElevenProduct(products, rawKeyword) {
41
+ let bestProduct = null;
42
+ let bestScore = -1;
43
+ for (const product of products) {
44
+ const score = scoreSevenElevenProduct(product, rawKeyword);
45
+ if (score > bestScore) {
46
+ bestScore = score;
47
+ bestProduct = product;
48
+ }
49
+ }
50
+ return bestScore > 0 ? bestProduct : null;
51
+ }
52
+ function scoreSevenElevenProduct(product, rawKeyword) {
53
+ const original = normalizeProductText(rawKeyword);
54
+ const stem = normalizeProductText(removeSuffixFamily(rawKeyword.replace(/\s+/g, '')));
55
+ const variants = buildSevenElevenProductKeywordVariants(rawKeyword)
56
+ .map(normalizeProductText)
57
+ .filter((value) => value.length > 0);
58
+ const hasSandwichSuffix = stem.length > 0 && stem !== original;
59
+ const normalizedName = normalizeProductText(product.itemName);
60
+ if (normalizedName.length === 0) {
61
+ return -1;
62
+ }
63
+ let score = 0;
64
+ if (original.length > 0 && normalizedName === original) {
65
+ score += 200;
66
+ }
67
+ if (original.length > 0 && normalizedName.includes(original)) {
68
+ score += 120;
69
+ }
70
+ if (stem.length > 0 && stem !== original && normalizedName.includes(stem)) {
71
+ score += 45;
72
+ }
73
+ for (const variant of variants) {
74
+ if (variant.length < 2 || variant === original || variant === stem) {
75
+ continue;
76
+ }
77
+ if (normalizedName.includes(variant)) {
78
+ score += 15;
79
+ }
80
+ }
81
+ if (hasSandwichSuffix && SANDWICH_SUFFIX_FAMILY.some((suffix) => normalizedName.includes(suffix))) {
82
+ score += 35;
83
+ }
84
+ if (hasSandwichSuffix &&
85
+ stem.length > 0 &&
86
+ SANDWICH_SUFFIX_FAMILY.some((suffix) => normalizedName.includes(`${stem}${suffix}`))) {
87
+ score += 50;
88
+ }
89
+ if (product.itemCode.length > 0) {
90
+ score += 1;
91
+ }
92
+ return score;
93
+ }
94
+ /**
95
+ * 보정 질의를 순차 적용해 세븐일레븐 상품을 찾습니다.
96
+ */
97
+ export async function searchSevenElevenProductsWithVariants(query, options = {}) {
98
+ const { page = 1, size = 20, sort = 'recommend', timeout } = options;
99
+ const appliedQueries = buildSevenElevenProductKeywordVariants(query);
100
+ const seenKeys = new Set();
101
+ const collectionIds = new Set();
102
+ const mergedProducts = [];
103
+ for (const candidate of appliedQueries) {
104
+ const result = await searchSevenElevenProducts({
105
+ query: candidate,
106
+ page,
107
+ size,
108
+ sort,
109
+ }, {
110
+ timeout,
111
+ });
112
+ for (const collectionId of result.collectionIds) {
113
+ collectionIds.add(collectionId);
114
+ }
115
+ for (const product of result.products) {
116
+ const key = product.itemCode || product.productNo || product.itemName;
117
+ if (!key || seenKeys.has(key)) {
118
+ continue;
119
+ }
120
+ seenKeys.add(key);
121
+ mergedProducts.push(product);
122
+ }
123
+ }
124
+ const rankedProducts = [...mergedProducts].sort((left, right) => {
125
+ const leftScore = scoreSevenElevenProduct(left, query);
126
+ const rightScore = scoreSevenElevenProduct(right, query);
127
+ if (leftScore !== rightScore) {
128
+ return rightScore - leftScore;
129
+ }
130
+ return 0;
131
+ });
132
+ const start = Math.max(page - 1, 0) * size;
133
+ const pagedProducts = rankedProducts.slice(start, start + size);
134
+ return {
135
+ query,
136
+ totalCount: rankedProducts.length,
137
+ products: pagedProducts,
138
+ collectionIds: [...collectionIds],
139
+ appliedQueries,
140
+ };
141
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * 세븐일레븐 재고 확인 도구
3
+ *
4
+ * 상품 검색 + 매장 검색을 결합하여 특정 지역 매장의 재고를 조회합니다.
5
+ * 재고 API가 암호화로 차단된 경우에도 상품/매장 정보를 함께 제공합니다.
6
+ */
7
+ /* c8 ignore start */
8
+ import * as z from 'zod';
9
+ import { checkSevenElevenInventory } from '../inventory.js';
10
+ function isEncryptedStockFailure(stockError) {
11
+ if (!stockError) {
12
+ return false;
13
+ }
14
+ const codeMatched = stockError.code === 501 || stockError.code === 503;
15
+ const messageSource = `${stockError.message} ${stockError.raw ?? ''}`;
16
+ const messageMatched = /RSA|복호화|암호화|서비스를 사용할 수 없습니다/i.test(messageSource);
17
+ return codeMatched || messageMatched;
18
+ }
19
+ async function checkInventory(args) {
20
+ const { keyword, storeKeyword = '', storeLimit = 20, timeoutMs = 20000, } = args;
21
+ if (!keyword || keyword.trim().length === 0) {
22
+ throw new Error('상품 검색어(keyword)를 입력해주세요.');
23
+ }
24
+ const result = await checkSevenElevenInventory({
25
+ productKeyword: keyword,
26
+ storeKeyword,
27
+ storeLimit,
28
+ }, {
29
+ timeout: timeoutMs,
30
+ });
31
+ const note = !result.product
32
+ ? '상품 검색 결과가 없어 재고를 조회하지 못했습니다. 다른 상품명이나 더 짧은 키워드로 다시 시도해주세요.'
33
+ : result.stockAvailable
34
+ ? '실시간 재고 데이터가 포함되어 있습니다.'
35
+ : isEncryptedStockFailure(result.stockError)
36
+ ? '실시간 재고 API가 암호화 검증에서 거절되었습니다. 현재 구현의 평문 요청만으로는 재고 수량을 조회할 수 없습니다.'
37
+ : result.stockError
38
+ ? `실시간 재고 API 호출에 실패했습니다: ${result.stockError.message}`
39
+ : '실시간 재고 API가 현재 제한되어 있어 매장 목록만 제공됩니다. stockQuantity가 -1인 경우 재고 수량 미확인 상태입니다.';
40
+ return {
41
+ content: [
42
+ {
43
+ type: 'text',
44
+ text: JSON.stringify({
45
+ keyword,
46
+ storeKeyword,
47
+ product: result.product,
48
+ stockAvailable: result.stockAvailable,
49
+ stockError: result.stockError,
50
+ note,
51
+ inventory: {
52
+ totalStoreCount: result.totalStoreCount,
53
+ inStockStoreCount: result.inStockStoreCount,
54
+ count: result.stores.length,
55
+ stores: result.stores,
56
+ },
57
+ }, null, 2),
58
+ },
59
+ ],
60
+ };
61
+ }
62
+ export function createCheckInventoryTool() {
63
+ return {
64
+ name: 'seveneleven_check_inventory',
65
+ metadata: {
66
+ title: '세븐일레븐 재고 확인',
67
+ description: '상품 키워드와 매장/지역 키워드로 세븐일레븐 매장별 재고를 조회합니다. 실시간 재고 API가 제한된 경우에도 상품 정보와 근처 매장 목록을 함께 제공합니다.',
68
+ inputSchema: {
69
+ keyword: z.string().describe('상품 검색어 (예: 핫식스, 삼각김밥, 도시락)'),
70
+ storeKeyword: z
71
+ .string()
72
+ .optional()
73
+ .default('')
74
+ .describe('매장/지역 검색 키워드 (예: 안산 중앙역, 강남역, 홍대)'),
75
+ storeLimit: z.number().optional().default(20).describe('반환할 최대 매장 수 (기본값: 20)'),
76
+ timeoutMs: z.number().optional().default(20000).describe('요청 제한 시간(ms, 기본값: 20000)'),
77
+ },
78
+ },
79
+ handler: checkInventory,
80
+ };
81
+ }
82
+ /* c8 ignore stop */
@@ -0,0 +1,55 @@
1
+ /**
2
+ * 세븐일레븐 카탈로그 스냅샷 도구
3
+ */
4
+ import * as z from 'zod';
5
+ import { fetchSevenElevenCatalogSnapshot } from '../client.js';
6
+ async function getCatalogSnapshot(args) {
7
+ const { includeIssues = true, includeExhibition = true, limit = 20, timeoutMs = 15000 } = args;
8
+ const result = await fetchSevenElevenCatalogSnapshot({
9
+ includeIssues,
10
+ includeExhibition,
11
+ timeout: timeoutMs,
12
+ });
13
+ return {
14
+ content: [
15
+ {
16
+ type: 'text',
17
+ text: JSON.stringify({
18
+ options: {
19
+ includeIssues,
20
+ includeExhibition,
21
+ limit,
22
+ },
23
+ pages: {
24
+ totalCount: result.pages.length,
25
+ items: result.pages.slice(0, limit),
26
+ },
27
+ issues: {
28
+ totalCount: result.issues.length,
29
+ items: result.issues.slice(0, limit),
30
+ },
31
+ exhibitions: {
32
+ totalCount: result.exhibitions.length,
33
+ items: result.exhibitions.slice(0, limit),
34
+ },
35
+ }, null, 2),
36
+ },
37
+ ],
38
+ };
39
+ }
40
+ export function createGetCatalogSnapshotTool() {
41
+ return {
42
+ name: 'seveneleven_get_catalog_snapshot',
43
+ metadata: {
44
+ title: '세븐일레븐 카탈로그 스냅샷',
45
+ description: '세븐일레븐 공개 카탈로그(페이지/이슈/기획전) 데이터를 조회합니다.',
46
+ inputSchema: {
47
+ includeIssues: z.boolean().optional().default(true).describe('이슈 상품 포함 여부 (기본값: true)'),
48
+ includeExhibition: z.boolean().optional().default(true).describe('기획전 포함 여부 (기본값: true)'),
49
+ limit: z.number().optional().default(20).describe('각 목록 최대 반환 수 (기본값: 20)'),
50
+ timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
51
+ },
52
+ },
53
+ handler: getCatalogSnapshot,
54
+ };
55
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * 세븐일레븐 인기 검색어 조회 도구
3
+ */
4
+ import * as z from 'zod';
5
+ import { fetchSevenElevenSearchPopwords } from '../client.js';
6
+ async function getSearchPopwords(args) {
7
+ const { label = 'home', timeoutMs = 15000 } = args;
8
+ const keywords = await fetchSevenElevenSearchPopwords(label, {
9
+ timeout: timeoutMs,
10
+ });
11
+ return {
12
+ content: [
13
+ {
14
+ type: 'text',
15
+ text: JSON.stringify({
16
+ label,
17
+ available: keywords.length > 0,
18
+ count: keywords.length,
19
+ keywords,
20
+ note: keywords.length === 0
21
+ ? '현재 응답에서 인기 검색어 목록을 찾지 못했습니다.'
22
+ : '홈 인기 검색어를 조회했습니다.',
23
+ }, null, 2),
24
+ },
25
+ ],
26
+ };
27
+ }
28
+ export function createGetSearchPopwordsTool() {
29
+ return {
30
+ name: 'seveneleven_get_search_popwords',
31
+ metadata: {
32
+ title: '세븐일레븐 인기 검색어 조회',
33
+ description: '세븐일레븐 인기 검색어 목록을 조회합니다.',
34
+ inputSchema: {
35
+ label: z.string().optional().default('home').describe('조회 라벨 (기본값: home)'),
36
+ timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
37
+ },
38
+ },
39
+ handler: getSearchPopwords,
40
+ };
41
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * 세븐일레븐 상품 검색 도구
3
+ */
4
+ import * as z from 'zod';
5
+ import { searchSevenElevenProductsWithVariants } from '../productKeyword.js';
6
+ function buildTextResponse(payload) {
7
+ return {
8
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
9
+ structuredContent: payload,
10
+ };
11
+ }
12
+ async function searchProducts(args) {
13
+ const { query, page = 1, size = 20, sort = 'recommend', timeoutMs = 15000 } = args;
14
+ if (!query || query.trim().length === 0) {
15
+ throw new Error('상품 검색어(query)를 입력해주세요.');
16
+ }
17
+ const result = await searchSevenElevenProductsWithVariants(query, {
18
+ page,
19
+ size,
20
+ sort,
21
+ timeout: timeoutMs,
22
+ });
23
+ return buildTextResponse({
24
+ query: result.query,
25
+ page,
26
+ size,
27
+ sort,
28
+ totalCount: result.totalCount,
29
+ count: result.products.length,
30
+ collectionIds: result.collectionIds,
31
+ appliedQueries: result.appliedQueries,
32
+ products: result.products,
33
+ });
34
+ }
35
+ const searchProductsOutputSchema = {
36
+ query: z.string().describe('적용된 검색어'),
37
+ page: z.number().describe('페이지 번호'),
38
+ size: z.number().describe('페이지 크기'),
39
+ sort: z.string().describe('정렬 기준'),
40
+ totalCount: z.number().describe('검색된 전체 상품 수'),
41
+ count: z.number().describe('반환된 상품 수'),
42
+ collectionIds: z.array(z.string()).describe('검색 컬렉션 ID 목록'),
43
+ appliedQueries: z.array(z.string()).describe('실제로 시도한 검색어 목록'),
44
+ products: z.array(z.unknown()).describe('세븐일레븐 상품 검색 결과'),
45
+ };
46
+ export function createSearchProductsTool() {
47
+ return {
48
+ name: 'seveneleven_search_products',
49
+ metadata: {
50
+ title: '세븐일레븐 상품 검색',
51
+ description: '키워드로 세븐일레븐 상품을 검색합니다.',
52
+ inputSchema: {
53
+ query: z.string().describe('검색어 (예: 삼각김밥, 콜라, 도시락)'),
54
+ page: z.number().optional().default(1).describe('페이지 번호 (기본값: 1)'),
55
+ size: z.number().optional().default(20).describe('페이지당 결과 수 (기본값: 20)'),
56
+ sort: z
57
+ .enum(['recommend', 'recent', 'popular'])
58
+ .optional()
59
+ .default('recommend')
60
+ .describe('정렬 기준 (기본값: recommend)'),
61
+ timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
62
+ },
63
+ outputSchema: searchProductsOutputSchema,
64
+ },
65
+ handler: searchProducts,
66
+ };
67
+ }