daiso 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +218 -537
- package/dist/api/actionsProxy.js +82 -0
- package/dist/api/cgvHandlers.js +103 -16
- package/dist/api/daisoHandlers.js +127 -0
- package/dist/api/emart24Handlers.js +176 -0
- package/dist/api/gs25Handlers.js +256 -0
- package/dist/api/handlers.js +184 -108
- package/dist/api/healthChecks.js +332 -0
- package/dist/api/lottecinemaHandlers.js +155 -0
- package/dist/api/lottemartHandlers.js +128 -0
- package/dist/api/megaboxHandlers.js +81 -50
- 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 +44 -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/cli/args.js +54 -0
- package/dist/cli/commands/convenience.js +241 -0
- package/dist/cli/commands/daiso.js +96 -0
- package/dist/cli/constants.js +5 -0
- package/dist/cli/deps.js +48 -0
- package/dist/cli/http.js +98 -0
- package/dist/cli/interactiveFetch.js +31 -0
- package/dist/cli/interactiveItemSearch.js +259 -0
- package/dist/cli/interactivePrompt.js +49 -0
- package/dist/cli/interactiveTypes.js +4 -0
- package/dist/cli/types.js +4 -0
- package/dist/cli.js +66 -181
- package/dist/cliHelp.js +216 -9
- package/dist/cliInteractive.js +174 -0
- package/dist/cliPicker.js +56 -0
- package/dist/cliRenderer.js +139 -0
- package/dist/index.js +147 -8
- 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 +138 -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 +135 -0
- package/dist/pages/openapiSpecPathsLottecinema.js +212 -0
- package/dist/pages/openapiSpecPathsMegabox.js +52 -3
- package/dist/pages/openapiSpecPathsOliveyoung.js +231 -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 +131 -0
- package/dist/pages/promptRetailCinemaText.js +273 -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 +59 -0
- package/dist/services/daiso/index.js +2 -0
- package/dist/services/daiso/product.js +17 -0
- package/dist/services/daiso/tools/checkInventory.js +76 -41
- 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 +173 -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 +63 -0
- package/dist/services/emart24/types.js +4 -0
- package/dist/services/gs25/api.js +9 -0
- package/dist/services/gs25/client.js +198 -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 +50 -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 +245 -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 +62 -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 +150 -9
- package/dist/services/oliveyoung/index.js +3 -1
- package/dist/services/oliveyoung/tools/checkInventory.js +15 -4
- package/dist/services/oliveyoung/tools/searchProducts.js +57 -0
- package/dist/services/seveneleven/api.js +15 -0
- package/dist/services/seveneleven/client.js +279 -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 +56 -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 +16 -2
- package/openapi.json +1977 -734
- package/openapi.yaml +1430 -543
- package/package.json +28 -15
- 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,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 롯데시네마 위치 해석 및 근처 극장 조회 보조 모듈
|
|
3
|
+
*/
|
|
4
|
+
import { fetchJson } from '../../utils/http.js';
|
|
5
|
+
import { fetchLotteCinemaTicketingPage } from './client.js';
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 15000;
|
|
7
|
+
const GEOCODE_CACHE_TTL_MS = 60 * 60 * 1000;
|
|
8
|
+
const LOCATION_STOPWORDS = new Set([
|
|
9
|
+
'롯데시네마',
|
|
10
|
+
'영화',
|
|
11
|
+
'극장',
|
|
12
|
+
'주변',
|
|
13
|
+
'근처',
|
|
14
|
+
'오늘',
|
|
15
|
+
'상영',
|
|
16
|
+
'영화목록',
|
|
17
|
+
'시간표',
|
|
18
|
+
'좌석',
|
|
19
|
+
'잔여',
|
|
20
|
+
'찾아',
|
|
21
|
+
'주세요',
|
|
22
|
+
]);
|
|
23
|
+
const geocodeCache = new Map();
|
|
24
|
+
function calculateDistanceKm(lat1, lng1, lat2, lng2) {
|
|
25
|
+
const toRad = (deg) => (deg * Math.PI) / 180;
|
|
26
|
+
const dLat = toRad(lat2 - lat1);
|
|
27
|
+
const dLng = toRad(lng2 - lng1);
|
|
28
|
+
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
29
|
+
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
|
30
|
+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
31
|
+
return 6371 * c;
|
|
32
|
+
}
|
|
33
|
+
function isValidCoordinate(value) {
|
|
34
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
35
|
+
}
|
|
36
|
+
function normalizeText(text) {
|
|
37
|
+
return text.replace(/\s+/g, '').toLowerCase();
|
|
38
|
+
}
|
|
39
|
+
function extractLocationTokens(...values) {
|
|
40
|
+
const tokens = new Set();
|
|
41
|
+
for (const value of values) {
|
|
42
|
+
const matches = (value || '').match(/[0-9A-Za-z가-힣]+/g) || [];
|
|
43
|
+
for (const match of matches) {
|
|
44
|
+
const normalized = normalizeText(match);
|
|
45
|
+
if (normalized.length < 2 || LOCATION_STOPWORDS.has(normalized)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
tokens.add(normalized);
|
|
49
|
+
if (normalized.endsWith('역') && normalized.length > 2) {
|
|
50
|
+
tokens.add(normalized.slice(0, -1));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return [...tokens];
|
|
55
|
+
}
|
|
56
|
+
function scoreTheater(theater, tokens) {
|
|
57
|
+
const normalizedName = normalizeText(theater.theaterName);
|
|
58
|
+
const normalizedAddress = normalizeText(theater.address || '');
|
|
59
|
+
return tokens.reduce((score, token) => {
|
|
60
|
+
if (normalizedName.includes(token)) {
|
|
61
|
+
return score + 2;
|
|
62
|
+
}
|
|
63
|
+
if (normalizedAddress.includes(token)) {
|
|
64
|
+
return score + 1;
|
|
65
|
+
}
|
|
66
|
+
return score;
|
|
67
|
+
}, 0);
|
|
68
|
+
}
|
|
69
|
+
async function geocodeAddress(query, options) {
|
|
70
|
+
const apiKey = (options.googleMapsApiKey || '').trim();
|
|
71
|
+
const trimmedQuery = query.trim();
|
|
72
|
+
if (apiKey.length === 0 || trimmedQuery.length === 0) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const cacheKey = `keyword:${trimmedQuery}`;
|
|
76
|
+
const cached = geocodeCache.get(cacheKey);
|
|
77
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
78
|
+
return cached.value;
|
|
79
|
+
}
|
|
80
|
+
const endpoint = new URL('https://maps.googleapis.com/maps/api/geocode/json');
|
|
81
|
+
endpoint.searchParams.set('address', trimmedQuery);
|
|
82
|
+
endpoint.searchParams.set('key', apiKey);
|
|
83
|
+
const body = await fetchJson(endpoint.toString(), {
|
|
84
|
+
method: 'GET',
|
|
85
|
+
timeout: options.timeout || DEFAULT_TIMEOUT_MS,
|
|
86
|
+
headers: { Accept: 'application/json' },
|
|
87
|
+
});
|
|
88
|
+
const firstResult = body.status === 'OK' ? body.results?.[0] : null;
|
|
89
|
+
const latitude = firstResult?.geometry?.location?.lat;
|
|
90
|
+
const longitude = firstResult?.geometry?.location?.lng;
|
|
91
|
+
const value = typeof latitude === 'number' && typeof longitude === 'number'
|
|
92
|
+
? {
|
|
93
|
+
latitude,
|
|
94
|
+
longitude,
|
|
95
|
+
formattedAddress: firstResult?.formatted_address || null,
|
|
96
|
+
}
|
|
97
|
+
: null;
|
|
98
|
+
geocodeCache.set(cacheKey, {
|
|
99
|
+
expiresAt: Date.now() + GEOCODE_CACHE_TTL_MS,
|
|
100
|
+
value,
|
|
101
|
+
});
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
export async function resolveLotteCinemaLocation(params, options = {}) {
|
|
105
|
+
const keyword = (params.keyword || '').trim();
|
|
106
|
+
const hasCoordinates = isValidCoordinate(params.latitude) && isValidCoordinate(params.longitude);
|
|
107
|
+
if (hasCoordinates) {
|
|
108
|
+
return {
|
|
109
|
+
keyword: keyword.length > 0 ? keyword : null,
|
|
110
|
+
latitude: params.latitude,
|
|
111
|
+
longitude: params.longitude,
|
|
112
|
+
geocodeUsed: false,
|
|
113
|
+
formattedAddress: null,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (keyword.length === 0) {
|
|
117
|
+
return {
|
|
118
|
+
keyword: null,
|
|
119
|
+
latitude: null,
|
|
120
|
+
longitude: null,
|
|
121
|
+
geocodeUsed: false,
|
|
122
|
+
formattedAddress: null,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const geocoded = await geocodeAddress(keyword, options);
|
|
126
|
+
return {
|
|
127
|
+
keyword,
|
|
128
|
+
latitude: geocoded?.latitude ?? null,
|
|
129
|
+
longitude: geocoded?.longitude ?? null,
|
|
130
|
+
geocodeUsed: Boolean(geocoded),
|
|
131
|
+
formattedAddress: geocoded?.formattedAddress ?? null,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export async function fetchLotteCinemaNearbyTheaters(params, options = {}) {
|
|
135
|
+
const timeout = typeof params.timeout === 'number' ? params.timeout : DEFAULT_TIMEOUT_MS;
|
|
136
|
+
const limit = typeof params.limit === 'number' ? Math.max(1, params.limit) : 10;
|
|
137
|
+
const location = await resolveLotteCinemaLocation(params, options);
|
|
138
|
+
const { theaters } = await fetchLotteCinemaTicketingPage(timeout);
|
|
139
|
+
let nearby;
|
|
140
|
+
if (location.latitude !== null && location.longitude !== null) {
|
|
141
|
+
nearby = theaters
|
|
142
|
+
.filter((theater) => theater.latitude !== null && theater.longitude !== null)
|
|
143
|
+
.map((theater) => ({
|
|
144
|
+
...theater,
|
|
145
|
+
distanceKm: Number(calculateDistanceKm(location.latitude, location.longitude, theater.latitude, theater.longitude).toFixed(2)),
|
|
146
|
+
}))
|
|
147
|
+
.sort((left, right) => left.distanceKm - right.distanceKm || left.theaterName.localeCompare(right.theaterName))
|
|
148
|
+
.slice(0, limit);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
const tokens = extractLocationTokens(location.keyword, location.formattedAddress);
|
|
152
|
+
nearby = theaters
|
|
153
|
+
.map((theater) => ({
|
|
154
|
+
theater,
|
|
155
|
+
score: scoreTheater(theater, tokens),
|
|
156
|
+
}))
|
|
157
|
+
.filter((item) => item.score > 0)
|
|
158
|
+
.sort((left, right) => right.score - left.score || left.theater.theaterName.localeCompare(right.theater.theaterName))
|
|
159
|
+
.slice(0, limit)
|
|
160
|
+
.map((item) => ({
|
|
161
|
+
...item.theater,
|
|
162
|
+
distanceKm: null,
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
...location,
|
|
167
|
+
playDate: params.playDate,
|
|
168
|
+
count: nearby.length,
|
|
169
|
+
theaters: nearby,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
export async function resolveLotteCinemaNearestTheater(params, options = {}) {
|
|
173
|
+
const nearby = await fetchLotteCinemaNearbyTheaters({
|
|
174
|
+
...params,
|
|
175
|
+
limit: 1,
|
|
176
|
+
timeout: params.timeout,
|
|
177
|
+
}, options);
|
|
178
|
+
return {
|
|
179
|
+
location: {
|
|
180
|
+
keyword: nearby.keyword,
|
|
181
|
+
latitude: nearby.latitude,
|
|
182
|
+
longitude: nearby.longitude,
|
|
183
|
+
geocodeUsed: nearby.geocodeUsed,
|
|
184
|
+
formattedAddress: nearby.formattedAddress,
|
|
185
|
+
},
|
|
186
|
+
theater: nearby.theaters[0] || null,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
export function __testOnlyClearLotteCinemaLocationCaches() {
|
|
190
|
+
geocodeCache.clear();
|
|
191
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 롯데시네마 주변 지점 탐색 도구
|
|
3
|
+
*/
|
|
4
|
+
import * as z from 'zod';
|
|
5
|
+
import { toYyyymmdd } from '../client.js';
|
|
6
|
+
import { fetchLotteCinemaNearbyTheaters } from '../location.js';
|
|
7
|
+
async function findNearbyTheaters(args, googleMapsApiKey) {
|
|
8
|
+
const { keyword, latitude, longitude, playDate = toYyyymmdd(), limit = 10, timeoutMs = 15000 } = args;
|
|
9
|
+
const hasCoordinates = typeof latitude === 'number' && typeof longitude === 'number';
|
|
10
|
+
const nearby = await fetchLotteCinemaNearbyTheaters({
|
|
11
|
+
keyword,
|
|
12
|
+
latitude: hasCoordinates ? latitude : keyword ? undefined : 37.5665,
|
|
13
|
+
longitude: hasCoordinates ? longitude : keyword ? undefined : 126.978,
|
|
14
|
+
playDate,
|
|
15
|
+
limit,
|
|
16
|
+
timeout: timeoutMs,
|
|
17
|
+
}, {
|
|
18
|
+
timeout: timeoutMs,
|
|
19
|
+
googleMapsApiKey: googleMapsApiKey || process.env.GOOGLE_MAPS_API_KEY,
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
content: [
|
|
23
|
+
{
|
|
24
|
+
type: 'text',
|
|
25
|
+
text: JSON.stringify(nearby, null, 2),
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function createFindNearbyTheatersTool(googleMapsApiKey) {
|
|
31
|
+
return {
|
|
32
|
+
name: 'lottecinema_find_nearby_theaters',
|
|
33
|
+
metadata: {
|
|
34
|
+
title: '롯데시네마 주변 지점 탐색',
|
|
35
|
+
description: '위치 키워드 또는 좌표 기준으로 롯데시네마 지점을 거리순으로 조회합니다.',
|
|
36
|
+
inputSchema: {
|
|
37
|
+
keyword: z.string().optional().describe('위치 키워드 (예: 안산 중앙역, 잠실역)'),
|
|
38
|
+
latitude: z.number().optional().default(37.5665).describe('위도 (기본값: 서울 시청 37.5665)'),
|
|
39
|
+
longitude: z.number().optional().default(126.978).describe('경도 (기본값: 서울 시청 126.978)'),
|
|
40
|
+
playDate: z.string().optional().describe('조회 날짜(YYYYMMDD, 기본값: 오늘)'),
|
|
41
|
+
limit: z.number().optional().default(10).describe('반환할 최대 지점 수 (기본값: 10)'),
|
|
42
|
+
timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
handler: ((args) => findNearbyTheaters(args, googleMapsApiKey)),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 롯데시네마 잔여 좌석 조회 도구
|
|
3
|
+
*/
|
|
4
|
+
import * as z from 'zod';
|
|
5
|
+
import { fetchLotteCinemaNowShowing, toYyyymmdd } from '../client.js';
|
|
6
|
+
import { resolveLotteCinemaNearestTheater } from '../location.js';
|
|
7
|
+
async function getRemainingSeats(args, googleMapsApiKey) {
|
|
8
|
+
const { playDate = toYyyymmdd(), theaterId, movieId, keyword, latitude, longitude, limit = 50, timeoutMs = 15000, } = args;
|
|
9
|
+
let resolvedTheater = null;
|
|
10
|
+
let targetTheaterId = theaterId;
|
|
11
|
+
if (!targetTheaterId && (keyword || typeof latitude === 'number' || typeof longitude === 'number')) {
|
|
12
|
+
const resolved = await resolveLotteCinemaNearestTheater({
|
|
13
|
+
playDate,
|
|
14
|
+
keyword,
|
|
15
|
+
latitude,
|
|
16
|
+
longitude,
|
|
17
|
+
timeout: timeoutMs,
|
|
18
|
+
}, {
|
|
19
|
+
timeout: timeoutMs,
|
|
20
|
+
googleMapsApiKey: googleMapsApiKey || process.env.GOOGLE_MAPS_API_KEY,
|
|
21
|
+
});
|
|
22
|
+
resolvedTheater = resolved.theater;
|
|
23
|
+
targetTheaterId = resolved.theater?.theaterId;
|
|
24
|
+
}
|
|
25
|
+
const shouldReturnEmpty = !targetTheaterId && (keyword || typeof latitude === 'number' || typeof longitude === 'number');
|
|
26
|
+
const { showtimes } = shouldReturnEmpty
|
|
27
|
+
? { showtimes: [] }
|
|
28
|
+
: await fetchLotteCinemaNowShowing({
|
|
29
|
+
playDate,
|
|
30
|
+
theaterId: targetTheaterId,
|
|
31
|
+
movieId,
|
|
32
|
+
timeout: timeoutMs,
|
|
33
|
+
});
|
|
34
|
+
const seats = showtimes
|
|
35
|
+
.filter((item) => (targetTheaterId ? item.theaterId === targetTheaterId : true))
|
|
36
|
+
.filter((item) => (movieId ? item.movieId === movieId : true))
|
|
37
|
+
.sort((a, b) => {
|
|
38
|
+
if (a.startTime === b.startTime) {
|
|
39
|
+
return a.theaterName.localeCompare(b.theaterName);
|
|
40
|
+
}
|
|
41
|
+
return a.startTime.localeCompare(b.startTime);
|
|
42
|
+
})
|
|
43
|
+
.slice(0, limit);
|
|
44
|
+
return {
|
|
45
|
+
content: [
|
|
46
|
+
{
|
|
47
|
+
type: 'text',
|
|
48
|
+
text: JSON.stringify({
|
|
49
|
+
playDate,
|
|
50
|
+
filters: {
|
|
51
|
+
theaterId: targetTheaterId || null,
|
|
52
|
+
movieId: movieId || null,
|
|
53
|
+
keyword: keyword || null,
|
|
54
|
+
latitude: latitude ?? null,
|
|
55
|
+
longitude: longitude ?? null,
|
|
56
|
+
limit,
|
|
57
|
+
},
|
|
58
|
+
resolvedTheater,
|
|
59
|
+
count: seats.length,
|
|
60
|
+
seats,
|
|
61
|
+
}, null, 2),
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export function createGetRemainingSeatsTool(googleMapsApiKey) {
|
|
67
|
+
return {
|
|
68
|
+
name: 'lottecinema_get_remaining_seats',
|
|
69
|
+
metadata: {
|
|
70
|
+
title: '롯데시네마 잔여 좌석 조회',
|
|
71
|
+
description: '영화/지점/날짜 조건으로 롯데시네마 상영 회차별 남은 좌석 수를 조회합니다. 지점 ID가 없으면 위치 키워드 기준 최근접 극장을 선택할 수 있습니다.',
|
|
72
|
+
inputSchema: {
|
|
73
|
+
playDate: z.string().optional().describe('조회 날짜(YYYYMMDD, 기본값: 오늘)'),
|
|
74
|
+
theaterId: z.string().optional().describe('롯데시네마 지점 ID (예: 1016)'),
|
|
75
|
+
movieId: z.string().optional().describe('롯데시네마 대표 영화 코드 (예: 23816)'),
|
|
76
|
+
keyword: z.string().optional().describe('위치 키워드 (예: 안산 중앙역, 잠실역)'),
|
|
77
|
+
latitude: z.number().optional().describe('위도'),
|
|
78
|
+
longitude: z.number().optional().describe('경도'),
|
|
79
|
+
limit: z.number().optional().default(50).describe('반환할 최대 회차 수 (기본값: 50)'),
|
|
80
|
+
timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
handler: ((args) => getRemainingSeats(args, googleMapsApiKey)),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 롯데시네마 영화 목록 조회 도구
|
|
3
|
+
*/
|
|
4
|
+
import * as z from 'zod';
|
|
5
|
+
import { fetchLotteCinemaNowShowing, toYyyymmdd } from '../client.js';
|
|
6
|
+
import { resolveLotteCinemaNearestTheater } from '../location.js';
|
|
7
|
+
async function listNowShowing(args, googleMapsApiKey) {
|
|
8
|
+
const { playDate = toYyyymmdd(), theaterId, movieId, keyword, latitude, longitude, timeoutMs = 15000 } = args;
|
|
9
|
+
let resolvedTheater = null;
|
|
10
|
+
let targetTheaterId = theaterId;
|
|
11
|
+
if (!targetTheaterId && (keyword || typeof latitude === 'number' || typeof longitude === 'number')) {
|
|
12
|
+
const resolved = await resolveLotteCinemaNearestTheater({
|
|
13
|
+
playDate,
|
|
14
|
+
keyword,
|
|
15
|
+
latitude,
|
|
16
|
+
longitude,
|
|
17
|
+
timeout: timeoutMs,
|
|
18
|
+
}, {
|
|
19
|
+
timeout: timeoutMs,
|
|
20
|
+
googleMapsApiKey: googleMapsApiKey || process.env.GOOGLE_MAPS_API_KEY,
|
|
21
|
+
});
|
|
22
|
+
resolvedTheater = resolved.theater;
|
|
23
|
+
targetTheaterId = resolved.theater?.theaterId;
|
|
24
|
+
}
|
|
25
|
+
const shouldReturnEmpty = !targetTheaterId && (keyword || typeof latitude === 'number' || typeof longitude === 'number');
|
|
26
|
+
const { theaters, movies, showtimes } = shouldReturnEmpty
|
|
27
|
+
? { theaters: [], movies: [], showtimes: [] }
|
|
28
|
+
: await fetchLotteCinemaNowShowing({
|
|
29
|
+
playDate,
|
|
30
|
+
theaterId: targetTheaterId,
|
|
31
|
+
movieId,
|
|
32
|
+
timeout: timeoutMs,
|
|
33
|
+
});
|
|
34
|
+
return {
|
|
35
|
+
content: [
|
|
36
|
+
{
|
|
37
|
+
type: 'text',
|
|
38
|
+
text: JSON.stringify({
|
|
39
|
+
playDate,
|
|
40
|
+
filters: {
|
|
41
|
+
theaterId: targetTheaterId || null,
|
|
42
|
+
movieId: movieId || null,
|
|
43
|
+
keyword: keyword || null,
|
|
44
|
+
latitude: latitude ?? null,
|
|
45
|
+
longitude: longitude ?? null,
|
|
46
|
+
},
|
|
47
|
+
resolvedTheater,
|
|
48
|
+
counts: {
|
|
49
|
+
theaters: theaters.length,
|
|
50
|
+
movies: movies.length,
|
|
51
|
+
showtimes: showtimes.length,
|
|
52
|
+
},
|
|
53
|
+
theaters,
|
|
54
|
+
movies,
|
|
55
|
+
showtimes,
|
|
56
|
+
}, null, 2),
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function createListNowShowingTool(googleMapsApiKey) {
|
|
62
|
+
return {
|
|
63
|
+
name: 'lottecinema_list_now_showing',
|
|
64
|
+
metadata: {
|
|
65
|
+
title: '롯데시네마 영화 목록 조회',
|
|
66
|
+
description: '날짜/지점/영화 조건으로 롯데시네마 영화 및 상영 회차 목록을 조회합니다. 지점 ID가 없으면 위치 키워드 기준 최근접 극장을 선택할 수 있습니다.',
|
|
67
|
+
inputSchema: {
|
|
68
|
+
playDate: z.string().optional().describe('조회 날짜(YYYYMMDD, 기본값: 오늘)'),
|
|
69
|
+
theaterId: z.string().optional().describe('롯데시네마 지점 ID (예: 1016)'),
|
|
70
|
+
movieId: z.string().optional().describe('롯데시네마 대표 영화 코드 (예: 23816)'),
|
|
71
|
+
keyword: z.string().optional().describe('위치 키워드 (예: 안산 중앙역, 잠실역)'),
|
|
72
|
+
latitude: z.number().optional().describe('위도'),
|
|
73
|
+
longitude: z.number().optional().describe('경도'),
|
|
74
|
+
timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
handler: ((args) => listNowShowing(args, googleMapsApiKey)),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 롯데마트 모바일 도와센터 엔드포인트
|
|
3
|
+
*/
|
|
4
|
+
export const LOTTEMART_API = {
|
|
5
|
+
BASE_URL: 'https://company.lottemart.com',
|
|
6
|
+
ORIGIN_HOST: 'company.lottemart.com',
|
|
7
|
+
SOCKET_HOST: '210.93.146.57',
|
|
8
|
+
ORIGIN_REFERER: 'https://company.lottemart.com/mobiledowa/',
|
|
9
|
+
MARKET_OPTIONS_PATH: '/mobiledowa/inc/asp/search_market_list.asp',
|
|
10
|
+
STORE_SEARCH_PATH: '/mobiledowa/market/search_shop.asp',
|
|
11
|
+
PRODUCT_SEARCH_PATH: '/mobiledowa/product/search_product.asp',
|
|
12
|
+
PRODUCT_PAGE_PATH: '/mobiledowa/inc/asp/search_product_list.asp',
|
|
13
|
+
};
|
|
14
|
+
export const LOTTEMART_AREAS = [
|
|
15
|
+
'서울',
|
|
16
|
+
'경기',
|
|
17
|
+
'인천',
|
|
18
|
+
'강원',
|
|
19
|
+
'충북',
|
|
20
|
+
'충남',
|
|
21
|
+
'대전',
|
|
22
|
+
'경북',
|
|
23
|
+
'경남',
|
|
24
|
+
'대구',
|
|
25
|
+
'부산',
|
|
26
|
+
'울산',
|
|
27
|
+
'전북',
|
|
28
|
+
'전남',
|
|
29
|
+
'광주',
|
|
30
|
+
'기타',
|
|
31
|
+
];
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 롯데마트 모바일 도와센터 클라이언트
|
|
3
|
+
*/
|
|
4
|
+
import { LOTTEMART_API } from './api.js';
|
|
5
|
+
import { DEFAULT_LOTTEMART_TIMEOUT_MS } from './config.js';
|
|
6
|
+
import { __testOnlyClearLotteMartGeocodeCache, geocodeLotteMartAddress } from './geocode.js';
|
|
7
|
+
import { __testOnlyClearLotteMartSessionCache, fetchLotteMartHtml, fetchLotteMartPageWithSession, getCachedLotteMartSessionCookie, } from './session.js';
|
|
8
|
+
import { attachDistance, fetchAllStoresForAreaList, fetchKeywordMatchedStores, getTargetAreas, normalizeArea, toDisplayArea, } from './storeSearch.js';
|
|
9
|
+
import { calculateDistanceM, dedupeProducts, matchesBrandVariant, matchesKeyword, parseMarketOptions, parseProductSummary, parseProducts, parseStores, sortStores, } from './parser.js';
|
|
10
|
+
import { createZettaFallbackStore, fetchZettaLotteMartProductsWithPrimaryError } from './zetta.js';
|
|
11
|
+
const STORE_CACHE_TTL_MS = 30 * 60 * 1000;
|
|
12
|
+
const LOTTEMART_PRODUCT_LEGACY_TIMEOUT_MS = 7000;
|
|
13
|
+
const storeCache = new Map();
|
|
14
|
+
async function getLotteMartSessionCookie(options) {
|
|
15
|
+
if (options.sessionCookie) {
|
|
16
|
+
return options.sessionCookie;
|
|
17
|
+
}
|
|
18
|
+
return getCachedLotteMartSessionCookie(options.timeout || DEFAULT_LOTTEMART_TIMEOUT_MS);
|
|
19
|
+
}
|
|
20
|
+
export async function fetchLotteMartMarketOptions(area, type, options = {}) {
|
|
21
|
+
const normalizedArea = normalizeArea(area);
|
|
22
|
+
if (!normalizedArea) {
|
|
23
|
+
throw new Error(`지원하지 않는 지역입니다: ${area}`);
|
|
24
|
+
}
|
|
25
|
+
const endpoint = new URL(LOTTEMART_API.MARKET_OPTIONS_PATH, LOTTEMART_API.BASE_URL);
|
|
26
|
+
endpoint.searchParams.set('p_area', normalizedArea);
|
|
27
|
+
endpoint.searchParams.set('p_type', type);
|
|
28
|
+
const html = await fetchLotteMartHtml(endpoint.toString(), {
|
|
29
|
+
method: 'GET',
|
|
30
|
+
headers: {
|
|
31
|
+
Accept: 'text/html, */*; q=0.01',
|
|
32
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
33
|
+
},
|
|
34
|
+
}, options.timeout || DEFAULT_LOTTEMART_TIMEOUT_MS, await getLotteMartSessionCookie(options), options.zyteApiKey);
|
|
35
|
+
return parseMarketOptions(toDisplayArea(normalizedArea), html);
|
|
36
|
+
}
|
|
37
|
+
export async function fetchLotteMartStoresByArea(area, options = {}) {
|
|
38
|
+
const normalizedArea = normalizeArea(area);
|
|
39
|
+
if (!normalizedArea) {
|
|
40
|
+
throw new Error(`지원하지 않는 지역입니다: ${area}`);
|
|
41
|
+
}
|
|
42
|
+
const cacheKey = normalizedArea;
|
|
43
|
+
const cached = storeCache.get(cacheKey);
|
|
44
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
45
|
+
return cached.stores;
|
|
46
|
+
}
|
|
47
|
+
const body = new URLSearchParams();
|
|
48
|
+
body.set('m_area', normalizedArea);
|
|
49
|
+
const html = await fetchLotteMartPageWithSession(LOTTEMART_API.STORE_SEARCH_PATH, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: {
|
|
52
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
53
|
+
},
|
|
54
|
+
body: body.toString(),
|
|
55
|
+
}, options.timeout || DEFAULT_LOTTEMART_TIMEOUT_MS, await getLotteMartSessionCookie(options), options.zyteApiKey);
|
|
56
|
+
const stores = parseStores(toDisplayArea(normalizedArea), html);
|
|
57
|
+
storeCache.set(cacheKey, {
|
|
58
|
+
expiresAt: Date.now() + STORE_CACHE_TTL_MS,
|
|
59
|
+
stores,
|
|
60
|
+
});
|
|
61
|
+
return stores;
|
|
62
|
+
}
|
|
63
|
+
async function fetchLotteMartStoresByAreaKeyword(area, keyword, options) {
|
|
64
|
+
const normalizedArea = normalizeArea(area);
|
|
65
|
+
const normalizedKeyword = keyword.trim();
|
|
66
|
+
const cacheKey = `${normalizedArea}:${normalizedKeyword}`;
|
|
67
|
+
const cached = storeCache.get(cacheKey);
|
|
68
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
69
|
+
return cached.stores;
|
|
70
|
+
}
|
|
71
|
+
const body = new URLSearchParams();
|
|
72
|
+
body.set('m_area', normalizedArea);
|
|
73
|
+
body.set('m_schWord', normalizedKeyword);
|
|
74
|
+
const html = await fetchLotteMartPageWithSession(LOTTEMART_API.STORE_SEARCH_PATH, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: {
|
|
77
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
78
|
+
},
|
|
79
|
+
body: body.toString(),
|
|
80
|
+
}, options.timeout, await getLotteMartSessionCookie(options), options.zyteApiKey);
|
|
81
|
+
const stores = parseStores(toDisplayArea(normalizedArea), html);
|
|
82
|
+
storeCache.set(cacheKey, {
|
|
83
|
+
expiresAt: Date.now() + STORE_CACHE_TTL_MS,
|
|
84
|
+
stores,
|
|
85
|
+
});
|
|
86
|
+
return stores;
|
|
87
|
+
}
|
|
88
|
+
export async function fetchLotteMartStores(params = {}, options = {}) {
|
|
89
|
+
const { area, keyword = '', brandVariant = '', latitude, longitude, limit = 20 } = params;
|
|
90
|
+
const { timeout = DEFAULT_LOTTEMART_TIMEOUT_MS, googleMapsApiKey } = options;
|
|
91
|
+
if (area && !normalizeArea(area)) {
|
|
92
|
+
throw new Error(`지원하지 않는 지역입니다: ${area}`);
|
|
93
|
+
}
|
|
94
|
+
let resolvedLatitude = latitude;
|
|
95
|
+
let resolvedLongitude = longitude;
|
|
96
|
+
let geocodeUsed = false;
|
|
97
|
+
if ((typeof resolvedLatitude !== 'number' || typeof resolvedLongitude !== 'number') &&
|
|
98
|
+
keyword.trim().length > 0) {
|
|
99
|
+
const geocoded = await geocodeLotteMartAddress(keyword, { timeout, googleMapsApiKey });
|
|
100
|
+
if (geocoded) {
|
|
101
|
+
resolvedLatitude = geocoded.latitude;
|
|
102
|
+
resolvedLongitude = geocoded.longitude;
|
|
103
|
+
geocodeUsed = true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const sessionCookie = await getLotteMartSessionCookie({ timeout });
|
|
107
|
+
const targetAreas = getTargetAreas(area);
|
|
108
|
+
const hasKeyword = keyword.trim().length > 0;
|
|
109
|
+
const fetchAreaStores = (currentArea) => hasKeyword
|
|
110
|
+
? fetchLotteMartStoresByAreaKeyword(currentArea, keyword, {
|
|
111
|
+
timeout,
|
|
112
|
+
sessionCookie,
|
|
113
|
+
zyteApiKey: options.zyteApiKey,
|
|
114
|
+
})
|
|
115
|
+
: fetchLotteMartStoresByArea(currentArea, { timeout, sessionCookie, zyteApiKey: options.zyteApiKey });
|
|
116
|
+
const keywordMatchedStores = hasKeyword
|
|
117
|
+
? await fetchKeywordMatchedStores(targetAreas, keyword, brandVariant, limit, fetchAreaStores)
|
|
118
|
+
: [];
|
|
119
|
+
const useKeywordMatchedStores = keywordMatchedStores.length > 0 || hasKeyword;
|
|
120
|
+
const stores = useKeywordMatchedStores
|
|
121
|
+
? keywordMatchedStores
|
|
122
|
+
: await fetchAllStoresForAreaList(targetAreas, fetchAreaStores);
|
|
123
|
+
const filtered = attachDistance(useKeywordMatchedStores
|
|
124
|
+
? stores
|
|
125
|
+
: stores
|
|
126
|
+
.filter((store) => matchesKeyword(store, keyword))
|
|
127
|
+
.filter((store) => matchesBrandVariant(store, brandVariant)), resolvedLatitude, resolvedLongitude);
|
|
128
|
+
const enriched = await Promise.all(filtered.map(async (store) => {
|
|
129
|
+
if (typeof resolvedLatitude !== 'number' ||
|
|
130
|
+
typeof resolvedLongitude !== 'number' ||
|
|
131
|
+
store.latitude !== 0 ||
|
|
132
|
+
store.longitude !== 0) {
|
|
133
|
+
return store;
|
|
134
|
+
}
|
|
135
|
+
const geocoded = await geocodeLotteMartAddress(store.address, { timeout, googleMapsApiKey });
|
|
136
|
+
if (!geocoded) {
|
|
137
|
+
return store;
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
...store,
|
|
141
|
+
latitude: geocoded.latitude,
|
|
142
|
+
longitude: geocoded.longitude,
|
|
143
|
+
distanceM: calculateDistanceM(resolvedLatitude, resolvedLongitude, geocoded.latitude, geocoded.longitude),
|
|
144
|
+
};
|
|
145
|
+
}));
|
|
146
|
+
return {
|
|
147
|
+
stores: sortStores(enriched).slice(0, limit),
|
|
148
|
+
location: typeof resolvedLatitude === 'number' && typeof resolvedLongitude === 'number'
|
|
149
|
+
? { latitude: resolvedLatitude, longitude: resolvedLongitude }
|
|
150
|
+
: null,
|
|
151
|
+
geocodeUsed,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
export async function resolveLotteMartStore(area, storeCode, storeName, options = {}) {
|
|
155
|
+
const normalizedStoreCode = (storeCode || '').trim();
|
|
156
|
+
if (normalizedStoreCode.length > 0) {
|
|
157
|
+
for (const currentArea of getTargetAreas(area)) {
|
|
158
|
+
const matched = (await fetchLotteMartMarketOptions(currentArea, '1', options)).find((item) => item.storeCode === normalizedStoreCode);
|
|
159
|
+
if (matched) {
|
|
160
|
+
return matched;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
const normalizedStoreName = (storeName || '').trim().toLowerCase();
|
|
166
|
+
if (normalizedStoreName.length === 0) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
const allOptions = (await Promise.all(getTargetAreas(area).map((currentArea) => fetchLotteMartMarketOptions(currentArea, '1', options)))).flat();
|
|
170
|
+
return (allOptions.find((item) => item.storeName.toLowerCase() === normalizedStoreName) ||
|
|
171
|
+
allOptions.find((item) => item.storeName.toLowerCase().includes(normalizedStoreName)) ||
|
|
172
|
+
null);
|
|
173
|
+
}
|
|
174
|
+
export async function searchLotteMartProducts(params, options = {}) {
|
|
175
|
+
const normalizedKeyword = params.keyword.trim();
|
|
176
|
+
if (normalizedKeyword.length === 0) {
|
|
177
|
+
throw new Error('상품 검색어(keyword)를 입력해주세요.');
|
|
178
|
+
}
|
|
179
|
+
const timeout = options.timeout || DEFAULT_LOTTEMART_TIMEOUT_MS;
|
|
180
|
+
const sessionCookie = await getLotteMartSessionCookie({ timeout: LOTTEMART_PRODUCT_LEGACY_TIMEOUT_MS });
|
|
181
|
+
let resolvedStore;
|
|
182
|
+
try {
|
|
183
|
+
resolvedStore = await resolveLotteMartStore(params.area, params.storeCode, params.storeName, {
|
|
184
|
+
timeout: LOTTEMART_PRODUCT_LEGACY_TIMEOUT_MS,
|
|
185
|
+
sessionCookie,
|
|
186
|
+
zyteApiKey: options.zyteApiKey,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
const fallbackStore = createZettaFallbackStore(params);
|
|
191
|
+
return fetchZettaLotteMartProductsWithPrimaryError(fallbackStore, normalizedKeyword, params.pageLimit || 3, timeout, error);
|
|
192
|
+
}
|
|
193
|
+
if (!resolvedStore) {
|
|
194
|
+
throw new Error('검색할 롯데마트 매장을 찾지 못했습니다. area와 storeCode/storeName을 확인해주세요.');
|
|
195
|
+
}
|
|
196
|
+
const initialBody = new URLSearchParams();
|
|
197
|
+
initialBody.set('p_area', normalizeArea(resolvedStore.area));
|
|
198
|
+
initialBody.set('p_market', resolvedStore.storeCode);
|
|
199
|
+
initialBody.set('p_schWord', normalizedKeyword);
|
|
200
|
+
let initialHtml;
|
|
201
|
+
try {
|
|
202
|
+
initialHtml = await fetchLotteMartPageWithSession(LOTTEMART_API.PRODUCT_SEARCH_PATH, {
|
|
203
|
+
method: 'POST',
|
|
204
|
+
headers: {
|
|
205
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
206
|
+
},
|
|
207
|
+
body: initialBody.toString(),
|
|
208
|
+
}, LOTTEMART_PRODUCT_LEGACY_TIMEOUT_MS, sessionCookie, options.zyteApiKey);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
return fetchZettaLotteMartProductsWithPrimaryError(resolvedStore, normalizedKeyword, params.pageLimit || 3, timeout, error);
|
|
212
|
+
}
|
|
213
|
+
const pagedSessionCookie = await getCachedLotteMartSessionCookie(LOTTEMART_PRODUCT_LEGACY_TIMEOUT_MS);
|
|
214
|
+
const summary = parseProductSummary(initialHtml);
|
|
215
|
+
const firstPageProducts = parseProducts(resolvedStore.area, resolvedStore.storeCode, resolvedStore.storeName, normalizedKeyword, initialHtml);
|
|
216
|
+
const maxPage = Math.min(Math.max(params.pageLimit || 3, 1), Math.max(summary.totalPages, 1));
|
|
217
|
+
const pageHtmlList = await Promise.all(Array.from({ length: Math.max(maxPage - 1, 0) }, (_, index) => index + 2).map((page) => {
|
|
218
|
+
const endpoint = new URL(LOTTEMART_API.PRODUCT_PAGE_PATH, LOTTEMART_API.BASE_URL);
|
|
219
|
+
endpoint.searchParams.set('p_market', resolvedStore.storeCode);
|
|
220
|
+
endpoint.searchParams.set('p_schWord', normalizedKeyword);
|
|
221
|
+
endpoint.searchParams.set('page', String(page));
|
|
222
|
+
return fetchLotteMartHtml(endpoint.toString(), {
|
|
223
|
+
method: 'GET',
|
|
224
|
+
headers: {
|
|
225
|
+
Accept: 'text/html, */*; q=0.01',
|
|
226
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
227
|
+
},
|
|
228
|
+
}, LOTTEMART_PRODUCT_LEGACY_TIMEOUT_MS, pagedSessionCookie, options.zyteApiKey);
|
|
229
|
+
}));
|
|
230
|
+
const pagedProducts = pageHtmlList.flatMap((html) => parseProducts(resolvedStore.area, resolvedStore.storeCode, resolvedStore.storeName, normalizedKeyword, html));
|
|
231
|
+
return {
|
|
232
|
+
area: resolvedStore.area,
|
|
233
|
+
storeCode: resolvedStore.storeCode,
|
|
234
|
+
storeName: resolvedStore.storeName,
|
|
235
|
+
totalCount: summary.totalCount,
|
|
236
|
+
totalPages: summary.totalPages,
|
|
237
|
+
products: dedupeProducts([...firstPageProducts, ...pagedProducts]),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
export { calculateDistanceM, geocodeLotteMartAddress };
|
|
241
|
+
export function __testOnlyClearLotteMartCaches() {
|
|
242
|
+
storeCache.clear();
|
|
243
|
+
__testOnlyClearLotteMartGeocodeCache();
|
|
244
|
+
__testOnlyClearLotteMartSessionCache();
|
|
245
|
+
}
|