daiso 1.2.7 → 1.2.9
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 +17 -17
- package/dist/api/actionsProxy.js +0 -10
- package/dist/api/cgvHandlers.js +4 -3
- package/dist/api/configStatus.js +7 -2
- package/dist/api/gs25Handlers.js +4 -3
- package/dist/api/handlers.js +11 -7
- package/dist/api/healthCheckDefinitions.js +0 -9
- package/dist/api/healthChecks.js +0 -1
- package/dist/api/lottecinemaHandlers.js +4 -3
- package/dist/api/lottemartHandlers.js +5 -142
- package/dist/api/megaboxHandlers.js +4 -3
- package/dist/api/opinetHandlers.js +2 -1
- package/dist/api/routes/cgvRoutes.js +3 -3
- package/dist/api/routes/cuRoutes.js +2 -2
- package/dist/api/routes/gs25Routes.js +3 -3
- package/dist/api/routes/lottecinemaRoutes.js +3 -3
- package/dist/api/routes/lottemartRoutes.js +4 -16
- package/dist/api/routes/megaboxRoutes.js +3 -3
- package/dist/api/routes/opinetRoutes.js +4 -4
- package/dist/cli.js +5 -5
- package/dist/cliHelp.js +0 -3
- package/dist/cliHelpDefinitions.js +2 -19
- package/dist/index.js +10 -11
- package/dist/middleware/dailyRateLimit.js +0 -1
- package/dist/pages/openapiFullSpec.js +0 -2
- package/dist/pages/openapiSpecActionParameters.js +4 -4
- package/dist/pages/openapiSpecComponents.js +0 -2
- package/dist/pages/openapiSpecPathsMegabox.js +1 -1
- package/dist/pages/promptReferenceText.js +0 -5
- package/dist/pages/promptRetailCinemaText.js +0 -26
- package/dist/services/cgv/index.js +3 -6
- package/dist/services/cgv/location.js +38 -125
- package/dist/services/cgv/tools/findTheaters.js +5 -4
- package/dist/services/cgv/tools/getTimetable.js +5 -4
- package/dist/services/cgv/tools/searchMovies.js +5 -4
- package/dist/services/cu/client.js +3 -35
- package/dist/services/cu/index.js +1 -0
- package/dist/services/cu/tools/checkInventory.js +3 -1
- package/dist/services/gs25/client.js +3 -33
- package/dist/services/gs25/index.js +2 -2
- package/dist/services/gs25/tools/checkInventory.js +3 -1
- package/dist/services/gs25/tools/findNearbyStores.js +3 -1
- package/dist/services/lottecinema/index.js +3 -6
- package/dist/services/lottecinema/location.js +13 -100
- package/dist/services/lottecinema/tools/findNearbyTheaters.js +7 -6
- package/dist/services/lottecinema/tools/getRemainingSeats.js +5 -4
- package/dist/services/lottecinema/tools/listNowShowing.js +5 -4
- package/dist/services/lottemart/client.js +5 -32
- package/dist/services/lottemart/geocode.js +3 -46
- package/dist/services/megabox/index.js +9 -8
- package/dist/services/megabox/location.js +19 -84
- package/dist/services/megabox/tools/findNearbyTheaters.js +7 -7
- package/dist/services/megabox/tools/getRemainingSeats.js +5 -5
- package/dist/services/megabox/tools/listNowShowing.js +5 -5
- package/dist/services/oliveyoung/tools/checkInventory.js +5 -5
- package/dist/services/oliveyoung/tools/findNearbyStores.js +3 -3
- package/dist/services/oliveyoung/tools/searchProducts.js +3 -3
- package/dist/services/oliveyoung/transport.js +49 -33
- package/dist/services/opinet/client.js +1 -1
- package/dist/services/opinet/index.js +1 -1
- package/dist/services/opinet/location.js +5 -56
- package/dist/services/opinet/tools.js +2 -2
- package/dist/utils/geocode.js +155 -0
- package/dist/utils/http.js +2 -2
- package/dist/utils/relayQuota.js +4 -2
- package/openapi.json +4 -183
- package/openapi.yaml +4 -131
- package/package.json +1 -2
- package/dist/pages/openapiSpecComponentsLotteMart.js +0 -85
- package/dist/pages/openapiSpecPathsLotteMart.js +0 -157
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import * as z from 'zod';
|
|
5
5
|
import { fetchCgvMovies, toYyyymmdd } from '../client.js';
|
|
6
6
|
import { resolveCgvNearestTheater } from '../location.js';
|
|
7
|
-
async function searchMovies(args, apiKey, googleMapsApiKey) {
|
|
7
|
+
async function searchMovies(args, apiKey, googleMapsApiKey, geocodeOptions = {}) {
|
|
8
8
|
const { playDate = toYyyymmdd(), theaterCode, keyword, latitude, longitude, timeoutMs = 15000 } = args;
|
|
9
9
|
let resolvedTheater = null;
|
|
10
10
|
let targetTheaterCode = theaterCode;
|
|
@@ -18,7 +18,8 @@ async function searchMovies(args, apiKey, googleMapsApiKey) {
|
|
|
18
18
|
}, {
|
|
19
19
|
timeout: timeoutMs,
|
|
20
20
|
zyteApiKey: apiKey,
|
|
21
|
-
|
|
21
|
+
...geocodeOptions,
|
|
22
|
+
googleMapsApiKey,
|
|
22
23
|
});
|
|
23
24
|
resolvedTheater = resolved.theater;
|
|
24
25
|
targetTheaterCode = resolved.theater?.theaterCode;
|
|
@@ -48,7 +49,7 @@ async function searchMovies(args, apiKey, googleMapsApiKey) {
|
|
|
48
49
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
49
50
|
};
|
|
50
51
|
}
|
|
51
|
-
export function createSearchMoviesTool(apiKey, googleMapsApiKey) {
|
|
52
|
+
export function createSearchMoviesTool(apiKey, googleMapsApiKey, geocodeOptions = {}) {
|
|
52
53
|
return {
|
|
53
54
|
name: 'cgv_search_movies',
|
|
54
55
|
metadata: {
|
|
@@ -63,6 +64,6 @@ export function createSearchMoviesTool(apiKey, googleMapsApiKey) {
|
|
|
63
64
|
timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
|
|
64
65
|
},
|
|
65
66
|
},
|
|
66
|
-
handler: ((args) => searchMovies(args, apiKey, googleMapsApiKey)),
|
|
67
|
+
handler: ((args) => searchMovies(args, apiKey, googleMapsApiKey, geocodeOptions)),
|
|
67
68
|
};
|
|
68
69
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { geocodeLocation } from '../../utils/geocode.js';
|
|
1
2
|
/**
|
|
2
3
|
* CU API 클라이언트
|
|
3
4
|
*/
|
|
@@ -74,41 +75,8 @@ async function requestCuWebHtml(path, body, _apiKey, timeout = 15000) {
|
|
|
74
75
|
throw new Error(`API 요청 실패: ${response.status} ${response.statusText}`);
|
|
75
76
|
}
|
|
76
77
|
export async function geocodeCuAddress(address, options = {}) {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
const { timeout = 15000, googleMapsApiKey } = options;
|
|
82
|
-
const apiKey = (googleMapsApiKey || '').trim();
|
|
83
|
-
if (apiKey.length === 0) {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
const endpoint = new URL('https://maps.googleapis.com/maps/api/geocode/json');
|
|
87
|
-
endpoint.searchParams.set('address', keyword);
|
|
88
|
-
endpoint.searchParams.set('key', apiKey);
|
|
89
|
-
const body = await fetchJson(endpoint.toString(), {
|
|
90
|
-
method: 'GET',
|
|
91
|
-
timeout,
|
|
92
|
-
headers: {
|
|
93
|
-
Accept: 'application/json',
|
|
94
|
-
},
|
|
95
|
-
});
|
|
96
|
-
if (body.status !== 'OK') {
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
const firstLocation = body.results?.[0]?.geometry?.location;
|
|
100
|
-
if (!firstLocation) {
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
const latitude = toNumber(firstLocation.lat);
|
|
104
|
-
const longitude = toNumber(firstLocation.lng);
|
|
105
|
-
if (latitude === 0 || longitude === 0) {
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
return {
|
|
109
|
-
latitude,
|
|
110
|
-
longitude,
|
|
111
|
-
};
|
|
78
|
+
const location = await geocodeLocation(address, options);
|
|
79
|
+
return location ? { latitude: location.latitude, longitude: location.longitude } : null;
|
|
112
80
|
}
|
|
113
81
|
function decodeHtmlEntities(value) {
|
|
114
82
|
return value
|
|
@@ -36,7 +36,8 @@ async function checkInventory(args) {
|
|
|
36
36
|
if (firstAddress.length > 0) {
|
|
37
37
|
const geocoded = await geocodeCuAddress(firstAddress, {
|
|
38
38
|
timeout: timeoutMs,
|
|
39
|
-
|
|
39
|
+
...args,
|
|
40
|
+
googleMapsApiKey,
|
|
40
41
|
});
|
|
41
42
|
if (geocoded) {
|
|
42
43
|
const hasStockSeed = !!firstStockItem?.itemCode;
|
|
@@ -136,6 +137,7 @@ export function createCheckInventoryTool(options = {}) {
|
|
|
136
137
|
},
|
|
137
138
|
handler: ((args) => checkInventory({
|
|
138
139
|
...args,
|
|
140
|
+
...options,
|
|
139
141
|
zyteApiKey: options.zyteApiKey ?? args.zyteApiKey,
|
|
140
142
|
googleMapsApiKey: options.googleMapsApiKey ?? args.googleMapsApiKey,
|
|
141
143
|
})),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { geocodeLocation } from '../../utils/geocode.js';
|
|
1
2
|
/**
|
|
2
3
|
* GS25 API 클라이언트
|
|
3
4
|
*/
|
|
@@ -57,39 +58,8 @@ function buildCacheKey(params) {
|
|
|
57
58
|
].join(':');
|
|
58
59
|
}
|
|
59
60
|
export async function geocodeGs25Address(address, options = {}) {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
const { timeout = 15000, googleMapsApiKey } = options;
|
|
65
|
-
const apiKey = (googleMapsApiKey || '').trim();
|
|
66
|
-
if (apiKey.length === 0) {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
const endpoint = new URL('https://maps.googleapis.com/maps/api/geocode/json');
|
|
70
|
-
endpoint.searchParams.set('address', keyword);
|
|
71
|
-
endpoint.searchParams.set('key', apiKey);
|
|
72
|
-
const body = await fetchJson(endpoint.toString(), {
|
|
73
|
-
...GS25_DEFAULT_FETCH_OPTIONS,
|
|
74
|
-
method: 'GET',
|
|
75
|
-
timeout,
|
|
76
|
-
headers: {
|
|
77
|
-
Accept: 'application/json',
|
|
78
|
-
},
|
|
79
|
-
});
|
|
80
|
-
if (body.status !== 'OK') {
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
const location = body.results?.[0]?.geometry?.location;
|
|
84
|
-
if (!location) {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
const latitude = toNumber(location.lat);
|
|
88
|
-
const longitude = toNumber(location.lng);
|
|
89
|
-
if (latitude === 0 || longitude === 0) {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
return { latitude, longitude };
|
|
61
|
+
const location = await geocodeLocation(address, options);
|
|
62
|
+
return location ? { latitude: location.latitude, longitude: location.longitude } : null;
|
|
93
63
|
}
|
|
94
64
|
export async function fetchGs25WebStores(keyword, options = {}) {
|
|
95
65
|
const query = keyword.trim();
|
|
@@ -18,9 +18,9 @@ class Gs25Service {
|
|
|
18
18
|
}
|
|
19
19
|
getTools() {
|
|
20
20
|
return [
|
|
21
|
-
createFindNearbyStoresTool(this.options.googleMapsApiKey, this.options.zyteApiKey, this.options.apiKey),
|
|
21
|
+
createFindNearbyStoresTool(this.options.googleMapsApiKey, this.options.zyteApiKey, this.options.apiKey, this.options),
|
|
22
22
|
createSearchProductsTool(this.options.zyteApiKey),
|
|
23
|
-
createCheckInventoryTool(this.options.googleMapsApiKey, this.options.zyteApiKey, this.options.apiKey),
|
|
23
|
+
createCheckInventoryTool(this.options.googleMapsApiKey, this.options.zyteApiKey, this.options.apiKey, this.options),
|
|
24
24
|
];
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -31,6 +31,7 @@ async function checkInventory(args) {
|
|
|
31
31
|
if (firstAddress.length > 0) {
|
|
32
32
|
const geocoded = await geocodeGs25Address(firstAddress, {
|
|
33
33
|
timeout: timeoutMs,
|
|
34
|
+
...args,
|
|
34
35
|
googleMapsApiKey,
|
|
35
36
|
});
|
|
36
37
|
if (geocoded) {
|
|
@@ -151,7 +152,7 @@ async function checkInventory(args) {
|
|
|
151
152
|
],
|
|
152
153
|
};
|
|
153
154
|
}
|
|
154
|
-
export function createCheckInventoryTool(googleMapsApiKey, zyteApiKey, apiKey) {
|
|
155
|
+
export function createCheckInventoryTool(googleMapsApiKey, zyteApiKey, apiKey, geocodeOptions = {}) {
|
|
155
156
|
return {
|
|
156
157
|
name: 'gs25_check_inventory',
|
|
157
158
|
metadata: {
|
|
@@ -186,6 +187,7 @@ export function createCheckInventoryTool(googleMapsApiKey, zyteApiKey, apiKey) {
|
|
|
186
187
|
},
|
|
187
188
|
handler: ((args) => checkInventory({
|
|
188
189
|
...args,
|
|
190
|
+
...geocodeOptions,
|
|
189
191
|
googleMapsApiKey: args.googleMapsApiKey || googleMapsApiKey,
|
|
190
192
|
zyteApiKey: args.zyteApiKey || zyteApiKey,
|
|
191
193
|
apiKey: args.apiKey || apiKey,
|
|
@@ -18,6 +18,7 @@ async function findNearbyStores(args) {
|
|
|
18
18
|
keyword.trim().length > 0) {
|
|
19
19
|
const geocoded = await geocodeGs25Address(keyword, {
|
|
20
20
|
timeout: timeoutMs,
|
|
21
|
+
...args,
|
|
21
22
|
googleMapsApiKey,
|
|
22
23
|
});
|
|
23
24
|
if (geocoded) {
|
|
@@ -103,7 +104,7 @@ async function findNearbyStores(args) {
|
|
|
103
104
|
],
|
|
104
105
|
};
|
|
105
106
|
}
|
|
106
|
-
export function createFindNearbyStoresTool(googleMapsApiKey, zyteApiKey, apiKey) {
|
|
107
|
+
export function createFindNearbyStoresTool(googleMapsApiKey, zyteApiKey, apiKey, geocodeOptions = {}) {
|
|
107
108
|
return {
|
|
108
109
|
name: 'gs25_find_nearby_stores',
|
|
109
110
|
metadata: {
|
|
@@ -124,6 +125,7 @@ export function createFindNearbyStoresTool(googleMapsApiKey, zyteApiKey, apiKey)
|
|
|
124
125
|
},
|
|
125
126
|
handler: ((args) => findNearbyStores({
|
|
126
127
|
...args,
|
|
128
|
+
...geocodeOptions,
|
|
127
129
|
googleMapsApiKey: args.googleMapsApiKey || googleMapsApiKey,
|
|
128
130
|
zyteApiKey: args.zyteApiKey || zyteApiKey,
|
|
129
131
|
apiKey: args.apiKey || apiKey,
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 롯데시네마 서비스 프로바이더
|
|
3
|
-
*/
|
|
4
1
|
import { createFindNearbyTheatersTool } from './tools/findNearbyTheaters.js';
|
|
5
2
|
import { createGetRemainingSeatsTool } from './tools/getRemainingSeats.js';
|
|
6
3
|
import { createListNowShowingTool } from './tools/listNowShowing.js';
|
|
@@ -18,9 +15,9 @@ class LotteCinemaService {
|
|
|
18
15
|
metadata = LOTTECINEMA_METADATA;
|
|
19
16
|
getTools() {
|
|
20
17
|
return [
|
|
21
|
-
createFindNearbyTheatersTool(this.options.googleMapsApiKey),
|
|
22
|
-
createListNowShowingTool(this.options.googleMapsApiKey),
|
|
23
|
-
createGetRemainingSeatsTool(this.options.googleMapsApiKey),
|
|
18
|
+
createFindNearbyTheatersTool(this.options.googleMapsApiKey, this.options),
|
|
19
|
+
createListNowShowingTool(this.options.googleMapsApiKey, this.options),
|
|
20
|
+
createGetRemainingSeatsTool(this.options.googleMapsApiKey, this.options),
|
|
24
21
|
];
|
|
25
22
|
}
|
|
26
23
|
}
|
|
@@ -1,26 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 롯데시네마 위치 해석 및 근처 극장 조회 보조 모듈
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { geocodeLocation, validKoreanCoordinates } from '../../utils/geocode.js';
|
|
5
5
|
import { fetchLotteCinemaTicketingPage } from './client.js';
|
|
6
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
7
|
function calculateDistanceKm(lat1, lng1, lat2, lng2) {
|
|
25
8
|
const toRad = (deg) => (deg * Math.PI) / 180;
|
|
26
9
|
const dLat = toRad(lat2 - lat1);
|
|
@@ -33,77 +16,17 @@ function calculateDistanceKm(lat1, lng1, lat2, lng2) {
|
|
|
33
16
|
function isValidCoordinate(value) {
|
|
34
17
|
return typeof value === 'number' && Number.isFinite(value);
|
|
35
18
|
}
|
|
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
19
|
async function geocodeAddress(query, options) {
|
|
70
|
-
const apiKey = (options.googleMapsApiKey || '').trim();
|
|
71
20
|
const trimmedQuery = query.trim();
|
|
72
|
-
|
|
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
|
-
});
|
|
21
|
+
const value = await geocodeLocation(trimmedQuery, options);
|
|
102
22
|
return value;
|
|
103
23
|
}
|
|
104
24
|
export async function resolveLotteCinemaLocation(params, options = {}) {
|
|
105
25
|
const keyword = (params.keyword || '').trim();
|
|
106
26
|
const hasCoordinates = isValidCoordinate(params.latitude) && isValidCoordinate(params.longitude);
|
|
27
|
+
if (hasCoordinates &&
|
|
28
|
+
!validKoreanCoordinates(params.latitude, params.longitude))
|
|
29
|
+
throw new Error('유효한 위도/경도를 입력해주세요.');
|
|
107
30
|
if (hasCoordinates) {
|
|
108
31
|
return {
|
|
109
32
|
keyword: keyword.length > 0 ? keyword : null,
|
|
@@ -123,12 +46,14 @@ export async function resolveLotteCinemaLocation(params, options = {}) {
|
|
|
123
46
|
};
|
|
124
47
|
}
|
|
125
48
|
const geocoded = await geocodeAddress(keyword, options);
|
|
49
|
+
if (!geocoded)
|
|
50
|
+
throw new Error(`위치를 좌표로 변환하지 못했습니다: ${keyword}`);
|
|
126
51
|
return {
|
|
127
52
|
keyword,
|
|
128
|
-
latitude: geocoded
|
|
129
|
-
longitude: geocoded
|
|
130
|
-
geocodeUsed:
|
|
131
|
-
formattedAddress: geocoded
|
|
53
|
+
latitude: geocoded.latitude,
|
|
54
|
+
longitude: geocoded.longitude,
|
|
55
|
+
geocodeUsed: true,
|
|
56
|
+
formattedAddress: geocoded.formattedAddress,
|
|
132
57
|
};
|
|
133
58
|
}
|
|
134
59
|
export async function fetchLotteCinemaNearbyTheaters(params, options = {}) {
|
|
@@ -148,19 +73,7 @@ export async function fetchLotteCinemaNearbyTheaters(params, options = {}) {
|
|
|
148
73
|
.slice(0, limit);
|
|
149
74
|
}
|
|
150
75
|
else {
|
|
151
|
-
|
|
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
|
-
}));
|
|
76
|
+
nearby = [];
|
|
164
77
|
}
|
|
165
78
|
return {
|
|
166
79
|
...location,
|
|
@@ -187,5 +100,5 @@ export async function resolveLotteCinemaNearestTheater(params, options = {}) {
|
|
|
187
100
|
};
|
|
188
101
|
}
|
|
189
102
|
export function __testOnlyClearLotteCinemaLocationCaches() {
|
|
190
|
-
|
|
103
|
+
// 캐시를 유지하지 않으므로 초기화할 상태가 없습니다.
|
|
191
104
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import * as z from 'zod';
|
|
5
5
|
import { toYyyymmdd } from '../client.js';
|
|
6
6
|
import { fetchLotteCinemaNearbyTheaters } from '../location.js';
|
|
7
|
-
async function findNearbyTheaters(args, googleMapsApiKey) {
|
|
7
|
+
async function findNearbyTheaters(args, googleMapsApiKey, geocodeOptions = {}) {
|
|
8
8
|
const { keyword, latitude, longitude, playDate = toYyyymmdd(), limit = 10, timeoutMs = 15000 } = args;
|
|
9
9
|
const hasCoordinates = typeof latitude === 'number' && typeof longitude === 'number';
|
|
10
10
|
const nearby = await fetchLotteCinemaNearbyTheaters({
|
|
@@ -16,7 +16,8 @@ async function findNearbyTheaters(args, googleMapsApiKey) {
|
|
|
16
16
|
timeout: timeoutMs,
|
|
17
17
|
}, {
|
|
18
18
|
timeout: timeoutMs,
|
|
19
|
-
|
|
19
|
+
...geocodeOptions,
|
|
20
|
+
googleMapsApiKey,
|
|
20
21
|
});
|
|
21
22
|
return {
|
|
22
23
|
content: [
|
|
@@ -27,7 +28,7 @@ async function findNearbyTheaters(args, googleMapsApiKey) {
|
|
|
27
28
|
],
|
|
28
29
|
};
|
|
29
30
|
}
|
|
30
|
-
export function createFindNearbyTheatersTool(googleMapsApiKey) {
|
|
31
|
+
export function createFindNearbyTheatersTool(googleMapsApiKey, geocodeOptions = {}) {
|
|
31
32
|
return {
|
|
32
33
|
name: 'lottecinema_find_nearby_theaters',
|
|
33
34
|
metadata: {
|
|
@@ -35,13 +36,13 @@ export function createFindNearbyTheatersTool(googleMapsApiKey) {
|
|
|
35
36
|
description: '위치 키워드 또는 좌표 기준으로 롯데시네마 지점을 거리순으로 조회합니다.',
|
|
36
37
|
inputSchema: {
|
|
37
38
|
keyword: z.string().optional().describe('위치 키워드 (예: 안산 중앙역, 잠실역)'),
|
|
38
|
-
latitude: z.number().optional().
|
|
39
|
-
longitude: z.number().optional().
|
|
39
|
+
latitude: z.number().optional().describe('위도 (좌표 또는 위치 키워드 입력)'),
|
|
40
|
+
longitude: z.number().optional().describe('경도 (좌표 또는 위치 키워드 입력)'),
|
|
40
41
|
playDate: z.string().optional().describe('조회 날짜(YYYYMMDD, 기본값: 오늘)'),
|
|
41
42
|
limit: z.number().optional().default(10).describe('반환할 최대 지점 수 (기본값: 10)'),
|
|
42
43
|
timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
|
|
43
44
|
},
|
|
44
45
|
},
|
|
45
|
-
handler: ((args) => findNearbyTheaters(args, googleMapsApiKey)),
|
|
46
|
+
handler: ((args) => findNearbyTheaters(args, googleMapsApiKey, geocodeOptions)),
|
|
46
47
|
};
|
|
47
48
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import * as z from 'zod';
|
|
5
5
|
import { fetchLotteCinemaNowShowing, toYyyymmdd } from '../client.js';
|
|
6
6
|
import { resolveLotteCinemaNearestTheater } from '../location.js';
|
|
7
|
-
async function getRemainingSeats(args, googleMapsApiKey) {
|
|
7
|
+
async function getRemainingSeats(args, googleMapsApiKey, geocodeOptions = {}) {
|
|
8
8
|
const { playDate = toYyyymmdd(), theaterId, movieId, keyword, latitude, longitude, limit = 50, timeoutMs = 15000, } = args;
|
|
9
9
|
let resolvedTheater = null;
|
|
10
10
|
let targetTheaterId = theaterId;
|
|
@@ -17,7 +17,8 @@ async function getRemainingSeats(args, googleMapsApiKey) {
|
|
|
17
17
|
timeout: timeoutMs,
|
|
18
18
|
}, {
|
|
19
19
|
timeout: timeoutMs,
|
|
20
|
-
|
|
20
|
+
...geocodeOptions,
|
|
21
|
+
googleMapsApiKey,
|
|
21
22
|
});
|
|
22
23
|
resolvedTheater = resolved.theater;
|
|
23
24
|
targetTheaterId = resolved.theater?.theaterId;
|
|
@@ -63,7 +64,7 @@ async function getRemainingSeats(args, googleMapsApiKey) {
|
|
|
63
64
|
],
|
|
64
65
|
};
|
|
65
66
|
}
|
|
66
|
-
export function createGetRemainingSeatsTool(googleMapsApiKey) {
|
|
67
|
+
export function createGetRemainingSeatsTool(googleMapsApiKey, geocodeOptions = {}) {
|
|
67
68
|
return {
|
|
68
69
|
name: 'lottecinema_get_remaining_seats',
|
|
69
70
|
metadata: {
|
|
@@ -80,6 +81,6 @@ export function createGetRemainingSeatsTool(googleMapsApiKey) {
|
|
|
80
81
|
timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
|
|
81
82
|
},
|
|
82
83
|
},
|
|
83
|
-
handler: ((args) => getRemainingSeats(args, googleMapsApiKey)),
|
|
84
|
+
handler: ((args) => getRemainingSeats(args, googleMapsApiKey, geocodeOptions)),
|
|
84
85
|
};
|
|
85
86
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import * as z from 'zod';
|
|
5
5
|
import { fetchLotteCinemaNowShowing, toYyyymmdd } from '../client.js';
|
|
6
6
|
import { resolveLotteCinemaNearestTheater } from '../location.js';
|
|
7
|
-
async function listNowShowing(args, googleMapsApiKey) {
|
|
7
|
+
async function listNowShowing(args, googleMapsApiKey, geocodeOptions = {}) {
|
|
8
8
|
const { playDate = toYyyymmdd(), theaterId, movieId, keyword, latitude, longitude, timeoutMs = 15000 } = args;
|
|
9
9
|
let resolvedTheater = null;
|
|
10
10
|
let targetTheaterId = theaterId;
|
|
@@ -17,7 +17,8 @@ async function listNowShowing(args, googleMapsApiKey) {
|
|
|
17
17
|
timeout: timeoutMs,
|
|
18
18
|
}, {
|
|
19
19
|
timeout: timeoutMs,
|
|
20
|
-
|
|
20
|
+
...geocodeOptions,
|
|
21
|
+
googleMapsApiKey,
|
|
21
22
|
});
|
|
22
23
|
resolvedTheater = resolved.theater;
|
|
23
24
|
targetTheaterId = resolved.theater?.theaterId;
|
|
@@ -58,7 +59,7 @@ async function listNowShowing(args, googleMapsApiKey) {
|
|
|
58
59
|
],
|
|
59
60
|
};
|
|
60
61
|
}
|
|
61
|
-
export function createListNowShowingTool(googleMapsApiKey) {
|
|
62
|
+
export function createListNowShowingTool(googleMapsApiKey, geocodeOptions = {}) {
|
|
62
63
|
return {
|
|
63
64
|
name: 'lottecinema_list_now_showing',
|
|
64
65
|
metadata: {
|
|
@@ -74,6 +75,6 @@ export function createListNowShowingTool(googleMapsApiKey) {
|
|
|
74
75
|
timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
|
|
75
76
|
},
|
|
76
77
|
},
|
|
77
|
-
handler: ((args) => listNowShowing(args, googleMapsApiKey)),
|
|
78
|
+
handler: ((args) => listNowShowing(args, googleMapsApiKey, geocodeOptions)),
|
|
78
79
|
};
|
|
79
80
|
}
|
|
@@ -87,22 +87,13 @@ async function fetchLotteMartStoresByAreaKeyword(area, keyword, options) {
|
|
|
87
87
|
}
|
|
88
88
|
export async function fetchLotteMartStores(params = {}, options = {}) {
|
|
89
89
|
const { area, keyword = '', brandVariant = '', latitude, longitude, limit = 20 } = params;
|
|
90
|
-
const { timeout = DEFAULT_LOTTEMART_TIMEOUT_MS
|
|
90
|
+
const { timeout = DEFAULT_LOTTEMART_TIMEOUT_MS } = options;
|
|
91
91
|
if (area && !normalizeArea(area)) {
|
|
92
92
|
throw new Error(`지원하지 않는 지역입니다: ${area}`);
|
|
93
93
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
}
|
|
94
|
+
const resolvedLatitude = latitude;
|
|
95
|
+
const resolvedLongitude = longitude;
|
|
96
|
+
const geocodeUsed = false;
|
|
106
97
|
const sessionCookie = await getLotteMartSessionCookie({ timeout });
|
|
107
98
|
const targetAreas = getTargetAreas(area);
|
|
108
99
|
const hasKeyword = keyword.trim().length > 0;
|
|
@@ -125,26 +116,8 @@ export async function fetchLotteMartStores(params = {}, options = {}) {
|
|
|
125
116
|
: stores
|
|
126
117
|
.filter((store) => matchesKeyword(store, keyword))
|
|
127
118
|
.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
119
|
return {
|
|
147
|
-
stores: sortStores(
|
|
120
|
+
stores: sortStores(filtered).slice(0, limit),
|
|
148
121
|
location: typeof resolvedLatitude === 'number' && typeof resolvedLongitude === 'number'
|
|
149
122
|
? { latitude: resolvedLatitude, longitude: resolvedLongitude }
|
|
150
123
|
: null,
|
|
@@ -1,49 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*/
|
|
4
|
-
import { fetchJson } from '../../utils/http.js';
|
|
5
|
-
import { DEFAULT_LOTTEMART_TIMEOUT_MS } from './config.js';
|
|
6
|
-
const GEOCODE_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
7
|
-
const geocodeCache = new Map();
|
|
8
|
-
export async function geocodeLotteMartAddress(address, options = {}) {
|
|
9
|
-
const keyword = address.trim();
|
|
10
|
-
if (keyword.length === 0) {
|
|
11
|
-
return null;
|
|
12
|
-
}
|
|
13
|
-
const cached = geocodeCache.get(keyword);
|
|
14
|
-
if (cached && cached.expiresAt > Date.now()) {
|
|
15
|
-
return cached.value;
|
|
16
|
-
}
|
|
17
|
-
const apiKey = (options.googleMapsApiKey || '').trim();
|
|
18
|
-
if (apiKey.length === 0) {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
const endpoint = new URL('https://maps.googleapis.com/maps/api/geocode/json');
|
|
22
|
-
endpoint.searchParams.set('address', keyword);
|
|
23
|
-
endpoint.searchParams.set('key', apiKey);
|
|
24
|
-
const body = await fetchJson(endpoint.toString(), {
|
|
25
|
-
method: 'GET',
|
|
26
|
-
timeout: options.timeout || DEFAULT_LOTTEMART_TIMEOUT_MS,
|
|
27
|
-
headers: {
|
|
28
|
-
Accept: 'application/json',
|
|
29
|
-
},
|
|
30
|
-
});
|
|
31
|
-
if (body.status !== 'OK') {
|
|
32
|
-
geocodeCache.set(keyword, {
|
|
33
|
-
expiresAt: Date.now() + GEOCODE_CACHE_TTL_MS,
|
|
34
|
-
value: null,
|
|
35
|
-
});
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
const latitude = body.results?.[0]?.geometry?.location?.lat;
|
|
39
|
-
const longitude = body.results?.[0]?.geometry?.location?.lng;
|
|
40
|
-
const value = typeof latitude === 'number' && typeof longitude === 'number' ? { latitude, longitude } : null;
|
|
41
|
-
geocodeCache.set(keyword, {
|
|
42
|
-
expiresAt: Date.now() + GEOCODE_CACHE_TTL_MS,
|
|
43
|
-
value,
|
|
44
|
-
});
|
|
45
|
-
return value;
|
|
1
|
+
export async function geocodeLotteMartAddress(_address, _options = {}) {
|
|
2
|
+
return null;
|
|
46
3
|
}
|
|
47
4
|
export function __testOnlyClearLotteMartGeocodeCache() {
|
|
48
|
-
|
|
5
|
+
// 지원 종료 후에는 캐시나 외부 요청을 유지하지 않습니다.
|
|
49
6
|
}
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 메가박스 서비스 프로바이더
|
|
3
|
-
*/
|
|
4
1
|
import { createFindNearbyTheatersTool } from './tools/findNearbyTheaters.js';
|
|
5
2
|
import { createListNowShowingTool } from './tools/listNowShowing.js';
|
|
6
3
|
import { createGetRemainingSeatsTool } from './tools/getRemainingSeats.js';
|
|
@@ -11,16 +8,20 @@ const MEGABOX_METADATA = {
|
|
|
11
8
|
description: '메가박스 주변 지점 탐색, 영화 목록 조회, 잔여 좌석 조회 서비스',
|
|
12
9
|
};
|
|
13
10
|
class MegaboxService {
|
|
11
|
+
options;
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.options = options;
|
|
14
|
+
}
|
|
14
15
|
metadata = MEGABOX_METADATA;
|
|
15
16
|
getTools() {
|
|
16
17
|
return [
|
|
17
|
-
createFindNearbyTheatersTool(),
|
|
18
|
-
createListNowShowingTool(),
|
|
19
|
-
createGetRemainingSeatsTool(),
|
|
18
|
+
createFindNearbyTheatersTool(this.options),
|
|
19
|
+
createListNowShowingTool(this.options),
|
|
20
|
+
createGetRemainingSeatsTool(this.options),
|
|
20
21
|
];
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
|
-
export function createMegaboxService() {
|
|
24
|
-
return new MegaboxService();
|
|
24
|
+
export function createMegaboxService(options = {}) {
|
|
25
|
+
return new MegaboxService(options);
|
|
25
26
|
}
|
|
26
27
|
export * from './types.js';
|