daiso 1.0.9 → 1.1.0
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 +88 -9
- package/dist/api/actionsProxy.js +3 -0
- package/dist/api/cgvHandlers.js +15 -3
- package/dist/api/configStatus.js +1 -1
- package/dist/api/dtryxHandlers.js +113 -0
- package/dist/api/emart24Handlers.js +6 -2
- package/dist/api/gs25Handlers.js +22 -19
- package/dist/api/handlers.js +3 -0
- package/dist/api/healthCheckDefinitions.js +56 -8
- package/dist/api/healthCheckShape.js +24 -8
- package/dist/api/healthChecks.js +48 -20
- package/dist/api/operationalAuth.js +32 -0
- package/dist/api/routes/dtryxRoutes.js +22 -0
- package/dist/api/routes/healthRoutes.js +19 -11
- package/dist/api/routes/rateLimitStatsRoutes.js +274 -0
- package/dist/api/routes/registerServiceRoutes.js +37 -0
- package/dist/api/sevenelevenHandlers.js +7 -3
- package/dist/cli/commandOptions.js +24 -0
- package/dist/cli/commands/cinema.js +27 -0
- package/dist/cli/commands/convenience.js +2 -2
- package/dist/cli.js +7 -1
- package/dist/cliHelp.js +3 -337
- package/dist/cliHelpDefinitions.js +369 -0
- package/dist/cliInteractive.js +8 -5
- package/dist/core/errors.js +17 -2
- package/dist/core/registry.js +13 -15
- package/dist/durableObjects/dailyRateLimiter.js +185 -0
- package/dist/durableObjects/rateLimitMetricsStore.js +144 -0
- package/dist/index.js +16 -30
- package/dist/middleware/dailyRateLimit.js +176 -0
- package/dist/pages/openapiFullSpec.js +2 -0
- package/dist/pages/openapiSpecActionParameters.js +429 -0
- package/dist/pages/openapiSpecActions.js +1 -391
- package/dist/pages/openapiSpecComponentsLottecinema.js +2 -2
- package/dist/pages/openapiSpecPathsDtryx.js +148 -0
- package/dist/pages/openapiSpecPathsLotteMart.js +10 -2
- package/dist/pages/promptRetailCinemaText.js +9 -1
- package/dist/services/cgv/client.js +7 -4
- package/dist/services/cgv/errors.js +12 -0
- package/dist/services/cgv/transport.js +19 -2
- package/dist/services/cu/client.js +42 -12
- package/dist/services/cu/index.js +13 -3
- package/dist/services/cu/tools/checkInventory.js +65 -45
- package/dist/services/cu/tools/findNearbyStores.js +4 -3
- package/dist/services/cu/upstreamError.js +15 -0
- package/dist/services/dtryx/api.js +25 -0
- package/dist/services/dtryx/client.js +133 -0
- package/dist/services/dtryx/index.js +24 -0
- package/dist/services/dtryx/location.js +216 -0
- package/dist/services/dtryx/tools/getRemainingSeats.js +117 -0
- package/dist/services/dtryx/tools/listCinemas.js +46 -0
- package/dist/services/dtryx/tools/listNowShowing.js +77 -0
- package/dist/services/dtryx/types.js +4 -0
- package/dist/services/dtryx/validation.js +21 -0
- package/dist/services/emart24/tools/searchProducts.js +37 -19
- package/dist/services/feedback/tools/submitDeveloperRequest.js +15 -1
- package/dist/services/gs25/api.js +2 -2
- package/dist/services/gs25/client.js +16 -123
- package/dist/services/gs25/errors.js +12 -0
- package/dist/services/gs25/index.js +2 -2
- package/dist/services/gs25/productSearch.js +1 -0
- package/dist/services/gs25/storeStockTransport.js +55 -0
- package/dist/services/gs25/tools/checkInventory.js +7 -2
- package/dist/services/gs25/tools/findNearbyStores.js +20 -5
- package/dist/services/lottecinema/client.js +4 -2
- package/dist/services/lottemart/config.js +1 -1
- package/dist/services/lottemart/session.js +29 -160
- package/dist/services/lottemart/socketTransport.js +149 -0
- package/dist/services/oliveyoung/client.js +1 -0
- package/dist/services/places/client.js +29 -8
- package/dist/services/seveneleven/client.js +18 -8
- package/dist/services/seveneleven/index.js +11 -7
- package/dist/services/seveneleven/inventory.js +5 -2
- package/dist/services/seveneleven/productKeyword.js +3 -2
- package/dist/services/seveneleven/tools/checkInventory.js +4 -3
- package/dist/services/seveneleven/tools/getCatalogSnapshot.js +4 -3
- package/dist/services/seveneleven/tools/getSearchPopwords.js +4 -3
- package/dist/services/seveneleven/tools/searchProducts.js +40 -20
- package/dist/services/seveneleven/tools/searchStores.js +4 -3
- package/dist/utils/format.js +10 -3
- package/dist/utils/http.js +38 -10
- package/dist/utils/zyte.js +13 -2
- package/dist/utils/zyteJsonFallback.js +67 -0
- package/openapi.json +51 -3
- package/openapi.yaml +36 -3
- package/package.json +28 -24
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* GS25 API 클라이언트
|
|
3
3
|
*/
|
|
4
4
|
/* c8 ignore start */
|
|
5
|
-
import { fetchJson
|
|
6
|
-
import { decodeZyteHttpBody, requestByZyte } from '../../utils/zyte.js';
|
|
5
|
+
import { fetchJson } from '../../utils/http.js';
|
|
7
6
|
import { GS25_API } from './api.js';
|
|
8
7
|
import { normalizeStore, toNumber } from './storeUtils.js';
|
|
8
|
+
import { fetchGs25StoreStockResponse } from './storeStockTransport.js';
|
|
9
9
|
export { fetchGs25NormalizedKeyword, fetchGs25SearchProducts, } from './productSearch.js';
|
|
10
10
|
export { attachDistanceToGs25Stores, calculateDistanceM, extractGs25ProductCandidates, filterGs25StoresByKeyword, selectGs25StoresForKeyword, sortGs25Stores, } from './storeUtils.js';
|
|
11
11
|
const GS25_DEFAULT_HEADERS = {
|
|
@@ -21,31 +21,15 @@ const GS25_DEFAULT_FETCH_OPTIONS = {
|
|
|
21
21
|
};
|
|
22
22
|
const GS25_STORES_CACHE_TTL_MS = 60 * 5 * 1000;
|
|
23
23
|
const gs25StoresCache = new Map();
|
|
24
|
-
function extractGs25WebCsrfToken(html) {
|
|
25
|
-
return (html.match(/name="CSRFToken"\s+value="([^"]+)"/)?.[1] ||
|
|
26
|
-
html.match(/ACC\.config\.CSRFToken\s*=\s*"([^"]+)"/)?.[1] ||
|
|
27
|
-
null);
|
|
28
|
-
}
|
|
29
|
-
function extractGs25WebCookieHeader(headers) {
|
|
30
|
-
const setCookie = headers.get('set-cookie') || '';
|
|
31
|
-
const jsessionId = setCookie.match(/JSESSIONID=[^;,\s]+/)?.[0];
|
|
32
|
-
return jsessionId;
|
|
33
|
-
}
|
|
34
|
-
function parseGs25WebLocationResponse(body) {
|
|
35
|
-
if (typeof body === 'string') {
|
|
36
|
-
return JSON.parse(body);
|
|
37
|
-
}
|
|
38
|
-
return body;
|
|
39
|
-
}
|
|
40
24
|
function normalizeGs25WebStore(raw) {
|
|
41
|
-
const propertyNames =
|
|
25
|
+
const propertyNames = [];
|
|
42
26
|
return {
|
|
43
27
|
storeCode: raw.shopCode || '',
|
|
44
28
|
storeName: raw.shopName || '',
|
|
45
29
|
address: raw.address || '',
|
|
46
30
|
phone: '',
|
|
47
|
-
latitude: toNumber(raw.
|
|
48
|
-
longitude: toNumber(raw.
|
|
31
|
+
latitude: toNumber(raw.posY),
|
|
32
|
+
longitude: toNumber(raw.posX),
|
|
49
33
|
serviceCode: '01',
|
|
50
34
|
realStockQuantity: 0,
|
|
51
35
|
pickupStockQuantity: 0,
|
|
@@ -63,31 +47,6 @@ function normalizeGs25WebStore(raw) {
|
|
|
63
47
|
distanceM: null,
|
|
64
48
|
};
|
|
65
49
|
}
|
|
66
|
-
async function fetchGs25StoreStock(url, options) {
|
|
67
|
-
try {
|
|
68
|
-
return await fetchJson(url, {
|
|
69
|
-
...GS25_DEFAULT_FETCH_OPTIONS,
|
|
70
|
-
method: 'GET',
|
|
71
|
-
timeout: options.timeout,
|
|
72
|
-
headers: GS25_DEFAULT_HEADERS,
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
catch (error) {
|
|
76
|
-
const zyteApiKey = options.zyteApiKey?.trim();
|
|
77
|
-
if (!(error instanceof HttpError) || error.status !== 403 || !zyteApiKey) {
|
|
78
|
-
throw error;
|
|
79
|
-
}
|
|
80
|
-
const result = await requestByZyte({
|
|
81
|
-
apiKey: zyteApiKey,
|
|
82
|
-
url,
|
|
83
|
-
method: 'GET',
|
|
84
|
-
timeout: options.timeout,
|
|
85
|
-
retries: 1,
|
|
86
|
-
headers: Object.entries(GS25_DEFAULT_HEADERS).map(([name, value]) => ({ name, value })),
|
|
87
|
-
});
|
|
88
|
-
return decodeZyteHttpBody(result);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
50
|
function buildCacheKey(params) {
|
|
92
51
|
return [
|
|
93
52
|
params.serviceCode,
|
|
@@ -138,85 +97,18 @@ export async function fetchGs25WebStores(keyword, options = {}) {
|
|
|
138
97
|
return { totalCount: 0, stores: [] };
|
|
139
98
|
}
|
|
140
99
|
const { timeout = 20000 } = options;
|
|
141
|
-
const
|
|
142
|
-
|
|
100
|
+
const endpoint = new URL(GS25_API.WEB_LOCATION_LIST_PATH, GS25_API.WEB_BASE_URL);
|
|
101
|
+
endpoint.searchParams.set('shopName', query);
|
|
102
|
+
const body = await fetchJson(endpoint.toString(), {
|
|
143
103
|
...GS25_DEFAULT_FETCH_OPTIONS,
|
|
144
|
-
method: 'GET',
|
|
145
104
|
timeout,
|
|
146
|
-
headers: {
|
|
147
|
-
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
148
|
-
'Accept-Language': GS25_DEFAULT_HEADERS['Accept-Language'],
|
|
149
|
-
'User-Agent': GS25_DEFAULT_HEADERS['User-Agent'],
|
|
150
|
-
},
|
|
105
|
+
headers: { Accept: 'application/json' },
|
|
151
106
|
});
|
|
152
|
-
if (!
|
|
153
|
-
throw new
|
|
154
|
-
}
|
|
155
|
-
const html = await locationsResponse.text();
|
|
156
|
-
const csrfToken = extractGs25WebCsrfToken(html);
|
|
157
|
-
if (!csrfToken) {
|
|
158
|
-
throw new Error('GS25 웹 매장 검색 CSRF 토큰을 찾을 수 없습니다.');
|
|
107
|
+
if (!Array.isArray(body)) {
|
|
108
|
+
throw new Error('GS25 매장 응답 형식이 올바르지 않습니다.');
|
|
159
109
|
}
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
pageSize: '50',
|
|
163
|
-
searchShopName: query,
|
|
164
|
-
searchSido: '',
|
|
165
|
-
searchGugun: '',
|
|
166
|
-
searchDong: '',
|
|
167
|
-
searchType: '',
|
|
168
|
-
searchTypeService: '0',
|
|
169
|
-
searchTypeToto: '0',
|
|
170
|
-
searchTypeCafe25: '0',
|
|
171
|
-
searchTypeInstant: '0',
|
|
172
|
-
searchTypeDrug: '0',
|
|
173
|
-
searchTypeSelf25: '0',
|
|
174
|
-
searchTypePost: '0',
|
|
175
|
-
searchTypeATM: '0',
|
|
176
|
-
searchTypeWithdrawal: '0',
|
|
177
|
-
searchTypeTaxrefund: '0',
|
|
178
|
-
searchTypeSmartAtm: '0',
|
|
179
|
-
searchTypeSelfCookingUtensils: '0',
|
|
180
|
-
searchTypeDeliveryService: '0',
|
|
181
|
-
searchTypeParcelService: '0',
|
|
182
|
-
searchTypePotatoes: '0',
|
|
183
|
-
searchTypeCardiacDefi: '0',
|
|
184
|
-
searchTypeFishShapedBun: '0',
|
|
185
|
-
searchTypeWine25: '0',
|
|
186
|
-
searchTypeGoPizza: '0',
|
|
187
|
-
searchTypeSpiritWine: '0',
|
|
188
|
-
searchTypeFreshGanghw: '0',
|
|
189
|
-
searchTypeMusinsa: '0',
|
|
190
|
-
searchTypePosa: '0',
|
|
191
|
-
});
|
|
192
|
-
const locationListUrl = new URL(GS25_API.WEB_LOCATION_LIST_PATH, GS25_API.WEB_BASE_URL);
|
|
193
|
-
locationListUrl.searchParams.set('CSRFToken', csrfToken);
|
|
194
|
-
const cookieHeader = extractGs25WebCookieHeader(locationsResponse.headers);
|
|
195
|
-
const body = parseGs25WebLocationResponse(await fetchJson(locationListUrl.toString(), {
|
|
196
|
-
...GS25_DEFAULT_FETCH_OPTIONS,
|
|
197
|
-
method: 'POST',
|
|
198
|
-
retryUnsafeMethods: true,
|
|
199
|
-
timeout,
|
|
200
|
-
headers: {
|
|
201
|
-
Accept: 'application/json, text/javascript, */*; q=0.01',
|
|
202
|
-
'Accept-Language': GS25_DEFAULT_HEADERS['Accept-Language'],
|
|
203
|
-
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
204
|
-
Referer: locationsUrl.toString(),
|
|
205
|
-
'User-Agent': GS25_DEFAULT_HEADERS['User-Agent'],
|
|
206
|
-
'X-Requested-With': 'XMLHttpRequest',
|
|
207
|
-
...(cookieHeader ? { Cookie: cookieHeader } : {}),
|
|
208
|
-
},
|
|
209
|
-
body: form.toString(),
|
|
210
|
-
}));
|
|
211
|
-
const stores = (body.results || [])
|
|
212
|
-
.map(normalizeGs25WebStore)
|
|
213
|
-
.filter((store) => store.storeCode.length > 0);
|
|
214
|
-
return {
|
|
215
|
-
totalCount: typeof body.pagination?.totalNumberOfResults === 'number'
|
|
216
|
-
? body.pagination.totalNumberOfResults
|
|
217
|
-
: stores.length,
|
|
218
|
-
stores,
|
|
219
|
-
};
|
|
110
|
+
const stores = body.map(normalizeGs25WebStore).filter((store) => store.storeCode.length > 0);
|
|
111
|
+
return { totalCount: stores.length, stores };
|
|
220
112
|
}
|
|
221
113
|
export async function fetchGs25Stores(params = {}, options = {}) {
|
|
222
114
|
const { serviceCode = '01', keyword = '', itemCode = '', storeCode = '', realTimeStockYn = 'Y', latitude, longitude, pageNumber, pageCount, useCache = true, } = params;
|
|
@@ -276,10 +168,11 @@ export async function fetchGs25Stores(params = {}, options = {}) {
|
|
|
276
168
|
endpoint.searchParams.set('isSuperDlvyStoreSelected', 'N');
|
|
277
169
|
endpoint.searchParams.set('isGs25DlvyStoreSelected', 'N');
|
|
278
170
|
}
|
|
279
|
-
const body = await
|
|
171
|
+
const body = await fetchGs25StoreStockResponse(endpoint.toString(), {
|
|
280
172
|
timeout,
|
|
281
173
|
zyteApiKey: options.zyteApiKey,
|
|
282
|
-
|
|
174
|
+
apiKey: options.apiKey,
|
|
175
|
+
}, GS25_DEFAULT_HEADERS);
|
|
283
176
|
const stores = (body.stores || [])
|
|
284
177
|
.map(normalizeStore)
|
|
285
178
|
.filter((store) => store.storeCode.length > 0);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GS25 원본 서비스 장애 오류
|
|
3
|
+
*/
|
|
4
|
+
export class Gs25UpstreamUnavailableError extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super('GS25 재고 서비스 인증을 사용할 수 없습니다. 운영자는 GS25_API_KEY 설정을 확인하세요.');
|
|
7
|
+
this.name = 'Gs25UpstreamUnavailableError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function isGs25UpstreamUnavailableError(error) {
|
|
11
|
+
return error instanceof Gs25UpstreamUnavailableError;
|
|
12
|
+
}
|
|
@@ -18,9 +18,9 @@ class Gs25Service {
|
|
|
18
18
|
}
|
|
19
19
|
getTools() {
|
|
20
20
|
return [
|
|
21
|
-
createFindNearbyStoresTool(this.options.googleMapsApiKey, this.options.zyteApiKey),
|
|
21
|
+
createFindNearbyStoresTool(this.options.googleMapsApiKey, this.options.zyteApiKey, this.options.apiKey),
|
|
22
22
|
createSearchProductsTool(this.options.zyteApiKey),
|
|
23
|
-
createCheckInventoryTool(this.options.googleMapsApiKey, this.options.zyteApiKey),
|
|
23
|
+
createCheckInventoryTool(this.options.googleMapsApiKey, this.options.zyteApiKey, this.options.apiKey),
|
|
24
24
|
];
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GS25 재고 원본 요청과 Zyte 대체 경로
|
|
3
|
+
*/
|
|
4
|
+
import { fetchJson, HttpError } from '../../utils/http.js';
|
|
5
|
+
import { decodeZyteHttpBody, requestByZyte } from '../../utils/zyte.js';
|
|
6
|
+
import { Gs25UpstreamUnavailableError } from './errors.js';
|
|
7
|
+
function withApiKey(headers, apiKey) {
|
|
8
|
+
const normalizedApiKey = apiKey?.trim();
|
|
9
|
+
return normalizedApiKey ? { ...headers, 'Api-Key': normalizedApiKey } : headers;
|
|
10
|
+
}
|
|
11
|
+
function isAuthenticationStatus(status) {
|
|
12
|
+
return status === 401 || status === 403;
|
|
13
|
+
}
|
|
14
|
+
export async function fetchGs25StoreStockResponse(url, options, headers) {
|
|
15
|
+
const requestHeaders = withApiKey(headers, options.apiKey);
|
|
16
|
+
try {
|
|
17
|
+
return await fetchJson(url, {
|
|
18
|
+
method: 'GET',
|
|
19
|
+
timeout: options.timeout,
|
|
20
|
+
retries: 1,
|
|
21
|
+
retryDelayMs: 250,
|
|
22
|
+
headers: requestHeaders,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (!(error instanceof HttpError) || !isAuthenticationStatus(error.status)) {
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
const zyteApiKey = options.zyteApiKey?.trim();
|
|
30
|
+
if (error.status === 401 || !zyteApiKey) {
|
|
31
|
+
throw new Gs25UpstreamUnavailableError();
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const result = await requestByZyte({
|
|
35
|
+
apiKey: zyteApiKey,
|
|
36
|
+
url,
|
|
37
|
+
method: 'GET',
|
|
38
|
+
timeout: options.timeout,
|
|
39
|
+
retries: 1,
|
|
40
|
+
headers: Object.entries(requestHeaders).map(([name, value]) => ({ name, value })),
|
|
41
|
+
tags: { service: 'gs25' },
|
|
42
|
+
});
|
|
43
|
+
if (isAuthenticationStatus(result.statusCode)) {
|
|
44
|
+
throw new Gs25UpstreamUnavailableError();
|
|
45
|
+
}
|
|
46
|
+
return decodeZyteHttpBody(result);
|
|
47
|
+
}
|
|
48
|
+
catch (fallbackError) {
|
|
49
|
+
if (fallbackError instanceof Gs25UpstreamUnavailableError) {
|
|
50
|
+
throw fallbackError;
|
|
51
|
+
}
|
|
52
|
+
throw new Gs25UpstreamUnavailableError();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -8,7 +8,7 @@ function getProcessEnvValue(name) {
|
|
|
8
8
|
return typeof process !== 'undefined' ? process.env[name] : undefined;
|
|
9
9
|
}
|
|
10
10
|
async function checkInventory(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;
|
|
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'), apiKey = getProcessEnvValue('GS25_API_KEY'), } = args;
|
|
12
12
|
if (!keyword || keyword.trim().length === 0) {
|
|
13
13
|
throw new Error('상품 검색어(keyword)를 입력해주세요.');
|
|
14
14
|
}
|
|
@@ -25,6 +25,7 @@ async function checkInventory(args) {
|
|
|
25
25
|
}, {
|
|
26
26
|
timeout: timeoutMs,
|
|
27
27
|
zyteApiKey,
|
|
28
|
+
apiKey,
|
|
28
29
|
});
|
|
29
30
|
const firstAddress = filterGs25StoresByKeyword(baseStores.stores, storeKeyword).find((store) => store.address.trim().length > 0)?.address || '';
|
|
30
31
|
if (firstAddress.length > 0) {
|
|
@@ -61,6 +62,7 @@ async function checkInventory(args) {
|
|
|
61
62
|
}, {
|
|
62
63
|
timeout: timeoutMs,
|
|
63
64
|
zyteApiKey,
|
|
65
|
+
apiKey,
|
|
64
66
|
});
|
|
65
67
|
}
|
|
66
68
|
else {
|
|
@@ -83,6 +85,7 @@ async function checkInventory(args) {
|
|
|
83
85
|
}, {
|
|
84
86
|
timeout: timeoutMs,
|
|
85
87
|
zyteApiKey,
|
|
88
|
+
apiKey,
|
|
86
89
|
});
|
|
87
90
|
}
|
|
88
91
|
else {
|
|
@@ -97,6 +100,7 @@ async function checkInventory(args) {
|
|
|
97
100
|
}, {
|
|
98
101
|
timeout: timeoutMs,
|
|
99
102
|
zyteApiKey,
|
|
103
|
+
apiKey,
|
|
100
104
|
});
|
|
101
105
|
}
|
|
102
106
|
}
|
|
@@ -147,7 +151,7 @@ async function checkInventory(args) {
|
|
|
147
151
|
],
|
|
148
152
|
};
|
|
149
153
|
}
|
|
150
|
-
export function createCheckInventoryTool(googleMapsApiKey, zyteApiKey) {
|
|
154
|
+
export function createCheckInventoryTool(googleMapsApiKey, zyteApiKey, apiKey) {
|
|
151
155
|
return {
|
|
152
156
|
name: 'gs25_check_inventory',
|
|
153
157
|
metadata: {
|
|
@@ -184,6 +188,7 @@ export function createCheckInventoryTool(googleMapsApiKey, zyteApiKey) {
|
|
|
184
188
|
...args,
|
|
185
189
|
googleMapsApiKey: args.googleMapsApiKey || googleMapsApiKey,
|
|
186
190
|
zyteApiKey: args.zyteApiKey || zyteApiKey,
|
|
191
|
+
apiKey: args.apiKey || apiKey,
|
|
187
192
|
})),
|
|
188
193
|
};
|
|
189
194
|
}
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
* GS25 주변 매장 탐색 도구
|
|
3
3
|
*/
|
|
4
4
|
import * as z from 'zod';
|
|
5
|
-
import {
|
|
5
|
+
import { isGs25UpstreamUnavailableError } from '../errors.js';
|
|
6
|
+
import { attachDistanceToGs25Stores, fetchGs25Stores, fetchGs25WebStores, geocodeGs25Address, selectGs25StoresForKeyword, sortGs25Stores, } from '../client.js';
|
|
6
7
|
const FALLBACK_STORE_LOOKUP_ITEM_CODE = '8801117752804';
|
|
7
8
|
function getProcessEnvValue(name) {
|
|
8
9
|
/* c8 ignore next -- Node 테스트와 CLI 런타임은 process를 제공하며, Worker에는 API에서 키를 주입합니다. */
|
|
9
10
|
return typeof process !== 'undefined' ? process.env[name] : undefined;
|
|
10
11
|
}
|
|
11
12
|
async function findNearbyStores(args) {
|
|
12
|
-
const { latitude, longitude, keyword = '', serviceCode = '01', limit = 20, timeoutMs = 20000, googleMapsApiKey = getProcessEnvValue('GOOGLE_MAPS_API_KEY'), zyteApiKey = getProcessEnvValue('ZYTE_API_KEY'), } = args;
|
|
13
|
+
const { latitude, longitude, keyword = '', serviceCode = '01', limit = 20, timeoutMs = 20000, googleMapsApiKey = getProcessEnvValue('GOOGLE_MAPS_API_KEY'), zyteApiKey = getProcessEnvValue('ZYTE_API_KEY'), apiKey = getProcessEnvValue('GS25_API_KEY'), } = args;
|
|
13
14
|
let resolvedLatitude = latitude;
|
|
14
15
|
let resolvedLongitude = longitude;
|
|
15
16
|
let geocodeUsed = false;
|
|
@@ -25,6 +26,7 @@ async function findNearbyStores(args) {
|
|
|
25
26
|
geocodeUsed = true;
|
|
26
27
|
}
|
|
27
28
|
}
|
|
29
|
+
let fallbackUsed = false;
|
|
28
30
|
let result = await fetchGs25Stores({
|
|
29
31
|
serviceCode,
|
|
30
32
|
latitude: resolvedLatitude,
|
|
@@ -32,9 +34,15 @@ async function findNearbyStores(args) {
|
|
|
32
34
|
}, {
|
|
33
35
|
timeout: timeoutMs,
|
|
34
36
|
zyteApiKey,
|
|
37
|
+
apiKey,
|
|
38
|
+
}).catch(async (error) => {
|
|
39
|
+
if (!isGs25UpstreamUnavailableError(error) || keyword.trim().length === 0)
|
|
40
|
+
throw error;
|
|
41
|
+
const webResult = await fetchGs25WebStores(keyword, { timeout: timeoutMs });
|
|
42
|
+
fallbackUsed = true;
|
|
43
|
+
return { ...webResult, cacheHit: false };
|
|
35
44
|
});
|
|
36
|
-
|
|
37
|
-
if (result.stores.length === 0 &&
|
|
45
|
+
if (!fallbackUsed && result.stores.length === 0 &&
|
|
38
46
|
typeof resolvedLatitude === 'number' &&
|
|
39
47
|
typeof resolvedLongitude === 'number') {
|
|
40
48
|
try {
|
|
@@ -48,6 +56,7 @@ async function findNearbyStores(args) {
|
|
|
48
56
|
}, {
|
|
49
57
|
timeout: timeoutMs,
|
|
50
58
|
zyteApiKey,
|
|
59
|
+
apiKey,
|
|
51
60
|
});
|
|
52
61
|
if (fallbackResult.stores.length > 0) {
|
|
53
62
|
result = fallbackResult;
|
|
@@ -58,6 +67,11 @@ async function findNearbyStores(args) {
|
|
|
58
67
|
fallbackUsed = false;
|
|
59
68
|
}
|
|
60
69
|
}
|
|
70
|
+
if (!fallbackUsed && result.stores.length === 0 && keyword.trim().length > 0) {
|
|
71
|
+
const webResult = await fetchGs25WebStores(keyword, { timeout: timeoutMs });
|
|
72
|
+
result = { ...webResult, cacheHit: false };
|
|
73
|
+
fallbackUsed = true;
|
|
74
|
+
}
|
|
61
75
|
const selected = selectGs25StoresForKeyword(result.stores, keyword, {
|
|
62
76
|
relaxWhenEmpty: typeof resolvedLatitude === 'number' && typeof resolvedLongitude === 'number',
|
|
63
77
|
});
|
|
@@ -89,7 +103,7 @@ async function findNearbyStores(args) {
|
|
|
89
103
|
],
|
|
90
104
|
};
|
|
91
105
|
}
|
|
92
|
-
export function createFindNearbyStoresTool(googleMapsApiKey, zyteApiKey) {
|
|
106
|
+
export function createFindNearbyStoresTool(googleMapsApiKey, zyteApiKey, apiKey) {
|
|
93
107
|
return {
|
|
94
108
|
name: 'gs25_find_nearby_stores',
|
|
95
109
|
metadata: {
|
|
@@ -112,6 +126,7 @@ export function createFindNearbyStoresTool(googleMapsApiKey, zyteApiKey) {
|
|
|
112
126
|
...args,
|
|
113
127
|
googleMapsApiKey: args.googleMapsApiKey || googleMapsApiKey,
|
|
114
128
|
zyteApiKey: args.zyteApiKey || zyteApiKey,
|
|
129
|
+
apiKey: args.apiKey || apiKey,
|
|
115
130
|
})),
|
|
116
131
|
};
|
|
117
132
|
}
|
|
@@ -105,7 +105,9 @@ async function fetchPlaySequenceByPair(playDate, theater, movie, timeout = 15000
|
|
|
105
105
|
.filter((item) => item.CinemaID && item.RepresentationMovieCode && item.ScreenID && item.PlaySequence)
|
|
106
106
|
.map((item) => {
|
|
107
107
|
const totalSeats = toNumber(item.TotalSeatCount);
|
|
108
|
-
|
|
108
|
+
// 공식 BookingSeatCount는 예매된 수가 아니라 예매 가능(잔여) 좌석 수다.
|
|
109
|
+
const remainingSeats = toNumber(item.BookingSeatCount);
|
|
110
|
+
const bookedSeats = Math.max(totalSeats - remainingSeats, 0);
|
|
109
111
|
const normalizedPlayDate = normalizeDateForOutput(item.PlayDt || playDate);
|
|
110
112
|
const theaterId = String(item.CinemaID);
|
|
111
113
|
const screenId = String(item.ScreenID);
|
|
@@ -123,7 +125,7 @@ async function fetchPlaySequenceByPair(playDate, theater, movie, timeout = 15000
|
|
|
123
125
|
endTime: formatTime(item.EndTime),
|
|
124
126
|
totalSeats,
|
|
125
127
|
bookedSeats,
|
|
126
|
-
remainingSeats
|
|
128
|
+
remainingSeats,
|
|
127
129
|
};
|
|
128
130
|
});
|
|
129
131
|
}
|