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.
- package/README.md +249 -533
- package/dist/api/actionsProxy.js +82 -0
- package/dist/api/cgvHandlers.js +103 -16
- package/dist/api/configStatus.js +19 -0
- package/dist/api/daisoHandlers.js +127 -0
- package/dist/api/emart24Handlers.js +176 -0
- package/dist/api/gs25Handlers.js +267 -0
- package/dist/api/handlers.js +209 -104
- package/dist/api/healthChecks.js +504 -0
- package/dist/api/lottecinemaHandlers.js +155 -0
- package/dist/api/lottemartHandlers.js +144 -0
- package/dist/api/megaboxHandlers.js +81 -50
- package/dist/api/response.js +2 -0
- package/dist/api/routes/cuRoutes.js +17 -0
- package/dist/api/routes/daisoRoutes.js +6 -1
- package/dist/api/routes/emart24Routes.js +22 -0
- package/dist/api/routes/gs25Routes.js +22 -0
- package/dist/api/routes/healthRoutes.js +61 -0
- package/dist/api/routes/lottecinemaRoutes.js +22 -0
- package/dist/api/routes/lottemartRoutes.js +18 -0
- package/dist/api/routes/oliveyoungRoutes.js +6 -1
- package/dist/api/routes/sevenelevenRoutes.js +32 -0
- package/dist/api/sevenelevenHandlers.js +200 -0
- package/dist/bin.js +0 -0
- package/dist/cli/args.js +75 -0
- package/dist/cli/commandOptions.js +38 -0
- package/dist/cli/commands/convenience.js +293 -0
- package/dist/cli/commands/daiso.js +112 -0
- package/dist/cli/constants.js +5 -0
- package/dist/cli/deps.js +48 -0
- package/dist/cli/http.js +156 -0
- package/dist/cli/interactiveFetch.js +31 -0
- package/dist/cli/interactiveItemSearch.js +261 -0
- package/dist/cli/interactivePrompt.js +50 -0
- package/dist/cli/interactiveTypes.js +4 -0
- package/dist/cli/types.js +4 -0
- package/dist/cli.js +87 -196
- package/dist/cliHelp.js +212 -1
- package/dist/cliInteractive.js +243 -0
- package/dist/cliPicker.js +57 -0
- package/dist/cliRenderer.js +90 -3
- package/dist/core/errors.js +66 -0
- package/dist/core/registry.js +227 -3
- package/dist/index.js +149 -9
- package/dist/pages/openapi.js +2 -1
- package/dist/pages/openapiFullSpec.js +58 -0
- package/dist/pages/openapiResponse.js +28 -0
- package/dist/pages/openapiSpec.js +2 -41
- package/dist/pages/openapiSpecActions.js +109 -0
- package/dist/pages/openapiSpecComponents.js +30 -391
- package/dist/pages/openapiSpecComponentsCgv.js +24 -0
- package/dist/pages/openapiSpecComponentsCu.js +112 -0
- package/dist/pages/openapiSpecComponentsDaisoOliveyoung.js +342 -0
- package/dist/pages/openapiSpecComponentsEmart24.js +141 -0
- package/dist/pages/openapiSpecComponentsGs25.js +109 -0
- package/dist/pages/openapiSpecComponentsLotteMart.js +85 -0
- package/dist/pages/openapiSpecComponentsLottecinema.js +163 -0
- package/dist/pages/openapiSpecComponentsMegabox.js +144 -0
- package/dist/pages/openapiSpecComponentsSeveneleven.js +82 -0
- package/dist/pages/openapiSpecPathsCgv.js +66 -3
- package/dist/pages/openapiSpecPathsCu.js +145 -0
- package/dist/pages/openapiSpecPathsDaiso.js +302 -0
- package/dist/pages/openapiSpecPathsDaisoOliveyoung.js +4 -376
- package/dist/pages/openapiSpecPathsEmart24.js +223 -0
- package/dist/pages/openapiSpecPathsGs25.js +166 -0
- package/dist/pages/openapiSpecPathsLotteMart.js +149 -0
- package/dist/pages/openapiSpecPathsLottecinema.js +212 -0
- package/dist/pages/openapiSpecPathsMegabox.js +52 -3
- package/dist/pages/openapiSpecPathsOliveyoung.js +266 -0
- package/dist/pages/openapiSpecPathsSeveneleven.js +234 -0
- package/dist/pages/prompt.js +8 -402
- package/dist/pages/promptCoreText.js +278 -0
- package/dist/pages/promptReferenceText.js +137 -0
- package/dist/pages/promptRetailCinemaText.js +274 -0
- package/dist/services/cgv/client.js +6 -1
- package/dist/services/cgv/index.js +3 -3
- package/dist/services/cgv/location.js +235 -0
- package/dist/services/cgv/tools/findTheaters.js +30 -5
- package/dist/services/cgv/tools/getTimetable.js +42 -14
- package/dist/services/cgv/tools/searchMovies.js +40 -12
- package/dist/services/cgv/transport.js +0 -3
- package/dist/services/cu/api.js +11 -0
- package/dist/services/cu/client.js +297 -0
- package/dist/services/cu/index.js +21 -0
- package/dist/services/cu/tools/checkInventory.js +123 -0
- package/dist/services/cu/tools/findNearbyStores.js +49 -0
- package/dist/services/cu/types.js +4 -0
- package/dist/services/daiso/api.js +22 -2
- package/dist/services/daiso/client.js +67 -0
- package/dist/services/daiso/index.js +4 -0
- package/dist/services/daiso/product.js +17 -0
- package/dist/services/daiso/tools/checkInventory.js +80 -43
- package/dist/services/daiso/tools/findInventoryByName.js +222 -0
- package/dist/services/daiso/tools/findStores.js +17 -7
- package/dist/services/daiso/tools/getDisplayLocation.js +75 -0
- package/dist/services/emart24/api.js +11 -0
- package/dist/services/emart24/client.js +182 -0
- package/dist/services/emart24/index.js +22 -0
- package/dist/services/emart24/inventoryLookup.js +134 -0
- package/dist/services/emart24/tools/checkInventory.js +54 -0
- package/dist/services/emart24/tools/findNearbyStores.js +72 -0
- package/dist/services/emart24/tools/searchProducts.js +73 -0
- package/dist/services/emart24/types.js +4 -0
- package/dist/services/gs25/api.js +9 -0
- package/dist/services/gs25/client.js +208 -0
- package/dist/services/gs25/index.js +22 -0
- package/dist/services/gs25/storeUtils.js +168 -0
- package/dist/services/gs25/tools/checkInventory.js +163 -0
- package/dist/services/gs25/tools/findNearbyStores.js +103 -0
- package/dist/services/gs25/tools/searchProducts.js +57 -0
- package/dist/services/gs25/types.js +4 -0
- package/dist/services/lottecinema/api.js +12 -0
- package/dist/services/lottecinema/client.js +161 -0
- package/dist/services/lottecinema/index.js +30 -0
- package/dist/services/lottecinema/location.js +191 -0
- package/dist/services/lottecinema/tools/findNearbyTheaters.js +47 -0
- package/dist/services/lottecinema/tools/getRemainingSeats.js +85 -0
- package/dist/services/lottecinema/tools/listNowShowing.js +79 -0
- package/dist/services/lottecinema/types.js +4 -0
- package/dist/services/lottemart/api.js +31 -0
- package/dist/services/lottemart/client.js +254 -0
- package/dist/services/lottemart/clientTypes.js +4 -0
- package/dist/services/lottemart/config.js +4 -0
- package/dist/services/lottemart/debug.js +92 -0
- package/dist/services/lottemart/geocode.js +49 -0
- package/dist/services/lottemart/index.js +30 -0
- package/dist/services/lottemart/keyword.js +29 -0
- package/dist/services/lottemart/parser.js +185 -0
- package/dist/services/lottemart/session.js +331 -0
- package/dist/services/lottemart/storeSearch.js +52 -0
- package/dist/services/lottemart/tools/findNearbyStores.js +67 -0
- package/dist/services/lottemart/tools/searchProducts.js +69 -0
- package/dist/services/lottemart/types.js +4 -0
- package/dist/services/lottemart/zetta.js +105 -0
- package/dist/services/megabox/location.js +219 -0
- package/dist/services/megabox/tools/findNearbyTheaters.js +13 -44
- package/dist/services/megabox/tools/getRemainingSeats.js +31 -3
- package/dist/services/megabox/tools/listNowShowing.js +31 -3
- package/dist/services/oliveyoung/api.js +2 -0
- package/dist/services/oliveyoung/client.js +225 -13
- package/dist/services/oliveyoung/index.js +3 -1
- package/dist/services/oliveyoung/tools/checkInventory.js +21 -5
- package/dist/services/oliveyoung/tools/searchProducts.js +57 -0
- package/dist/services/seveneleven/api.js +15 -0
- package/dist/services/seveneleven/client.js +286 -0
- package/dist/services/seveneleven/index.js +30 -0
- package/dist/services/seveneleven/inventory.js +302 -0
- package/dist/services/seveneleven/productKeyword.js +141 -0
- package/dist/services/seveneleven/tools/checkInventory.js +82 -0
- package/dist/services/seveneleven/tools/getCatalogSnapshot.js +55 -0
- package/dist/services/seveneleven/tools/getSearchPopwords.js +41 -0
- package/dist/services/seveneleven/tools/searchProducts.js +67 -0
- package/dist/services/seveneleven/tools/searchStores.js +45 -0
- package/dist/services/seveneleven/types.js +4 -0
- package/dist/utils/cliInteractiveHelpers.js +63 -0
- package/dist/utils/daisoKeyword.js +30 -0
- package/dist/utils/http.js +98 -12
- package/dist/utils/zyte.js +47 -26
- package/openapi.json +1979 -736
- package/openapi.yaml +1428 -541
- package/package.json +49 -18
- package/dist/tools/checkInventory.js +0 -98
- package/dist/tools/findStores.js +0 -125
- package/dist/tools/getPriceInfo.js +0 -77
- package/dist/tools/searchProducts.js +0 -61
- package/dist/types/index.js +0 -4
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 메가박스 위치 해석 및 근처 지점 조회 보조 모듈
|
|
3
|
+
*/
|
|
4
|
+
import { fetchJson } from '../../utils/http.js';
|
|
5
|
+
import { fetchMegaboxBookingList, fetchMegaboxTheaterInfo } from './client.js';
|
|
6
|
+
const DEFAULT_LATITUDE = 37.5665;
|
|
7
|
+
const DEFAULT_LONGITUDE = 126.978;
|
|
8
|
+
const DEFAULT_AREA_CODE = '11';
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 15000;
|
|
10
|
+
const GEOCODE_CACHE_TTL_MS = 60 * 60 * 1000;
|
|
11
|
+
const megaboxGeocodeCache = new Map();
|
|
12
|
+
const MEGABOX_AREA_CODE_BY_REGION = [
|
|
13
|
+
['서울특별시', '11'],
|
|
14
|
+
['서울', '11'],
|
|
15
|
+
['부산광역시', '26'],
|
|
16
|
+
['부산', '26'],
|
|
17
|
+
['대구광역시', '27'],
|
|
18
|
+
['대구', '27'],
|
|
19
|
+
['인천광역시', '28'],
|
|
20
|
+
['인천', '28'],
|
|
21
|
+
['광주광역시', '29'],
|
|
22
|
+
['광주', '29'],
|
|
23
|
+
['대전광역시', '30'],
|
|
24
|
+
['대전', '30'],
|
|
25
|
+
['울산광역시', '31'],
|
|
26
|
+
['울산', '31'],
|
|
27
|
+
['세종특별자치시', '36'],
|
|
28
|
+
['세종', '36'],
|
|
29
|
+
['경기도', '41'],
|
|
30
|
+
['경기', '41'],
|
|
31
|
+
['강원특별자치도', '42'],
|
|
32
|
+
['강원도', '42'],
|
|
33
|
+
['강원', '42'],
|
|
34
|
+
['충청북도', '43'],
|
|
35
|
+
['충북', '43'],
|
|
36
|
+
['충청남도', '44'],
|
|
37
|
+
['충남', '44'],
|
|
38
|
+
['전북특별자치도', '45'],
|
|
39
|
+
['전라북도', '45'],
|
|
40
|
+
['전북', '45'],
|
|
41
|
+
['전라남도', '46'],
|
|
42
|
+
['전남', '46'],
|
|
43
|
+
['경상북도', '47'],
|
|
44
|
+
['경북', '47'],
|
|
45
|
+
['경상남도', '48'],
|
|
46
|
+
['경남', '48'],
|
|
47
|
+
['제주특별자치도', '50'],
|
|
48
|
+
['제주도', '50'],
|
|
49
|
+
['제주', '50'],
|
|
50
|
+
];
|
|
51
|
+
function calculateDistanceKm(lat1, lng1, lat2, lng2) {
|
|
52
|
+
const toRad = (deg) => (deg * Math.PI) / 180;
|
|
53
|
+
const dLat = toRad(lat2 - lat1);
|
|
54
|
+
const dLng = toRad(lng2 - lng1);
|
|
55
|
+
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
56
|
+
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
|
57
|
+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
58
|
+
return 6371 * c;
|
|
59
|
+
}
|
|
60
|
+
function isValidCoordinate(value) {
|
|
61
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
62
|
+
}
|
|
63
|
+
function resolveAreaCodeFromText(text) {
|
|
64
|
+
const normalized = text.replace(/\s+/g, '');
|
|
65
|
+
for (const [region, areaCode] of MEGABOX_AREA_CODE_BY_REGION) {
|
|
66
|
+
if (normalized.includes(region.replace(/\s+/g, ''))) {
|
|
67
|
+
return areaCode;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
function resolveAreaCodeFromGeocodeResult(result) {
|
|
73
|
+
for (const component of result.address_components || []) {
|
|
74
|
+
const candidates = [component.long_name || '', component.short_name || ''];
|
|
75
|
+
for (const candidate of candidates) {
|
|
76
|
+
const areaCode = resolveAreaCodeFromText(candidate);
|
|
77
|
+
if (areaCode) {
|
|
78
|
+
return areaCode;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return resolveAreaCodeFromText(result.formatted_address || '');
|
|
83
|
+
}
|
|
84
|
+
async function geocodeMegaboxLocation(params, options = {}) {
|
|
85
|
+
const apiKey = (options.googleMapsApiKey || '').trim();
|
|
86
|
+
if (apiKey.length === 0) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const hasCoordinates = isValidCoordinate(params.latitude) && isValidCoordinate(params.longitude);
|
|
90
|
+
const keyword = (params.keyword || '').trim();
|
|
91
|
+
if (!hasCoordinates && keyword.length === 0) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const cacheKey = hasCoordinates ? `coords:${params.latitude},${params.longitude}` : `keyword:${keyword}`;
|
|
95
|
+
const cached = megaboxGeocodeCache.get(cacheKey);
|
|
96
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
97
|
+
return cached.value;
|
|
98
|
+
}
|
|
99
|
+
const endpoint = new URL('https://maps.googleapis.com/maps/api/geocode/json');
|
|
100
|
+
if (hasCoordinates) {
|
|
101
|
+
endpoint.searchParams.set('latlng', `${params.latitude},${params.longitude}`);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
endpoint.searchParams.set('address', keyword);
|
|
105
|
+
}
|
|
106
|
+
endpoint.searchParams.set('key', apiKey);
|
|
107
|
+
const body = await fetchJson(endpoint.toString(), {
|
|
108
|
+
method: 'GET',
|
|
109
|
+
timeout: options.timeout || DEFAULT_TIMEOUT_MS,
|
|
110
|
+
headers: {
|
|
111
|
+
Accept: 'application/json',
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
if (body.status !== 'OK') {
|
|
115
|
+
megaboxGeocodeCache.set(cacheKey, {
|
|
116
|
+
expiresAt: Date.now() + GEOCODE_CACHE_TTL_MS,
|
|
117
|
+
value: null,
|
|
118
|
+
});
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const firstResult = body.results?.[0];
|
|
122
|
+
const latitude = firstResult?.geometry?.location?.lat;
|
|
123
|
+
const longitude = firstResult?.geometry?.location?.lng;
|
|
124
|
+
const areaCode = firstResult ? resolveAreaCodeFromGeocodeResult(firstResult) : null;
|
|
125
|
+
const value = typeof latitude === 'number' && typeof longitude === 'number'
|
|
126
|
+
? { latitude, longitude, areaCode }
|
|
127
|
+
: null;
|
|
128
|
+
megaboxGeocodeCache.set(cacheKey, {
|
|
129
|
+
expiresAt: Date.now() + GEOCODE_CACHE_TTL_MS,
|
|
130
|
+
value,
|
|
131
|
+
});
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
export async function resolveMegaboxLocation(params, options = {}) {
|
|
135
|
+
const explicitAreaCode = (params.areaCode || '').trim();
|
|
136
|
+
const keyword = (params.keyword || '').trim();
|
|
137
|
+
const hasCoordinates = isValidCoordinate(params.latitude) && isValidCoordinate(params.longitude);
|
|
138
|
+
let latitude = hasCoordinates ? params.latitude : DEFAULT_LATITUDE;
|
|
139
|
+
let longitude = hasCoordinates ? params.longitude : DEFAULT_LONGITUDE;
|
|
140
|
+
let areaCode = explicitAreaCode || '';
|
|
141
|
+
let geocodeUsed = false;
|
|
142
|
+
if (!explicitAreaCode || !hasCoordinates) {
|
|
143
|
+
const geocoded = await geocodeMegaboxLocation(params, options);
|
|
144
|
+
if (geocoded) {
|
|
145
|
+
if (!hasCoordinates) {
|
|
146
|
+
latitude = geocoded.latitude;
|
|
147
|
+
longitude = geocoded.longitude;
|
|
148
|
+
}
|
|
149
|
+
if (!explicitAreaCode && geocoded.areaCode) {
|
|
150
|
+
areaCode = geocoded.areaCode;
|
|
151
|
+
}
|
|
152
|
+
geocodeUsed = true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
keyword: keyword.length > 0 ? keyword : null,
|
|
157
|
+
latitude,
|
|
158
|
+
longitude,
|
|
159
|
+
areaCode: areaCode || DEFAULT_AREA_CODE,
|
|
160
|
+
geocodeUsed,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
export async function fetchMegaboxNearbyTheaters(params, options = {}) {
|
|
164
|
+
const timeout = params.timeout || DEFAULT_TIMEOUT_MS;
|
|
165
|
+
const limit = Math.max(1, params.limit || 10);
|
|
166
|
+
const resolved = await resolveMegaboxLocation(params, options);
|
|
167
|
+
const { theaters } = await fetchMegaboxBookingList({
|
|
168
|
+
playDate: params.playDate,
|
|
169
|
+
areaCode: resolved.areaCode,
|
|
170
|
+
timeout,
|
|
171
|
+
});
|
|
172
|
+
const infoResults = await Promise.allSettled(theaters.map((theater) => fetchMegaboxTheaterInfo(theater.theaterId, timeout)));
|
|
173
|
+
const merged = theaters
|
|
174
|
+
.map((theater, index) => {
|
|
175
|
+
const infoResult = infoResults[index];
|
|
176
|
+
if (infoResult.status !== 'fulfilled') {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
const info = infoResult.value;
|
|
180
|
+
if (info.latitude === null || info.longitude === null) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
theaterId: theater.theaterId,
|
|
185
|
+
theaterName: theater.theaterName,
|
|
186
|
+
address: info.address,
|
|
187
|
+
latitude: info.latitude,
|
|
188
|
+
longitude: info.longitude,
|
|
189
|
+
distanceKm: Number(calculateDistanceKm(resolved.latitude, resolved.longitude, info.latitude, info.longitude).toFixed(2)),
|
|
190
|
+
};
|
|
191
|
+
})
|
|
192
|
+
.filter((theater) => theater !== null)
|
|
193
|
+
.sort((left, right) => left.distanceKm - right.distanceKm)
|
|
194
|
+
.slice(0, limit);
|
|
195
|
+
return {
|
|
196
|
+
...resolved,
|
|
197
|
+
playDate: params.playDate,
|
|
198
|
+
count: merged.length,
|
|
199
|
+
theaters: merged,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
export async function resolveMegaboxNearestTheater(params, options = {}) {
|
|
203
|
+
const location = await resolveMegaboxLocation(params, options);
|
|
204
|
+
const nearby = await fetchMegaboxNearbyTheaters({
|
|
205
|
+
...params,
|
|
206
|
+
playDate: params.playDate,
|
|
207
|
+
limit: 1,
|
|
208
|
+
areaCode: location.areaCode,
|
|
209
|
+
latitude: location.latitude,
|
|
210
|
+
longitude: location.longitude,
|
|
211
|
+
}, options);
|
|
212
|
+
return {
|
|
213
|
+
theater: nearby.theaters[0] || null,
|
|
214
|
+
location,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
export function __testOnlyClearMegaboxLocationCaches() {
|
|
218
|
+
megaboxGeocodeCache.clear();
|
|
219
|
+
}
|
|
@@ -2,54 +2,22 @@
|
|
|
2
2
|
* 메가박스 주변 지점 탐색 도구
|
|
3
3
|
*/
|
|
4
4
|
import * as z from 'zod';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
const toRad = (deg) => (deg * Math.PI) / 180;
|
|
8
|
-
const dLat = toRad(lat2 - lat1);
|
|
9
|
-
const dLng = toRad(lng2 - lng1);
|
|
10
|
-
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
11
|
-
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
|
12
|
-
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
13
|
-
return 6371 * c;
|
|
14
|
-
}
|
|
5
|
+
import { toYyyymmdd } from '../client.js';
|
|
6
|
+
import { fetchMegaboxNearbyTheaters } from '../location.js';
|
|
15
7
|
async function findNearbyTheaters(args) {
|
|
16
|
-
const { latitude
|
|
17
|
-
const
|
|
8
|
+
const { keyword, latitude, longitude, playDate = toYyyymmdd(), areaCode, limit = 10, timeoutMs = 15000, } = args;
|
|
9
|
+
const result = await fetchMegaboxNearbyTheaters({
|
|
10
|
+
keyword,
|
|
11
|
+
latitude,
|
|
12
|
+
longitude,
|
|
18
13
|
playDate,
|
|
19
14
|
areaCode,
|
|
15
|
+
limit,
|
|
16
|
+
timeout: timeoutMs,
|
|
17
|
+
}, {
|
|
18
|
+
googleMapsApiKey: process.env.GOOGLE_MAPS_API_KEY,
|
|
20
19
|
timeout: timeoutMs,
|
|
21
20
|
});
|
|
22
|
-
const infoResults = await Promise.allSettled(theaters.map((theater) => fetchMegaboxTheaterInfo(theater.theaterId, timeoutMs)));
|
|
23
|
-
const merged = theaters
|
|
24
|
-
.map((theater, index) => {
|
|
25
|
-
const infoResult = infoResults[index];
|
|
26
|
-
if (infoResult.status !== 'fulfilled') {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
const info = infoResult.value;
|
|
30
|
-
if (info.latitude === null || info.longitude === null) {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
const distanceKm = calculateDistanceKm(latitude, longitude, info.latitude, info.longitude);
|
|
34
|
-
return {
|
|
35
|
-
theaterId: theater.theaterId,
|
|
36
|
-
theaterName: theater.theaterName,
|
|
37
|
-
address: info.address,
|
|
38
|
-
latitude: info.latitude,
|
|
39
|
-
longitude: info.longitude,
|
|
40
|
-
distanceKm: Number(distanceKm.toFixed(2)),
|
|
41
|
-
};
|
|
42
|
-
})
|
|
43
|
-
.filter((theater) => theater !== null)
|
|
44
|
-
.sort((a, b) => a.distanceKm - b.distanceKm)
|
|
45
|
-
.slice(0, limit);
|
|
46
|
-
const result = {
|
|
47
|
-
location: { latitude, longitude },
|
|
48
|
-
playDate,
|
|
49
|
-
areaCode,
|
|
50
|
-
count: merged.length,
|
|
51
|
-
theaters: merged,
|
|
52
|
-
};
|
|
53
21
|
return {
|
|
54
22
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
55
23
|
};
|
|
@@ -59,8 +27,9 @@ export function createFindNearbyTheatersTool() {
|
|
|
59
27
|
name: 'megabox_find_nearby_theaters',
|
|
60
28
|
metadata: {
|
|
61
29
|
title: '메가박스 주변 지점 탐색',
|
|
62
|
-
description: '사용자 좌표 기준으로 메가박스 지점을 거리순으로 조회합니다.',
|
|
30
|
+
description: '사용자 좌표 또는 위치 키워드 기준으로 메가박스 지점을 거리순으로 조회합니다.',
|
|
63
31
|
inputSchema: {
|
|
32
|
+
keyword: z.string().optional().describe('위치 키워드 (예: 안산 중앙역, 강남역)'),
|
|
64
33
|
latitude: z.number().optional().default(37.5665).describe('위도 (기본값: 서울 시청 37.5665)'),
|
|
65
34
|
longitude: z.number().optional().default(126.978).describe('경도 (기본값: 서울 시청 126.978)'),
|
|
66
35
|
playDate: z.string().optional().describe('조회 날짜(YYYYMMDD, 기본값: 오늘)'),
|
|
@@ -3,13 +3,34 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import * as z from 'zod';
|
|
5
5
|
import { fetchMegaboxBookingList, toYyyymmdd } from '../client.js';
|
|
6
|
+
import { resolveMegaboxNearestTheater } from '../location.js';
|
|
6
7
|
async function getRemainingSeats(args) {
|
|
7
|
-
const { playDate = toYyyymmdd(), theaterId, movieId,
|
|
8
|
+
const { playDate = toYyyymmdd(), theaterId: inputTheaterId, movieId, keyword, latitude, longitude, areaCode, limit = 50, timeoutMs = 15000, } = args;
|
|
9
|
+
let theaterId = inputTheaterId;
|
|
10
|
+
let resolvedTheater = null;
|
|
11
|
+
let resolvedLocation = null;
|
|
12
|
+
if (!theaterId && (typeof latitude === 'number' || typeof longitude === 'number' || (keyword || '').trim().length > 0)) {
|
|
13
|
+
const resolved = await resolveMegaboxNearestTheater({
|
|
14
|
+
keyword,
|
|
15
|
+
latitude,
|
|
16
|
+
longitude,
|
|
17
|
+
areaCode,
|
|
18
|
+
playDate,
|
|
19
|
+
timeout: timeoutMs,
|
|
20
|
+
}, {
|
|
21
|
+
googleMapsApiKey: process.env.GOOGLE_MAPS_API_KEY,
|
|
22
|
+
timeout: timeoutMs,
|
|
23
|
+
});
|
|
24
|
+
theaterId = resolved.theater?.theaterId;
|
|
25
|
+
resolvedTheater = resolved.theater;
|
|
26
|
+
resolvedLocation = resolved.location;
|
|
27
|
+
}
|
|
28
|
+
const resolvedAreaCode = resolvedLocation?.areaCode || areaCode || '11';
|
|
8
29
|
const { showtimes } = await fetchMegaboxBookingList({
|
|
9
30
|
playDate,
|
|
10
31
|
theaterId,
|
|
11
32
|
movieId,
|
|
12
|
-
areaCode,
|
|
33
|
+
areaCode: resolvedAreaCode,
|
|
13
34
|
timeout: timeoutMs,
|
|
14
35
|
});
|
|
15
36
|
const filteredShowtimes = showtimes
|
|
@@ -27,9 +48,13 @@ async function getRemainingSeats(args) {
|
|
|
27
48
|
filters: {
|
|
28
49
|
theaterId: theaterId || null,
|
|
29
50
|
movieId: movieId || null,
|
|
30
|
-
|
|
51
|
+
keyword: keyword || null,
|
|
52
|
+
latitude: typeof latitude === 'number' ? latitude : null,
|
|
53
|
+
longitude: typeof longitude === 'number' ? longitude : null,
|
|
54
|
+
areaCode: resolvedAreaCode,
|
|
31
55
|
limit,
|
|
32
56
|
},
|
|
57
|
+
resolvedTheater,
|
|
33
58
|
count: filteredShowtimes.length,
|
|
34
59
|
seats: filteredShowtimes,
|
|
35
60
|
};
|
|
@@ -47,6 +72,9 @@ export function createGetRemainingSeatsTool() {
|
|
|
47
72
|
playDate: z.string().optional().describe('조회 날짜(YYYYMMDD, 기본값: 오늘)'),
|
|
48
73
|
theaterId: z.string().optional().describe('메가박스 지점 번호 (예: 1372)'),
|
|
49
74
|
movieId: z.string().optional().describe('메가박스 영화 번호 (예: 25104500)'),
|
|
75
|
+
keyword: z.string().optional().describe('위치 키워드 (예: 안산 중앙역, 강남역)'),
|
|
76
|
+
latitude: z.number().optional().describe('위도'),
|
|
77
|
+
longitude: z.number().optional().describe('경도'),
|
|
50
78
|
areaCode: z.string().optional().default('11').describe('지역 코드 (기본값: 11, 서울)'),
|
|
51
79
|
limit: z.number().optional().default(50).describe('반환할 최대 회차 수 (기본값: 50)'),
|
|
52
80
|
timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
|
|
@@ -3,13 +3,34 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import * as z from 'zod';
|
|
5
5
|
import { fetchMegaboxBookingList, toYyyymmdd } from '../client.js';
|
|
6
|
+
import { resolveMegaboxNearestTheater } from '../location.js';
|
|
6
7
|
async function listNowShowing(args) {
|
|
7
|
-
const { playDate = toYyyymmdd(), theaterId, movieId,
|
|
8
|
+
const { playDate = toYyyymmdd(), theaterId: inputTheaterId, movieId, keyword, latitude, longitude, areaCode, timeoutMs = 15000, } = args;
|
|
9
|
+
let theaterId = inputTheaterId;
|
|
10
|
+
let resolvedTheater = null;
|
|
11
|
+
let resolvedLocation = null;
|
|
12
|
+
if (!theaterId && (typeof latitude === 'number' || typeof longitude === 'number' || (keyword || '').trim().length > 0)) {
|
|
13
|
+
const resolved = await resolveMegaboxNearestTheater({
|
|
14
|
+
keyword,
|
|
15
|
+
latitude,
|
|
16
|
+
longitude,
|
|
17
|
+
areaCode,
|
|
18
|
+
playDate,
|
|
19
|
+
timeout: timeoutMs,
|
|
20
|
+
}, {
|
|
21
|
+
googleMapsApiKey: process.env.GOOGLE_MAPS_API_KEY,
|
|
22
|
+
timeout: timeoutMs,
|
|
23
|
+
});
|
|
24
|
+
theaterId = resolved.theater?.theaterId;
|
|
25
|
+
resolvedTheater = resolved.theater;
|
|
26
|
+
resolvedLocation = resolved.location;
|
|
27
|
+
}
|
|
28
|
+
const resolvedAreaCode = resolvedLocation?.areaCode || areaCode || '11';
|
|
8
29
|
const { theaters, movies, showtimes } = await fetchMegaboxBookingList({
|
|
9
30
|
playDate,
|
|
10
31
|
theaterId,
|
|
11
32
|
movieId,
|
|
12
|
-
areaCode,
|
|
33
|
+
areaCode: resolvedAreaCode,
|
|
13
34
|
timeout: timeoutMs,
|
|
14
35
|
});
|
|
15
36
|
const result = {
|
|
@@ -17,8 +38,12 @@ async function listNowShowing(args) {
|
|
|
17
38
|
filters: {
|
|
18
39
|
theaterId: theaterId || null,
|
|
19
40
|
movieId: movieId || null,
|
|
20
|
-
|
|
41
|
+
keyword: keyword || null,
|
|
42
|
+
latitude: typeof latitude === 'number' ? latitude : null,
|
|
43
|
+
longitude: typeof longitude === 'number' ? longitude : null,
|
|
44
|
+
areaCode: resolvedAreaCode,
|
|
21
45
|
},
|
|
46
|
+
resolvedTheater,
|
|
22
47
|
counts: {
|
|
23
48
|
theaters: theaters.length,
|
|
24
49
|
movies: movies.length,
|
|
@@ -42,6 +67,9 @@ export function createListNowShowingTool() {
|
|
|
42
67
|
playDate: z.string().optional().describe('조회 날짜(YYYYMMDD, 기본값: 오늘)'),
|
|
43
68
|
theaterId: z.string().optional().describe('메가박스 지점 번호 (예: 1372)'),
|
|
44
69
|
movieId: z.string().optional().describe('메가박스 영화 번호 (예: 25104500)'),
|
|
70
|
+
keyword: z.string().optional().describe('위치 키워드 (예: 안산 중앙역, 강남역)'),
|
|
71
|
+
latitude: z.number().optional().describe('위도'),
|
|
72
|
+
longitude: z.number().optional().describe('경도'),
|
|
45
73
|
areaCode: z.string().optional().default('11').describe('지역 코드 (기본값: 11, 서울)'),
|
|
46
74
|
timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
|
|
47
75
|
},
|
|
@@ -5,4 +5,6 @@ export const OLIVEYOUNG_API = {
|
|
|
5
5
|
BASE_URL: 'https://www.oliveyoung.co.kr',
|
|
6
6
|
STORE_FINDER_PATH: '/oystore/api/storeFinder/find-store',
|
|
7
7
|
PRODUCT_SEARCH_PATH: '/oystore/api/stock/product-search-v3',
|
|
8
|
+
STOCK_GOODS_INFO_PATH: '/oystore/api/stock/stock-goods-info-v3',
|
|
9
|
+
STOCK_STORES_PATH: '/oystore/api/stock/stock-stores',
|
|
8
10
|
};
|
|
@@ -5,6 +5,88 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { OLIVEYOUNG_API } from './api.js';
|
|
7
7
|
import { decodeBase64, requestByZyte } from '../../utils/zyte.js';
|
|
8
|
+
const OLIVEYOUNG_IMAGE_HOST = 'https://image.oliveyoung.co.kr';
|
|
9
|
+
const OLIVEYOUNG_PRODUCT_ID_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
10
|
+
const OLIVEYOUNG_PRODUCT_SEARCH_STALE_CACHE_TTL_MS = 30 * 60 * 1000;
|
|
11
|
+
const oliveyoungProductIdCache = new Map();
|
|
12
|
+
const oliveyoungProductSearchCache = new Map();
|
|
13
|
+
const OLIVEYOUNG_IMAGE_PATH_PREFIX = '/uploads/images/goods';
|
|
14
|
+
function resolveOliveyoungInStock(o2oStockFlag, o2oRemainQuantity) {
|
|
15
|
+
return o2oStockFlag || o2oRemainQuantity > 0;
|
|
16
|
+
}
|
|
17
|
+
function resolveOliveyoungImageUrl(imagePath) {
|
|
18
|
+
if (!imagePath) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
if (/^https?:\/\//i.test(imagePath)) {
|
|
22
|
+
return imagePath;
|
|
23
|
+
}
|
|
24
|
+
if (imagePath.startsWith('//')) {
|
|
25
|
+
return `https:${imagePath}`;
|
|
26
|
+
}
|
|
27
|
+
const normalizedPath = imagePath.startsWith('/') ? imagePath : `/${imagePath}`;
|
|
28
|
+
if (normalizedPath.startsWith(`${OLIVEYOUNG_IMAGE_PATH_PREFIX}/`)) {
|
|
29
|
+
return `${OLIVEYOUNG_IMAGE_HOST}${normalizedPath}`;
|
|
30
|
+
}
|
|
31
|
+
return `${OLIVEYOUNG_IMAGE_HOST}${OLIVEYOUNG_IMAGE_PATH_PREFIX}${normalizedPath}`;
|
|
32
|
+
}
|
|
33
|
+
function resolveOliveyoungStoreQuantity(remainQuantity, o2oRemainQuantity) {
|
|
34
|
+
return Math.max(remainQuantity, o2oRemainQuantity);
|
|
35
|
+
}
|
|
36
|
+
function resolveOliveyoungStoreStock(rawStore) {
|
|
37
|
+
const remainQuantity = rawStore?.remainQuantity || 0;
|
|
38
|
+
const o2oRemainQuantity = rawStore?.o2oRemainQuantity || 0;
|
|
39
|
+
const quantity = resolveOliveyoungStoreQuantity(remainQuantity, o2oRemainQuantity);
|
|
40
|
+
const salesStoreYn = Boolean(rawStore?.salesStoreYn);
|
|
41
|
+
let stockStatus = 'out_of_stock';
|
|
42
|
+
let stockLabel = '품절';
|
|
43
|
+
if (!salesStoreYn) {
|
|
44
|
+
stockStatus = 'not_sold';
|
|
45
|
+
stockLabel = '미판매';
|
|
46
|
+
}
|
|
47
|
+
else if (quantity > 0) {
|
|
48
|
+
stockStatus = 'in_stock';
|
|
49
|
+
stockLabel = quantity >= 9 ? '재고 9개 이상' : `재고 ${quantity}개`;
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
storeCode: rawStore?.storeCode || '',
|
|
53
|
+
storeName: rawStore?.storeName || '',
|
|
54
|
+
address: rawStore?.address || '',
|
|
55
|
+
latitude: rawStore?.latitude || 0,
|
|
56
|
+
longitude: rawStore?.longitude || 0,
|
|
57
|
+
distance: rawStore?.distance || 0,
|
|
58
|
+
pickupYn: Boolean(rawStore?.pickupYn),
|
|
59
|
+
salesStoreYn,
|
|
60
|
+
remainQuantity,
|
|
61
|
+
o2oRemainQuantity,
|
|
62
|
+
stockStatus,
|
|
63
|
+
stockLabel,
|
|
64
|
+
openYn: Boolean(rawStore?.openYn),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function createOliveyoungProductSearchCacheKey(params) {
|
|
68
|
+
return JSON.stringify([params.keyword, params.page, params.size, params.sort, params.includeSoldOut]);
|
|
69
|
+
}
|
|
70
|
+
function cloneOliveyoungProductSearchResult(result) {
|
|
71
|
+
return {
|
|
72
|
+
totalCount: result.totalCount,
|
|
73
|
+
nextPage: result.nextPage,
|
|
74
|
+
products: result.products.map((product) => ({ ...product })),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function readStaleOliveyoungProductSearchResult(cacheKey) {
|
|
78
|
+
const cached = oliveyoungProductSearchCache.get(cacheKey);
|
|
79
|
+
if (!cached || cached.expiresAt <= Date.now()) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
return cloneOliveyoungProductSearchResult(cached.result);
|
|
83
|
+
}
|
|
84
|
+
function writeStaleOliveyoungProductSearchResult(cacheKey, result) {
|
|
85
|
+
oliveyoungProductSearchCache.set(cacheKey, {
|
|
86
|
+
expiresAt: Date.now() + OLIVEYOUNG_PRODUCT_SEARCH_STALE_CACHE_TTL_MS,
|
|
87
|
+
result: cloneOliveyoungProductSearchResult(result),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
8
90
|
async function zyteExtract(targetPath, requestBody, options = {}) {
|
|
9
91
|
const { timeout = 15000, apiKey } = options;
|
|
10
92
|
try {
|
|
@@ -71,20 +153,150 @@ export async function fetchOliveyoungProducts(params, options = {}) {
|
|
|
71
153
|
sort: params.sort,
|
|
72
154
|
size: params.size,
|
|
73
155
|
};
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
156
|
+
const cacheKey = createOliveyoungProductSearchCacheKey(params);
|
|
157
|
+
try {
|
|
158
|
+
const body = await zyteExtract(OLIVEYOUNG_API.PRODUCT_SEARCH_PATH, payload, options);
|
|
159
|
+
const list = body.data?.serachList || body.data?.searchList || [];
|
|
160
|
+
const products = list.map((product) => {
|
|
161
|
+
const o2oStockFlag = Boolean(product.o2oStockFlag);
|
|
162
|
+
const o2oRemainQuantity = product.o2oRemainQuantity || 0;
|
|
163
|
+
const inStock = resolveOliveyoungInStock(o2oStockFlag, o2oRemainQuantity);
|
|
164
|
+
const stockStatus = inStock ? 'in_stock' : 'out_of_stock';
|
|
165
|
+
const stockSource = 'global_search';
|
|
166
|
+
return {
|
|
167
|
+
goodsNumber: product.goodsNumber || '',
|
|
168
|
+
goodsName: product.goodsName || '',
|
|
169
|
+
imageUrl: resolveOliveyoungImageUrl(product.imagePath),
|
|
170
|
+
priceToPay: product.priceToPay || 0,
|
|
171
|
+
originalPrice: product.originalPrice || 0,
|
|
172
|
+
discountRate: product.discountRate || 0,
|
|
173
|
+
o2oStockFlag,
|
|
174
|
+
o2oRemainQuantity,
|
|
175
|
+
inStock,
|
|
176
|
+
stockStatus,
|
|
177
|
+
stockSource,
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
const result = {
|
|
181
|
+
totalCount: body.data?.totalCount || 0,
|
|
182
|
+
nextPage: Boolean(body.data?.nextPage),
|
|
183
|
+
products,
|
|
184
|
+
};
|
|
185
|
+
writeStaleOliveyoungProductSearchResult(cacheKey, result);
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
const staleResult = readStaleOliveyoungProductSearchResult(cacheKey);
|
|
190
|
+
if (staleResult) {
|
|
191
|
+
return staleResult;
|
|
192
|
+
}
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async function fetchOliveyoungProductId(goodsNumber, options = {}) {
|
|
197
|
+
const normalizedGoodsNumber = goodsNumber.trim();
|
|
198
|
+
if (!normalizedGoodsNumber) {
|
|
199
|
+
return '';
|
|
200
|
+
}
|
|
201
|
+
const cached = oliveyoungProductIdCache.get(normalizedGoodsNumber);
|
|
202
|
+
const now = Date.now();
|
|
203
|
+
if (cached && cached.expiresAt > now) {
|
|
204
|
+
return cached.productId;
|
|
205
|
+
}
|
|
206
|
+
const body = await zyteExtract(OLIVEYOUNG_API.STOCK_GOODS_INFO_PATH, { goodsNo: normalizedGoodsNumber }, options);
|
|
207
|
+
const productId = body.data?.goodsInfo?.masterGoodsNumber || '';
|
|
208
|
+
if (productId) {
|
|
209
|
+
oliveyoungProductIdCache.set(normalizedGoodsNumber, {
|
|
210
|
+
expiresAt: now + OLIVEYOUNG_PRODUCT_ID_CACHE_TTL_MS,
|
|
211
|
+
productId,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return productId;
|
|
215
|
+
}
|
|
216
|
+
async function fetchOliveyoungStockStores(params, options = {}) {
|
|
217
|
+
const body = await zyteExtract(OLIVEYOUNG_API.STOCK_STORES_PATH, {
|
|
218
|
+
productId: params.productId,
|
|
219
|
+
lat: params.latitude,
|
|
220
|
+
lon: params.longitude,
|
|
221
|
+
pageIdx: params.pageIdx,
|
|
222
|
+
searchWords: params.searchWords,
|
|
223
|
+
mapLat: params.latitude,
|
|
224
|
+
mapLon: params.longitude,
|
|
225
|
+
}, options);
|
|
226
|
+
const stores = (body.data?.storeList || []).map((store) => resolveOliveyoungStoreStock(store));
|
|
227
|
+
const inStockCount = stores.filter((store) => store.stockStatus === 'in_stock').length;
|
|
228
|
+
const notSoldCount = stores.filter((store) => store.stockStatus === 'not_sold').length;
|
|
85
229
|
return {
|
|
86
230
|
totalCount: body.data?.totalCount || 0,
|
|
87
|
-
|
|
88
|
-
|
|
231
|
+
inStockCount,
|
|
232
|
+
outOfStockCount: stores.length - inStockCount - notSoldCount,
|
|
233
|
+
notSoldCount,
|
|
234
|
+
stores,
|
|
89
235
|
};
|
|
90
236
|
}
|
|
237
|
+
function sortOliveyoungProducts(products) {
|
|
238
|
+
const score = (product) => {
|
|
239
|
+
if (product.stockSource === 'nearby_store') {
|
|
240
|
+
return product.inStock ? 4 : 3;
|
|
241
|
+
}
|
|
242
|
+
return product.inStock ? 2 : 1;
|
|
243
|
+
};
|
|
244
|
+
return [...products].sort((left, right) => score(right) - score(left));
|
|
245
|
+
}
|
|
246
|
+
export async function enrichOliveyoungProductsWithNearbyStoreInventory(products, params, options = {}) {
|
|
247
|
+
const maxProducts = Math.max(0, Math.min(products.length, params.maxProducts));
|
|
248
|
+
if (maxProducts === 0) {
|
|
249
|
+
return { checkedCount: 0, products };
|
|
250
|
+
}
|
|
251
|
+
const checkedProducts = await Promise.all(products.slice(0, maxProducts).map(async (product) => {
|
|
252
|
+
let productId = '';
|
|
253
|
+
try {
|
|
254
|
+
productId = await fetchOliveyoungProductId(product.goodsNumber, options);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return { checked: false, product };
|
|
258
|
+
}
|
|
259
|
+
if (!productId) {
|
|
260
|
+
return { checked: false, product };
|
|
261
|
+
}
|
|
262
|
+
let storeInventory;
|
|
263
|
+
try {
|
|
264
|
+
storeInventory = await fetchOliveyoungStockStores({
|
|
265
|
+
productId,
|
|
266
|
+
latitude: params.latitude,
|
|
267
|
+
longitude: params.longitude,
|
|
268
|
+
pageIdx: 1,
|
|
269
|
+
searchWords: params.storeKeyword,
|
|
270
|
+
}, options);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return { checked: false, product };
|
|
274
|
+
}
|
|
275
|
+
const inStock = storeInventory.inStockCount > 0;
|
|
276
|
+
const stockStatus = inStock ? 'in_stock' : 'out_of_stock';
|
|
277
|
+
const stockSource = 'nearby_store';
|
|
278
|
+
return {
|
|
279
|
+
checked: true,
|
|
280
|
+
product: {
|
|
281
|
+
...product,
|
|
282
|
+
inStock,
|
|
283
|
+
stockStatus,
|
|
284
|
+
stockSource,
|
|
285
|
+
storeInventory,
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}));
|
|
289
|
+
const enrichedProducts = [
|
|
290
|
+
...checkedProducts.map((result) => result.product),
|
|
291
|
+
...products.slice(maxProducts),
|
|
292
|
+
];
|
|
293
|
+
const checkedCount = checkedProducts.filter((result) => result.checked).length;
|
|
294
|
+
return {
|
|
295
|
+
checkedCount,
|
|
296
|
+
products: sortOliveyoungProducts(enrichedProducts),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
export function __testOnlyClearOliveyoungCaches() {
|
|
300
|
+
oliveyoungProductIdCache.clear();
|
|
301
|
+
oliveyoungProductSearchCache.clear();
|
|
302
|
+
}
|