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
|
@@ -3,8 +3,27 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import * as z from 'zod';
|
|
5
5
|
import { fetchCgvTheaters, toYyyymmdd } from '../client.js';
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import { fetchCgvNearbyTheaters } from '../location.js';
|
|
7
|
+
async function findTheaters(args, apiKey, googleMapsApiKey) {
|
|
8
|
+
const { playDate = toYyyymmdd(), regionCode, keyword, latitude, longitude, limit = 30, timeoutMs = 15000, } = args;
|
|
9
|
+
if (keyword || typeof latitude === 'number' || typeof longitude === 'number') {
|
|
10
|
+
const result = await fetchCgvNearbyTheaters({
|
|
11
|
+
playDate,
|
|
12
|
+
regionCode,
|
|
13
|
+
keyword,
|
|
14
|
+
latitude,
|
|
15
|
+
longitude,
|
|
16
|
+
limit,
|
|
17
|
+
timeout: timeoutMs,
|
|
18
|
+
}, {
|
|
19
|
+
timeout: timeoutMs,
|
|
20
|
+
zyteApiKey: apiKey,
|
|
21
|
+
googleMapsApiKey: googleMapsApiKey || process.env.GOOGLE_MAPS_API_KEY,
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
8
27
|
const theaters = await fetchCgvTheaters({
|
|
9
28
|
playDate,
|
|
10
29
|
regionCode,
|
|
@@ -16,6 +35,9 @@ async function findTheaters(args, apiKey) {
|
|
|
16
35
|
playDate,
|
|
17
36
|
filters: {
|
|
18
37
|
regionCode: regionCode || null,
|
|
38
|
+
keyword: keyword || null,
|
|
39
|
+
latitude: latitude ?? null,
|
|
40
|
+
longitude: longitude ?? null,
|
|
19
41
|
limit,
|
|
20
42
|
},
|
|
21
43
|
count: sliced.length,
|
|
@@ -25,19 +47,22 @@ async function findTheaters(args, apiKey) {
|
|
|
25
47
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
26
48
|
};
|
|
27
49
|
}
|
|
28
|
-
export function createFindTheatersTool(apiKey) {
|
|
50
|
+
export function createFindTheatersTool(apiKey, googleMapsApiKey) {
|
|
29
51
|
return {
|
|
30
52
|
name: 'cgv_find_theaters',
|
|
31
53
|
metadata: {
|
|
32
54
|
title: 'CGV 극장 검색',
|
|
33
|
-
description: '지역 코드 기준으로 CGV 극장 목록을 조회합니다.',
|
|
55
|
+
description: '지역 코드 또는 위치 키워드 기준으로 CGV 극장 목록을 조회합니다.',
|
|
34
56
|
inputSchema: {
|
|
35
57
|
playDate: z.string().optional().describe('조회 날짜(YYYYMMDD, 기본값: 오늘)'),
|
|
36
58
|
regionCode: z.string().optional().describe('지역 코드 (예: 01 서울)'),
|
|
59
|
+
keyword: z.string().optional().describe('위치 키워드 (예: 안산 중앙역, 강남역)'),
|
|
60
|
+
latitude: z.number().optional().describe('위도'),
|
|
61
|
+
longitude: z.number().optional().describe('경도'),
|
|
37
62
|
limit: z.number().optional().default(30).describe('최대 결과 수 (기본값: 30)'),
|
|
38
63
|
timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
|
|
39
64
|
},
|
|
40
65
|
},
|
|
41
|
-
handler: ((args) => findTheaters(args, apiKey)),
|
|
66
|
+
handler: ((args) => findTheaters(args, apiKey, googleMapsApiKey)),
|
|
42
67
|
};
|
|
43
68
|
}
|
|
@@ -3,24 +3,49 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import * as z from 'zod';
|
|
5
5
|
import { fetchCgvTimetable, toYyyymmdd } from '../client.js';
|
|
6
|
+
import { resolveCgvNearestTheater } from '../location.js';
|
|
6
7
|
import { filterAndSortTimetable } from '../timetable.js';
|
|
7
|
-
async function getTimetable(args, apiKey) {
|
|
8
|
-
const { playDate = toYyyymmdd(), theaterCode, movieCode, limit = 50, timeoutMs = 15000, } = args;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
async function getTimetable(args, apiKey, googleMapsApiKey) {
|
|
9
|
+
const { playDate = toYyyymmdd(), theaterCode, movieCode, keyword, latitude, longitude, limit = 50, timeoutMs = 15000, } = args;
|
|
10
|
+
let resolvedTheater = null;
|
|
11
|
+
let targetTheaterCode = theaterCode;
|
|
12
|
+
if (!targetTheaterCode && (keyword || typeof latitude === 'number' || typeof longitude === 'number')) {
|
|
13
|
+
const resolved = await resolveCgvNearestTheater({
|
|
14
|
+
playDate,
|
|
15
|
+
keyword,
|
|
16
|
+
latitude,
|
|
17
|
+
longitude,
|
|
18
|
+
timeout: timeoutMs,
|
|
19
|
+
}, {
|
|
20
|
+
timeout: timeoutMs,
|
|
21
|
+
zyteApiKey: apiKey,
|
|
22
|
+
googleMapsApiKey: googleMapsApiKey || process.env.GOOGLE_MAPS_API_KEY,
|
|
23
|
+
});
|
|
24
|
+
resolvedTheater = resolved.theater;
|
|
25
|
+
targetTheaterCode = resolved.theater?.theaterCode;
|
|
26
|
+
}
|
|
27
|
+
const shouldReturnEmpty = !targetTheaterCode && (keyword || typeof latitude === 'number' || typeof longitude === 'number');
|
|
28
|
+
const timetable = shouldReturnEmpty
|
|
29
|
+
? []
|
|
30
|
+
: await fetchCgvTimetable({
|
|
31
|
+
playDate,
|
|
32
|
+
theaterCode: targetTheaterCode,
|
|
33
|
+
movieCode,
|
|
34
|
+
timeout: timeoutMs,
|
|
35
|
+
zyteApiKey: apiKey,
|
|
36
|
+
});
|
|
37
|
+
const filtered = filterAndSortTimetable(timetable, { theaterCode: targetTheaterCode, movieCode, limit });
|
|
17
38
|
const result = {
|
|
18
39
|
playDate,
|
|
19
40
|
filters: {
|
|
20
|
-
theaterCode:
|
|
41
|
+
theaterCode: targetTheaterCode || null,
|
|
21
42
|
movieCode: movieCode || null,
|
|
43
|
+
keyword: keyword || null,
|
|
44
|
+
latitude: latitude ?? null,
|
|
45
|
+
longitude: longitude ?? null,
|
|
22
46
|
limit,
|
|
23
47
|
},
|
|
48
|
+
resolvedTheater,
|
|
24
49
|
count: filtered.length,
|
|
25
50
|
timetable: filtered,
|
|
26
51
|
};
|
|
@@ -28,20 +53,23 @@ async function getTimetable(args, apiKey) {
|
|
|
28
53
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
29
54
|
};
|
|
30
55
|
}
|
|
31
|
-
export function createGetTimetableTool(apiKey) {
|
|
56
|
+
export function createGetTimetableTool(apiKey, googleMapsApiKey) {
|
|
32
57
|
return {
|
|
33
58
|
name: 'cgv_get_timetable',
|
|
34
59
|
metadata: {
|
|
35
60
|
title: 'CGV 시간표 조회',
|
|
36
|
-
description: '날짜/극장/영화 조건으로 CGV 상영 시간표를 조회합니다.',
|
|
61
|
+
description: '날짜/극장/영화 조건으로 CGV 상영 시간표를 조회합니다. 극장 코드가 없으면 위치 키워드 기준 최근접 극장을 선택할 수 있습니다.',
|
|
37
62
|
inputSchema: {
|
|
38
63
|
playDate: z.string().optional().describe('조회 날짜(YYYYMMDD, 기본값: 오늘)'),
|
|
39
64
|
theaterCode: z.string().optional().describe('CGV 극장 코드 (예: 0056)'),
|
|
40
65
|
movieCode: z.string().optional().describe('CGV 영화 코드'),
|
|
66
|
+
keyword: z.string().optional().describe('위치 키워드 (예: 안산 중앙역, 강남역)'),
|
|
67
|
+
latitude: z.number().optional().describe('위도'),
|
|
68
|
+
longitude: z.number().optional().describe('경도'),
|
|
41
69
|
limit: z.number().optional().default(50).describe('최대 결과 수 (기본값: 50)'),
|
|
42
70
|
timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
|
|
43
71
|
},
|
|
44
72
|
},
|
|
45
|
-
handler: ((args) => getTimetable(args, apiKey)),
|
|
73
|
+
handler: ((args) => getTimetable(args, apiKey, googleMapsApiKey)),
|
|
46
74
|
};
|
|
47
75
|
}
|
|
@@ -3,19 +3,44 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import * as z from 'zod';
|
|
5
5
|
import { fetchCgvMovies, toYyyymmdd } from '../client.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
import { resolveCgvNearestTheater } from '../location.js';
|
|
7
|
+
async function searchMovies(args, apiKey, googleMapsApiKey) {
|
|
8
|
+
const { playDate = toYyyymmdd(), theaterCode, keyword, latitude, longitude, timeoutMs = 15000 } = args;
|
|
9
|
+
let resolvedTheater = null;
|
|
10
|
+
let targetTheaterCode = theaterCode;
|
|
11
|
+
if (!targetTheaterCode && (keyword || typeof latitude === 'number' || typeof longitude === 'number')) {
|
|
12
|
+
const resolved = await resolveCgvNearestTheater({
|
|
13
|
+
playDate,
|
|
14
|
+
keyword,
|
|
15
|
+
latitude,
|
|
16
|
+
longitude,
|
|
17
|
+
timeout: timeoutMs,
|
|
18
|
+
}, {
|
|
19
|
+
timeout: timeoutMs,
|
|
20
|
+
zyteApiKey: apiKey,
|
|
21
|
+
googleMapsApiKey: googleMapsApiKey || process.env.GOOGLE_MAPS_API_KEY,
|
|
22
|
+
});
|
|
23
|
+
resolvedTheater = resolved.theater;
|
|
24
|
+
targetTheaterCode = resolved.theater?.theaterCode;
|
|
25
|
+
}
|
|
26
|
+
const shouldReturnEmpty = !targetTheaterCode && (keyword || typeof latitude === 'number' || typeof longitude === 'number');
|
|
27
|
+
const movies = shouldReturnEmpty
|
|
28
|
+
? []
|
|
29
|
+
: await fetchCgvMovies({
|
|
30
|
+
playDate,
|
|
31
|
+
theaterCode: targetTheaterCode,
|
|
32
|
+
timeout: timeoutMs,
|
|
33
|
+
zyteApiKey: apiKey,
|
|
34
|
+
});
|
|
14
35
|
const result = {
|
|
15
36
|
playDate,
|
|
16
37
|
filters: {
|
|
17
|
-
theaterCode:
|
|
38
|
+
theaterCode: targetTheaterCode || null,
|
|
39
|
+
keyword: keyword || null,
|
|
40
|
+
latitude: latitude ?? null,
|
|
41
|
+
longitude: longitude ?? null,
|
|
18
42
|
},
|
|
43
|
+
resolvedTheater,
|
|
19
44
|
count: movies.length,
|
|
20
45
|
movies,
|
|
21
46
|
};
|
|
@@ -23,18 +48,21 @@ async function searchMovies(args, apiKey) {
|
|
|
23
48
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
24
49
|
};
|
|
25
50
|
}
|
|
26
|
-
export function createSearchMoviesTool(apiKey) {
|
|
51
|
+
export function createSearchMoviesTool(apiKey, googleMapsApiKey) {
|
|
27
52
|
return {
|
|
28
53
|
name: 'cgv_search_movies',
|
|
29
54
|
metadata: {
|
|
30
55
|
title: 'CGV 영화 검색',
|
|
31
|
-
description: 'CGV 상영 영화 목록을 조회합니다.',
|
|
56
|
+
description: 'CGV 상영 영화 목록을 조회합니다. 극장 코드가 없으면 위치 키워드 기준 최근접 극장을 선택할 수 있습니다.',
|
|
32
57
|
inputSchema: {
|
|
33
58
|
playDate: z.string().optional().describe('조회 날짜(YYYYMMDD, 기본값: 오늘)'),
|
|
34
59
|
theaterCode: z.string().optional().describe('CGV 극장 코드 (예: 0056)'),
|
|
60
|
+
keyword: z.string().optional().describe('위치 키워드 (예: 안산 중앙역, 강남역)'),
|
|
61
|
+
latitude: z.number().optional().describe('위도'),
|
|
62
|
+
longitude: z.number().optional().describe('경도'),
|
|
35
63
|
timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
|
|
36
64
|
},
|
|
37
65
|
},
|
|
38
|
-
handler: ((args) => searchMovies(args, apiKey)),
|
|
66
|
+
handler: ((args) => searchMovies(args, apiKey, googleMapsApiKey)),
|
|
39
67
|
};
|
|
40
68
|
}
|
|
@@ -7,11 +7,9 @@ import { createTimeoutController } from '../../utils/http.js';
|
|
|
7
7
|
import { decodeZyteHttpBody, requestByZyte } from '../../utils/zyte.js';
|
|
8
8
|
import { CGV_API } from './api.js';
|
|
9
9
|
function toBase64(bytes) {
|
|
10
|
-
/* c8 ignore start */
|
|
11
10
|
if (typeof Buffer !== 'undefined') {
|
|
12
11
|
return Buffer.from(bytes).toString('base64');
|
|
13
12
|
}
|
|
14
|
-
/* c8 ignore end */
|
|
15
13
|
let binary = '';
|
|
16
14
|
for (const byte of bytes) {
|
|
17
15
|
binary += String.fromCharCode(byte);
|
|
@@ -19,7 +17,6 @@ function toBase64(bytes) {
|
|
|
19
17
|
if (typeof btoa === 'function') {
|
|
20
18
|
return btoa(binary);
|
|
21
19
|
}
|
|
22
|
-
/* c8 ignore next */
|
|
23
20
|
throw new Error('Base64 인코딩을 지원하지 않는 런타임입니다.');
|
|
24
21
|
}
|
|
25
22
|
async function createSignature(pathname, bodyText, timestamp) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CU API 엔드포인트 중앙 관리
|
|
3
|
+
*/
|
|
4
|
+
export const CU_API = {
|
|
5
|
+
BASE_URL: 'https://www.pocketcu.co.kr',
|
|
6
|
+
STORE_PATH: '/api/store',
|
|
7
|
+
STOCK_DISPLAY_PATH: '/api/search/display/stock',
|
|
8
|
+
STOCK_MAIN_PATH: '/api/search/rest/stock/main',
|
|
9
|
+
WEB_BASE_URL: 'https://cu.bgfretail.com',
|
|
10
|
+
WEB_STORE_LIST_PATH: '/store/list_Ajax.do',
|
|
11
|
+
};
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CU API 클라이언트
|
|
3
|
+
*/
|
|
4
|
+
import { fetchJson } from '../../utils/http.js';
|
|
5
|
+
import { decodeBase64, requestByZyte } from '../../utils/zyte.js';
|
|
6
|
+
import { CU_API } from './api.js';
|
|
7
|
+
const CU_DEFAULT_HEADERS = {
|
|
8
|
+
'Content-Type': 'application/json',
|
|
9
|
+
Accept: 'application/json, text/javascript, */*; q=0.01',
|
|
10
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
11
|
+
};
|
|
12
|
+
const CU_WEB_DEFAULT_HEADERS = {
|
|
13
|
+
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
14
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
15
|
+
Accept: 'text/html, */*; q=0.01',
|
|
16
|
+
'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
|
|
17
|
+
Origin: 'https://cu.bgfretail.com',
|
|
18
|
+
Referer: 'https://cu.bgfretail.com/store/list.do?category=store',
|
|
19
|
+
};
|
|
20
|
+
function toNumber(value) {
|
|
21
|
+
if (typeof value === 'number') {
|
|
22
|
+
return Number.isFinite(value) ? value : 0;
|
|
23
|
+
}
|
|
24
|
+
if (typeof value === 'string') {
|
|
25
|
+
const parsed = Number.parseFloat(value);
|
|
26
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
27
|
+
}
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
function toYnBoolean(value) {
|
|
31
|
+
if (typeof value === 'boolean') {
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
if (typeof value === 'string') {
|
|
35
|
+
const normalized = value.trim().toUpperCase();
|
|
36
|
+
return normalized === 'Y' || normalized === 'TRUE';
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
function toStoreAddress(raw) {
|
|
41
|
+
const road = [raw.doroStoreAddr1 || '', raw.doroStoreAddr2 || ''].join(' ').trim();
|
|
42
|
+
if (road.length > 0) {
|
|
43
|
+
return road;
|
|
44
|
+
}
|
|
45
|
+
return [raw.addrFst || '', raw.addrDetail || ''].join(' ').trim();
|
|
46
|
+
}
|
|
47
|
+
async function requestCuJson(path, body, timeout = 15000) {
|
|
48
|
+
return fetchJson(`${CU_API.BASE_URL}${path}`, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
timeout,
|
|
51
|
+
headers: CU_DEFAULT_HEADERS,
|
|
52
|
+
body: JSON.stringify(body),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async function requestCuWebHtml(path, body, apiKey, timeout = 15000) {
|
|
56
|
+
const form = new URLSearchParams(body);
|
|
57
|
+
const formText = form.toString();
|
|
58
|
+
const targetUrl = `${CU_API.WEB_BASE_URL}${path}`;
|
|
59
|
+
const response = await fetch(`${CU_API.WEB_BASE_URL}${path}`, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: CU_WEB_DEFAULT_HEADERS,
|
|
62
|
+
body: formText,
|
|
63
|
+
signal: AbortSignal.timeout(timeout),
|
|
64
|
+
});
|
|
65
|
+
if (response.ok) {
|
|
66
|
+
return response.text();
|
|
67
|
+
}
|
|
68
|
+
if (response.status === 400 || response.status === 403 || response.status === 429) {
|
|
69
|
+
try {
|
|
70
|
+
const result = await requestByZyte({
|
|
71
|
+
apiKey,
|
|
72
|
+
url: targetUrl,
|
|
73
|
+
timeout,
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: Object.entries(CU_WEB_DEFAULT_HEADERS).map(([name, value]) => ({ name, value })),
|
|
76
|
+
bodyText: formText,
|
|
77
|
+
});
|
|
78
|
+
if (result.statusCode === 200 && result.httpResponseBody) {
|
|
79
|
+
return decodeBase64(result.httpResponseBody);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// 직접 호출 실패 시 Zyte 재시도도 실패하면 원본 에러를 반환합니다.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
throw new Error(`API 요청 실패: ${response.status} ${response.statusText}`);
|
|
87
|
+
}
|
|
88
|
+
export async function geocodeCuAddress(address, options = {}) {
|
|
89
|
+
const keyword = address.trim();
|
|
90
|
+
if (keyword.length === 0) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const { timeout = 15000, googleMapsApiKey } = options;
|
|
94
|
+
const apiKey = (googleMapsApiKey || '').trim();
|
|
95
|
+
if (apiKey.length === 0) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const endpoint = new URL('https://maps.googleapis.com/maps/api/geocode/json');
|
|
99
|
+
endpoint.searchParams.set('address', keyword);
|
|
100
|
+
endpoint.searchParams.set('key', apiKey);
|
|
101
|
+
const body = await fetchJson(endpoint.toString(), {
|
|
102
|
+
method: 'GET',
|
|
103
|
+
timeout,
|
|
104
|
+
headers: {
|
|
105
|
+
Accept: 'application/json',
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
if (body.status !== 'OK') {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const firstLocation = body.results?.[0]?.geometry?.location;
|
|
112
|
+
if (!firstLocation) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const latitude = toNumber(firstLocation.lat);
|
|
116
|
+
const longitude = toNumber(firstLocation.lng);
|
|
117
|
+
if (latitude === 0 || longitude === 0) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
latitude,
|
|
122
|
+
longitude,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function decodeHtmlEntities(value) {
|
|
126
|
+
return value
|
|
127
|
+
.replace(/ /g, ' ')
|
|
128
|
+
.replace(/</g, '<')
|
|
129
|
+
.replace(/>/g, '>')
|
|
130
|
+
.replace(/"/g, '"')
|
|
131
|
+
.replace(/'/g, "'")
|
|
132
|
+
.replace(/&/g, '&');
|
|
133
|
+
}
|
|
134
|
+
function sanitizeHtmlText(value) {
|
|
135
|
+
const stripped = value.replace(/<[^>]*>/g, ' ');
|
|
136
|
+
return decodeHtmlEntities(stripped).replace(/\s+/g, ' ').trim();
|
|
137
|
+
}
|
|
138
|
+
function parseCuWebStores(html) {
|
|
139
|
+
const rows = html.match(/<tr>[\s\S]*?<\/tr>/g) || [];
|
|
140
|
+
const stores = [];
|
|
141
|
+
for (const row of rows) {
|
|
142
|
+
const nameMatch = row.match(/<span class="name">([\s\S]*?)<\/span>/);
|
|
143
|
+
if (!nameMatch) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const phoneMatch = row.match(/<span class="tel">([\s\S]*?)<\/span>/);
|
|
147
|
+
const addressMatch = row.match(/<address>[\s\S]*?<a [^>]*>([\s\S]*?)<\/a>/);
|
|
148
|
+
const codeMatch = row.match(/searchLatLng\('[^']*',\s*'([^']+)'\)/);
|
|
149
|
+
stores.push({
|
|
150
|
+
storeCode: codeMatch?.[1] || '',
|
|
151
|
+
storeName: sanitizeHtmlText(nameMatch[1]),
|
|
152
|
+
phone: sanitizeHtmlText(phoneMatch?.[1] || ''),
|
|
153
|
+
address: sanitizeHtmlText(addressMatch?.[1] || ''),
|
|
154
|
+
latitude: 0,
|
|
155
|
+
longitude: 0,
|
|
156
|
+
distanceM: 0,
|
|
157
|
+
stock: 0,
|
|
158
|
+
deliveryYn: false,
|
|
159
|
+
pickupYn: false,
|
|
160
|
+
reserveYn: false,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return stores;
|
|
164
|
+
}
|
|
165
|
+
function normalizeCuStore(raw) {
|
|
166
|
+
return {
|
|
167
|
+
storeCode: raw.storeCd || '',
|
|
168
|
+
storeName: raw.storeNm || '',
|
|
169
|
+
phone: raw.storeTelNo || '',
|
|
170
|
+
address: toStoreAddress(raw),
|
|
171
|
+
latitude: toNumber(raw.latVal),
|
|
172
|
+
longitude: toNumber(raw.longVal),
|
|
173
|
+
distanceM: toNumber(raw.distance),
|
|
174
|
+
stock: toNumber(raw.stock),
|
|
175
|
+
deliveryYn: toYnBoolean(raw.deliveryYn) || toYnBoolean(raw.deliveryPickYn),
|
|
176
|
+
pickupYn: toYnBoolean(raw.jumpoPickYn),
|
|
177
|
+
reserveYn: toYnBoolean(raw.reserveYn),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* 좌표 기반 CU 점포 목록을 조회합니다.
|
|
182
|
+
*/
|
|
183
|
+
export async function fetchCuStores(params, options = {}) {
|
|
184
|
+
const { timeout = 15000, apiKey } = options;
|
|
185
|
+
const searchWord = (params.searchWord || '').trim();
|
|
186
|
+
const hasLatitude = typeof params.latitude === 'number' && Number.isFinite(params.latitude);
|
|
187
|
+
const hasLongitude = typeof params.longitude === 'number' && Number.isFinite(params.longitude);
|
|
188
|
+
// 좌표가 없고 검색어가 있으면 웹 매장 검색으로 폴백합니다.
|
|
189
|
+
if (searchWord.length > 0 && (!hasLatitude || !hasLongitude)) {
|
|
190
|
+
const html = await requestCuWebHtml(CU_API.WEB_STORE_LIST_PATH, {
|
|
191
|
+
pageIndex: '1',
|
|
192
|
+
listType: '',
|
|
193
|
+
jumpoCode: '',
|
|
194
|
+
jumpoLotto: '',
|
|
195
|
+
jumpoToto: '',
|
|
196
|
+
jumpoCash: '',
|
|
197
|
+
jumpoHour: '',
|
|
198
|
+
jumpoCafe: '',
|
|
199
|
+
jumpoDelivery: '',
|
|
200
|
+
jumpoBakery: '',
|
|
201
|
+
jumpoFry: '',
|
|
202
|
+
jumpoMultiDevice: '',
|
|
203
|
+
jumpoPosCash: '',
|
|
204
|
+
jumpoBattery: '',
|
|
205
|
+
jumpoAdderss: '',
|
|
206
|
+
jumpoSido: '',
|
|
207
|
+
jumpoGugun: '',
|
|
208
|
+
jumpodong: '',
|
|
209
|
+
searchWord,
|
|
210
|
+
}, apiKey, timeout);
|
|
211
|
+
const stores = parseCuWebStores(html);
|
|
212
|
+
return {
|
|
213
|
+
totalCount: stores.length,
|
|
214
|
+
stores,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const latitude = hasLatitude ? params.latitude : 37.5665;
|
|
218
|
+
const longitude = hasLongitude ? params.longitude : 126.978;
|
|
219
|
+
const payload = {
|
|
220
|
+
latVal: String(latitude),
|
|
221
|
+
longVal: String(longitude),
|
|
222
|
+
baseLatVal: String(latitude),
|
|
223
|
+
baseLongVal: String(longitude),
|
|
224
|
+
tabId: '2',
|
|
225
|
+
filterSvcList: [],
|
|
226
|
+
filterAdtList: [],
|
|
227
|
+
stockCdcYn: 'N',
|
|
228
|
+
searchStock: false,
|
|
229
|
+
pickupType: 'change',
|
|
230
|
+
getRoute: 'IOS',
|
|
231
|
+
areaTplNo: '0',
|
|
232
|
+
childMealPickUpYn: 'N',
|
|
233
|
+
isCurrentSearch: 'N',
|
|
234
|
+
pageType: params.pageType || 'search_improve',
|
|
235
|
+
searchWord,
|
|
236
|
+
isRecommend: params.isRecommend || '',
|
|
237
|
+
recommendId: params.recommendId || '',
|
|
238
|
+
jipCd: params.jipCd || params.itemCd || '',
|
|
239
|
+
itemCd: params.itemCd || '',
|
|
240
|
+
item_cd: params.itemCd || '',
|
|
241
|
+
onItemNo: params.onItemNo || '',
|
|
242
|
+
};
|
|
243
|
+
const body = await requestCuJson(CU_API.STORE_PATH, payload, timeout);
|
|
244
|
+
const stores = (body.storeList || []).map((store) => normalizeCuStore(store));
|
|
245
|
+
return {
|
|
246
|
+
totalCount: toNumber(body.totalCnt) || stores.length,
|
|
247
|
+
stores,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* 재고 검색 전 초기 화면 데이터를 조회합니다.
|
|
252
|
+
* 정책 변경 시 사전 호출이 필요한 경우를 대비한 워밍업 요청입니다.
|
|
253
|
+
*/
|
|
254
|
+
export async function primeCuStockDisplay(options = {}) {
|
|
255
|
+
const { timeout = 15000 } = options;
|
|
256
|
+
await requestCuJson(CU_API.STOCK_DISPLAY_PATH, {}, timeout);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* CU 상품 재고 검색 결과를 조회합니다.
|
|
260
|
+
*/
|
|
261
|
+
export async function fetchCuStock(params, options = {}) {
|
|
262
|
+
const { timeout = 15000 } = options;
|
|
263
|
+
try {
|
|
264
|
+
await primeCuStockDisplay({ timeout });
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// 사전 워밍업 실패는 본 검색으로 재시도합니다.
|
|
268
|
+
}
|
|
269
|
+
const payload = {
|
|
270
|
+
searchWord: params.keyword,
|
|
271
|
+
prevSearchWord: '',
|
|
272
|
+
spellModifyUseYn: 'Y',
|
|
273
|
+
offset: params.offset,
|
|
274
|
+
limit: params.limit,
|
|
275
|
+
searchSort: params.searchSort,
|
|
276
|
+
};
|
|
277
|
+
const body = await requestCuJson(CU_API.STOCK_MAIN_PATH, payload, timeout);
|
|
278
|
+
const result = body.data?.stockResult?.result;
|
|
279
|
+
const rows = result?.rows || [];
|
|
280
|
+
const items = rows
|
|
281
|
+
.map((row) => row.fields)
|
|
282
|
+
.filter((fields) => Boolean(fields))
|
|
283
|
+
.map((fields) => ({
|
|
284
|
+
itemCode: fields.item_cd || '',
|
|
285
|
+
onItemNo: fields.on_item_no || '',
|
|
286
|
+
itemName: fields.item_nm || '',
|
|
287
|
+
price: toNumber(fields.hyun_maega),
|
|
288
|
+
pickupYn: toYnBoolean(fields.pickup_yn),
|
|
289
|
+
deliveryYn: toYnBoolean(fields.deliv_yn),
|
|
290
|
+
reserveYn: toYnBoolean(fields.reserv_yn),
|
|
291
|
+
}));
|
|
292
|
+
return {
|
|
293
|
+
totalCount: toNumber(result?.total_count) || items.length,
|
|
294
|
+
spellModifyYn: body.spellModifyYn || 'N',
|
|
295
|
+
items,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CU 서비스 프로바이더
|
|
3
|
+
*/
|
|
4
|
+
import { createFindNearbyStoresTool } from './tools/findNearbyStores.js';
|
|
5
|
+
import { createCheckInventoryTool } from './tools/checkInventory.js';
|
|
6
|
+
const CU_METADATA = {
|
|
7
|
+
id: 'cu',
|
|
8
|
+
name: 'CU 편의점',
|
|
9
|
+
version: '1.0.0',
|
|
10
|
+
description: 'CU 매장 탐색 및 상품 재고 조회 서비스',
|
|
11
|
+
};
|
|
12
|
+
class CuService {
|
|
13
|
+
metadata = CU_METADATA;
|
|
14
|
+
getTools() {
|
|
15
|
+
return [createFindNearbyStoresTool(), createCheckInventoryTool()];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function createCuService() {
|
|
19
|
+
return new CuService();
|
|
20
|
+
}
|
|
21
|
+
export * from './types.js';
|