daiso 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +249 -533
- package/dist/api/actionsProxy.js +82 -0
- package/dist/api/cgvHandlers.js +103 -16
- package/dist/api/configStatus.js +19 -0
- package/dist/api/daisoHandlers.js +127 -0
- package/dist/api/emart24Handlers.js +176 -0
- package/dist/api/gs25Handlers.js +267 -0
- package/dist/api/handlers.js +209 -104
- package/dist/api/healthChecks.js +504 -0
- package/dist/api/lottecinemaHandlers.js +155 -0
- package/dist/api/lottemartHandlers.js +144 -0
- package/dist/api/megaboxHandlers.js +81 -50
- package/dist/api/response.js +2 -0
- package/dist/api/routes/cuRoutes.js +17 -0
- package/dist/api/routes/daisoRoutes.js +6 -1
- package/dist/api/routes/emart24Routes.js +22 -0
- package/dist/api/routes/gs25Routes.js +22 -0
- package/dist/api/routes/healthRoutes.js +61 -0
- package/dist/api/routes/lottecinemaRoutes.js +22 -0
- package/dist/api/routes/lottemartRoutes.js +18 -0
- package/dist/api/routes/oliveyoungRoutes.js +6 -1
- package/dist/api/routes/sevenelevenRoutes.js +32 -0
- package/dist/api/sevenelevenHandlers.js +200 -0
- package/dist/bin.js +0 -0
- package/dist/cli/args.js +75 -0
- package/dist/cli/commandOptions.js +38 -0
- package/dist/cli/commands/convenience.js +293 -0
- package/dist/cli/commands/daiso.js +112 -0
- package/dist/cli/constants.js +5 -0
- package/dist/cli/deps.js +48 -0
- package/dist/cli/http.js +156 -0
- package/dist/cli/interactiveFetch.js +31 -0
- package/dist/cli/interactiveItemSearch.js +261 -0
- package/dist/cli/interactivePrompt.js +50 -0
- package/dist/cli/interactiveTypes.js +4 -0
- package/dist/cli/types.js +4 -0
- package/dist/cli.js +87 -196
- package/dist/cliHelp.js +212 -1
- package/dist/cliInteractive.js +243 -0
- package/dist/cliPicker.js +57 -0
- package/dist/cliRenderer.js +90 -3
- package/dist/core/errors.js +66 -0
- package/dist/core/registry.js +227 -3
- package/dist/index.js +149 -9
- package/dist/pages/openapi.js +2 -1
- package/dist/pages/openapiFullSpec.js +58 -0
- package/dist/pages/openapiResponse.js +28 -0
- package/dist/pages/openapiSpec.js +2 -41
- package/dist/pages/openapiSpecActions.js +109 -0
- package/dist/pages/openapiSpecComponents.js +30 -391
- package/dist/pages/openapiSpecComponentsCgv.js +24 -0
- package/dist/pages/openapiSpecComponentsCu.js +112 -0
- package/dist/pages/openapiSpecComponentsDaisoOliveyoung.js +342 -0
- package/dist/pages/openapiSpecComponentsEmart24.js +141 -0
- package/dist/pages/openapiSpecComponentsGs25.js +109 -0
- package/dist/pages/openapiSpecComponentsLotteMart.js +85 -0
- package/dist/pages/openapiSpecComponentsLottecinema.js +163 -0
- package/dist/pages/openapiSpecComponentsMegabox.js +144 -0
- package/dist/pages/openapiSpecComponentsSeveneleven.js +82 -0
- package/dist/pages/openapiSpecPathsCgv.js +66 -3
- package/dist/pages/openapiSpecPathsCu.js +145 -0
- package/dist/pages/openapiSpecPathsDaiso.js +302 -0
- package/dist/pages/openapiSpecPathsDaisoOliveyoung.js +4 -376
- package/dist/pages/openapiSpecPathsEmart24.js +223 -0
- package/dist/pages/openapiSpecPathsGs25.js +166 -0
- package/dist/pages/openapiSpecPathsLotteMart.js +149 -0
- package/dist/pages/openapiSpecPathsLottecinema.js +212 -0
- package/dist/pages/openapiSpecPathsMegabox.js +52 -3
- package/dist/pages/openapiSpecPathsOliveyoung.js +266 -0
- package/dist/pages/openapiSpecPathsSeveneleven.js +234 -0
- package/dist/pages/prompt.js +8 -402
- package/dist/pages/promptCoreText.js +278 -0
- package/dist/pages/promptReferenceText.js +137 -0
- package/dist/pages/promptRetailCinemaText.js +274 -0
- package/dist/services/cgv/client.js +6 -1
- package/dist/services/cgv/index.js +3 -3
- package/dist/services/cgv/location.js +235 -0
- package/dist/services/cgv/tools/findTheaters.js +30 -5
- package/dist/services/cgv/tools/getTimetable.js +42 -14
- package/dist/services/cgv/tools/searchMovies.js +40 -12
- package/dist/services/cgv/transport.js +0 -3
- package/dist/services/cu/api.js +11 -0
- package/dist/services/cu/client.js +297 -0
- package/dist/services/cu/index.js +21 -0
- package/dist/services/cu/tools/checkInventory.js +123 -0
- package/dist/services/cu/tools/findNearbyStores.js +49 -0
- package/dist/services/cu/types.js +4 -0
- package/dist/services/daiso/api.js +22 -2
- package/dist/services/daiso/client.js +67 -0
- package/dist/services/daiso/index.js +4 -0
- package/dist/services/daiso/product.js +17 -0
- package/dist/services/daiso/tools/checkInventory.js +80 -43
- package/dist/services/daiso/tools/findInventoryByName.js +222 -0
- package/dist/services/daiso/tools/findStores.js +17 -7
- package/dist/services/daiso/tools/getDisplayLocation.js +75 -0
- package/dist/services/emart24/api.js +11 -0
- package/dist/services/emart24/client.js +182 -0
- package/dist/services/emart24/index.js +22 -0
- package/dist/services/emart24/inventoryLookup.js +134 -0
- package/dist/services/emart24/tools/checkInventory.js +54 -0
- package/dist/services/emart24/tools/findNearbyStores.js +72 -0
- package/dist/services/emart24/tools/searchProducts.js +73 -0
- package/dist/services/emart24/types.js +4 -0
- package/dist/services/gs25/api.js +9 -0
- package/dist/services/gs25/client.js +208 -0
- package/dist/services/gs25/index.js +22 -0
- package/dist/services/gs25/storeUtils.js +168 -0
- package/dist/services/gs25/tools/checkInventory.js +163 -0
- package/dist/services/gs25/tools/findNearbyStores.js +103 -0
- package/dist/services/gs25/tools/searchProducts.js +57 -0
- package/dist/services/gs25/types.js +4 -0
- package/dist/services/lottecinema/api.js +12 -0
- package/dist/services/lottecinema/client.js +161 -0
- package/dist/services/lottecinema/index.js +30 -0
- package/dist/services/lottecinema/location.js +191 -0
- package/dist/services/lottecinema/tools/findNearbyTheaters.js +47 -0
- package/dist/services/lottecinema/tools/getRemainingSeats.js +85 -0
- package/dist/services/lottecinema/tools/listNowShowing.js +79 -0
- package/dist/services/lottecinema/types.js +4 -0
- package/dist/services/lottemart/api.js +31 -0
- package/dist/services/lottemart/client.js +254 -0
- package/dist/services/lottemart/clientTypes.js +4 -0
- package/dist/services/lottemart/config.js +4 -0
- package/dist/services/lottemart/debug.js +92 -0
- package/dist/services/lottemart/geocode.js +49 -0
- package/dist/services/lottemart/index.js +30 -0
- package/dist/services/lottemart/keyword.js +29 -0
- package/dist/services/lottemart/parser.js +185 -0
- package/dist/services/lottemart/session.js +331 -0
- package/dist/services/lottemart/storeSearch.js +52 -0
- package/dist/services/lottemart/tools/findNearbyStores.js +67 -0
- package/dist/services/lottemart/tools/searchProducts.js +69 -0
- package/dist/services/lottemart/types.js +4 -0
- package/dist/services/lottemart/zetta.js +105 -0
- package/dist/services/megabox/location.js +219 -0
- package/dist/services/megabox/tools/findNearbyTheaters.js +13 -44
- package/dist/services/megabox/tools/getRemainingSeats.js +31 -3
- package/dist/services/megabox/tools/listNowShowing.js +31 -3
- package/dist/services/oliveyoung/api.js +2 -0
- package/dist/services/oliveyoung/client.js +225 -13
- package/dist/services/oliveyoung/index.js +3 -1
- package/dist/services/oliveyoung/tools/checkInventory.js +21 -5
- package/dist/services/oliveyoung/tools/searchProducts.js +57 -0
- package/dist/services/seveneleven/api.js +15 -0
- package/dist/services/seveneleven/client.js +286 -0
- package/dist/services/seveneleven/index.js +30 -0
- package/dist/services/seveneleven/inventory.js +302 -0
- package/dist/services/seveneleven/productKeyword.js +141 -0
- package/dist/services/seveneleven/tools/checkInventory.js +82 -0
- package/dist/services/seveneleven/tools/getCatalogSnapshot.js +55 -0
- package/dist/services/seveneleven/tools/getSearchPopwords.js +41 -0
- package/dist/services/seveneleven/tools/searchProducts.js +67 -0
- package/dist/services/seveneleven/tools/searchStores.js +45 -0
- package/dist/services/seveneleven/types.js +4 -0
- package/dist/utils/cliInteractiveHelpers.js +63 -0
- package/dist/utils/daisoKeyword.js +30 -0
- package/dist/utils/http.js +98 -12
- package/dist/utils/zyte.js +47 -26
- package/openapi.json +1979 -736
- package/openapi.yaml +1428 -541
- package/package.json +49 -18
- package/dist/tools/checkInventory.js +0 -98
- package/dist/tools/findStores.js +0 -125
- package/dist/tools/getPriceInfo.js +0 -77
- package/dist/tools/searchProducts.js +0 -61
- package/dist/types/index.js +0 -4
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CU 재고 확인 도구
|
|
3
|
+
*/
|
|
4
|
+
import * as z from 'zod';
|
|
5
|
+
import { fetchCuStock, fetchCuStores, geocodeCuAddress } from '../client.js';
|
|
6
|
+
async function checkInventory(args) {
|
|
7
|
+
const { keyword, latitude, longitude, storeKeyword = '', size = 20, offset = 0, searchSort = 'recom', storeLimit = 10, timeoutMs = 15000, } = args;
|
|
8
|
+
if (!keyword || keyword.trim().length === 0) {
|
|
9
|
+
throw new Error('상품 검색어(keyword)를 입력해주세요.');
|
|
10
|
+
}
|
|
11
|
+
const stockResult = await fetchCuStock({
|
|
12
|
+
keyword,
|
|
13
|
+
limit: size,
|
|
14
|
+
offset,
|
|
15
|
+
searchSort,
|
|
16
|
+
}, {
|
|
17
|
+
timeout: timeoutMs,
|
|
18
|
+
});
|
|
19
|
+
const firstStockItem = stockResult.items.find((item) => item.itemCode.trim().length > 0) || null;
|
|
20
|
+
const hasInputLocation = typeof latitude === 'number' && typeof longitude === 'number';
|
|
21
|
+
const resolvedLatitude = hasInputLocation ? latitude : undefined;
|
|
22
|
+
const resolvedLongitude = hasInputLocation ? longitude : undefined;
|
|
23
|
+
let storeResult = null;
|
|
24
|
+
// 좌표 미입력 + 매장 키워드 입력 시, 키워드 기반 매장 검색 결과를 우선 사용합니다.
|
|
25
|
+
if (!hasInputLocation && storeKeyword.trim().length > 0) {
|
|
26
|
+
const keywordStoreResult = await fetchCuStores({
|
|
27
|
+
searchWord: storeKeyword,
|
|
28
|
+
}, {
|
|
29
|
+
timeout: timeoutMs,
|
|
30
|
+
});
|
|
31
|
+
const firstAddress = keywordStoreResult.stores.find((store) => store.address.trim().length > 0)?.address || '';
|
|
32
|
+
if (firstAddress.length > 0) {
|
|
33
|
+
const googleMapsApiKey = process.env.GOOGLE_MAPS_API_KEY;
|
|
34
|
+
const geocoded = await geocodeCuAddress(firstAddress, {
|
|
35
|
+
timeout: timeoutMs,
|
|
36
|
+
googleMapsApiKey,
|
|
37
|
+
});
|
|
38
|
+
if (geocoded) {
|
|
39
|
+
const hasStockSeed = !!firstStockItem?.itemCode;
|
|
40
|
+
storeResult = await fetchCuStores({
|
|
41
|
+
latitude: geocoded.latitude,
|
|
42
|
+
longitude: geocoded.longitude,
|
|
43
|
+
searchWord: storeKeyword,
|
|
44
|
+
itemCd: firstStockItem?.itemCode || '',
|
|
45
|
+
onItemNo: firstStockItem?.onItemNo || '',
|
|
46
|
+
jipCd: firstStockItem?.itemCode || '',
|
|
47
|
+
isRecommend: hasStockSeed ? 'Y' : '',
|
|
48
|
+
recommendId: hasStockSeed ? 'stock' : '',
|
|
49
|
+
pageType: hasStockSeed ? 'search_improve stock_sch_improve' : 'search_improve',
|
|
50
|
+
}, {
|
|
51
|
+
timeout: timeoutMs,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
storeResult ||= keywordStoreResult;
|
|
56
|
+
}
|
|
57
|
+
if (!storeResult) {
|
|
58
|
+
const hasStockSeed = !!firstStockItem?.itemCode;
|
|
59
|
+
storeResult = await fetchCuStores({
|
|
60
|
+
latitude: resolvedLatitude,
|
|
61
|
+
longitude: resolvedLongitude,
|
|
62
|
+
searchWord: storeKeyword,
|
|
63
|
+
itemCd: firstStockItem?.itemCode || '',
|
|
64
|
+
onItemNo: firstStockItem?.onItemNo || '',
|
|
65
|
+
jipCd: firstStockItem?.itemCode || '',
|
|
66
|
+
isRecommend: hasStockSeed ? 'Y' : '',
|
|
67
|
+
recommendId: hasStockSeed ? 'stock' : '',
|
|
68
|
+
pageType: hasStockSeed ? 'search_improve stock_sch_improve' : 'search_improve',
|
|
69
|
+
}, {
|
|
70
|
+
timeout: timeoutMs,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const limitedStores = storeResult.stores.slice(0, storeLimit);
|
|
74
|
+
const result = {
|
|
75
|
+
keyword,
|
|
76
|
+
searchOptions: {
|
|
77
|
+
size,
|
|
78
|
+
offset,
|
|
79
|
+
searchSort,
|
|
80
|
+
storeKeyword,
|
|
81
|
+
},
|
|
82
|
+
location: typeof resolvedLatitude === 'number' && typeof resolvedLongitude === 'number'
|
|
83
|
+
? { latitude: resolvedLatitude, longitude: resolvedLongitude }
|
|
84
|
+
: null,
|
|
85
|
+
nearbyStores: {
|
|
86
|
+
totalCount: storeResult.totalCount,
|
|
87
|
+
count: limitedStores.length,
|
|
88
|
+
stockItemCode: firstStockItem?.itemCode || null,
|
|
89
|
+
stockItemName: firstStockItem?.itemName || null,
|
|
90
|
+
stores: limitedStores,
|
|
91
|
+
},
|
|
92
|
+
inventory: {
|
|
93
|
+
totalCount: stockResult.totalCount,
|
|
94
|
+
count: stockResult.items.length,
|
|
95
|
+
spellModifyYn: stockResult.spellModifyYn,
|
|
96
|
+
items: stockResult.items,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export function createCheckInventoryTool() {
|
|
104
|
+
return {
|
|
105
|
+
name: 'cu_check_inventory',
|
|
106
|
+
metadata: {
|
|
107
|
+
title: 'CU 재고 확인',
|
|
108
|
+
description: '상품 키워드로 CU 재고를 조회하고 주변 매장 정보를 함께 반환합니다.',
|
|
109
|
+
inputSchema: {
|
|
110
|
+
keyword: z.string().describe('재고를 확인할 상품 검색어 (예: 과자, 커피, 도시락)'),
|
|
111
|
+
latitude: z.number().optional().describe('위도 (없으면 storeKeyword 기반으로 좌표 추정 시도)'),
|
|
112
|
+
longitude: z.number().optional().describe('경도 (없으면 storeKeyword 기반으로 좌표 추정 시도)'),
|
|
113
|
+
storeKeyword: z.string().optional().describe('주변 매장 필터 키워드 (예: 강남, 안산)'),
|
|
114
|
+
size: z.number().optional().default(20).describe('재고 검색 결과 수 (기본값: 20)'),
|
|
115
|
+
offset: z.number().optional().default(0).describe('재고 검색 시작 오프셋 (기본값: 0)'),
|
|
116
|
+
searchSort: z.string().optional().default('recom').describe('재고 정렬 방식 (기본값: recom)'),
|
|
117
|
+
storeLimit: z.number().optional().default(10).describe('반환할 주변 매장 최대 수 (기본값: 10)'),
|
|
118
|
+
timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
handler: checkInventory,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CU 주변 매장 탐색 도구
|
|
3
|
+
*/
|
|
4
|
+
import * as z from 'zod';
|
|
5
|
+
import { fetchCuStores } from '../client.js';
|
|
6
|
+
async function findNearbyStores(args) {
|
|
7
|
+
const { latitude, longitude, keyword = '', limit = 20, timeoutMs = 15000, } = args;
|
|
8
|
+
const { totalCount, stores } = await fetchCuStores({
|
|
9
|
+
latitude,
|
|
10
|
+
longitude,
|
|
11
|
+
searchWord: keyword,
|
|
12
|
+
}, {
|
|
13
|
+
timeout: timeoutMs,
|
|
14
|
+
});
|
|
15
|
+
const limitedStores = stores.slice(0, limit);
|
|
16
|
+
return {
|
|
17
|
+
content: [
|
|
18
|
+
{
|
|
19
|
+
type: 'text',
|
|
20
|
+
text: JSON.stringify({
|
|
21
|
+
location: typeof latitude === 'number' && typeof longitude === 'number'
|
|
22
|
+
? { latitude, longitude }
|
|
23
|
+
: null,
|
|
24
|
+
keyword,
|
|
25
|
+
totalCount,
|
|
26
|
+
count: limitedStores.length,
|
|
27
|
+
stores: limitedStores,
|
|
28
|
+
}, null, 2),
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function createFindNearbyStoresTool() {
|
|
34
|
+
return {
|
|
35
|
+
name: 'cu_find_nearby_stores',
|
|
36
|
+
metadata: {
|
|
37
|
+
title: 'CU 주변 매장 탐색',
|
|
38
|
+
description: '위치(위도/경도)와 키워드로 CU 매장을 검색합니다.',
|
|
39
|
+
inputSchema: {
|
|
40
|
+
latitude: z.number().optional().describe('위도 (없으면 키워드 기반 검색)'),
|
|
41
|
+
longitude: z.number().optional().describe('경도 (없으면 키워드 기반 검색)'),
|
|
42
|
+
keyword: z.string().optional().describe('매장 검색 키워드 (예: 강남, 명동, 안산)'),
|
|
43
|
+
limit: z.number().optional().default(20).describe('반환할 최대 매장 수 (기본값: 20)'),
|
|
44
|
+
timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
handler: findNearbyStores,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -12,10 +12,18 @@ export const DAISOMALL_API = {
|
|
|
12
12
|
SEARCH_PRODUCTS: 'https://prdm.daisomall.co.kr/ssn/search/FindStoreGoods',
|
|
13
13
|
/** 온라인 재고 조회 API */
|
|
14
14
|
ONLINE_STOCK: 'https://mapi.daisomall.co.kr/ms/msg/selOnlStck',
|
|
15
|
-
/**
|
|
15
|
+
/** 구 재고 조회 API (현재 403 Unauthorized 발생) */
|
|
16
16
|
STORE_INVENTORY: 'https://mapi.daisomall.co.kr/ms/msg/newIntSelStr',
|
|
17
|
+
/** 현재 사용 중인 인증 토큰 요청 API */
|
|
18
|
+
AUTH_REQUEST: 'https://fapi.daisomall.co.kr/auth/request',
|
|
19
|
+
/** 현재 사용 중인 위치 기반 매장 조회 API */
|
|
20
|
+
STORE_SEARCH_V2: 'https://fapi.daisomall.co.kr/ms/msg/selStr',
|
|
21
|
+
/** 현재 사용 중인 매장별 재고 조회 API */
|
|
22
|
+
STORE_INVENTORY_V2: 'https://fapi.daisomall.co.kr/pd/pdh/selStrPkupStck',
|
|
23
|
+
/** 매장 내 상품 진열 위치 조회 API */
|
|
24
|
+
DISPLAY_LOCATION: 'https://fapi.daisomall.co.kr/pdo/selIntPdStDispInfo',
|
|
17
25
|
/** 이미지 CDN 베이스 URL */
|
|
18
|
-
IMAGE_BASE_URL: 'https://
|
|
26
|
+
IMAGE_BASE_URL: 'https://cdn.daisomall.co.kr',
|
|
19
27
|
};
|
|
20
28
|
/**
|
|
21
29
|
* 다이소 공식 사이트 API (daiso.co.kr)
|
|
@@ -37,6 +45,18 @@ export const DAISO_WEB_API = {
|
|
|
37
45
|
export function getImageUrl(path) {
|
|
38
46
|
if (!path)
|
|
39
47
|
return undefined;
|
|
48
|
+
if (path.startsWith('http://') || path.startsWith('https://')) {
|
|
49
|
+
try {
|
|
50
|
+
const url = new URL(path);
|
|
51
|
+
if (url.hostname === 'img.daisomall.co.kr') {
|
|
52
|
+
url.hostname = 'cdn.daisomall.co.kr';
|
|
53
|
+
}
|
|
54
|
+
return url.toString();
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return path;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
40
60
|
return `${DAISOMALL_API.IMAGE_BASE_URL}${path}`;
|
|
41
61
|
}
|
|
42
62
|
/**
|
|
@@ -4,11 +4,17 @@
|
|
|
4
4
|
* 다이소 API 호출 시 필요한 기본 헤더를 적용합니다.
|
|
5
5
|
*/
|
|
6
6
|
import { fetchJson, fetchText, fetchWithTimeout } from '../../utils/http.js';
|
|
7
|
+
import { DAISOMALL_API } from './api.js';
|
|
8
|
+
const DAISO_AUTH_KEY = 'PRE_AUTH_ENC_KEY';
|
|
7
9
|
const DAISO_DEFAULT_HEADERS = {
|
|
8
10
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
9
11
|
Accept: 'application/json, text/html, */*',
|
|
10
12
|
'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
|
|
11
13
|
};
|
|
14
|
+
const DAISO_DEFAULT_FETCH_OPTIONS = {
|
|
15
|
+
retries: 1,
|
|
16
|
+
retryDelayMs: 250,
|
|
17
|
+
};
|
|
12
18
|
function withDaisoHeaders(headers = {}) {
|
|
13
19
|
return {
|
|
14
20
|
...DAISO_DEFAULT_HEADERS,
|
|
@@ -17,19 +23,80 @@ function withDaisoHeaders(headers = {}) {
|
|
|
17
23
|
}
|
|
18
24
|
export async function daisoFetch(url, options = {}) {
|
|
19
25
|
return fetchWithTimeout(url, {
|
|
26
|
+
...DAISO_DEFAULT_FETCH_OPTIONS,
|
|
20
27
|
...options,
|
|
21
28
|
headers: withDaisoHeaders(options.headers),
|
|
22
29
|
});
|
|
23
30
|
}
|
|
24
31
|
export async function fetchDaisoJson(url, options = {}) {
|
|
25
32
|
return fetchJson(url, {
|
|
33
|
+
...DAISO_DEFAULT_FETCH_OPTIONS,
|
|
26
34
|
...options,
|
|
27
35
|
headers: withDaisoHeaders(options.headers),
|
|
28
36
|
});
|
|
29
37
|
}
|
|
30
38
|
export async function fetchDaisoHtml(url, options = {}) {
|
|
31
39
|
return fetchText(url, {
|
|
40
|
+
...DAISO_DEFAULT_FETCH_OPTIONS,
|
|
32
41
|
...options,
|
|
33
42
|
headers: withDaisoHeaders(options.headers),
|
|
34
43
|
});
|
|
35
44
|
}
|
|
45
|
+
function base64FromBytes(bytes) {
|
|
46
|
+
/* c8 ignore next -- Node 20 CI always provides Buffer; the fallback below is for browser-like runtimes. */
|
|
47
|
+
if (typeof Buffer !== 'undefined') {
|
|
48
|
+
return Buffer.from(bytes).toString('base64');
|
|
49
|
+
}
|
|
50
|
+
/* c8 ignore start -- Node 20 CI always provides Buffer; this fallback is for browser-like runtimes. */
|
|
51
|
+
let binary = '';
|
|
52
|
+
for (const byte of bytes) {
|
|
53
|
+
binary += String.fromCharCode(byte);
|
|
54
|
+
}
|
|
55
|
+
if (typeof btoa === 'function') {
|
|
56
|
+
return btoa(binary);
|
|
57
|
+
}
|
|
58
|
+
throw new Error('Base64 인코딩을 지원하지 않는 환경입니다.');
|
|
59
|
+
/* c8 ignore stop */
|
|
60
|
+
}
|
|
61
|
+
async function createDaisoAuthHeader(token) {
|
|
62
|
+
const encoder = new TextEncoder();
|
|
63
|
+
const iv = crypto.getRandomValues(new Uint8Array(16));
|
|
64
|
+
const key = await crypto.subtle.importKey('raw', encoder.encode(DAISO_AUTH_KEY), { name: 'AES-CBC' }, false, ['encrypt']);
|
|
65
|
+
const encrypted = await crypto.subtle.encrypt({ name: 'AES-CBC', iv }, key, encoder.encode(token));
|
|
66
|
+
return `${base64FromBytes(iv)}${base64FromBytes(new Uint8Array(encrypted))}`;
|
|
67
|
+
}
|
|
68
|
+
export async function createDaisoAuthContext() {
|
|
69
|
+
const response = await daisoFetch(DAISOMALL_API.AUTH_REQUEST, {
|
|
70
|
+
method: 'GET',
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
throw new Error(`다이소 인증 토큰 요청 실패: ${response.status} ${response.statusText}`);
|
|
74
|
+
}
|
|
75
|
+
const token = (await response.text()).trim();
|
|
76
|
+
const dmUid = response.headers.get('x-dm-uid')?.trim() || '';
|
|
77
|
+
if (!token) {
|
|
78
|
+
throw new Error('다이소 인증 토큰이 비어 있습니다.');
|
|
79
|
+
}
|
|
80
|
+
if (!dmUid) {
|
|
81
|
+
throw new Error('다이소 인증 응답에 X-DM-UID 헤더가 없습니다.');
|
|
82
|
+
}
|
|
83
|
+
const authorization = await createDaisoAuthHeader(token);
|
|
84
|
+
return {
|
|
85
|
+
authorization: `Bearer ${authorization}`,
|
|
86
|
+
dmUid,
|
|
87
|
+
cookie: `DM_UID=${dmUid}`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export async function fetchDaisoJsonWithAuth(url, options = {}) {
|
|
91
|
+
const auth = await createDaisoAuthContext();
|
|
92
|
+
return fetchJson(url, {
|
|
93
|
+
...DAISO_DEFAULT_FETCH_OPTIONS,
|
|
94
|
+
...options,
|
|
95
|
+
headers: withDaisoHeaders({
|
|
96
|
+
...options.headers,
|
|
97
|
+
Authorization: auth.authorization,
|
|
98
|
+
'X-DM-UID': auth.dmUid,
|
|
99
|
+
Cookie: auth.cookie,
|
|
100
|
+
}),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
import { createSearchProductsTool } from './tools/searchProducts.js';
|
|
7
7
|
import { createFindStoresTool } from './tools/findStores.js';
|
|
8
8
|
import { createCheckInventoryTool } from './tools/checkInventory.js';
|
|
9
|
+
import { createFindInventoryByNameTool } from './tools/findInventoryByName.js';
|
|
9
10
|
import { createGetPriceInfoTool } from './tools/getPriceInfo.js';
|
|
11
|
+
import { createGetDisplayLocationTool } from './tools/getDisplayLocation.js';
|
|
10
12
|
/**
|
|
11
13
|
* 다이소 서비스 메타데이터
|
|
12
14
|
*/
|
|
@@ -26,7 +28,9 @@ class DaisoService {
|
|
|
26
28
|
createSearchProductsTool(),
|
|
27
29
|
createFindStoresTool(),
|
|
28
30
|
createCheckInventoryTool(),
|
|
31
|
+
createFindInventoryByNameTool(),
|
|
29
32
|
createGetPriceInfoTool(),
|
|
33
|
+
createGetDisplayLocationTool(),
|
|
30
34
|
];
|
|
31
35
|
}
|
|
32
36
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 다이소 상품 매핑 유틸리티
|
|
3
|
+
*/
|
|
4
|
+
import { getImageUrl } from './api.js';
|
|
5
|
+
/**
|
|
6
|
+
* 검색/상세 응답의 상품 문서를 공통 요약 형태로 변환합니다.
|
|
7
|
+
*/
|
|
8
|
+
export function toProductSummary(product) {
|
|
9
|
+
return {
|
|
10
|
+
id: product.PD_NO,
|
|
11
|
+
name: product.PDNM || product.EXH_PD_NM || '',
|
|
12
|
+
imageUrl: getImageUrl(product.ATCH_FILE_URL),
|
|
13
|
+
brand: product.BRND_NM || undefined,
|
|
14
|
+
soldOut: product.SOLD_OUT_YN === 'Y',
|
|
15
|
+
isNew: product.NEW_PD_YN === 'Y',
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -5,7 +5,23 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as z from 'zod';
|
|
7
7
|
import { DAISOMALL_API } from '../api.js';
|
|
8
|
-
import { fetchDaisoJson } from '../client.js';
|
|
8
|
+
import { fetchDaisoJson, fetchDaisoJsonWithAuth } from '../client.js';
|
|
9
|
+
import { buildDaisoStoreKeywordVariants } from '../../../utils/daisoKeyword.js';
|
|
10
|
+
import { fetchProductById } from './getPriceInfo.js';
|
|
11
|
+
import { toProductSummary } from '../product.js';
|
|
12
|
+
/**
|
|
13
|
+
* 재고 응답에 함께 노출할 상품 요약 정보를 조회합니다.
|
|
14
|
+
* 부가 정보이므로 실패해도 전체 재고 조회는 계속 진행합니다.
|
|
15
|
+
*/
|
|
16
|
+
async function fetchInventoryProduct(productId) {
|
|
17
|
+
try {
|
|
18
|
+
const product = await fetchProductById(productId);
|
|
19
|
+
return product ? toProductSummary(product) : undefined;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
9
25
|
/**
|
|
10
26
|
* 온라인 재고 조회
|
|
11
27
|
*/
|
|
@@ -24,46 +40,63 @@ export async function fetchOnlineStock(productNo) {
|
|
|
24
40
|
* 매장별 재고 조회
|
|
25
41
|
*/
|
|
26
42
|
export async function fetchStoreInventory(productNo, lat, lng, page = 1, pageSize = 30, keyword = '') {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
const searchKeywords = keyword ? buildDaisoStoreKeywordVariants(keyword) : [''];
|
|
44
|
+
for (const searchKeyword of searchKeywords) {
|
|
45
|
+
const storeSearch = await fetchDaisoJson(DAISOMALL_API.STORE_SEARCH_V2, {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: { 'Content-Type': 'application/json' },
|
|
48
|
+
body: JSON.stringify({
|
|
49
|
+
inclusiveStrCd: '',
|
|
50
|
+
keyword: searchKeyword,
|
|
51
|
+
curLttd: lat,
|
|
52
|
+
curLitd: lng,
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
const allStores = storeSearch.data || [];
|
|
56
|
+
const searchedStores = allStores.slice((page - 1) * pageSize, page * pageSize);
|
|
57
|
+
if (searchedStores.length === 0) {
|
|
58
|
+
if (searchKeyword === searchKeywords[searchKeywords.length - 1]) {
|
|
59
|
+
return { stores: [], totalCount: 0 };
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const inventoryResponse = await fetchDaisoJsonWithAuth(DAISOMALL_API.STORE_INVENTORY_V2, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: { 'Content-Type': 'application/json' },
|
|
66
|
+
body: JSON.stringify(searchedStores.map((store) => ({
|
|
67
|
+
pdNo: productNo,
|
|
68
|
+
strCd: store.strCd,
|
|
69
|
+
}))),
|
|
70
|
+
});
|
|
71
|
+
const quantities = new Map((inventoryResponse.data || []).map((item) => [item.strCd, parseInt(item.stck) || 0]));
|
|
72
|
+
const stores = searchedStores.map((store) => ({
|
|
73
|
+
storeCode: store.strCd,
|
|
74
|
+
storeName: store.strNm,
|
|
75
|
+
address: store.strAddr,
|
|
76
|
+
phone: store.strTno,
|
|
77
|
+
openTime: store.opngTime,
|
|
78
|
+
closeTime: store.clsngTime,
|
|
79
|
+
lat: store.strLttd,
|
|
80
|
+
lng: store.strLitd,
|
|
81
|
+
distance: store.km,
|
|
82
|
+
quantity: quantities.get(store.strCd) ?? 0,
|
|
83
|
+
options: {
|
|
84
|
+
parking: store.parkYn === 'Y',
|
|
85
|
+
simCard: store.usimYn === 'Y',
|
|
86
|
+
pickup: store.pkupYn === 'Y',
|
|
87
|
+
taxFree: store.taxfYn === 'Y',
|
|
88
|
+
elevator: store.elvtYn === 'Y',
|
|
89
|
+
ramp: store.entrRampYn === 'Y',
|
|
90
|
+
cashless: store.nocashYn === 'Y',
|
|
91
|
+
},
|
|
92
|
+
}));
|
|
93
|
+
return {
|
|
94
|
+
stores,
|
|
95
|
+
totalCount: allStores.length,
|
|
96
|
+
};
|
|
44
97
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
storeName: store.strNm,
|
|
48
|
-
address: store.strAddr,
|
|
49
|
-
phone: store.strTno,
|
|
50
|
-
openTime: store.opngTime,
|
|
51
|
-
closeTime: store.clsngTime,
|
|
52
|
-
lat: store.strLttd,
|
|
53
|
-
lng: store.strLitd,
|
|
54
|
-
distance: store.km,
|
|
55
|
-
quantity: parseInt(store.qty) || 0,
|
|
56
|
-
options: {
|
|
57
|
-
parking: store.parkYn === 'Y',
|
|
58
|
-
simCard: store.usimYn === 'Y',
|
|
59
|
-
pickup: store.pkupYn === 'Y',
|
|
60
|
-
taxFree: store.taxfYn === 'Y',
|
|
61
|
-
elevator: store.elvtYn === 'Y',
|
|
62
|
-
ramp: store.entrRampYn === 'Y',
|
|
63
|
-
cashless: store.nocashYn === 'Y',
|
|
64
|
-
},
|
|
65
|
-
}));
|
|
66
|
-
return { stores, totalCount: data.data.intStrCont || stores.length };
|
|
98
|
+
/* c8 ignore next -- searchKeywords is always non-empty; this is a defensive fallback. */
|
|
99
|
+
return { stores: [], totalCount: 0 };
|
|
67
100
|
}
|
|
68
101
|
/**
|
|
69
102
|
* 재고 확인 핸들러
|
|
@@ -75,15 +108,17 @@ async function checkInventory(args) {
|
|
|
75
108
|
throw new Error('상품 ID(productId)를 입력해주세요.');
|
|
76
109
|
}
|
|
77
110
|
// 온라인 재고와 매장 재고 동시 조회
|
|
78
|
-
const [onlineStock, storeResult] = await Promise.all([
|
|
111
|
+
const [onlineStock, storeResult, product] = await Promise.all([
|
|
79
112
|
fetchOnlineStock(productId),
|
|
80
113
|
fetchStoreInventory(productId, latitude, longitude, page, pageSize, storeQuery),
|
|
114
|
+
fetchInventoryProduct(productId),
|
|
81
115
|
]);
|
|
82
116
|
// 재고 있는 매장과 없는 매장 분류
|
|
83
117
|
const inStockStores = storeResult.stores.filter((s) => s.quantity > 0);
|
|
84
118
|
const outOfStockStores = storeResult.stores.filter((s) => s.quantity === 0);
|
|
85
119
|
const result = {
|
|
86
120
|
productId,
|
|
121
|
+
product,
|
|
87
122
|
location: { latitude, longitude },
|
|
88
123
|
onlineStock,
|
|
89
124
|
storeInventory: {
|
|
@@ -107,9 +142,11 @@ export function createCheckInventoryTool() {
|
|
|
107
142
|
name: 'daiso_check_inventory',
|
|
108
143
|
metadata: {
|
|
109
144
|
title: '재고 확인',
|
|
110
|
-
description: '특정 제품의 매장별 재고를 확인합니다. 매장명/주소 검색 또는 위치 기반으로 조회합니다.',
|
|
145
|
+
description: '특정 제품의 매장별 재고를 확인합니다. 상품명만 아는 경우 daiso_search_products로 후보를 먼저 찾은 뒤 productId를 전달하세요. 매장명/주소 검색 또는 위치 기반으로 조회합니다. "근처/주변"처럼 위치가 모호하면 storeQuery, latitude/longitude 또는 사용자 위치를 먼저 확인하세요.',
|
|
111
146
|
inputSchema: {
|
|
112
|
-
productId: z
|
|
147
|
+
productId: z
|
|
148
|
+
.string()
|
|
149
|
+
.describe('제품 ID. 상품명만 알면 먼저 daiso_search_products로 상품의 id를 확인하세요.'),
|
|
113
150
|
storeQuery: z.string().optional().describe('매장 검색어 (매장명 또는 주소, 예: 안산 중앙역)'),
|
|
114
151
|
latitude: z.number().optional().default(37.5665).describe('위도 (기본값: 서울 시청 37.5665)'),
|
|
115
152
|
longitude: z.number().optional().default(126.978).describe('경도 (기본값: 서울 시청 126.978)'),
|