daiso 1.0.5 → 1.0.7
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.
- package/README.md +101 -7
- package/dist/api/actionsProxy.js +111 -21
- package/dist/api/configStatus.js +23 -0
- package/dist/api/gs25Handlers.js +68 -22
- package/dist/api/handlers.js +32 -3
- package/dist/api/healthChecks.js +233 -20
- package/dist/api/lottemartHandlers.js +17 -1
- package/dist/api/placesHandlers.js +44 -0
- package/dist/api/response.js +2 -0
- package/dist/api/routes/healthRoutes.js +20 -3
- package/dist/api/routes/placesRoutes.js +9 -0
- package/dist/bin.js +0 -0
- package/dist/cli/args.js +21 -0
- package/dist/cli/commandOptions.js +96 -0
- package/dist/cli/commands/convenience.js +73 -1
- package/dist/cli/commands/daiso.js +18 -2
- package/dist/cli/http.js +58 -0
- package/dist/cli/interactiveItemSearch.js +4 -2
- package/dist/cli/interactivePrompt.js +3 -2
- package/dist/cli.js +25 -1
- package/dist/cliHelp.js +20 -6
- package/dist/cliInteractive.js +75 -6
- package/dist/cliPicker.js +1 -0
- package/dist/cliRenderer.js +93 -3
- package/dist/core/errors.js +66 -0
- package/dist/core/registry.js +246 -3
- package/dist/index.js +19 -4
- package/dist/pages/openapiFullSpec.js +3 -0
- package/dist/pages/openapiSpecActions.js +308 -42
- package/dist/pages/openapiSpecPathsCu.js +7 -0
- package/dist/pages/openapiSpecPathsLotteMart.js +14 -0
- package/dist/pages/openapiSpecPathsOliveyoung.js +35 -0
- package/dist/pages/openapiSpecPathsPlaces.js +85 -0
- package/dist/pages/promptReferenceText.js +16 -7
- package/dist/pages/promptRetailCinemaText.js +24 -2
- package/dist/services/daiso/client.js +8 -0
- package/dist/services/daiso/index.js +2 -0
- package/dist/services/daiso/tools/checkInventory.js +4 -2
- package/dist/services/daiso/tools/findInventoryByName.js +222 -0
- package/dist/services/daiso/tools/getDisplayLocation.js +2 -2
- package/dist/services/emart24/client.js +9 -0
- package/dist/services/emart24/tools/searchProducts.js +27 -17
- package/dist/services/gs25/api.js +3 -0
- package/dist/services/gs25/client.js +216 -30
- package/dist/services/gs25/index.js +11 -3
- package/dist/services/gs25/tools/checkInventory.js +39 -12
- package/dist/services/gs25/tools/findNearbyStores.js +19 -5
- package/dist/services/gs25/tools/searchProducts.js +39 -25
- package/dist/services/lottemart/client.js +10 -1
- package/dist/services/lottemart/tools/searchProducts.js +8 -1
- package/dist/services/lottemart/zetta.js +1 -1
- package/dist/services/oliveyoung/client.js +206 -86
- package/dist/services/oliveyoung/tools/checkInventory.js +7 -2
- package/dist/services/places/client.js +116 -0
- package/dist/services/places/index.js +23 -0
- package/dist/services/places/tools/searchNearby.js +56 -0
- package/dist/services/places/types.js +1 -0
- package/dist/services/seveneleven/client.js +7 -0
- package/dist/services/seveneleven/tools/searchProducts.js +29 -18
- package/dist/utils/http.js +82 -10
- package/dist/utils/zyte.js +74 -26
- package/openapi.json +22 -3
- package/openapi.yaml +16 -3
- package/package.json +28 -9
|
@@ -2,15 +2,129 @@
|
|
|
2
2
|
* GS25 API 클라이언트
|
|
3
3
|
*/
|
|
4
4
|
/* c8 ignore start */
|
|
5
|
-
import { fetchJson } from '../../utils/http.js';
|
|
5
|
+
import { fetchJson, fetchWithTimeout, HttpError } from '../../utils/http.js';
|
|
6
|
+
import { decodeZyteHttpBody, requestByZyte } from '../../utils/zyte.js';
|
|
6
7
|
import { GS25_API } from './api.js';
|
|
7
8
|
import { normalizeStore, toNumber } from './storeUtils.js';
|
|
8
9
|
export { attachDistanceToGs25Stores, calculateDistanceM, extractGs25ProductCandidates, filterGs25StoresByKeyword, selectGs25StoresForKeyword, sortGs25Stores, } from './storeUtils.js';
|
|
9
10
|
const GS25_DEFAULT_HEADERS = {
|
|
10
11
|
Accept: 'application/json, text/plain, */*',
|
|
12
|
+
'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
|
|
13
|
+
'User-Agent': 'Mozilla/5.0 (Linux; Android 15; SM-S928N) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/124.0 Mobile Safari/537.36',
|
|
14
|
+
Origin: 'https://woodongs.com',
|
|
15
|
+
Referer: 'https://woodongs.com/',
|
|
16
|
+
};
|
|
17
|
+
const GS25_TOTAL_SEARCH_HEADERS = {
|
|
18
|
+
...GS25_DEFAULT_HEADERS,
|
|
19
|
+
'Content-Type': 'application/json',
|
|
20
|
+
};
|
|
21
|
+
const GS25_DEFAULT_FETCH_OPTIONS = {
|
|
22
|
+
retries: 1,
|
|
23
|
+
retryDelayMs: 250,
|
|
11
24
|
};
|
|
12
25
|
const GS25_STORES_CACHE_TTL_MS = 60 * 5 * 1000;
|
|
13
26
|
const gs25StoresCache = new Map();
|
|
27
|
+
function extractGs25WebCsrfToken(html) {
|
|
28
|
+
return (html.match(/name="CSRFToken"\s+value="([^"]+)"/)?.[1] ||
|
|
29
|
+
html.match(/ACC\.config\.CSRFToken\s*=\s*"([^"]+)"/)?.[1] ||
|
|
30
|
+
null);
|
|
31
|
+
}
|
|
32
|
+
function extractGs25WebCookieHeader(headers) {
|
|
33
|
+
const setCookie = headers.get('set-cookie') || '';
|
|
34
|
+
const jsessionId = setCookie.match(/JSESSIONID=[^;,\s]+/)?.[0];
|
|
35
|
+
return jsessionId;
|
|
36
|
+
}
|
|
37
|
+
function parseGs25WebLocationResponse(body) {
|
|
38
|
+
if (typeof body === 'string') {
|
|
39
|
+
return JSON.parse(body);
|
|
40
|
+
}
|
|
41
|
+
return body;
|
|
42
|
+
}
|
|
43
|
+
function normalizeGs25WebStore(raw) {
|
|
44
|
+
const propertyNames = (raw.offeringService || []).filter((item) => item.trim().length > 0);
|
|
45
|
+
return {
|
|
46
|
+
storeCode: raw.shopCode || '',
|
|
47
|
+
storeName: raw.shopName || '',
|
|
48
|
+
address: raw.address || '',
|
|
49
|
+
phone: '',
|
|
50
|
+
latitude: toNumber(raw.longs),
|
|
51
|
+
longitude: toNumber(raw.lat),
|
|
52
|
+
serviceCode: '01',
|
|
53
|
+
realStockQuantity: 0,
|
|
54
|
+
pickupStockQuantity: 0,
|
|
55
|
+
deliveryStockQuantity: 0,
|
|
56
|
+
isSoldOut: false,
|
|
57
|
+
searchItemName: '',
|
|
58
|
+
searchItemSellPrice: null,
|
|
59
|
+
propertyNames,
|
|
60
|
+
properties: propertyNames.map((name) => ({
|
|
61
|
+
code: name,
|
|
62
|
+
name,
|
|
63
|
+
type: 'service',
|
|
64
|
+
imageUrl: '',
|
|
65
|
+
})),
|
|
66
|
+
distanceM: null,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async function fetchGs25StoreStock(url, options) {
|
|
70
|
+
try {
|
|
71
|
+
return await fetchJson(url, {
|
|
72
|
+
...GS25_DEFAULT_FETCH_OPTIONS,
|
|
73
|
+
method: 'GET',
|
|
74
|
+
timeout: options.timeout,
|
|
75
|
+
headers: GS25_DEFAULT_HEADERS,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const zyteApiKey = options.zyteApiKey?.trim();
|
|
80
|
+
if (!(error instanceof HttpError) || error.status !== 403 || !zyteApiKey) {
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
const result = await requestByZyte({
|
|
84
|
+
apiKey: zyteApiKey,
|
|
85
|
+
url,
|
|
86
|
+
method: 'GET',
|
|
87
|
+
timeout: options.timeout,
|
|
88
|
+
retries: 1,
|
|
89
|
+
headers: Object.entries(GS25_DEFAULT_HEADERS).map(([name, value]) => ({ name, value })),
|
|
90
|
+
});
|
|
91
|
+
return decodeZyteHttpBody(result);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function fetchGs25TotalSearchResponse(query, options) {
|
|
95
|
+
const { timeout = 20000 } = options;
|
|
96
|
+
const endpoint = new URL(GS25_API.TOTAL_SEARCH_PATH, GS25_API.APIGW_BASE_URL);
|
|
97
|
+
const bodyText = JSON.stringify({ query });
|
|
98
|
+
try {
|
|
99
|
+
return await fetchJson(endpoint.toString(), {
|
|
100
|
+
...GS25_DEFAULT_FETCH_OPTIONS,
|
|
101
|
+
method: 'POST',
|
|
102
|
+
retryUnsafeMethods: true,
|
|
103
|
+
timeout,
|
|
104
|
+
headers: GS25_TOTAL_SEARCH_HEADERS,
|
|
105
|
+
body: bodyText,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
const zyteApiKey = options.zyteApiKey?.trim();
|
|
110
|
+
if (!(error instanceof HttpError) || error.status !== 403 || !zyteApiKey) {
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
const result = await requestByZyte({
|
|
114
|
+
apiKey: zyteApiKey,
|
|
115
|
+
url: endpoint.toString(),
|
|
116
|
+
method: 'POST',
|
|
117
|
+
timeout,
|
|
118
|
+
retries: 1,
|
|
119
|
+
headers: Object.entries(GS25_TOTAL_SEARCH_HEADERS).map(([name, value]) => ({
|
|
120
|
+
name,
|
|
121
|
+
value,
|
|
122
|
+
})),
|
|
123
|
+
bodyText,
|
|
124
|
+
});
|
|
125
|
+
return decodeZyteHttpBody(result);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
14
128
|
function buildCacheKey(params) {
|
|
15
129
|
return [
|
|
16
130
|
params.serviceCode,
|
|
@@ -34,6 +148,7 @@ export async function geocodeGs25Address(address, options = {}) {
|
|
|
34
148
|
endpoint.searchParams.set('address', keyword);
|
|
35
149
|
endpoint.searchParams.set('key', apiKey);
|
|
36
150
|
const body = await fetchJson(endpoint.toString(), {
|
|
151
|
+
...GS25_DEFAULT_FETCH_OPTIONS,
|
|
37
152
|
method: 'GET',
|
|
38
153
|
timeout,
|
|
39
154
|
headers: {
|
|
@@ -54,8 +169,94 @@ export async function geocodeGs25Address(address, options = {}) {
|
|
|
54
169
|
}
|
|
55
170
|
return { latitude, longitude };
|
|
56
171
|
}
|
|
172
|
+
export async function fetchGs25WebStores(keyword, options = {}) {
|
|
173
|
+
const query = keyword.trim();
|
|
174
|
+
if (query.length === 0) {
|
|
175
|
+
return { totalCount: 0, stores: [] };
|
|
176
|
+
}
|
|
177
|
+
const { timeout = 20000 } = options;
|
|
178
|
+
const locationsUrl = new URL(GS25_API.WEB_LOCATIONS_PATH, GS25_API.WEB_BASE_URL);
|
|
179
|
+
const locationsResponse = await fetchWithTimeout(locationsUrl.toString(), {
|
|
180
|
+
...GS25_DEFAULT_FETCH_OPTIONS,
|
|
181
|
+
method: 'GET',
|
|
182
|
+
timeout,
|
|
183
|
+
headers: {
|
|
184
|
+
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
185
|
+
'Accept-Language': GS25_DEFAULT_HEADERS['Accept-Language'],
|
|
186
|
+
'User-Agent': GS25_DEFAULT_HEADERS['User-Agent'],
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
if (!locationsResponse.ok) {
|
|
190
|
+
throw new HttpError(locationsResponse.status, locationsResponse.statusText, await locationsResponse.text());
|
|
191
|
+
}
|
|
192
|
+
const html = await locationsResponse.text();
|
|
193
|
+
const csrfToken = extractGs25WebCsrfToken(html);
|
|
194
|
+
if (!csrfToken) {
|
|
195
|
+
throw new Error('GS25 웹 매장 검색 CSRF 토큰을 찾을 수 없습니다.');
|
|
196
|
+
}
|
|
197
|
+
const form = new URLSearchParams({
|
|
198
|
+
pageNum: '1',
|
|
199
|
+
pageSize: '50',
|
|
200
|
+
searchShopName: query,
|
|
201
|
+
searchSido: '',
|
|
202
|
+
searchGugun: '',
|
|
203
|
+
searchDong: '',
|
|
204
|
+
searchType: '',
|
|
205
|
+
searchTypeService: '0',
|
|
206
|
+
searchTypeToto: '0',
|
|
207
|
+
searchTypeCafe25: '0',
|
|
208
|
+
searchTypeInstant: '0',
|
|
209
|
+
searchTypeDrug: '0',
|
|
210
|
+
searchTypeSelf25: '0',
|
|
211
|
+
searchTypePost: '0',
|
|
212
|
+
searchTypeATM: '0',
|
|
213
|
+
searchTypeWithdrawal: '0',
|
|
214
|
+
searchTypeTaxrefund: '0',
|
|
215
|
+
searchTypeSmartAtm: '0',
|
|
216
|
+
searchTypeSelfCookingUtensils: '0',
|
|
217
|
+
searchTypeDeliveryService: '0',
|
|
218
|
+
searchTypeParcelService: '0',
|
|
219
|
+
searchTypePotatoes: '0',
|
|
220
|
+
searchTypeCardiacDefi: '0',
|
|
221
|
+
searchTypeFishShapedBun: '0',
|
|
222
|
+
searchTypeWine25: '0',
|
|
223
|
+
searchTypeGoPizza: '0',
|
|
224
|
+
searchTypeSpiritWine: '0',
|
|
225
|
+
searchTypeFreshGanghw: '0',
|
|
226
|
+
searchTypeMusinsa: '0',
|
|
227
|
+
searchTypePosa: '0',
|
|
228
|
+
});
|
|
229
|
+
const locationListUrl = new URL(GS25_API.WEB_LOCATION_LIST_PATH, GS25_API.WEB_BASE_URL);
|
|
230
|
+
locationListUrl.searchParams.set('CSRFToken', csrfToken);
|
|
231
|
+
const cookieHeader = extractGs25WebCookieHeader(locationsResponse.headers);
|
|
232
|
+
const body = parseGs25WebLocationResponse(await fetchJson(locationListUrl.toString(), {
|
|
233
|
+
...GS25_DEFAULT_FETCH_OPTIONS,
|
|
234
|
+
method: 'POST',
|
|
235
|
+
retryUnsafeMethods: true,
|
|
236
|
+
timeout,
|
|
237
|
+
headers: {
|
|
238
|
+
Accept: 'application/json, text/javascript, */*; q=0.01',
|
|
239
|
+
'Accept-Language': GS25_DEFAULT_HEADERS['Accept-Language'],
|
|
240
|
+
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
241
|
+
Referer: locationsUrl.toString(),
|
|
242
|
+
'User-Agent': GS25_DEFAULT_HEADERS['User-Agent'],
|
|
243
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
244
|
+
...(cookieHeader ? { Cookie: cookieHeader } : {}),
|
|
245
|
+
},
|
|
246
|
+
body: form.toString(),
|
|
247
|
+
}));
|
|
248
|
+
const stores = (body.results || [])
|
|
249
|
+
.map(normalizeGs25WebStore)
|
|
250
|
+
.filter((store) => store.storeCode.length > 0);
|
|
251
|
+
return {
|
|
252
|
+
totalCount: typeof body.pagination?.totalNumberOfResults === 'number'
|
|
253
|
+
? body.pagination.totalNumberOfResults
|
|
254
|
+
: stores.length,
|
|
255
|
+
stores,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
57
258
|
export async function fetchGs25Stores(params = {}, options = {}) {
|
|
58
|
-
const { serviceCode = '01', keyword = '', itemCode = '', storeCode = '', realTimeStockYn = 'Y', latitude, longitude, pageNumber
|
|
259
|
+
const { serviceCode = '01', keyword = '', itemCode = '', storeCode = '', realTimeStockYn = 'Y', latitude, longitude, pageNumber, pageCount, useCache = true, } = params;
|
|
59
260
|
const { timeout = 20000 } = options;
|
|
60
261
|
const cacheKey = buildCacheKey({
|
|
61
262
|
serviceCode,
|
|
@@ -77,9 +278,13 @@ export async function fetchGs25Stores(params = {}, options = {}) {
|
|
|
77
278
|
}
|
|
78
279
|
const endpoint = new URL(GS25_API.STORE_STOCK_PATH, GS25_API.BFF_BASE_URL);
|
|
79
280
|
endpoint.searchParams.set('serviceCode', serviceCode);
|
|
80
|
-
endpoint.searchParams.set('pageNumber', String(pageNumber));
|
|
81
|
-
endpoint.searchParams.set('pageCount', String(pageCount));
|
|
82
281
|
endpoint.searchParams.set('realTimeStockYn', realTimeStockYn);
|
|
282
|
+
if (typeof pageNumber === 'number' && Number.isFinite(pageNumber)) {
|
|
283
|
+
endpoint.searchParams.set('pageNumber', String(Math.trunc(pageNumber)));
|
|
284
|
+
}
|
|
285
|
+
if (typeof pageCount === 'number' && Number.isFinite(pageCount)) {
|
|
286
|
+
endpoint.searchParams.set('pageCount', String(Math.trunc(pageCount)));
|
|
287
|
+
}
|
|
83
288
|
// itemCode가 있으면 itemCode 사용, 없으면 keyword 사용
|
|
84
289
|
// 주의: itemCode 사용 시 좌표도 필수
|
|
85
290
|
if (itemCode.trim().length > 0) {
|
|
@@ -108,12 +313,13 @@ export async function fetchGs25Stores(params = {}, options = {}) {
|
|
|
108
313
|
endpoint.searchParams.set('isSuperDlvyStoreSelected', 'N');
|
|
109
314
|
endpoint.searchParams.set('isGs25DlvyStoreSelected', 'N');
|
|
110
315
|
}
|
|
111
|
-
const body = await
|
|
112
|
-
method: 'GET',
|
|
316
|
+
const body = await fetchGs25StoreStock(endpoint.toString(), {
|
|
113
317
|
timeout,
|
|
114
|
-
|
|
318
|
+
zyteApiKey: options.zyteApiKey,
|
|
115
319
|
});
|
|
116
|
-
const stores = (body.stores || [])
|
|
320
|
+
const stores = (body.stores || [])
|
|
321
|
+
.map(normalizeStore)
|
|
322
|
+
.filter((store) => store.storeCode.length > 0);
|
|
117
323
|
if (useCache) {
|
|
118
324
|
gs25StoresCache.set(cacheKey, {
|
|
119
325
|
stores,
|
|
@@ -131,17 +337,7 @@ export async function fetchGs25NormalizedKeyword(keyword, options = {}) {
|
|
|
131
337
|
if (query.length === 0) {
|
|
132
338
|
return null;
|
|
133
339
|
}
|
|
134
|
-
const
|
|
135
|
-
const endpoint = new URL(GS25_API.TOTAL_SEARCH_PATH, GS25_API.APIGW_BASE_URL);
|
|
136
|
-
const body = await fetchJson(endpoint.toString(), {
|
|
137
|
-
method: 'POST',
|
|
138
|
-
timeout,
|
|
139
|
-
headers: {
|
|
140
|
-
Accept: 'application/json, text/plain, */*',
|
|
141
|
-
'Content-Type': 'application/json',
|
|
142
|
-
},
|
|
143
|
-
body: JSON.stringify({ query }),
|
|
144
|
-
});
|
|
340
|
+
const body = await fetchGs25TotalSearchResponse(query, options);
|
|
145
341
|
const normalizedKeyword = body.SearchQueryResult?.keywordInfo?.keyword?.trim() || '';
|
|
146
342
|
const normalizedSearchKeyword = body.SearchQueryResult?.keywordInfo?.searchKeyword?.trim() || '';
|
|
147
343
|
if (normalizedKeyword.length === 0 && normalizedSearchKeyword.length === 0) {
|
|
@@ -161,17 +357,7 @@ export async function fetchGs25SearchProducts(keyword, options = {}) {
|
|
|
161
357
|
if (query.length === 0) {
|
|
162
358
|
return [];
|
|
163
359
|
}
|
|
164
|
-
const
|
|
165
|
-
const endpoint = new URL(GS25_API.TOTAL_SEARCH_PATH, GS25_API.APIGW_BASE_URL);
|
|
166
|
-
const body = await fetchJson(endpoint.toString(), {
|
|
167
|
-
method: 'POST',
|
|
168
|
-
timeout,
|
|
169
|
-
headers: {
|
|
170
|
-
Accept: 'application/json, text/plain, */*',
|
|
171
|
-
'Content-Type': 'application/json',
|
|
172
|
-
},
|
|
173
|
-
body: JSON.stringify({ query }),
|
|
174
|
-
});
|
|
360
|
+
const body = await fetchGs25TotalSearchResponse(query, options);
|
|
175
361
|
const products = [];
|
|
176
362
|
const collections = body.SearchQueryResult?.Collection || [];
|
|
177
363
|
for (const collection of collections) {
|
|
@@ -11,12 +11,20 @@ const GS25_METADATA = {
|
|
|
11
11
|
description: 'GS25 매장 탐색, 상품 키워드 검색, 재고 조회 서비스',
|
|
12
12
|
};
|
|
13
13
|
class Gs25Service {
|
|
14
|
+
options;
|
|
14
15
|
metadata = GS25_METADATA;
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.options = options;
|
|
18
|
+
}
|
|
15
19
|
getTools() {
|
|
16
|
-
return [
|
|
20
|
+
return [
|
|
21
|
+
createFindNearbyStoresTool(this.options.googleMapsApiKey, this.options.zyteApiKey),
|
|
22
|
+
createSearchProductsTool(this.options.zyteApiKey),
|
|
23
|
+
createCheckInventoryTool(this.options.googleMapsApiKey, this.options.zyteApiKey),
|
|
24
|
+
];
|
|
17
25
|
}
|
|
18
26
|
}
|
|
19
|
-
export function createGs25Service() {
|
|
20
|
-
return new Gs25Service();
|
|
27
|
+
export function createGs25Service(options = {}) {
|
|
28
|
+
return new Gs25Service(options);
|
|
21
29
|
}
|
|
22
30
|
export * from './types.js';
|
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
/* c8 ignore start */
|
|
5
5
|
import * as z from 'zod';
|
|
6
6
|
import { attachDistanceToGs25Stores, fetchGs25SearchProducts, fetchGs25Stores, filterGs25StoresByKeyword, geocodeGs25Address, selectGs25StoresForKeyword, sortGs25Stores, } from '../client.js';
|
|
7
|
+
function getProcessEnvValue(name) {
|
|
8
|
+
return typeof process !== 'undefined' ? process.env[name] : undefined;
|
|
9
|
+
}
|
|
7
10
|
async function checkInventory(args) {
|
|
8
|
-
const { keyword, itemCode: providedItemCode, latitude, longitude, storeKeyword = '', serviceCode = '01', storeLimit = 20, timeoutMs = 20000, } = args;
|
|
11
|
+
const { keyword, itemCode: providedItemCode, latitude, longitude, storeKeyword = '', serviceCode = '01', storeLimit = 20, timeoutMs = 20000, googleMapsApiKey = getProcessEnvValue('GOOGLE_MAPS_API_KEY'), zyteApiKey = getProcessEnvValue('ZYTE_API_KEY'), } = args;
|
|
9
12
|
if (!keyword || keyword.trim().length === 0) {
|
|
10
13
|
throw new Error('상품 검색어(keyword)를 입력해주세요.');
|
|
11
14
|
}
|
|
@@ -21,13 +24,13 @@ async function checkInventory(args) {
|
|
|
21
24
|
serviceCode,
|
|
22
25
|
}, {
|
|
23
26
|
timeout: timeoutMs,
|
|
27
|
+
zyteApiKey,
|
|
24
28
|
});
|
|
25
|
-
const firstAddress = filterGs25StoresByKeyword(baseStores.stores, storeKeyword).find((store) => store.address.trim().length > 0)
|
|
26
|
-
?.address || '';
|
|
29
|
+
const firstAddress = filterGs25StoresByKeyword(baseStores.stores, storeKeyword).find((store) => store.address.trim().length > 0)?.address || '';
|
|
27
30
|
if (firstAddress.length > 0) {
|
|
28
31
|
const geocoded = await geocodeGs25Address(firstAddress, {
|
|
29
32
|
timeout: timeoutMs,
|
|
30
|
-
googleMapsApiKey
|
|
33
|
+
googleMapsApiKey,
|
|
31
34
|
});
|
|
32
35
|
if (geocoded) {
|
|
33
36
|
resolvedLatitude = geocoded.latitude;
|
|
@@ -57,11 +60,15 @@ async function checkInventory(args) {
|
|
|
57
60
|
useCache: false,
|
|
58
61
|
}, {
|
|
59
62
|
timeout: timeoutMs,
|
|
63
|
+
zyteApiKey,
|
|
60
64
|
});
|
|
61
65
|
}
|
|
62
66
|
else {
|
|
63
67
|
// itemCode가 없으면 totalSearch API로 키워드 → itemCode 변환
|
|
64
|
-
const searchProducts = await fetchGs25SearchProducts(keyword, {
|
|
68
|
+
const searchProducts = await fetchGs25SearchProducts(keyword, {
|
|
69
|
+
timeout: timeoutMs,
|
|
70
|
+
zyteApiKey,
|
|
71
|
+
});
|
|
65
72
|
firstProduct = searchProducts.find((p) => p.itemCode.length > 0);
|
|
66
73
|
if (firstProduct) {
|
|
67
74
|
resolvedItemCode = firstProduct.itemCode;
|
|
@@ -75,6 +82,7 @@ async function checkInventory(args) {
|
|
|
75
82
|
useCache: false,
|
|
76
83
|
}, {
|
|
77
84
|
timeout: timeoutMs,
|
|
85
|
+
zyteApiKey,
|
|
78
86
|
});
|
|
79
87
|
}
|
|
80
88
|
else {
|
|
@@ -88,6 +96,7 @@ async function checkInventory(args) {
|
|
|
88
96
|
useCache: false,
|
|
89
97
|
}, {
|
|
90
98
|
timeout: timeoutMs,
|
|
99
|
+
zyteApiKey,
|
|
91
100
|
});
|
|
92
101
|
}
|
|
93
102
|
}
|
|
@@ -101,7 +110,8 @@ async function checkInventory(args) {
|
|
|
101
110
|
const totalStockQuantity = filteredByStoreKeyword.reduce((sum, item) => sum + Math.max(item.realStockQuantity, 0), 0);
|
|
102
111
|
// searchProducts에서 상품 정보 사용
|
|
103
112
|
const firstProductName = firstProduct?.itemName || firstProduct?.shortItemName || null;
|
|
104
|
-
const firstProductPrice = filteredByStoreKeyword.find((item) => item.searchItemSellPrice !== null)?.searchItemSellPrice ??
|
|
113
|
+
const firstProductPrice = filteredByStoreKeyword.find((item) => item.searchItemSellPrice !== null)?.searchItemSellPrice ??
|
|
114
|
+
null;
|
|
105
115
|
return {
|
|
106
116
|
content: [
|
|
107
117
|
{
|
|
@@ -137,7 +147,7 @@ async function checkInventory(args) {
|
|
|
137
147
|
],
|
|
138
148
|
};
|
|
139
149
|
}
|
|
140
|
-
export function createCheckInventoryTool() {
|
|
150
|
+
export function createCheckInventoryTool(googleMapsApiKey, zyteApiKey) {
|
|
141
151
|
return {
|
|
142
152
|
name: 'gs25_check_inventory',
|
|
143
153
|
metadata: {
|
|
@@ -149,15 +159,32 @@ export function createCheckInventoryTool() {
|
|
|
149
159
|
.string()
|
|
150
160
|
.optional()
|
|
151
161
|
.describe('상품 코드 (gs25_search_products로 조회한 정확한 코드, 제공 시 keyword 검색 건너뜀)'),
|
|
152
|
-
latitude: z
|
|
153
|
-
|
|
154
|
-
|
|
162
|
+
latitude: z
|
|
163
|
+
.number()
|
|
164
|
+
.optional()
|
|
165
|
+
.describe('위도 (선택, 미입력 시 storeKeyword 지오코딩 시도)'),
|
|
166
|
+
longitude: z
|
|
167
|
+
.number()
|
|
168
|
+
.optional()
|
|
169
|
+
.describe('경도 (선택, 미입력 시 storeKeyword 지오코딩 시도)'),
|
|
170
|
+
storeKeyword: z
|
|
171
|
+
.string()
|
|
172
|
+
.optional()
|
|
173
|
+
.describe('매장명/주소 키워드 필터 (예: 강남, 안산 중앙역)'),
|
|
155
174
|
serviceCode: z.string().optional().default('01').describe('서비스 코드 (기본값: 01=GS25)'),
|
|
156
175
|
storeLimit: z.number().optional().default(20).describe('반환할 매장 수 (기본값: 20)'),
|
|
157
|
-
timeoutMs: z
|
|
176
|
+
timeoutMs: z
|
|
177
|
+
.number()
|
|
178
|
+
.optional()
|
|
179
|
+
.default(20000)
|
|
180
|
+
.describe('요청 제한 시간(ms, 기본값: 20000)'),
|
|
158
181
|
},
|
|
159
182
|
},
|
|
160
|
-
handler: checkInventory
|
|
183
|
+
handler: ((args) => checkInventory({
|
|
184
|
+
...args,
|
|
185
|
+
googleMapsApiKey: args.googleMapsApiKey || googleMapsApiKey,
|
|
186
|
+
zyteApiKey: args.zyteApiKey || zyteApiKey,
|
|
187
|
+
})),
|
|
161
188
|
};
|
|
162
189
|
}
|
|
163
190
|
/* c8 ignore stop */
|
|
@@ -4,8 +4,12 @@
|
|
|
4
4
|
import * as z from 'zod';
|
|
5
5
|
import { attachDistanceToGs25Stores, fetchGs25Stores, geocodeGs25Address, selectGs25StoresForKeyword, sortGs25Stores, } from '../client.js';
|
|
6
6
|
const FALLBACK_STORE_LOOKUP_ITEM_CODE = '8801117752804';
|
|
7
|
+
function getProcessEnvValue(name) {
|
|
8
|
+
/* c8 ignore next -- Node 테스트와 CLI 런타임은 process를 제공하며, Worker에는 API에서 키를 주입합니다. */
|
|
9
|
+
return typeof process !== 'undefined' ? process.env[name] : undefined;
|
|
10
|
+
}
|
|
7
11
|
async function findNearbyStores(args) {
|
|
8
|
-
const { latitude, longitude, keyword = '', serviceCode = '01', limit = 20, timeoutMs = 20000, } = args;
|
|
12
|
+
const { latitude, longitude, keyword = '', serviceCode = '01', limit = 20, timeoutMs = 20000, googleMapsApiKey = getProcessEnvValue('GOOGLE_MAPS_API_KEY'), zyteApiKey = getProcessEnvValue('ZYTE_API_KEY'), } = args;
|
|
9
13
|
let resolvedLatitude = latitude;
|
|
10
14
|
let resolvedLongitude = longitude;
|
|
11
15
|
let geocodeUsed = false;
|
|
@@ -13,7 +17,7 @@ async function findNearbyStores(args) {
|
|
|
13
17
|
keyword.trim().length > 0) {
|
|
14
18
|
const geocoded = await geocodeGs25Address(keyword, {
|
|
15
19
|
timeout: timeoutMs,
|
|
16
|
-
googleMapsApiKey
|
|
20
|
+
googleMapsApiKey,
|
|
17
21
|
});
|
|
18
22
|
if (geocoded) {
|
|
19
23
|
resolvedLatitude = geocoded.latitude;
|
|
@@ -27,6 +31,7 @@ async function findNearbyStores(args) {
|
|
|
27
31
|
longitude: resolvedLongitude,
|
|
28
32
|
}, {
|
|
29
33
|
timeout: timeoutMs,
|
|
34
|
+
zyteApiKey,
|
|
30
35
|
});
|
|
31
36
|
let fallbackUsed = false;
|
|
32
37
|
if (result.stores.length === 0 &&
|
|
@@ -42,6 +47,7 @@ async function findNearbyStores(args) {
|
|
|
42
47
|
useCache: false,
|
|
43
48
|
}, {
|
|
44
49
|
timeout: timeoutMs,
|
|
50
|
+
zyteApiKey,
|
|
45
51
|
});
|
|
46
52
|
if (fallbackResult.stores.length > 0) {
|
|
47
53
|
result = fallbackResult;
|
|
@@ -83,7 +89,7 @@ async function findNearbyStores(args) {
|
|
|
83
89
|
],
|
|
84
90
|
};
|
|
85
91
|
}
|
|
86
|
-
export function createFindNearbyStoresTool() {
|
|
92
|
+
export function createFindNearbyStoresTool(googleMapsApiKey, zyteApiKey) {
|
|
87
93
|
return {
|
|
88
94
|
name: 'gs25_find_nearby_stores',
|
|
89
95
|
metadata: {
|
|
@@ -95,9 +101,17 @@ export function createFindNearbyStoresTool() {
|
|
|
95
101
|
keyword: z.string().optional().describe('매장명/주소 키워드 (예: 강남, 안산 중앙역)'),
|
|
96
102
|
serviceCode: z.string().optional().default('01').describe('서비스 코드 (기본값: 01=GS25)'),
|
|
97
103
|
limit: z.number().optional().default(20).describe('반환할 최대 매장 수 (기본값: 20)'),
|
|
98
|
-
timeoutMs: z
|
|
104
|
+
timeoutMs: z
|
|
105
|
+
.number()
|
|
106
|
+
.optional()
|
|
107
|
+
.default(20000)
|
|
108
|
+
.describe('요청 제한 시간(ms, 기본값: 20000)'),
|
|
99
109
|
},
|
|
100
110
|
},
|
|
101
|
-
handler: findNearbyStores
|
|
111
|
+
handler: ((args) => findNearbyStores({
|
|
112
|
+
...args,
|
|
113
|
+
googleMapsApiKey: args.googleMapsApiKey || googleMapsApiKey,
|
|
114
|
+
zyteApiKey: args.zyteApiKey || zyteApiKey,
|
|
115
|
+
})),
|
|
102
116
|
};
|
|
103
117
|
}
|
|
@@ -3,37 +3,50 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import * as z from 'zod';
|
|
5
5
|
import { fetchGs25SearchProducts } from '../client.js';
|
|
6
|
+
function getProcessEnvValue(name) {
|
|
7
|
+
if (typeof process !== 'undefined') {
|
|
8
|
+
return process.env[name];
|
|
9
|
+
}
|
|
10
|
+
/* c8 ignore next -- process가 없는 런타임 방어 분기 */
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
function buildTextResponse(payload) {
|
|
14
|
+
return {
|
|
15
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
16
|
+
structuredContent: payload,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
6
19
|
async function searchProducts(args) {
|
|
7
|
-
const { keyword, limit = 20, timeoutMs = 20000 } = args;
|
|
20
|
+
const { keyword, limit = 20, timeoutMs = 20000, zyteApiKey = getProcessEnvValue('ZYTE_API_KEY'), } = args;
|
|
8
21
|
if (!keyword || keyword.trim().length === 0) {
|
|
9
22
|
throw new Error('상품 검색어(keyword)를 입력해주세요.');
|
|
10
23
|
}
|
|
11
|
-
const products = await fetchGs25SearchProducts(keyword, { timeout: timeoutMs });
|
|
24
|
+
const products = await fetchGs25SearchProducts(keyword, { timeout: timeoutMs, zyteApiKey });
|
|
12
25
|
const limitedProducts = products.slice(0, limit);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
})),
|
|
28
|
-
note: limitedProducts.length === 0
|
|
29
|
-
? '검색 결과가 없습니다. 다른 키워드로 검색해보세요.'
|
|
30
|
-
: '재고 확인 시 itemCode를 gs25_check_inventory에 전달하면 정확한 상품 재고를 조회할 수 있습니다.',
|
|
31
|
-
}, null, 2),
|
|
32
|
-
},
|
|
33
|
-
],
|
|
26
|
+
const payload = {
|
|
27
|
+
keyword,
|
|
28
|
+
count: limitedProducts.length,
|
|
29
|
+
products: limitedProducts.map((p) => ({
|
|
30
|
+
itemCode: p.itemCode,
|
|
31
|
+
itemName: p.itemName,
|
|
32
|
+
shortItemName: p.shortItemName,
|
|
33
|
+
imageUrl: p.imageUrl,
|
|
34
|
+
rating: p.rating,
|
|
35
|
+
stockCheckEnabled: p.stockCheckEnabled,
|
|
36
|
+
})),
|
|
37
|
+
note: limitedProducts.length === 0
|
|
38
|
+
? '검색 결과가 없습니다. 다른 키워드로 검색해보세요.'
|
|
39
|
+
: '재고 확인 시 itemCode를 gs25_check_inventory에 전달하면 정확한 상품 재고를 조회할 수 있습니다.',
|
|
34
40
|
};
|
|
41
|
+
return buildTextResponse(payload);
|
|
35
42
|
}
|
|
36
|
-
|
|
43
|
+
const searchProductsOutputSchema = {
|
|
44
|
+
keyword: z.string().describe('검색어'),
|
|
45
|
+
count: z.number().describe('반환된 상품 수'),
|
|
46
|
+
products: z.array(z.unknown()).describe('GS25 상품 검색 결과'),
|
|
47
|
+
note: z.string().describe('후속 재고 조회 안내'),
|
|
48
|
+
};
|
|
49
|
+
export function createSearchProductsTool(zyteApiKey) {
|
|
37
50
|
return {
|
|
38
51
|
name: 'gs25_search_products',
|
|
39
52
|
metadata: {
|
|
@@ -44,7 +57,8 @@ export function createSearchProductsTool() {
|
|
|
44
57
|
limit: z.number().optional().default(20).describe('반환할 최대 상품 수 (기본값: 20)'),
|
|
45
58
|
timeoutMs: z.number().optional().default(20000).describe('요청 제한 시간(ms, 기본값: 20000)'),
|
|
46
59
|
},
|
|
60
|
+
outputSchema: searchProductsOutputSchema,
|
|
47
61
|
},
|
|
48
|
-
handler: searchProducts,
|
|
62
|
+
handler: ((args) => searchProducts({ ...args, zyteApiKey: zyteApiKey ?? args.zyteApiKey })),
|
|
49
63
|
};
|
|
50
64
|
}
|
|
@@ -7,7 +7,7 @@ import { __testOnlyClearLotteMartGeocodeCache, geocodeLotteMartAddress } from '.
|
|
|
7
7
|
import { __testOnlyClearLotteMartSessionCache, fetchLotteMartHtml, fetchLotteMartPageWithSession, getCachedLotteMartSessionCookie, } from './session.js';
|
|
8
8
|
import { attachDistance, fetchAllStoresForAreaList, fetchKeywordMatchedStores, getTargetAreas, normalizeArea, toDisplayArea, } from './storeSearch.js';
|
|
9
9
|
import { calculateDistanceM, dedupeProducts, matchesBrandVariant, matchesKeyword, parseMarketOptions, parseProductSummary, parseProducts, parseStores, sortStores, } from './parser.js';
|
|
10
|
-
import { createZettaFallbackStore, fetchZettaLotteMartProductsWithPrimaryError } from './zetta.js';
|
|
10
|
+
import { createZettaFallbackStore, fetchZettaLotteMartProducts, fetchZettaLotteMartProductsWithPrimaryError } from './zetta.js';
|
|
11
11
|
const STORE_CACHE_TTL_MS = 30 * 60 * 1000;
|
|
12
12
|
const LOTTEMART_PRODUCT_LEGACY_TIMEOUT_MS = 7000;
|
|
13
13
|
const storeCache = new Map();
|
|
@@ -177,6 +177,9 @@ export async function searchLotteMartProducts(params, options = {}) {
|
|
|
177
177
|
throw new Error('상품 검색어(keyword)를 입력해주세요.');
|
|
178
178
|
}
|
|
179
179
|
const timeout = options.timeout || DEFAULT_LOTTEMART_TIMEOUT_MS;
|
|
180
|
+
if (params.source === 'zetta') {
|
|
181
|
+
return fetchZettaLotteMartProducts(createZettaFallbackStore(params), normalizedKeyword, params.pageLimit || 3, timeout);
|
|
182
|
+
}
|
|
180
183
|
const sessionCookie = await getLotteMartSessionCookie({ timeout: LOTTEMART_PRODUCT_LEGACY_TIMEOUT_MS });
|
|
181
184
|
let resolvedStore;
|
|
182
185
|
try {
|
|
@@ -187,6 +190,9 @@ export async function searchLotteMartProducts(params, options = {}) {
|
|
|
187
190
|
});
|
|
188
191
|
}
|
|
189
192
|
catch (error) {
|
|
193
|
+
if (params.source === 'legacy') {
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
190
196
|
const fallbackStore = createZettaFallbackStore(params);
|
|
191
197
|
return fetchZettaLotteMartProductsWithPrimaryError(fallbackStore, normalizedKeyword, params.pageLimit || 3, timeout, error);
|
|
192
198
|
}
|
|
@@ -208,6 +214,9 @@ export async function searchLotteMartProducts(params, options = {}) {
|
|
|
208
214
|
}, LOTTEMART_PRODUCT_LEGACY_TIMEOUT_MS, sessionCookie, options.zyteApiKey);
|
|
209
215
|
}
|
|
210
216
|
catch (error) {
|
|
217
|
+
if (params.source === 'legacy') {
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
211
220
|
return fetchZettaLotteMartProductsWithPrimaryError(resolvedStore, normalizedKeyword, params.pageLimit || 3, timeout, error);
|
|
212
221
|
}
|
|
213
222
|
const pagedSessionCookie = await getCachedLotteMartSessionCookie(LOTTEMART_PRODUCT_LEGACY_TIMEOUT_MS);
|
|
@@ -5,13 +5,14 @@ import * as z from 'zod';
|
|
|
5
5
|
import { searchLotteMartProducts } from '../client.js';
|
|
6
6
|
import { DEFAULT_LOTTEMART_TIMEOUT_MS } from '../config.js';
|
|
7
7
|
async function searchProducts(args) {
|
|
8
|
-
const { area, storeCode, storeName, keyword, pageLimit = 3, timeoutMs = DEFAULT_LOTTEMART_TIMEOUT_MS, zyteApiKey, } = args;
|
|
8
|
+
const { area, storeCode, storeName, keyword, pageLimit = 3, source = 'auto', timeoutMs = DEFAULT_LOTTEMART_TIMEOUT_MS, zyteApiKey, } = args;
|
|
9
9
|
const result = await searchLotteMartProducts({
|
|
10
10
|
area,
|
|
11
11
|
storeCode,
|
|
12
12
|
storeName,
|
|
13
13
|
keyword,
|
|
14
14
|
pageLimit,
|
|
15
|
+
source,
|
|
15
16
|
}, {
|
|
16
17
|
timeout: timeoutMs,
|
|
17
18
|
zyteApiKey,
|
|
@@ -26,6 +27,7 @@ async function searchProducts(args) {
|
|
|
26
27
|
storeName: result.storeName,
|
|
27
28
|
keyword,
|
|
28
29
|
pageLimit,
|
|
30
|
+
source,
|
|
29
31
|
totalCount: result.totalCount,
|
|
30
32
|
totalPages: result.totalPages,
|
|
31
33
|
count: result.products.length,
|
|
@@ -47,6 +49,11 @@ export function createSearchProductsTool(zyteApiKey) {
|
|
|
47
49
|
storeName: z.string().optional().describe('매장명 (예: 강변점)'),
|
|
48
50
|
keyword: z.string().describe('상품 검색어 (예: 콜라, 우유, 과자)'),
|
|
49
51
|
pageLimit: z.number().optional().default(3).describe('추가 조회할 최대 페이지 수 (기본값: 3)'),
|
|
52
|
+
source: z
|
|
53
|
+
.enum(['auto', 'legacy', 'zetta'])
|
|
54
|
+
.optional()
|
|
55
|
+
.default('auto')
|
|
56
|
+
.describe('상품 검색 경로 (auto: 구형 경로 후 제타 대체, zetta: 빠른 제타 API 우선, legacy: 구형 경로만 사용)'),
|
|
50
57
|
timeoutMs: z
|
|
51
58
|
.number()
|
|
52
59
|
.optional()
|