daiso 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +218 -540
- package/dist/api/actionsProxy.js +82 -0
- package/dist/api/cgvHandlers.js +103 -16
- package/dist/api/daisoHandlers.js +127 -0
- package/dist/api/emart24Handlers.js +176 -0
- package/dist/api/gs25Handlers.js +256 -0
- package/dist/api/handlers.js +184 -108
- package/dist/api/healthChecks.js +332 -0
- package/dist/api/lottecinemaHandlers.js +155 -0
- package/dist/api/lottemartHandlers.js +128 -0
- package/dist/api/megaboxHandlers.js +81 -50
- package/dist/api/routes/cuRoutes.js +17 -0
- package/dist/api/routes/daisoRoutes.js +6 -1
- package/dist/api/routes/emart24Routes.js +22 -0
- package/dist/api/routes/gs25Routes.js +22 -0
- package/dist/api/routes/healthRoutes.js +44 -0
- package/dist/api/routes/lottecinemaRoutes.js +22 -0
- package/dist/api/routes/lottemartRoutes.js +18 -0
- package/dist/api/routes/oliveyoungRoutes.js +6 -1
- package/dist/api/routes/sevenelevenRoutes.js +32 -0
- package/dist/api/sevenelevenHandlers.js +200 -0
- package/dist/cli/args.js +54 -0
- package/dist/cli/commands/convenience.js +241 -0
- package/dist/cli/commands/daiso.js +96 -0
- package/dist/cli/constants.js +5 -0
- package/dist/cli/deps.js +48 -0
- package/dist/cli/http.js +98 -0
- package/dist/cli/interactiveFetch.js +31 -0
- package/dist/cli/interactiveItemSearch.js +259 -0
- package/dist/cli/interactivePrompt.js +49 -0
- package/dist/cli/interactiveTypes.js +4 -0
- package/dist/cli/types.js +4 -0
- package/dist/cli.js +66 -197
- package/dist/cliHelp.js +206 -1
- package/dist/cliInteractive.js +174 -0
- package/dist/cliPicker.js +56 -0
- package/dist/cliRenderer.js +4 -0
- package/dist/index.js +147 -8
- package/dist/pages/openapi.js +2 -1
- package/dist/pages/openapiFullSpec.js +58 -0
- package/dist/pages/openapiResponse.js +28 -0
- package/dist/pages/openapiSpec.js +2 -41
- package/dist/pages/openapiSpecActions.js +109 -0
- package/dist/pages/openapiSpecComponents.js +30 -391
- package/dist/pages/openapiSpecComponentsCgv.js +24 -0
- package/dist/pages/openapiSpecComponentsCu.js +112 -0
- package/dist/pages/openapiSpecComponentsDaisoOliveyoung.js +342 -0
- package/dist/pages/openapiSpecComponentsEmart24.js +141 -0
- package/dist/pages/openapiSpecComponentsGs25.js +109 -0
- package/dist/pages/openapiSpecComponentsLotteMart.js +85 -0
- package/dist/pages/openapiSpecComponentsLottecinema.js +163 -0
- package/dist/pages/openapiSpecComponentsMegabox.js +144 -0
- package/dist/pages/openapiSpecComponentsSeveneleven.js +82 -0
- package/dist/pages/openapiSpecPathsCgv.js +66 -3
- package/dist/pages/openapiSpecPathsCu.js +138 -0
- package/dist/pages/openapiSpecPathsDaiso.js +302 -0
- package/dist/pages/openapiSpecPathsDaisoOliveyoung.js +4 -376
- package/dist/pages/openapiSpecPathsEmart24.js +223 -0
- package/dist/pages/openapiSpecPathsGs25.js +166 -0
- package/dist/pages/openapiSpecPathsLotteMart.js +135 -0
- package/dist/pages/openapiSpecPathsLottecinema.js +212 -0
- package/dist/pages/openapiSpecPathsMegabox.js +52 -3
- package/dist/pages/openapiSpecPathsOliveyoung.js +231 -0
- package/dist/pages/openapiSpecPathsSeveneleven.js +234 -0
- package/dist/pages/prompt.js +8 -402
- package/dist/pages/promptCoreText.js +278 -0
- package/dist/pages/promptReferenceText.js +131 -0
- package/dist/pages/promptRetailCinemaText.js +273 -0
- package/dist/services/cgv/client.js +6 -1
- package/dist/services/cgv/index.js +3 -3
- package/dist/services/cgv/location.js +235 -0
- package/dist/services/cgv/tools/findTheaters.js +30 -5
- package/dist/services/cgv/tools/getTimetable.js +42 -14
- package/dist/services/cgv/tools/searchMovies.js +40 -12
- package/dist/services/cgv/transport.js +0 -3
- package/dist/services/cu/api.js +11 -0
- package/dist/services/cu/client.js +297 -0
- package/dist/services/cu/index.js +21 -0
- package/dist/services/cu/tools/checkInventory.js +123 -0
- package/dist/services/cu/tools/findNearbyStores.js +49 -0
- package/dist/services/cu/types.js +4 -0
- package/dist/services/daiso/api.js +22 -2
- package/dist/services/daiso/client.js +59 -0
- package/dist/services/daiso/index.js +2 -0
- package/dist/services/daiso/product.js +17 -0
- package/dist/services/daiso/tools/checkInventory.js +76 -41
- package/dist/services/daiso/tools/findStores.js +17 -7
- package/dist/services/daiso/tools/getDisplayLocation.js +75 -0
- package/dist/services/emart24/api.js +11 -0
- package/dist/services/emart24/client.js +173 -0
- package/dist/services/emart24/index.js +22 -0
- package/dist/services/emart24/inventoryLookup.js +134 -0
- package/dist/services/emart24/tools/checkInventory.js +54 -0
- package/dist/services/emart24/tools/findNearbyStores.js +72 -0
- package/dist/services/emart24/tools/searchProducts.js +63 -0
- package/dist/services/emart24/types.js +4 -0
- package/dist/services/gs25/api.js +9 -0
- package/dist/services/gs25/client.js +198 -0
- package/dist/services/gs25/index.js +22 -0
- package/dist/services/gs25/storeUtils.js +168 -0
- package/dist/services/gs25/tools/checkInventory.js +163 -0
- package/dist/services/gs25/tools/findNearbyStores.js +103 -0
- package/dist/services/gs25/tools/searchProducts.js +50 -0
- package/dist/services/gs25/types.js +4 -0
- package/dist/services/lottecinema/api.js +12 -0
- package/dist/services/lottecinema/client.js +161 -0
- package/dist/services/lottecinema/index.js +30 -0
- package/dist/services/lottecinema/location.js +191 -0
- package/dist/services/lottecinema/tools/findNearbyTheaters.js +47 -0
- package/dist/services/lottecinema/tools/getRemainingSeats.js +85 -0
- package/dist/services/lottecinema/tools/listNowShowing.js +79 -0
- package/dist/services/lottecinema/types.js +4 -0
- package/dist/services/lottemart/api.js +31 -0
- package/dist/services/lottemart/client.js +245 -0
- package/dist/services/lottemart/clientTypes.js +4 -0
- package/dist/services/lottemart/config.js +4 -0
- package/dist/services/lottemart/debug.js +92 -0
- package/dist/services/lottemart/geocode.js +49 -0
- package/dist/services/lottemart/index.js +30 -0
- package/dist/services/lottemart/keyword.js +29 -0
- package/dist/services/lottemart/parser.js +185 -0
- package/dist/services/lottemart/session.js +331 -0
- package/dist/services/lottemart/storeSearch.js +52 -0
- package/dist/services/lottemart/tools/findNearbyStores.js +67 -0
- package/dist/services/lottemart/tools/searchProducts.js +62 -0
- package/dist/services/lottemart/types.js +4 -0
- package/dist/services/lottemart/zetta.js +105 -0
- package/dist/services/megabox/location.js +219 -0
- package/dist/services/megabox/tools/findNearbyTheaters.js +13 -44
- package/dist/services/megabox/tools/getRemainingSeats.js +31 -3
- package/dist/services/megabox/tools/listNowShowing.js +31 -3
- package/dist/services/oliveyoung/api.js +2 -0
- package/dist/services/oliveyoung/client.js +150 -9
- package/dist/services/oliveyoung/index.js +3 -1
- package/dist/services/oliveyoung/tools/checkInventory.js +15 -4
- package/dist/services/oliveyoung/tools/searchProducts.js +57 -0
- package/dist/services/seveneleven/api.js +15 -0
- package/dist/services/seveneleven/client.js +279 -0
- package/dist/services/seveneleven/index.js +30 -0
- package/dist/services/seveneleven/inventory.js +302 -0
- package/dist/services/seveneleven/productKeyword.js +141 -0
- package/dist/services/seveneleven/tools/checkInventory.js +82 -0
- package/dist/services/seveneleven/tools/getCatalogSnapshot.js +55 -0
- package/dist/services/seveneleven/tools/getSearchPopwords.js +41 -0
- package/dist/services/seveneleven/tools/searchProducts.js +56 -0
- package/dist/services/seveneleven/tools/searchStores.js +45 -0
- package/dist/services/seveneleven/types.js +4 -0
- package/dist/utils/cliInteractiveHelpers.js +63 -0
- package/dist/utils/daisoKeyword.js +30 -0
- package/dist/utils/http.js +16 -2
- package/openapi.json +1977 -734
- package/openapi.yaml +1430 -543
- package/package.json +28 -15
- package/dist/tools/checkInventory.js +0 -98
- package/dist/tools/findStores.js +0 -125
- package/dist/tools/getPriceInfo.js +0 -77
- package/dist/tools/searchProducts.js +0 -61
- package/dist/types/index.js +0 -4
|
@@ -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: {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import * as z from 'zod';
|
|
7
7
|
import { DAISO_WEB_API, formatTime } from '../api.js';
|
|
8
8
|
import { fetchDaisoHtml, fetchDaisoJson } from '../client.js';
|
|
9
|
+
import { buildDaisoStoreKeywordVariants } from '../../../utils/daisoKeyword.js';
|
|
9
10
|
/**
|
|
10
11
|
* HTML에서 매장 정보 파싱
|
|
11
12
|
*/
|
|
@@ -75,13 +76,22 @@ function parseStoresFromHtml(html) {
|
|
|
75
76
|
* 다이소 매장 검색 API 호출
|
|
76
77
|
*/
|
|
77
78
|
export async function fetchStores(keyword, sido, gugun, dong) {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
const searchKeywords = keyword && !sido && !gugun && !dong
|
|
80
|
+
? buildDaisoStoreKeywordVariants(keyword)
|
|
81
|
+
: [keyword || ''];
|
|
82
|
+
for (const searchKeyword of searchKeywords) {
|
|
83
|
+
const url = new URL(DAISO_WEB_API.SHOP_SEARCH);
|
|
84
|
+
url.searchParams.set('name_address', searchKeyword);
|
|
85
|
+
url.searchParams.set('sido', sido || '');
|
|
86
|
+
url.searchParams.set('gugun', gugun || '');
|
|
87
|
+
url.searchParams.set('dong', dong || '');
|
|
88
|
+
const html = await fetchDaisoHtml(url.toString());
|
|
89
|
+
const stores = parseStoresFromHtml(html);
|
|
90
|
+
if (stores.length > 0) {
|
|
91
|
+
return stores;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return [];
|
|
85
95
|
}
|
|
86
96
|
/**
|
|
87
97
|
* 시/도별 구/군 목록 조회
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 진열 위치 조회 도구
|
|
3
|
+
*
|
|
4
|
+
* 다이소몰 API를 사용하여 매장 내 상품 진열 위치(구역/층)를 조회합니다.
|
|
5
|
+
*/
|
|
6
|
+
import * as z from 'zod';
|
|
7
|
+
import { DAISOMALL_API } from '../api.js';
|
|
8
|
+
import { fetchDaisoJsonWithAuth } from '../client.js';
|
|
9
|
+
/**
|
|
10
|
+
* 매장 내 상품 진열 위치 조회
|
|
11
|
+
*/
|
|
12
|
+
export async function fetchDisplayLocation(productId, storeCode) {
|
|
13
|
+
const data = await fetchDaisoJsonWithAuth(DAISOMALL_API.DISPLAY_LOCATION, {
|
|
14
|
+
method: 'POST',
|
|
15
|
+
headers: { 'Content-Type': 'application/json' },
|
|
16
|
+
body: JSON.stringify({ pdNo: productId, strCd: storeCode }),
|
|
17
|
+
});
|
|
18
|
+
if (!data.success) {
|
|
19
|
+
return {
|
|
20
|
+
productId,
|
|
21
|
+
storeCode,
|
|
22
|
+
hasLocation: false,
|
|
23
|
+
locations: [],
|
|
24
|
+
message: data.message ?? null,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const locations = Array.isArray(data.data)
|
|
28
|
+
? data.data.map((item) => ({
|
|
29
|
+
zoneNo: String(item.zoneNo ?? ''),
|
|
30
|
+
stairNo: String(item.stairNo ?? ''),
|
|
31
|
+
storeErp: String(item.storeErp ?? storeCode),
|
|
32
|
+
}))
|
|
33
|
+
: [];
|
|
34
|
+
return {
|
|
35
|
+
productId,
|
|
36
|
+
storeCode,
|
|
37
|
+
hasLocation: locations.length > 0,
|
|
38
|
+
locations,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 진열 위치 조회 핸들러
|
|
43
|
+
*/
|
|
44
|
+
async function getDisplayLocation(args) {
|
|
45
|
+
const { productId, storeCode } = args;
|
|
46
|
+
if (!productId || productId.trim().length === 0) {
|
|
47
|
+
throw new Error('상품 ID(productId)를 입력해주세요.');
|
|
48
|
+
}
|
|
49
|
+
if (!storeCode || storeCode.trim().length === 0) {
|
|
50
|
+
throw new Error('매장 코드(storeCode)를 입력해주세요.');
|
|
51
|
+
}
|
|
52
|
+
const result = await fetchDisplayLocation(productId, storeCode);
|
|
53
|
+
return {
|
|
54
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 도구 등록 정보 생성
|
|
59
|
+
*/
|
|
60
|
+
export function createGetDisplayLocationTool() {
|
|
61
|
+
return {
|
|
62
|
+
name: 'daiso_get_display_location',
|
|
63
|
+
metadata: {
|
|
64
|
+
title: '진열 위치 조회',
|
|
65
|
+
description: '다이소 매장 내 상품의 진열 위치(구역/층)를 조회합니다. 상품 ID와 매장 코드가 필요합니다.',
|
|
66
|
+
inputSchema: {
|
|
67
|
+
productId: z.string().describe('상품 ID (daiso_search_products로 조회한 상품의 id)'),
|
|
68
|
+
storeCode: z
|
|
69
|
+
.string()
|
|
70
|
+
.describe('매장 코드 (daiso_check_inventory로 조회한 매장의 storeCode)'),
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
handler: getDisplayLocation,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 이마트24 API 엔드포인트 중앙 관리
|
|
3
|
+
*/
|
|
4
|
+
export const EMART24_API = {
|
|
5
|
+
WEB_BASE_URL: 'https://emart24.co.kr',
|
|
6
|
+
EVERSE_BASE_URL: 'https://everse.emart24.co.kr',
|
|
7
|
+
STORE_PATH: '/api1/store',
|
|
8
|
+
PRODUCT_SEARCH_PATH: '/stock/stock/search',
|
|
9
|
+
STOCK_SEARCH_STORE_PATH: '/api/stock/v2/stock-search/store',
|
|
10
|
+
STORE_DETAIL_PATH_PREFIX: '/api/stock/stock/store/',
|
|
11
|
+
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 이마트24 API 클라이언트
|
|
3
|
+
*/
|
|
4
|
+
import { fetchJson } from '../../utils/http.js';
|
|
5
|
+
import { EMART24_API } from './api.js';
|
|
6
|
+
const EMART24_JSON_HEADERS = {
|
|
7
|
+
Accept: 'application/json, text/javascript, */*; q=0.01',
|
|
8
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
9
|
+
};
|
|
10
|
+
const EMART24_FORM_HEADERS = {
|
|
11
|
+
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
12
|
+
Accept: 'application/json, text/javascript, */*; q=0.01',
|
|
13
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
14
|
+
};
|
|
15
|
+
function buildKeywordVariants(keyword) {
|
|
16
|
+
const trimmed = keyword.trim();
|
|
17
|
+
if (trimmed.length === 0) {
|
|
18
|
+
return [''];
|
|
19
|
+
}
|
|
20
|
+
const tokens = trimmed.split(/\s+/).filter((token) => token.length > 0);
|
|
21
|
+
const variants = [
|
|
22
|
+
trimmed,
|
|
23
|
+
trimmed.replace(/\s+/g, ''),
|
|
24
|
+
...tokens,
|
|
25
|
+
...tokens.map((token) => token.replace(/역$/, '')),
|
|
26
|
+
].filter((value) => value.length > 0);
|
|
27
|
+
return [...new Set(variants)];
|
|
28
|
+
}
|
|
29
|
+
function toNumber(value) {
|
|
30
|
+
if (typeof value === 'number') {
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
if (typeof value === 'string') {
|
|
34
|
+
const parsed = Number.parseFloat(value);
|
|
35
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
36
|
+
}
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
function toBooleanFlag(value) {
|
|
40
|
+
if (typeof value === 'boolean') {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
const normalized = String(value || '').trim().toUpperCase();
|
|
44
|
+
return normalized === '1' || normalized === 'Y' || normalized === 'TRUE';
|
|
45
|
+
}
|
|
46
|
+
function toStore(item) {
|
|
47
|
+
return {
|
|
48
|
+
storeCode: item.CODE || '',
|
|
49
|
+
storeName: item.TITLE || '',
|
|
50
|
+
address: item.ADDRESS || '',
|
|
51
|
+
addressDetail: item.ADDRESS_DE || '',
|
|
52
|
+
phone: item.PHONE || '',
|
|
53
|
+
latitude: toNumber(item.LATITUDE),
|
|
54
|
+
longitude: toNumber(item.LONGITUDE),
|
|
55
|
+
openTime: item.START_HHMM || '',
|
|
56
|
+
closeTime: item.END_HHMM || '',
|
|
57
|
+
openDate: item.OPEN_DATE || '',
|
|
58
|
+
endDate: item.END_DATE || '',
|
|
59
|
+
service24h: toBooleanFlag(item.SVR_24),
|
|
60
|
+
distanceM: null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function toProduct(item) {
|
|
64
|
+
return {
|
|
65
|
+
pluCd: item.pluCd || '',
|
|
66
|
+
goodsName: item.goodsNm || '',
|
|
67
|
+
originPrice: toNumber(item.originPrice),
|
|
68
|
+
viewPrice: toNumber(item.viewPrice),
|
|
69
|
+
category: item.categoryNm || '',
|
|
70
|
+
kind: item.kindNm || '',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function calculateDistanceM(lat1, lng1, lat2, lng2) {
|
|
74
|
+
const toRad = (deg) => (deg * Math.PI) / 180;
|
|
75
|
+
const dLat = toRad(lat2 - lat1);
|
|
76
|
+
const dLng = toRad(lng2 - lng1);
|
|
77
|
+
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
78
|
+
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
|
79
|
+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
80
|
+
return Math.round(6371000 * c);
|
|
81
|
+
}
|
|
82
|
+
export async function fetchEmart24Stores(params = {}, options = {}) {
|
|
83
|
+
const { timeout = 15000 } = options;
|
|
84
|
+
const { keyword = '', area1 = '', area2 = '', page = 1, service24h = false } = params;
|
|
85
|
+
const keywordVariants = buildKeywordVariants(keyword);
|
|
86
|
+
let lastError;
|
|
87
|
+
for (let index = 0; index < keywordVariants.length; index += 1) {
|
|
88
|
+
const searchKeyword = keywordVariants[index];
|
|
89
|
+
const endpoint = new URL(EMART24_API.STORE_PATH, EMART24_API.WEB_BASE_URL);
|
|
90
|
+
endpoint.searchParams.set('page', String(page));
|
|
91
|
+
if (searchKeyword.length > 0) {
|
|
92
|
+
endpoint.searchParams.set('search', searchKeyword);
|
|
93
|
+
}
|
|
94
|
+
if (area1.trim().length > 0) {
|
|
95
|
+
endpoint.searchParams.set('AREA1', area1.trim());
|
|
96
|
+
}
|
|
97
|
+
if (area2.trim().length > 0) {
|
|
98
|
+
endpoint.searchParams.set('AREA2', area2.trim());
|
|
99
|
+
}
|
|
100
|
+
if (service24h) {
|
|
101
|
+
endpoint.searchParams.set('SVR_24', '1');
|
|
102
|
+
}
|
|
103
|
+
const body = await fetchJson(endpoint.toString(), {
|
|
104
|
+
method: 'GET',
|
|
105
|
+
timeout,
|
|
106
|
+
headers: EMART24_JSON_HEADERS,
|
|
107
|
+
});
|
|
108
|
+
if (body.error === 0 || body.error === undefined) {
|
|
109
|
+
const stores = (body.data || []).map(toStore).filter((store) => store.storeCode.length > 0);
|
|
110
|
+
return {
|
|
111
|
+
totalCount: toNumber(body.count),
|
|
112
|
+
stores,
|
|
113
|
+
appliedKeyword: searchKeyword,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
lastError = body.error;
|
|
117
|
+
const hasMoreVariants = index < keywordVariants.length - 1;
|
|
118
|
+
if (body.error !== 1 || !hasMoreVariants) {
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
throw new Error(`이마트24 매장 조회 실패: error=${lastError}`);
|
|
123
|
+
}
|
|
124
|
+
export async function searchEmart24Products(params, options = {}) {
|
|
125
|
+
const { keyword, page = 1, pageSize = 10, sortType = 'SALE', saleProductYn = 'N', } = params;
|
|
126
|
+
const { timeout = 15000 } = options;
|
|
127
|
+
const form = new URLSearchParams();
|
|
128
|
+
form.set('currentPage', String(page));
|
|
129
|
+
form.set('pageCnt', String(pageSize));
|
|
130
|
+
form.set('sortType', sortType);
|
|
131
|
+
form.set('saleProductYn', saleProductYn);
|
|
132
|
+
form.set('searchWord', keyword.trim());
|
|
133
|
+
const body = await fetchJson(`${EMART24_API.EVERSE_BASE_URL}${EMART24_API.PRODUCT_SEARCH_PATH}`, {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
timeout,
|
|
136
|
+
headers: EMART24_FORM_HEADERS,
|
|
137
|
+
body: form.toString(),
|
|
138
|
+
});
|
|
139
|
+
const products = (body.productList || []).map(toProduct).filter((item) => item.pluCd.length > 0);
|
|
140
|
+
return {
|
|
141
|
+
totalCount: toNumber(body.totalCnt),
|
|
142
|
+
products,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
export async function searchEmart24StockByStores(params, options = {}) {
|
|
146
|
+
const { timeout = 15000 } = options;
|
|
147
|
+
const { pluCd, bizNos } = params;
|
|
148
|
+
const filteredBizNos = bizNos.map((value) => value.trim()).filter((value) => value.length > 0);
|
|
149
|
+
if (filteredBizNos.length === 0) {
|
|
150
|
+
return {
|
|
151
|
+
storeGoodsInfo: {
|
|
152
|
+
pluCd,
|
|
153
|
+
},
|
|
154
|
+
storeGoodsQty: [],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const endpoint = new URL(EMART24_API.STOCK_SEARCH_STORE_PATH, EMART24_API.EVERSE_BASE_URL);
|
|
158
|
+
endpoint.searchParams.set('searchPluCode', pluCd);
|
|
159
|
+
endpoint.searchParams.set('bizNoArr', filteredBizNos.join(','));
|
|
160
|
+
return fetchJson(endpoint.toString(), {
|
|
161
|
+
method: 'GET',
|
|
162
|
+
timeout,
|
|
163
|
+
headers: EMART24_JSON_HEADERS,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
export async function fetchEmart24StoreDetail(bizNo, options = {}) {
|
|
167
|
+
const { timeout = 15000 } = options;
|
|
168
|
+
return fetchJson(`${EMART24_API.EVERSE_BASE_URL}${EMART24_API.STORE_DETAIL_PATH_PREFIX}${encodeURIComponent(bizNo)}`, {
|
|
169
|
+
method: 'GET',
|
|
170
|
+
timeout,
|
|
171
|
+
headers: EMART24_JSON_HEADERS,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 이마트24 서비스 프로바이더
|
|
3
|
+
*/
|
|
4
|
+
import { createFindNearbyStoresTool } from './tools/findNearbyStores.js';
|
|
5
|
+
import { createSearchProductsTool } from './tools/searchProducts.js';
|
|
6
|
+
import { createCheckInventoryTool } from './tools/checkInventory.js';
|
|
7
|
+
const EMART24_METADATA = {
|
|
8
|
+
id: 'emart24',
|
|
9
|
+
name: '이마트24',
|
|
10
|
+
version: '1.0.0',
|
|
11
|
+
description: '이마트24 매장 탐색, 상품 검색, 매장별 재고 조회 서비스',
|
|
12
|
+
};
|
|
13
|
+
class Emart24Service {
|
|
14
|
+
metadata = EMART24_METADATA;
|
|
15
|
+
getTools() {
|
|
16
|
+
return [createFindNearbyStoresTool(), createSearchProductsTool(), createCheckInventoryTool()];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function createEmart24Service() {
|
|
20
|
+
return new Emart24Service();
|
|
21
|
+
}
|
|
22
|
+
export * from './types.js';
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 이마트24 재고 조회 공통 로직
|
|
3
|
+
*/
|
|
4
|
+
import { calculateDistanceM, fetchEmart24StoreDetail, fetchEmart24Stores, searchEmart24Products, searchEmart24StockByStores, } from './client.js';
|
|
5
|
+
async function resolvePluCd(pluCd, keyword, productPage, productPageSize, timeoutMs) {
|
|
6
|
+
const normalizedPluCd = (pluCd || '').trim();
|
|
7
|
+
if (normalizedPluCd.length > 0) {
|
|
8
|
+
return {
|
|
9
|
+
pluCd: normalizedPluCd,
|
|
10
|
+
products: [],
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
if (keyword.trim().length === 0) {
|
|
14
|
+
throw new Error('pluCd 또는 keyword 중 하나는 반드시 입력해주세요.');
|
|
15
|
+
}
|
|
16
|
+
const productResult = await searchEmart24Products({
|
|
17
|
+
keyword,
|
|
18
|
+
page: productPage,
|
|
19
|
+
pageSize: productPageSize,
|
|
20
|
+
}, {
|
|
21
|
+
timeout: timeoutMs,
|
|
22
|
+
});
|
|
23
|
+
const firstProduct = productResult.products.find((item) => item.pluCd.trim().length > 0);
|
|
24
|
+
if (!firstProduct) {
|
|
25
|
+
throw new Error('상품 검색 결과에서 PLU 코드를 찾을 수 없습니다.');
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
pluCd: firstProduct.pluCd,
|
|
29
|
+
products: productResult.products,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function sortStoresByDistance(stores, latitude, longitude) {
|
|
33
|
+
return [...stores]
|
|
34
|
+
.map((store) => {
|
|
35
|
+
if (typeof latitude !== 'number' ||
|
|
36
|
+
typeof longitude !== 'number' ||
|
|
37
|
+
store.latitude === 0 ||
|
|
38
|
+
store.longitude === 0) {
|
|
39
|
+
return {
|
|
40
|
+
store,
|
|
41
|
+
sortDistance: Number.MAX_SAFE_INTEGER,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const distanceM = calculateDistanceM(latitude, longitude, store.latitude, store.longitude);
|
|
45
|
+
return {
|
|
46
|
+
store: {
|
|
47
|
+
...store,
|
|
48
|
+
distanceM,
|
|
49
|
+
},
|
|
50
|
+
sortDistance: distanceM,
|
|
51
|
+
};
|
|
52
|
+
})
|
|
53
|
+
.sort((a, b) => a.sortDistance - b.sortDistance)
|
|
54
|
+
.map((entry) => entry.store);
|
|
55
|
+
}
|
|
56
|
+
export async function lookupEmart24Inventory(args) {
|
|
57
|
+
const { pluCd, keyword = '', latitude, longitude, storeKeyword = '', area1 = '', area2 = '', service24h = false, productPage = 1, productPageSize = 10, storeLimit = 10, timeoutMs = 15000, bizNos = [], includeRequestedStoresWithoutQty = false, } = args;
|
|
58
|
+
const resolved = await resolvePluCd(pluCd, keyword, productPage, productPageSize, timeoutMs);
|
|
59
|
+
const normalizedBizNos = [...new Set(bizNos.map((value) => value.trim()).filter((value) => value.length > 0))];
|
|
60
|
+
let nearbyStores = [];
|
|
61
|
+
let nearbyTotalCount = normalizedBizNos.length;
|
|
62
|
+
let appliedStoreKeyword = storeKeyword;
|
|
63
|
+
if (normalizedBizNos.length === 0) {
|
|
64
|
+
const nearbyStoreResult = await fetchEmart24Stores({
|
|
65
|
+
keyword: storeKeyword,
|
|
66
|
+
area1,
|
|
67
|
+
area2,
|
|
68
|
+
service24h,
|
|
69
|
+
page: 1,
|
|
70
|
+
}, {
|
|
71
|
+
timeout: timeoutMs,
|
|
72
|
+
});
|
|
73
|
+
appliedStoreKeyword = nearbyStoreResult.appliedKeyword;
|
|
74
|
+
nearbyTotalCount = nearbyStoreResult.totalCount;
|
|
75
|
+
nearbyStores = sortStoresByDistance(nearbyStoreResult.stores, latitude, longitude).slice(0, storeLimit);
|
|
76
|
+
}
|
|
77
|
+
const requestedBizNos = normalizedBizNos.length > 0 ? normalizedBizNos : nearbyStores.map((store) => store.storeCode);
|
|
78
|
+
const stockResult = await searchEmart24StockByStores({
|
|
79
|
+
pluCd: resolved.pluCd,
|
|
80
|
+
bizNos: requestedBizNos,
|
|
81
|
+
}, {
|
|
82
|
+
timeout: timeoutMs,
|
|
83
|
+
});
|
|
84
|
+
const qtyByBizNo = new Map((stockResult.storeGoodsQty || []).map((item) => [
|
|
85
|
+
String(item.BIZNO || '').trim(),
|
|
86
|
+
Number.parseInt(String(item.BIZQTY || 0), 10) || 0,
|
|
87
|
+
]));
|
|
88
|
+
const inventoryBizNos = includeRequestedStoresWithoutQty && requestedBizNos.length > 0
|
|
89
|
+
? requestedBizNos
|
|
90
|
+
: (stockResult.storeGoodsQty || [])
|
|
91
|
+
.map((item) => String(item.BIZNO || '').trim())
|
|
92
|
+
.filter((value) => value.length > 0);
|
|
93
|
+
const detailResults = await Promise.allSettled(inventoryBizNos.map((bizNo) => fetchEmart24StoreDetail(bizNo, { timeout: timeoutMs })));
|
|
94
|
+
const nearbyStoreByCode = new Map(nearbyStores.map((store) => [store.storeCode, store]));
|
|
95
|
+
const stores = inventoryBizNos.map((bizNo, index) => {
|
|
96
|
+
const detail = detailResults[index];
|
|
97
|
+
const detailInfo = detail.status === 'fulfilled' ? detail.value.storeInfo : undefined;
|
|
98
|
+
const nearby = nearbyStoreByCode.get(bizNo);
|
|
99
|
+
return {
|
|
100
|
+
bizNo,
|
|
101
|
+
bizQty: qtyByBizNo.get(bizNo) ?? 0,
|
|
102
|
+
storeName: detailInfo?.storeNm || nearby?.storeName || '',
|
|
103
|
+
address: detailInfo?.storeAddr || nearby?.address || '',
|
|
104
|
+
phone: detailInfo?.tel || nearby?.phone || '',
|
|
105
|
+
distanceM: nearby?.distanceM ?? null,
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
keyword,
|
|
110
|
+
pluCd: resolved.pluCd,
|
|
111
|
+
productCandidates: resolved.products,
|
|
112
|
+
location: typeof latitude === 'number' && typeof longitude === 'number'
|
|
113
|
+
? { latitude, longitude }
|
|
114
|
+
: null,
|
|
115
|
+
storeFilters: {
|
|
116
|
+
storeKeyword,
|
|
117
|
+
appliedStoreKeyword,
|
|
118
|
+
area1,
|
|
119
|
+
area2,
|
|
120
|
+
service24h,
|
|
121
|
+
storeLimit,
|
|
122
|
+
directBizNos: normalizedBizNos,
|
|
123
|
+
},
|
|
124
|
+
nearbyStores: {
|
|
125
|
+
totalCount: nearbyTotalCount,
|
|
126
|
+
stores: nearbyStores,
|
|
127
|
+
},
|
|
128
|
+
inventory: {
|
|
129
|
+
goodsInfo: stockResult.storeGoodsInfo || null,
|
|
130
|
+
count: stores.length,
|
|
131
|
+
stores,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 이마트24 재고 확인 도구
|
|
3
|
+
*/
|
|
4
|
+
import * as z from 'zod';
|
|
5
|
+
import { lookupEmart24Inventory } from '../inventoryLookup.js';
|
|
6
|
+
async function checkInventory(args) {
|
|
7
|
+
const { pluCd, keyword = '', latitude, longitude, storeKeyword = '', area1 = '', area2 = '', service24h = false, productPage = 1, productPageSize = 10, storeLimit = 10, timeoutMs = 15000, } = args;
|
|
8
|
+
const result = await lookupEmart24Inventory({
|
|
9
|
+
pluCd,
|
|
10
|
+
keyword,
|
|
11
|
+
latitude,
|
|
12
|
+
longitude,
|
|
13
|
+
storeKeyword,
|
|
14
|
+
area1,
|
|
15
|
+
area2,
|
|
16
|
+
service24h,
|
|
17
|
+
productPage,
|
|
18
|
+
productPageSize,
|
|
19
|
+
storeLimit,
|
|
20
|
+
timeoutMs,
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
content: [
|
|
24
|
+
{
|
|
25
|
+
type: 'text',
|
|
26
|
+
text: JSON.stringify(result, null, 2),
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function createCheckInventoryTool() {
|
|
32
|
+
return {
|
|
33
|
+
name: 'emart24_check_inventory',
|
|
34
|
+
metadata: {
|
|
35
|
+
title: '이마트24 재고 확인',
|
|
36
|
+
description: 'PLU 코드 또는 상품 키워드로 매장별 재고 수량을 조회합니다.',
|
|
37
|
+
inputSchema: {
|
|
38
|
+
pluCd: z.string().optional().describe('상품 PLU 코드 (예: 8800244010504)'),
|
|
39
|
+
keyword: z.string().optional().describe('상품 검색어 (pluCd 미입력 시 필수)'),
|
|
40
|
+
latitude: z.number().optional().describe('위도 (선택)'),
|
|
41
|
+
longitude: z.number().optional().describe('경도 (선택)'),
|
|
42
|
+
storeKeyword: z.string().optional().describe('매장 키워드 필터 (예: 강남)'),
|
|
43
|
+
area1: z.string().optional().describe('시/도 (예: 서울특별시)'),
|
|
44
|
+
area2: z.string().optional().describe('구/군 (예: 강남구)'),
|
|
45
|
+
service24h: z.boolean().optional().default(false).describe('24시간 매장만 대상으로 조회'),
|
|
46
|
+
productPage: z.number().optional().default(1).describe('상품 검색 페이지 (기본값: 1)'),
|
|
47
|
+
productPageSize: z.number().optional().default(10).describe('상품 검색 수 (기본값: 10)'),
|
|
48
|
+
storeLimit: z.number().optional().default(10).describe('조회할 매장 수 (기본값: 10)'),
|
|
49
|
+
timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
handler: checkInventory,
|
|
53
|
+
};
|
|
54
|
+
}
|