daiso 1.0.4 → 1.0.5

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 (158) hide show
  1. package/README.md +218 -540
  2. package/dist/api/actionsProxy.js +82 -0
  3. package/dist/api/cgvHandlers.js +103 -16
  4. package/dist/api/daisoHandlers.js +127 -0
  5. package/dist/api/emart24Handlers.js +176 -0
  6. package/dist/api/gs25Handlers.js +256 -0
  7. package/dist/api/handlers.js +184 -108
  8. package/dist/api/healthChecks.js +332 -0
  9. package/dist/api/lottecinemaHandlers.js +155 -0
  10. package/dist/api/lottemartHandlers.js +128 -0
  11. package/dist/api/megaboxHandlers.js +81 -50
  12. package/dist/api/routes/cuRoutes.js +17 -0
  13. package/dist/api/routes/daisoRoutes.js +6 -1
  14. package/dist/api/routes/emart24Routes.js +22 -0
  15. package/dist/api/routes/gs25Routes.js +22 -0
  16. package/dist/api/routes/healthRoutes.js +44 -0
  17. package/dist/api/routes/lottecinemaRoutes.js +22 -0
  18. package/dist/api/routes/lottemartRoutes.js +18 -0
  19. package/dist/api/routes/oliveyoungRoutes.js +6 -1
  20. package/dist/api/routes/sevenelevenRoutes.js +32 -0
  21. package/dist/api/sevenelevenHandlers.js +200 -0
  22. package/dist/cli/args.js +54 -0
  23. package/dist/cli/commands/convenience.js +241 -0
  24. package/dist/cli/commands/daiso.js +96 -0
  25. package/dist/cli/constants.js +5 -0
  26. package/dist/cli/deps.js +48 -0
  27. package/dist/cli/http.js +98 -0
  28. package/dist/cli/interactiveFetch.js +31 -0
  29. package/dist/cli/interactiveItemSearch.js +259 -0
  30. package/dist/cli/interactivePrompt.js +49 -0
  31. package/dist/cli/interactiveTypes.js +4 -0
  32. package/dist/cli/types.js +4 -0
  33. package/dist/cli.js +66 -197
  34. package/dist/cliHelp.js +206 -1
  35. package/dist/cliInteractive.js +174 -0
  36. package/dist/cliPicker.js +56 -0
  37. package/dist/cliRenderer.js +4 -0
  38. package/dist/index.js +147 -8
  39. package/dist/pages/openapi.js +2 -1
  40. package/dist/pages/openapiFullSpec.js +58 -0
  41. package/dist/pages/openapiResponse.js +28 -0
  42. package/dist/pages/openapiSpec.js +2 -41
  43. package/dist/pages/openapiSpecActions.js +109 -0
  44. package/dist/pages/openapiSpecComponents.js +30 -391
  45. package/dist/pages/openapiSpecComponentsCgv.js +24 -0
  46. package/dist/pages/openapiSpecComponentsCu.js +112 -0
  47. package/dist/pages/openapiSpecComponentsDaisoOliveyoung.js +342 -0
  48. package/dist/pages/openapiSpecComponentsEmart24.js +141 -0
  49. package/dist/pages/openapiSpecComponentsGs25.js +109 -0
  50. package/dist/pages/openapiSpecComponentsLotteMart.js +85 -0
  51. package/dist/pages/openapiSpecComponentsLottecinema.js +163 -0
  52. package/dist/pages/openapiSpecComponentsMegabox.js +144 -0
  53. package/dist/pages/openapiSpecComponentsSeveneleven.js +82 -0
  54. package/dist/pages/openapiSpecPathsCgv.js +66 -3
  55. package/dist/pages/openapiSpecPathsCu.js +138 -0
  56. package/dist/pages/openapiSpecPathsDaiso.js +302 -0
  57. package/dist/pages/openapiSpecPathsDaisoOliveyoung.js +4 -376
  58. package/dist/pages/openapiSpecPathsEmart24.js +223 -0
  59. package/dist/pages/openapiSpecPathsGs25.js +166 -0
  60. package/dist/pages/openapiSpecPathsLotteMart.js +135 -0
  61. package/dist/pages/openapiSpecPathsLottecinema.js +212 -0
  62. package/dist/pages/openapiSpecPathsMegabox.js +52 -3
  63. package/dist/pages/openapiSpecPathsOliveyoung.js +231 -0
  64. package/dist/pages/openapiSpecPathsSeveneleven.js +234 -0
  65. package/dist/pages/prompt.js +8 -402
  66. package/dist/pages/promptCoreText.js +278 -0
  67. package/dist/pages/promptReferenceText.js +131 -0
  68. package/dist/pages/promptRetailCinemaText.js +273 -0
  69. package/dist/services/cgv/client.js +6 -1
  70. package/dist/services/cgv/index.js +3 -3
  71. package/dist/services/cgv/location.js +235 -0
  72. package/dist/services/cgv/tools/findTheaters.js +30 -5
  73. package/dist/services/cgv/tools/getTimetable.js +42 -14
  74. package/dist/services/cgv/tools/searchMovies.js +40 -12
  75. package/dist/services/cgv/transport.js +0 -3
  76. package/dist/services/cu/api.js +11 -0
  77. package/dist/services/cu/client.js +297 -0
  78. package/dist/services/cu/index.js +21 -0
  79. package/dist/services/cu/tools/checkInventory.js +123 -0
  80. package/dist/services/cu/tools/findNearbyStores.js +49 -0
  81. package/dist/services/cu/types.js +4 -0
  82. package/dist/services/daiso/api.js +22 -2
  83. package/dist/services/daiso/client.js +59 -0
  84. package/dist/services/daiso/index.js +2 -0
  85. package/dist/services/daiso/product.js +17 -0
  86. package/dist/services/daiso/tools/checkInventory.js +76 -41
  87. package/dist/services/daiso/tools/findStores.js +17 -7
  88. package/dist/services/daiso/tools/getDisplayLocation.js +75 -0
  89. package/dist/services/emart24/api.js +11 -0
  90. package/dist/services/emart24/client.js +173 -0
  91. package/dist/services/emart24/index.js +22 -0
  92. package/dist/services/emart24/inventoryLookup.js +134 -0
  93. package/dist/services/emart24/tools/checkInventory.js +54 -0
  94. package/dist/services/emart24/tools/findNearbyStores.js +72 -0
  95. package/dist/services/emart24/tools/searchProducts.js +63 -0
  96. package/dist/services/emart24/types.js +4 -0
  97. package/dist/services/gs25/api.js +9 -0
  98. package/dist/services/gs25/client.js +198 -0
  99. package/dist/services/gs25/index.js +22 -0
  100. package/dist/services/gs25/storeUtils.js +168 -0
  101. package/dist/services/gs25/tools/checkInventory.js +163 -0
  102. package/dist/services/gs25/tools/findNearbyStores.js +103 -0
  103. package/dist/services/gs25/tools/searchProducts.js +50 -0
  104. package/dist/services/gs25/types.js +4 -0
  105. package/dist/services/lottecinema/api.js +12 -0
  106. package/dist/services/lottecinema/client.js +161 -0
  107. package/dist/services/lottecinema/index.js +30 -0
  108. package/dist/services/lottecinema/location.js +191 -0
  109. package/dist/services/lottecinema/tools/findNearbyTheaters.js +47 -0
  110. package/dist/services/lottecinema/tools/getRemainingSeats.js +85 -0
  111. package/dist/services/lottecinema/tools/listNowShowing.js +79 -0
  112. package/dist/services/lottecinema/types.js +4 -0
  113. package/dist/services/lottemart/api.js +31 -0
  114. package/dist/services/lottemart/client.js +245 -0
  115. package/dist/services/lottemart/clientTypes.js +4 -0
  116. package/dist/services/lottemart/config.js +4 -0
  117. package/dist/services/lottemart/debug.js +92 -0
  118. package/dist/services/lottemart/geocode.js +49 -0
  119. package/dist/services/lottemart/index.js +30 -0
  120. package/dist/services/lottemart/keyword.js +29 -0
  121. package/dist/services/lottemart/parser.js +185 -0
  122. package/dist/services/lottemart/session.js +331 -0
  123. package/dist/services/lottemart/storeSearch.js +52 -0
  124. package/dist/services/lottemart/tools/findNearbyStores.js +67 -0
  125. package/dist/services/lottemart/tools/searchProducts.js +62 -0
  126. package/dist/services/lottemart/types.js +4 -0
  127. package/dist/services/lottemart/zetta.js +105 -0
  128. package/dist/services/megabox/location.js +219 -0
  129. package/dist/services/megabox/tools/findNearbyTheaters.js +13 -44
  130. package/dist/services/megabox/tools/getRemainingSeats.js +31 -3
  131. package/dist/services/megabox/tools/listNowShowing.js +31 -3
  132. package/dist/services/oliveyoung/api.js +2 -0
  133. package/dist/services/oliveyoung/client.js +150 -9
  134. package/dist/services/oliveyoung/index.js +3 -1
  135. package/dist/services/oliveyoung/tools/checkInventory.js +15 -4
  136. package/dist/services/oliveyoung/tools/searchProducts.js +57 -0
  137. package/dist/services/seveneleven/api.js +15 -0
  138. package/dist/services/seveneleven/client.js +279 -0
  139. package/dist/services/seveneleven/index.js +30 -0
  140. package/dist/services/seveneleven/inventory.js +302 -0
  141. package/dist/services/seveneleven/productKeyword.js +141 -0
  142. package/dist/services/seveneleven/tools/checkInventory.js +82 -0
  143. package/dist/services/seveneleven/tools/getCatalogSnapshot.js +55 -0
  144. package/dist/services/seveneleven/tools/getSearchPopwords.js +41 -0
  145. package/dist/services/seveneleven/tools/searchProducts.js +56 -0
  146. package/dist/services/seveneleven/tools/searchStores.js +45 -0
  147. package/dist/services/seveneleven/types.js +4 -0
  148. package/dist/utils/cliInteractiveHelpers.js +63 -0
  149. package/dist/utils/daisoKeyword.js +30 -0
  150. package/dist/utils/http.js +16 -2
  151. package/openapi.json +1977 -734
  152. package/openapi.yaml +1430 -543
  153. package/package.json +28 -15
  154. package/dist/tools/checkInventory.js +0 -98
  155. package/dist/tools/findStores.js +0 -125
  156. package/dist/tools/getPriceInfo.js +0 -77
  157. package/dist/tools/searchProducts.js +0 -61
  158. package/dist/types/index.js +0 -4
@@ -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,279 @@
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
+ function toNumber(value) {
13
+ if (typeof value === 'number') {
14
+ return Number.isFinite(value) ? value : 0;
15
+ }
16
+ if (typeof value === 'string') {
17
+ const parsed = Number.parseFloat(value);
18
+ return Number.isFinite(parsed) ? parsed : 0;
19
+ }
20
+ return 0;
21
+ }
22
+ function toStringValue(value) {
23
+ return typeof value === 'string' ? value : '';
24
+ }
25
+ function normalizeStoreKeyword(keyword) {
26
+ return keyword
27
+ .trim()
28
+ .replace(/\s+/g, '')
29
+ .replace(/(역|지점|점)$/g, '');
30
+ }
31
+ function toBooleanYn(value) {
32
+ const normalized = toStringValue(value).trim().toUpperCase();
33
+ return normalized === 'Y' || normalized === 'TRUE' || normalized === '1';
34
+ }
35
+ function toRawProduct(input) {
36
+ if (!input || typeof input !== 'object') {
37
+ return {};
38
+ }
39
+ const record = input;
40
+ if (record.field && typeof record.field === 'object') {
41
+ return record.field;
42
+ }
43
+ return record;
44
+ }
45
+ function normalizeProduct(raw) {
46
+ return {
47
+ productNo: toStringValue(raw.prdNo),
48
+ itemCode: toStringValue(raw.itemCd),
49
+ itemName: toStringValue(raw.itemOnm),
50
+ salePrice: toNumber(raw.onlinePrice),
51
+ originalPrice: toNumber(raw.onlineCost),
52
+ imageUrl: toStringValue(raw.repImgUrl),
53
+ eventName: toStringValue(raw.eventGbnNm),
54
+ itemType: toStringValue(raw.itemGbnNm),
55
+ makerName: toStringValue(raw.makerNm),
56
+ reviewScore: raw.avgEvalScore === undefined ? null : toNumber(raw.avgEvalScore),
57
+ reviewCount: toNumber(raw.productReviewCnt),
58
+ };
59
+ }
60
+ function normalizeProducts(items) {
61
+ return items
62
+ .map(toRawProduct)
63
+ .map(normalizeProduct)
64
+ .filter((item) => item.itemCode.length > 0 || item.itemName.length > 0);
65
+ }
66
+ function normalizeStore(raw) {
67
+ const address1 = toStringValue(raw.addr1);
68
+ const address2 = toStringValue(raw.addr2);
69
+ const mergedAddress = `${address1} ${address2}`.trim();
70
+ return {
71
+ storeCode: toStringValue(raw.storeCode || raw.storeCd || raw.strCd || raw.storCd || raw.shopCd),
72
+ storeName: toStringValue(raw.storeName || raw.storeNm || raw.strNm || raw.storNm || raw.shopNm),
73
+ address: toStringValue(raw.address || raw.addr || raw.roadAddr || raw.shopAddr) || mergedAddress,
74
+ latitude: toNumber(raw.latitude || raw.storeLat || raw.lat || raw.yPos || raw.y || raw.yCoord),
75
+ longitude: toNumber(raw.longitude || raw.storeLon || raw.lng || raw.xPos || raw.x || raw.xCoord),
76
+ pickupEnabled: toBooleanYn(raw.pickupEnabled || raw.pickupYn),
77
+ deliveryEnabled: toBooleanYn(raw.deliveryEnabled || raw.deliveryYn || raw.dlvyYn),
78
+ closeYn: toStringValue(raw.closeYn || raw.storeCloseYn || raw.closeYN || raw.clsYn),
79
+ };
80
+ }
81
+ function normalizeStockProductMeta(raw) {
82
+ return {
83
+ productNo: toStringValue(raw.prdNo),
84
+ itemCode: toStringValue(raw.itemCd),
85
+ itemName: toStringValue(raw.itemOnm),
86
+ smCode: toStringValue(raw.smCd),
87
+ stockManagementCode: toStringValue(raw.stokMngCd),
88
+ stockManagementQuantity: toNumber(raw.stokMngQty),
89
+ stockApplicationRate: toStringValue(raw.stockApplicationRate),
90
+ };
91
+ }
92
+ function normalizeStores(items) {
93
+ return items
94
+ .map((item) => {
95
+ if (!item || typeof item !== 'object') {
96
+ return {};
97
+ }
98
+ const record = item;
99
+ if (record.field && typeof record.field === 'object') {
100
+ return record.field;
101
+ }
102
+ return record;
103
+ })
104
+ .map(normalizeStore)
105
+ .filter((store) => store.storeCode.length > 0 || store.storeName.length > 0);
106
+ }
107
+ async function requestSevenElevenJson(path, method, body, options = {}) {
108
+ const { timeout = 15000 } = options;
109
+ const url = `${SEVENELEVEN_API.BASE_URL}${path}`;
110
+ return fetchJson(url, {
111
+ method,
112
+ timeout,
113
+ headers: SEVENELEVEN_DEFAULT_HEADERS,
114
+ body: method === 'POST' ? JSON.stringify(body || {}) : undefined,
115
+ });
116
+ }
117
+ export async function searchSevenElevenProducts(params, options = {}) {
118
+ const { query, page = 1, size = 20 } = params;
119
+ const pageNo = Math.max(Math.trunc(page) - 1, 0);
120
+ const pageSize = Math.max(Math.trunc(size), 1);
121
+ const response = await requestSevenElevenJson(SEVENELEVEN_API.SEARCH_GOODS_PATH, 'POST', {
122
+ // 공개 검색 엔드포인트는 pageNo/pageSize만 안정적으로 동작한다.
123
+ // sort를 함께 보내면 빈 결과가 내려오는 케이스가 확인되어 제외한다.
124
+ query,
125
+ pageNo,
126
+ pageSize,
127
+ }, options);
128
+ const data = response.data || {};
129
+ const queryResult = data.SearchQueryResult;
130
+ const collections = queryResult?.Collection || [];
131
+ const collectionIds = collections.map((item) => item.CollectionId || '').filter((id) => id.length > 0);
132
+ let totalCount = 0;
133
+ const allDocuments = [];
134
+ for (const collection of collections) {
135
+ totalCount += toNumber(collection.Documentset?.totalCount);
136
+ allDocuments.push(...(collection.Documentset?.Document || []));
137
+ }
138
+ const collectionProducts = normalizeProducts(allDocuments);
139
+ const contentProducts = normalizeProducts(Array.isArray(data.content) ? data.content : []);
140
+ const products = (collectionProducts.length > 0 ? collectionProducts : contentProducts).slice(0, pageSize);
141
+ return {
142
+ query: queryResult?.query || query,
143
+ totalCount,
144
+ products,
145
+ collectionIds,
146
+ };
147
+ }
148
+ export async function fetchSevenElevenStoresByKeyword(params, options = {}) {
149
+ const { keyword, limit = 20 } = params;
150
+ const query = normalizeStoreKeyword(keyword) || keyword.trim();
151
+ const safeLimit = Number.isFinite(limit) ? Math.trunc(limit) : 20;
152
+ const listCount = Math.max(1, Math.min(safeLimit, 9999));
153
+ const response = await requestSevenElevenJson(SEVENELEVEN_API.SEARCH_STORE_PATH, 'POST', {
154
+ collection: 'store',
155
+ query,
156
+ sort: 'Date/desc',
157
+ listCount,
158
+ }, options);
159
+ const data = response.data || {};
160
+ const queryResult = data.SearchQueryResult;
161
+ const collections = queryResult?.Collection || [];
162
+ let totalCount = 0;
163
+ const allDocuments = [];
164
+ for (const collection of collections) {
165
+ totalCount += toNumber(collection.Documentset?.totalCount);
166
+ allDocuments.push(...(collection.Documentset?.Document || []));
167
+ }
168
+ const stores = normalizeStores(allDocuments);
169
+ return {
170
+ query: queryResult?.query || query,
171
+ totalCount,
172
+ stores,
173
+ };
174
+ }
175
+ export async function fetchSevenElevenSearchPopwords(label = 'home', options = {}) {
176
+ const encodedLabel = encodeURIComponent(label);
177
+ const response = await requestSevenElevenJson(`${SEVENELEVEN_API.SEARCH_POPWORD_PATH}?label=${encodedLabel}`, 'POST', {}, options);
178
+ const { data } = response;
179
+ if (Array.isArray(data)) {
180
+ return data.map((item) => toStringValue(item)).filter((item) => item.length > 0);
181
+ }
182
+ if (!data || typeof data !== 'object') {
183
+ return [];
184
+ }
185
+ const record = data;
186
+ const candidates = [record.keywords, record.popwords, record.wordList, record.list];
187
+ for (const candidate of candidates) {
188
+ if (!Array.isArray(candidate)) {
189
+ continue;
190
+ }
191
+ const values = candidate
192
+ .map((item) => {
193
+ if (typeof item === 'string') {
194
+ return item;
195
+ }
196
+ if (item && typeof item === 'object') {
197
+ const value = item;
198
+ return toStringValue(value.keyword || value.word || value.text || value.name);
199
+ }
200
+ return '';
201
+ })
202
+ .filter((item) => item.length > 0);
203
+ if (values.length > 0) {
204
+ return values;
205
+ }
206
+ }
207
+ return [];
208
+ }
209
+ export async function fetchSevenElevenStockProductMeta(itemCode, options = {}) {
210
+ const { timeout = 15000 } = options;
211
+ const encodedItemCode = encodeURIComponent(itemCode.trim());
212
+ const url = `${SEVENELEVEN_API.BASE_URL}${SEVENELEVEN_API.PRODUCT_SEARCH_STOCK_PATH}?itemCd=${encodedItemCode}`;
213
+ const response = await fetchJson(url, {
214
+ method: 'GET',
215
+ timeout,
216
+ headers: {
217
+ Accept: SEVENELEVEN_DEFAULT_HEADERS.Accept,
218
+ 'User-Agent': SEVENELEVEN_DEFAULT_HEADERS['User-Agent'],
219
+ },
220
+ });
221
+ const productMeta = normalizeStockProductMeta(response);
222
+ if (productMeta.itemCode.length === 0 || productMeta.smCode.length === 0) {
223
+ return null;
224
+ }
225
+ return productMeta;
226
+ }
227
+ function normalizeExhibitions(items) {
228
+ return items
229
+ .map((item) => {
230
+ const value = (item || {});
231
+ const productList = Array.isArray(value.exhibitionProductList) ? value.exhibitionProductList : [];
232
+ return {
233
+ exhibitionIdx: toNumber(value.exhibitionIdx),
234
+ exhibitionName: toStringValue(value.exhibitionName),
235
+ startDate: toStringValue(value.exhibitionStartDate),
236
+ endDate: toStringValue(value.exhibitionEndDate),
237
+ productCount: productList.length,
238
+ };
239
+ })
240
+ .filter((item) => item.exhibitionIdx > 0 || item.exhibitionName.length > 0);
241
+ }
242
+ function extractContentArray(data) {
243
+ if (Array.isArray(data)) {
244
+ return data;
245
+ }
246
+ if (!data || typeof data !== 'object') {
247
+ return [];
248
+ }
249
+ const record = data;
250
+ if (Array.isArray(record.content)) {
251
+ return record.content;
252
+ }
253
+ if (Array.isArray(record.list)) {
254
+ return record.list;
255
+ }
256
+ return [];
257
+ }
258
+ export async function fetchSevenElevenCatalogSnapshot(options = {}) {
259
+ const { includeIssues = true, includeExhibition = true, timeout = 15000 } = options;
260
+ const tasks = [
261
+ requestSevenElevenJson(SEVENELEVEN_API.PRODUCT_PAGES_PATH, 'GET', null, { timeout }),
262
+ includeIssues
263
+ ? requestSevenElevenJson(SEVENELEVEN_API.PRODUCT_ISSUES_PATH, 'GET', null, { timeout })
264
+ : Promise.resolve({ data: { content: [] } }),
265
+ includeExhibition
266
+ ? requestSevenElevenJson(SEVENELEVEN_API.EXHIBITION_MAIN_PATH, 'GET', null, { timeout })
267
+ : Promise.resolve({ data: [] }),
268
+ ];
269
+ const [pagesResult, issuesResult, exhibitionsResult] = await Promise.allSettled(tasks);
270
+ const pagesData = pagesResult.status === 'fulfilled' ? pagesResult.value.data : [];
271
+ const issuesData = issuesResult.status === 'fulfilled' ? issuesResult.value.data : [];
272
+ const exhibitionData = exhibitionsResult.status === 'fulfilled' ? exhibitionsResult.value.data : [];
273
+ return {
274
+ pages: normalizeProducts(extractContentArray(pagesData)),
275
+ issues: normalizeProducts(extractContentArray(issuesData)),
276
+ exhibitions: normalizeExhibitions(extractContentArray(exhibitionData)),
277
+ };
278
+ }
279
+ /* 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';
@@ -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 */