daiso 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +218 -537
- package/dist/api/actionsProxy.js +82 -0
- package/dist/api/cgvHandlers.js +103 -16
- package/dist/api/daisoHandlers.js +127 -0
- package/dist/api/emart24Handlers.js +176 -0
- package/dist/api/gs25Handlers.js +256 -0
- package/dist/api/handlers.js +184 -108
- package/dist/api/healthChecks.js +332 -0
- package/dist/api/lottecinemaHandlers.js +155 -0
- package/dist/api/lottemartHandlers.js +128 -0
- package/dist/api/megaboxHandlers.js +81 -50
- package/dist/api/routes/cuRoutes.js +17 -0
- package/dist/api/routes/daisoRoutes.js +6 -1
- package/dist/api/routes/emart24Routes.js +22 -0
- package/dist/api/routes/gs25Routes.js +22 -0
- package/dist/api/routes/healthRoutes.js +44 -0
- package/dist/api/routes/lottecinemaRoutes.js +22 -0
- package/dist/api/routes/lottemartRoutes.js +18 -0
- package/dist/api/routes/oliveyoungRoutes.js +6 -1
- package/dist/api/routes/sevenelevenRoutes.js +32 -0
- package/dist/api/sevenelevenHandlers.js +200 -0
- package/dist/cli/args.js +54 -0
- package/dist/cli/commands/convenience.js +241 -0
- package/dist/cli/commands/daiso.js +96 -0
- package/dist/cli/constants.js +5 -0
- package/dist/cli/deps.js +48 -0
- package/dist/cli/http.js +98 -0
- package/dist/cli/interactiveFetch.js +31 -0
- package/dist/cli/interactiveItemSearch.js +259 -0
- package/dist/cli/interactivePrompt.js +49 -0
- package/dist/cli/interactiveTypes.js +4 -0
- package/dist/cli/types.js +4 -0
- package/dist/cli.js +66 -181
- package/dist/cliHelp.js +216 -9
- package/dist/cliInteractive.js +174 -0
- package/dist/cliPicker.js +56 -0
- package/dist/cliRenderer.js +139 -0
- package/dist/index.js +147 -8
- package/dist/pages/openapi.js +2 -1
- package/dist/pages/openapiFullSpec.js +58 -0
- package/dist/pages/openapiResponse.js +28 -0
- package/dist/pages/openapiSpec.js +2 -41
- package/dist/pages/openapiSpecActions.js +109 -0
- package/dist/pages/openapiSpecComponents.js +30 -391
- package/dist/pages/openapiSpecComponentsCgv.js +24 -0
- package/dist/pages/openapiSpecComponentsCu.js +112 -0
- package/dist/pages/openapiSpecComponentsDaisoOliveyoung.js +342 -0
- package/dist/pages/openapiSpecComponentsEmart24.js +141 -0
- package/dist/pages/openapiSpecComponentsGs25.js +109 -0
- package/dist/pages/openapiSpecComponentsLotteMart.js +85 -0
- package/dist/pages/openapiSpecComponentsLottecinema.js +163 -0
- package/dist/pages/openapiSpecComponentsMegabox.js +144 -0
- package/dist/pages/openapiSpecComponentsSeveneleven.js +82 -0
- package/dist/pages/openapiSpecPathsCgv.js +66 -3
- package/dist/pages/openapiSpecPathsCu.js +138 -0
- package/dist/pages/openapiSpecPathsDaiso.js +302 -0
- package/dist/pages/openapiSpecPathsDaisoOliveyoung.js +4 -376
- package/dist/pages/openapiSpecPathsEmart24.js +223 -0
- package/dist/pages/openapiSpecPathsGs25.js +166 -0
- package/dist/pages/openapiSpecPathsLotteMart.js +135 -0
- package/dist/pages/openapiSpecPathsLottecinema.js +212 -0
- package/dist/pages/openapiSpecPathsMegabox.js +52 -3
- package/dist/pages/openapiSpecPathsOliveyoung.js +231 -0
- package/dist/pages/openapiSpecPathsSeveneleven.js +234 -0
- package/dist/pages/prompt.js +8 -402
- package/dist/pages/promptCoreText.js +278 -0
- package/dist/pages/promptReferenceText.js +131 -0
- package/dist/pages/promptRetailCinemaText.js +273 -0
- package/dist/services/cgv/client.js +6 -1
- package/dist/services/cgv/index.js +3 -3
- package/dist/services/cgv/location.js +235 -0
- package/dist/services/cgv/tools/findTheaters.js +30 -5
- package/dist/services/cgv/tools/getTimetable.js +42 -14
- package/dist/services/cgv/tools/searchMovies.js +40 -12
- package/dist/services/cgv/transport.js +0 -3
- package/dist/services/cu/api.js +11 -0
- package/dist/services/cu/client.js +297 -0
- package/dist/services/cu/index.js +21 -0
- package/dist/services/cu/tools/checkInventory.js +123 -0
- package/dist/services/cu/tools/findNearbyStores.js +49 -0
- package/dist/services/cu/types.js +4 -0
- package/dist/services/daiso/api.js +22 -2
- package/dist/services/daiso/client.js +59 -0
- package/dist/services/daiso/index.js +2 -0
- package/dist/services/daiso/product.js +17 -0
- package/dist/services/daiso/tools/checkInventory.js +76 -41
- package/dist/services/daiso/tools/findStores.js +17 -7
- package/dist/services/daiso/tools/getDisplayLocation.js +75 -0
- package/dist/services/emart24/api.js +11 -0
- package/dist/services/emart24/client.js +173 -0
- package/dist/services/emart24/index.js +22 -0
- package/dist/services/emart24/inventoryLookup.js +134 -0
- package/dist/services/emart24/tools/checkInventory.js +54 -0
- package/dist/services/emart24/tools/findNearbyStores.js +72 -0
- package/dist/services/emart24/tools/searchProducts.js +63 -0
- package/dist/services/emart24/types.js +4 -0
- package/dist/services/gs25/api.js +9 -0
- package/dist/services/gs25/client.js +198 -0
- package/dist/services/gs25/index.js +22 -0
- package/dist/services/gs25/storeUtils.js +168 -0
- package/dist/services/gs25/tools/checkInventory.js +163 -0
- package/dist/services/gs25/tools/findNearbyStores.js +103 -0
- package/dist/services/gs25/tools/searchProducts.js +50 -0
- package/dist/services/gs25/types.js +4 -0
- package/dist/services/lottecinema/api.js +12 -0
- package/dist/services/lottecinema/client.js +161 -0
- package/dist/services/lottecinema/index.js +30 -0
- package/dist/services/lottecinema/location.js +191 -0
- package/dist/services/lottecinema/tools/findNearbyTheaters.js +47 -0
- package/dist/services/lottecinema/tools/getRemainingSeats.js +85 -0
- package/dist/services/lottecinema/tools/listNowShowing.js +79 -0
- package/dist/services/lottecinema/types.js +4 -0
- package/dist/services/lottemart/api.js +31 -0
- package/dist/services/lottemart/client.js +245 -0
- package/dist/services/lottemart/clientTypes.js +4 -0
- package/dist/services/lottemart/config.js +4 -0
- package/dist/services/lottemart/debug.js +92 -0
- package/dist/services/lottemart/geocode.js +49 -0
- package/dist/services/lottemart/index.js +30 -0
- package/dist/services/lottemart/keyword.js +29 -0
- package/dist/services/lottemart/parser.js +185 -0
- package/dist/services/lottemart/session.js +331 -0
- package/dist/services/lottemart/storeSearch.js +52 -0
- package/dist/services/lottemart/tools/findNearbyStores.js +67 -0
- package/dist/services/lottemart/tools/searchProducts.js +62 -0
- package/dist/services/lottemart/types.js +4 -0
- package/dist/services/lottemart/zetta.js +105 -0
- package/dist/services/megabox/location.js +219 -0
- package/dist/services/megabox/tools/findNearbyTheaters.js +13 -44
- package/dist/services/megabox/tools/getRemainingSeats.js +31 -3
- package/dist/services/megabox/tools/listNowShowing.js +31 -3
- package/dist/services/oliveyoung/api.js +2 -0
- package/dist/services/oliveyoung/client.js +150 -9
- package/dist/services/oliveyoung/index.js +3 -1
- package/dist/services/oliveyoung/tools/checkInventory.js +15 -4
- package/dist/services/oliveyoung/tools/searchProducts.js +57 -0
- package/dist/services/seveneleven/api.js +15 -0
- package/dist/services/seveneleven/client.js +279 -0
- package/dist/services/seveneleven/index.js +30 -0
- package/dist/services/seveneleven/inventory.js +302 -0
- package/dist/services/seveneleven/productKeyword.js +141 -0
- package/dist/services/seveneleven/tools/checkInventory.js +82 -0
- package/dist/services/seveneleven/tools/getCatalogSnapshot.js +55 -0
- package/dist/services/seveneleven/tools/getSearchPopwords.js +41 -0
- package/dist/services/seveneleven/tools/searchProducts.js +56 -0
- package/dist/services/seveneleven/tools/searchStores.js +45 -0
- package/dist/services/seveneleven/types.js +4 -0
- package/dist/utils/cliInteractiveHelpers.js +63 -0
- package/dist/utils/daisoKeyword.js +30 -0
- package/dist/utils/http.js +16 -2
- package/openapi.json +1977 -734
- package/openapi.yaml +1430 -543
- package/package.json +28 -15
- package/dist/tools/checkInventory.js +0 -98
- package/dist/tools/findStores.js +0 -125
- package/dist/tools/getPriceInfo.js +0 -77
- package/dist/tools/searchProducts.js +0 -61
- package/dist/types/index.js +0 -4
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 올리브영 상품 검색 도구
|
|
3
|
+
*/
|
|
4
|
+
import * as z from 'zod';
|
|
5
|
+
import { fetchOliveyoungProducts } from '../client.js';
|
|
6
|
+
async function searchProducts(args) {
|
|
7
|
+
const { keyword, page = 1, size = 20, sort = '01', includeSoldOut = false, timeoutMs = 15000, zyteApiKey, } = args;
|
|
8
|
+
if (!keyword || keyword.trim().length === 0) {
|
|
9
|
+
throw new Error('상품 검색어(keyword)를 입력해주세요.');
|
|
10
|
+
}
|
|
11
|
+
const result = await fetchOliveyoungProducts({
|
|
12
|
+
keyword,
|
|
13
|
+
page,
|
|
14
|
+
size,
|
|
15
|
+
sort,
|
|
16
|
+
includeSoldOut,
|
|
17
|
+
}, {
|
|
18
|
+
timeout: timeoutMs,
|
|
19
|
+
apiKey: zyteApiKey,
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
content: [
|
|
23
|
+
{
|
|
24
|
+
type: 'text',
|
|
25
|
+
text: JSON.stringify({
|
|
26
|
+
keyword,
|
|
27
|
+
page,
|
|
28
|
+
size,
|
|
29
|
+
sort,
|
|
30
|
+
includeSoldOut,
|
|
31
|
+
totalCount: result.totalCount,
|
|
32
|
+
nextPage: result.nextPage,
|
|
33
|
+
count: result.products.length,
|
|
34
|
+
products: result.products,
|
|
35
|
+
}, null, 2),
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function createSearchProductsTool(apiKey) {
|
|
41
|
+
return {
|
|
42
|
+
name: 'oliveyoung_search_products',
|
|
43
|
+
metadata: {
|
|
44
|
+
title: '올리브영 상품 검색',
|
|
45
|
+
description: '키워드로 올리브영 상품 목록을 조회합니다. "어떤 거 있나요" 같은 질문에는 먼저 이 도구를 사용하세요.',
|
|
46
|
+
inputSchema: {
|
|
47
|
+
keyword: z.string().describe('상품 검색어 (예: 마스크팩, 선크림, 립밤)'),
|
|
48
|
+
page: z.number().optional().default(1).describe('상품 검색 페이지 번호 (기본값: 1)'),
|
|
49
|
+
size: z.number().optional().default(20).describe('페이지당 상품 수 (기본값: 20)'),
|
|
50
|
+
sort: z.string().optional().default('01').describe('정렬 코드 (기본값: 01)'),
|
|
51
|
+
includeSoldOut: z.boolean().optional().default(false).describe('품절 상품 포함 여부 (기본값: false)'),
|
|
52
|
+
timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
handler: ((args) => searchProducts({ ...args, zyteApiKey: apiKey })),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 세븐일레븐 API 엔드포인트
|
|
3
|
+
*/
|
|
4
|
+
export const SEVENELEVEN_API = {
|
|
5
|
+
BASE_URL: 'https://new.7-elevenapp.co.kr',
|
|
6
|
+
SEARCH_GOODS_PATH: '/api/v1/open/search/goods',
|
|
7
|
+
SEARCH_STORE_PATH: '/api/v1/open/search/store',
|
|
8
|
+
SEARCH_POPWORD_PATH: '/api/v1/open/search/popword',
|
|
9
|
+
PRODUCT_SEARCH_STOCK_PATH: '/api/v1/open/product/search/stock',
|
|
10
|
+
STOCK_SEARCH_STORES_PATH: '/api/v1/open/stock/search/stores',
|
|
11
|
+
REAL_STOCK_MULTI_PATH: '/api/v1/open/real-stock/multi',
|
|
12
|
+
PRODUCT_PAGES_PATH: '/api/v1/product/pages',
|
|
13
|
+
PRODUCT_ISSUES_PATH: '/api/v1/product/issues',
|
|
14
|
+
EXHIBITION_MAIN_PATH: '/api/v1/exhibition/main/list',
|
|
15
|
+
};
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 세븐일레븐 API 클라이언트
|
|
3
|
+
*/
|
|
4
|
+
/* c8 ignore start */
|
|
5
|
+
import { fetchJson } from '../../utils/http.js';
|
|
6
|
+
import { SEVENELEVEN_API } from './api.js';
|
|
7
|
+
const SEVENELEVEN_DEFAULT_HEADERS = {
|
|
8
|
+
Accept: 'application/json, text/plain, */*',
|
|
9
|
+
'Content-Type': 'application/json',
|
|
10
|
+
'User-Agent': 'Mozilla/5.0 (Linux; Android 15)',
|
|
11
|
+
};
|
|
12
|
+
function toNumber(value) {
|
|
13
|
+
if (typeof value === 'number') {
|
|
14
|
+
return Number.isFinite(value) ? value : 0;
|
|
15
|
+
}
|
|
16
|
+
if (typeof value === 'string') {
|
|
17
|
+
const parsed = Number.parseFloat(value);
|
|
18
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
19
|
+
}
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
function toStringValue(value) {
|
|
23
|
+
return typeof value === 'string' ? value : '';
|
|
24
|
+
}
|
|
25
|
+
function normalizeStoreKeyword(keyword) {
|
|
26
|
+
return keyword
|
|
27
|
+
.trim()
|
|
28
|
+
.replace(/\s+/g, '')
|
|
29
|
+
.replace(/(역|지점|점)$/g, '');
|
|
30
|
+
}
|
|
31
|
+
function toBooleanYn(value) {
|
|
32
|
+
const normalized = toStringValue(value).trim().toUpperCase();
|
|
33
|
+
return normalized === 'Y' || normalized === 'TRUE' || normalized === '1';
|
|
34
|
+
}
|
|
35
|
+
function toRawProduct(input) {
|
|
36
|
+
if (!input || typeof input !== 'object') {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
const record = input;
|
|
40
|
+
if (record.field && typeof record.field === 'object') {
|
|
41
|
+
return record.field;
|
|
42
|
+
}
|
|
43
|
+
return record;
|
|
44
|
+
}
|
|
45
|
+
function normalizeProduct(raw) {
|
|
46
|
+
return {
|
|
47
|
+
productNo: toStringValue(raw.prdNo),
|
|
48
|
+
itemCode: toStringValue(raw.itemCd),
|
|
49
|
+
itemName: toStringValue(raw.itemOnm),
|
|
50
|
+
salePrice: toNumber(raw.onlinePrice),
|
|
51
|
+
originalPrice: toNumber(raw.onlineCost),
|
|
52
|
+
imageUrl: toStringValue(raw.repImgUrl),
|
|
53
|
+
eventName: toStringValue(raw.eventGbnNm),
|
|
54
|
+
itemType: toStringValue(raw.itemGbnNm),
|
|
55
|
+
makerName: toStringValue(raw.makerNm),
|
|
56
|
+
reviewScore: raw.avgEvalScore === undefined ? null : toNumber(raw.avgEvalScore),
|
|
57
|
+
reviewCount: toNumber(raw.productReviewCnt),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function normalizeProducts(items) {
|
|
61
|
+
return items
|
|
62
|
+
.map(toRawProduct)
|
|
63
|
+
.map(normalizeProduct)
|
|
64
|
+
.filter((item) => item.itemCode.length > 0 || item.itemName.length > 0);
|
|
65
|
+
}
|
|
66
|
+
function normalizeStore(raw) {
|
|
67
|
+
const address1 = toStringValue(raw.addr1);
|
|
68
|
+
const address2 = toStringValue(raw.addr2);
|
|
69
|
+
const mergedAddress = `${address1} ${address2}`.trim();
|
|
70
|
+
return {
|
|
71
|
+
storeCode: toStringValue(raw.storeCode || raw.storeCd || raw.strCd || raw.storCd || raw.shopCd),
|
|
72
|
+
storeName: toStringValue(raw.storeName || raw.storeNm || raw.strNm || raw.storNm || raw.shopNm),
|
|
73
|
+
address: toStringValue(raw.address || raw.addr || raw.roadAddr || raw.shopAddr) || mergedAddress,
|
|
74
|
+
latitude: toNumber(raw.latitude || raw.storeLat || raw.lat || raw.yPos || raw.y || raw.yCoord),
|
|
75
|
+
longitude: toNumber(raw.longitude || raw.storeLon || raw.lng || raw.xPos || raw.x || raw.xCoord),
|
|
76
|
+
pickupEnabled: toBooleanYn(raw.pickupEnabled || raw.pickupYn),
|
|
77
|
+
deliveryEnabled: toBooleanYn(raw.deliveryEnabled || raw.deliveryYn || raw.dlvyYn),
|
|
78
|
+
closeYn: toStringValue(raw.closeYn || raw.storeCloseYn || raw.closeYN || raw.clsYn),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function normalizeStockProductMeta(raw) {
|
|
82
|
+
return {
|
|
83
|
+
productNo: toStringValue(raw.prdNo),
|
|
84
|
+
itemCode: toStringValue(raw.itemCd),
|
|
85
|
+
itemName: toStringValue(raw.itemOnm),
|
|
86
|
+
smCode: toStringValue(raw.smCd),
|
|
87
|
+
stockManagementCode: toStringValue(raw.stokMngCd),
|
|
88
|
+
stockManagementQuantity: toNumber(raw.stokMngQty),
|
|
89
|
+
stockApplicationRate: toStringValue(raw.stockApplicationRate),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function normalizeStores(items) {
|
|
93
|
+
return items
|
|
94
|
+
.map((item) => {
|
|
95
|
+
if (!item || typeof item !== 'object') {
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
const record = item;
|
|
99
|
+
if (record.field && typeof record.field === 'object') {
|
|
100
|
+
return record.field;
|
|
101
|
+
}
|
|
102
|
+
return record;
|
|
103
|
+
})
|
|
104
|
+
.map(normalizeStore)
|
|
105
|
+
.filter((store) => store.storeCode.length > 0 || store.storeName.length > 0);
|
|
106
|
+
}
|
|
107
|
+
async function requestSevenElevenJson(path, method, body, options = {}) {
|
|
108
|
+
const { timeout = 15000 } = options;
|
|
109
|
+
const url = `${SEVENELEVEN_API.BASE_URL}${path}`;
|
|
110
|
+
return fetchJson(url, {
|
|
111
|
+
method,
|
|
112
|
+
timeout,
|
|
113
|
+
headers: SEVENELEVEN_DEFAULT_HEADERS,
|
|
114
|
+
body: method === 'POST' ? JSON.stringify(body || {}) : undefined,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
export async function searchSevenElevenProducts(params, options = {}) {
|
|
118
|
+
const { query, page = 1, size = 20 } = params;
|
|
119
|
+
const pageNo = Math.max(Math.trunc(page) - 1, 0);
|
|
120
|
+
const pageSize = Math.max(Math.trunc(size), 1);
|
|
121
|
+
const response = await requestSevenElevenJson(SEVENELEVEN_API.SEARCH_GOODS_PATH, 'POST', {
|
|
122
|
+
// 공개 검색 엔드포인트는 pageNo/pageSize만 안정적으로 동작한다.
|
|
123
|
+
// sort를 함께 보내면 빈 결과가 내려오는 케이스가 확인되어 제외한다.
|
|
124
|
+
query,
|
|
125
|
+
pageNo,
|
|
126
|
+
pageSize,
|
|
127
|
+
}, options);
|
|
128
|
+
const data = response.data || {};
|
|
129
|
+
const queryResult = data.SearchQueryResult;
|
|
130
|
+
const collections = queryResult?.Collection || [];
|
|
131
|
+
const collectionIds = collections.map((item) => item.CollectionId || '').filter((id) => id.length > 0);
|
|
132
|
+
let totalCount = 0;
|
|
133
|
+
const allDocuments = [];
|
|
134
|
+
for (const collection of collections) {
|
|
135
|
+
totalCount += toNumber(collection.Documentset?.totalCount);
|
|
136
|
+
allDocuments.push(...(collection.Documentset?.Document || []));
|
|
137
|
+
}
|
|
138
|
+
const collectionProducts = normalizeProducts(allDocuments);
|
|
139
|
+
const contentProducts = normalizeProducts(Array.isArray(data.content) ? data.content : []);
|
|
140
|
+
const products = (collectionProducts.length > 0 ? collectionProducts : contentProducts).slice(0, pageSize);
|
|
141
|
+
return {
|
|
142
|
+
query: queryResult?.query || query,
|
|
143
|
+
totalCount,
|
|
144
|
+
products,
|
|
145
|
+
collectionIds,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
export async function fetchSevenElevenStoresByKeyword(params, options = {}) {
|
|
149
|
+
const { keyword, limit = 20 } = params;
|
|
150
|
+
const query = normalizeStoreKeyword(keyword) || keyword.trim();
|
|
151
|
+
const safeLimit = Number.isFinite(limit) ? Math.trunc(limit) : 20;
|
|
152
|
+
const listCount = Math.max(1, Math.min(safeLimit, 9999));
|
|
153
|
+
const response = await requestSevenElevenJson(SEVENELEVEN_API.SEARCH_STORE_PATH, 'POST', {
|
|
154
|
+
collection: 'store',
|
|
155
|
+
query,
|
|
156
|
+
sort: 'Date/desc',
|
|
157
|
+
listCount,
|
|
158
|
+
}, options);
|
|
159
|
+
const data = response.data || {};
|
|
160
|
+
const queryResult = data.SearchQueryResult;
|
|
161
|
+
const collections = queryResult?.Collection || [];
|
|
162
|
+
let totalCount = 0;
|
|
163
|
+
const allDocuments = [];
|
|
164
|
+
for (const collection of collections) {
|
|
165
|
+
totalCount += toNumber(collection.Documentset?.totalCount);
|
|
166
|
+
allDocuments.push(...(collection.Documentset?.Document || []));
|
|
167
|
+
}
|
|
168
|
+
const stores = normalizeStores(allDocuments);
|
|
169
|
+
return {
|
|
170
|
+
query: queryResult?.query || query,
|
|
171
|
+
totalCount,
|
|
172
|
+
stores,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
export async function fetchSevenElevenSearchPopwords(label = 'home', options = {}) {
|
|
176
|
+
const encodedLabel = encodeURIComponent(label);
|
|
177
|
+
const response = await requestSevenElevenJson(`${SEVENELEVEN_API.SEARCH_POPWORD_PATH}?label=${encodedLabel}`, 'POST', {}, options);
|
|
178
|
+
const { data } = response;
|
|
179
|
+
if (Array.isArray(data)) {
|
|
180
|
+
return data.map((item) => toStringValue(item)).filter((item) => item.length > 0);
|
|
181
|
+
}
|
|
182
|
+
if (!data || typeof data !== 'object') {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
const record = data;
|
|
186
|
+
const candidates = [record.keywords, record.popwords, record.wordList, record.list];
|
|
187
|
+
for (const candidate of candidates) {
|
|
188
|
+
if (!Array.isArray(candidate)) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const values = candidate
|
|
192
|
+
.map((item) => {
|
|
193
|
+
if (typeof item === 'string') {
|
|
194
|
+
return item;
|
|
195
|
+
}
|
|
196
|
+
if (item && typeof item === 'object') {
|
|
197
|
+
const value = item;
|
|
198
|
+
return toStringValue(value.keyword || value.word || value.text || value.name);
|
|
199
|
+
}
|
|
200
|
+
return '';
|
|
201
|
+
})
|
|
202
|
+
.filter((item) => item.length > 0);
|
|
203
|
+
if (values.length > 0) {
|
|
204
|
+
return values;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
export async function fetchSevenElevenStockProductMeta(itemCode, options = {}) {
|
|
210
|
+
const { timeout = 15000 } = options;
|
|
211
|
+
const encodedItemCode = encodeURIComponent(itemCode.trim());
|
|
212
|
+
const url = `${SEVENELEVEN_API.BASE_URL}${SEVENELEVEN_API.PRODUCT_SEARCH_STOCK_PATH}?itemCd=${encodedItemCode}`;
|
|
213
|
+
const response = await fetchJson(url, {
|
|
214
|
+
method: 'GET',
|
|
215
|
+
timeout,
|
|
216
|
+
headers: {
|
|
217
|
+
Accept: SEVENELEVEN_DEFAULT_HEADERS.Accept,
|
|
218
|
+
'User-Agent': SEVENELEVEN_DEFAULT_HEADERS['User-Agent'],
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
const productMeta = normalizeStockProductMeta(response);
|
|
222
|
+
if (productMeta.itemCode.length === 0 || productMeta.smCode.length === 0) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
return productMeta;
|
|
226
|
+
}
|
|
227
|
+
function normalizeExhibitions(items) {
|
|
228
|
+
return items
|
|
229
|
+
.map((item) => {
|
|
230
|
+
const value = (item || {});
|
|
231
|
+
const productList = Array.isArray(value.exhibitionProductList) ? value.exhibitionProductList : [];
|
|
232
|
+
return {
|
|
233
|
+
exhibitionIdx: toNumber(value.exhibitionIdx),
|
|
234
|
+
exhibitionName: toStringValue(value.exhibitionName),
|
|
235
|
+
startDate: toStringValue(value.exhibitionStartDate),
|
|
236
|
+
endDate: toStringValue(value.exhibitionEndDate),
|
|
237
|
+
productCount: productList.length,
|
|
238
|
+
};
|
|
239
|
+
})
|
|
240
|
+
.filter((item) => item.exhibitionIdx > 0 || item.exhibitionName.length > 0);
|
|
241
|
+
}
|
|
242
|
+
function extractContentArray(data) {
|
|
243
|
+
if (Array.isArray(data)) {
|
|
244
|
+
return data;
|
|
245
|
+
}
|
|
246
|
+
if (!data || typeof data !== 'object') {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
const record = data;
|
|
250
|
+
if (Array.isArray(record.content)) {
|
|
251
|
+
return record.content;
|
|
252
|
+
}
|
|
253
|
+
if (Array.isArray(record.list)) {
|
|
254
|
+
return record.list;
|
|
255
|
+
}
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
export async function fetchSevenElevenCatalogSnapshot(options = {}) {
|
|
259
|
+
const { includeIssues = true, includeExhibition = true, timeout = 15000 } = options;
|
|
260
|
+
const tasks = [
|
|
261
|
+
requestSevenElevenJson(SEVENELEVEN_API.PRODUCT_PAGES_PATH, 'GET', null, { timeout }),
|
|
262
|
+
includeIssues
|
|
263
|
+
? requestSevenElevenJson(SEVENELEVEN_API.PRODUCT_ISSUES_PATH, 'GET', null, { timeout })
|
|
264
|
+
: Promise.resolve({ data: { content: [] } }),
|
|
265
|
+
includeExhibition
|
|
266
|
+
? requestSevenElevenJson(SEVENELEVEN_API.EXHIBITION_MAIN_PATH, 'GET', null, { timeout })
|
|
267
|
+
: Promise.resolve({ data: [] }),
|
|
268
|
+
];
|
|
269
|
+
const [pagesResult, issuesResult, exhibitionsResult] = await Promise.allSettled(tasks);
|
|
270
|
+
const pagesData = pagesResult.status === 'fulfilled' ? pagesResult.value.data : [];
|
|
271
|
+
const issuesData = issuesResult.status === 'fulfilled' ? issuesResult.value.data : [];
|
|
272
|
+
const exhibitionData = exhibitionsResult.status === 'fulfilled' ? exhibitionsResult.value.data : [];
|
|
273
|
+
return {
|
|
274
|
+
pages: normalizeProducts(extractContentArray(pagesData)),
|
|
275
|
+
issues: normalizeProducts(extractContentArray(issuesData)),
|
|
276
|
+
exhibitions: normalizeExhibitions(extractContentArray(exhibitionData)),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/* c8 ignore stop */
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 세븐일레븐 서비스 프로바이더
|
|
3
|
+
*/
|
|
4
|
+
import { createSearchProductsTool } from './tools/searchProducts.js';
|
|
5
|
+
import { createSearchStoresTool } from './tools/searchStores.js';
|
|
6
|
+
import { createCheckInventoryTool } from './tools/checkInventory.js';
|
|
7
|
+
import { createGetSearchPopwordsTool } from './tools/getSearchPopwords.js';
|
|
8
|
+
import { createGetCatalogSnapshotTool } from './tools/getCatalogSnapshot.js';
|
|
9
|
+
const SEVENELEVEN_METADATA = {
|
|
10
|
+
id: 'seveneleven',
|
|
11
|
+
name: '세븐일레븐',
|
|
12
|
+
version: '1.0.0',
|
|
13
|
+
description: '세븐일레븐 공개 상품/검색/카탈로그 조회 서비스',
|
|
14
|
+
};
|
|
15
|
+
class SevenElevenService {
|
|
16
|
+
metadata = SEVENELEVEN_METADATA;
|
|
17
|
+
getTools() {
|
|
18
|
+
return [
|
|
19
|
+
createSearchProductsTool(),
|
|
20
|
+
createSearchStoresTool(),
|
|
21
|
+
createCheckInventoryTool(),
|
|
22
|
+
createGetSearchPopwordsTool(),
|
|
23
|
+
createGetCatalogSnapshotTool(),
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function createSevenElevenService() {
|
|
28
|
+
return new SevenElevenService();
|
|
29
|
+
}
|
|
30
|
+
export * from './types.js';
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 세븐일레븐 재고 조회 클라이언트
|
|
3
|
+
*
|
|
4
|
+
* 상품 검색 + 매장 검색 + 재고 API를 조합하여 재고 정보를 제공합니다.
|
|
5
|
+
* 재고 API가 암호화로 차단된 경우에도 상품/매장 정보를 graceful하게 반환합니다.
|
|
6
|
+
*/
|
|
7
|
+
/* c8 ignore start */
|
|
8
|
+
import { fetchJson, HttpError } from '../../utils/http.js';
|
|
9
|
+
import { SEVENELEVEN_API } from './api.js';
|
|
10
|
+
import { fetchSevenElevenStockProductMeta, fetchSevenElevenStoresByKeyword, } from './client.js';
|
|
11
|
+
import { pickBestSevenElevenProduct, searchSevenElevenProductsWithVariants } from './productKeyword.js';
|
|
12
|
+
const SEVENELEVEN_DEFAULT_HEADERS = {
|
|
13
|
+
Accept: 'application/json, text/plain, */*',
|
|
14
|
+
'Content-Type': 'application/json',
|
|
15
|
+
'User-Agent': 'Mozilla/5.0 (Linux; Android 15)',
|
|
16
|
+
};
|
|
17
|
+
function toNumber(value) {
|
|
18
|
+
if (typeof value === 'number') {
|
|
19
|
+
return Number.isFinite(value) ? value : 0;
|
|
20
|
+
}
|
|
21
|
+
if (typeof value === 'string') {
|
|
22
|
+
const parsed = Number.parseFloat(value);
|
|
23
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
24
|
+
}
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
function toStringValue(value) {
|
|
28
|
+
return typeof value === 'string' ? value : '';
|
|
29
|
+
}
|
|
30
|
+
function normalizeStockStore(raw) {
|
|
31
|
+
return {
|
|
32
|
+
storeCode: toStringValue(raw.storeCd),
|
|
33
|
+
storeName: '',
|
|
34
|
+
address: '',
|
|
35
|
+
latitude: 0,
|
|
36
|
+
longitude: 0,
|
|
37
|
+
stockQuantity: toNumber(raw.stock),
|
|
38
|
+
isSoldOut: toNumber(raw.stock) <= 0,
|
|
39
|
+
distanceM: null,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function tryParseJsonObject(text) {
|
|
43
|
+
if (text.trim().length === 0) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(text);
|
|
48
|
+
return typeof parsed === 'object' && parsed !== null ? parsed : null;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function toNullableNumber(value) {
|
|
55
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
59
|
+
const parsed = Number.parseFloat(value);
|
|
60
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
function normalizeEnvelopeError(response) {
|
|
65
|
+
const hasErrorFlag = response.success === false;
|
|
66
|
+
const hasErrorCode = typeof response.code === 'number' && (response.code < 200 || response.code >= 300);
|
|
67
|
+
const hasErrorMessage = typeof response.message === 'string' && response.message.trim().length > 0;
|
|
68
|
+
if (response.success === true && !hasErrorCode) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
if (!hasErrorFlag && !hasErrorCode && !hasErrorMessage) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
cause: 'api',
|
|
76
|
+
httpStatus: null,
|
|
77
|
+
code: toNullableNumber(response.code),
|
|
78
|
+
message: typeof response.message === 'string' ? response.message : '재고 API가 오류를 반환했습니다.',
|
|
79
|
+
raw: JSON.stringify(response),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function normalizeStockError(error) {
|
|
83
|
+
if (error instanceof HttpError) {
|
|
84
|
+
const parsedBody = tryParseJsonObject(error.bodyText);
|
|
85
|
+
return {
|
|
86
|
+
cause: 'api',
|
|
87
|
+
httpStatus: error.status,
|
|
88
|
+
code: toNullableNumber(parsedBody?.code),
|
|
89
|
+
message: typeof parsedBody?.message === 'string' && parsedBody.message.trim().length > 0
|
|
90
|
+
? parsedBody.message
|
|
91
|
+
: error.message,
|
|
92
|
+
raw: error.bodyText.trim().length > 0 ? error.bodyText : null,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (error instanceof Error) {
|
|
96
|
+
return {
|
|
97
|
+
cause: 'network',
|
|
98
|
+
httpStatus: null,
|
|
99
|
+
code: null,
|
|
100
|
+
message: error.message,
|
|
101
|
+
raw: null,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
cause: 'unknown',
|
|
106
|
+
httpStatus: null,
|
|
107
|
+
code: null,
|
|
108
|
+
message: '알 수 없는 재고 API 오류',
|
|
109
|
+
raw: null,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function haversineDistanceM(lat1, lon1, lat2, lon2) {
|
|
113
|
+
const R = 6371000;
|
|
114
|
+
const toRad = (deg) => (deg * Math.PI) / 180;
|
|
115
|
+
const dLat = toRad(lat2 - lat1);
|
|
116
|
+
const dLon = toRad(lon2 - lon1);
|
|
117
|
+
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
118
|
+
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
|
119
|
+
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
120
|
+
}
|
|
121
|
+
function attachDistanceToStockStores(stores, refLat, refLon) {
|
|
122
|
+
return stores.map((store) => {
|
|
123
|
+
if (store.latitude === 0 && store.longitude === 0) {
|
|
124
|
+
return store;
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
...store,
|
|
128
|
+
distanceM: Math.round(haversineDistanceM(refLat, refLon, store.latitude, store.longitude)),
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function normalizeStoreKeyword(keyword) {
|
|
133
|
+
return keyword
|
|
134
|
+
.trim()
|
|
135
|
+
.replace(/\s+/g, '')
|
|
136
|
+
.replace(/(역|지점|점)$/g, '')
|
|
137
|
+
.toLowerCase();
|
|
138
|
+
}
|
|
139
|
+
function tokenizeStoreKeyword(keyword) {
|
|
140
|
+
return keyword
|
|
141
|
+
.trim()
|
|
142
|
+
.split(/\s+/)
|
|
143
|
+
.map((token) => normalizeStoreKeyword(token))
|
|
144
|
+
.filter((token) => token.length > 0);
|
|
145
|
+
}
|
|
146
|
+
function filterStoresByKeyword(stores, keyword) {
|
|
147
|
+
if (!keyword || keyword.trim().length === 0) {
|
|
148
|
+
return stores;
|
|
149
|
+
}
|
|
150
|
+
const normalized = normalizeStoreKeyword(keyword);
|
|
151
|
+
const exactMatched = stores.filter((store) => {
|
|
152
|
+
const nameNorm = store.storeName.replace(/\s+/g, '').toLowerCase();
|
|
153
|
+
const addrNorm = store.address.replace(/\s+/g, '').toLowerCase();
|
|
154
|
+
return nameNorm.includes(normalized) || addrNorm.includes(normalized);
|
|
155
|
+
});
|
|
156
|
+
if (exactMatched.length > 0) {
|
|
157
|
+
return exactMatched;
|
|
158
|
+
}
|
|
159
|
+
const tokens = tokenizeStoreKeyword(keyword).filter((token) => token.length >= 2);
|
|
160
|
+
if (tokens.length === 0) {
|
|
161
|
+
return stores;
|
|
162
|
+
}
|
|
163
|
+
const tokenMatched = stores.filter((store) => {
|
|
164
|
+
const combined = `${store.storeName} ${store.address}`.replace(/\s+/g, '').toLowerCase();
|
|
165
|
+
return tokens.every((token) => combined.includes(token));
|
|
166
|
+
});
|
|
167
|
+
return tokenMatched.length > 0 ? tokenMatched : stores;
|
|
168
|
+
}
|
|
169
|
+
async function tryStockApi(stockProduct, stores, options = {}) {
|
|
170
|
+
const { timeout = 15000 } = options;
|
|
171
|
+
const url = `${SEVENELEVEN_API.BASE_URL}${SEVENELEVEN_API.REAL_STOCK_MULTI_PATH}/01/stocks`;
|
|
172
|
+
const payload = {
|
|
173
|
+
smCd: stockProduct.smCode,
|
|
174
|
+
stokMngCd: stockProduct.stockManagementCode,
|
|
175
|
+
stokMngQty: stockProduct.stockManagementQuantity,
|
|
176
|
+
stockApplicationRate: stockProduct.stockApplicationRate,
|
|
177
|
+
storeList: stores.map((store) => store.storeCode).filter((storeCode) => storeCode.length > 0),
|
|
178
|
+
};
|
|
179
|
+
if (payload.smCd.length === 0 || payload.stokMngCd.length === 0 || payload.storeList.length === 0) {
|
|
180
|
+
return { stores: null, error: null };
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const response = await fetchJson(url, {
|
|
184
|
+
method: 'POST',
|
|
185
|
+
timeout,
|
|
186
|
+
headers: SEVENELEVEN_DEFAULT_HEADERS,
|
|
187
|
+
body: JSON.stringify(payload),
|
|
188
|
+
});
|
|
189
|
+
const responseError = normalizeEnvelopeError(response);
|
|
190
|
+
if (responseError) {
|
|
191
|
+
return { stores: null, error: responseError };
|
|
192
|
+
}
|
|
193
|
+
const data = response.data || {};
|
|
194
|
+
const rawStores = Array.isArray(data.storeList) ? data.storeList : [];
|
|
195
|
+
return {
|
|
196
|
+
stores: rawStores
|
|
197
|
+
.map((item) => normalizeStockStore(item))
|
|
198
|
+
.filter((store) => store.storeCode.length > 0),
|
|
199
|
+
error: null,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
return {
|
|
204
|
+
stores: null,
|
|
205
|
+
error: normalizeStockError(error),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
export async function checkSevenElevenInventory(params, options = {}) {
|
|
210
|
+
const { productKeyword, storeKeyword, storeLimit = 20 } = params;
|
|
211
|
+
// 1) 상품 검색
|
|
212
|
+
const productResult = await searchSevenElevenProductsWithVariants(productKeyword, options);
|
|
213
|
+
const firstProduct = pickBestSevenElevenProduct(productResult.products, productKeyword) ||
|
|
214
|
+
productResult.products.find((p) => p.itemCode.length > 0) ||
|
|
215
|
+
productResult.products[0] ||
|
|
216
|
+
null;
|
|
217
|
+
// 2) 매장 검색
|
|
218
|
+
const storeResult = await fetchSevenElevenStoresByKeyword({ keyword: storeKeyword, limit: 100 }, options);
|
|
219
|
+
const matchedStores = filterStoresByKeyword(storeResult.stores, storeKeyword);
|
|
220
|
+
// 3) 재고 API 시도 (실패 시 graceful fallback)
|
|
221
|
+
let stockStores = null;
|
|
222
|
+
let stockError = null;
|
|
223
|
+
if (firstProduct && firstProduct.itemCode.length > 0) {
|
|
224
|
+
try {
|
|
225
|
+
const stockProduct = await fetchSevenElevenStockProductMeta(firstProduct.itemCode, options);
|
|
226
|
+
if (stockProduct) {
|
|
227
|
+
const stockAttempt = await tryStockApi(stockProduct, matchedStores, options);
|
|
228
|
+
stockStores = stockAttempt.stores;
|
|
229
|
+
stockError = stockAttempt.error;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
stockError = normalizeStockError(error);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// 4) 결과 조합
|
|
237
|
+
let resultStores;
|
|
238
|
+
let stockAvailable;
|
|
239
|
+
if (stockStores !== null && stockStores.length > 0) {
|
|
240
|
+
stockAvailable = true;
|
|
241
|
+
const stockMap = new Map(stockStores.map((store) => [store.storeCode, store]));
|
|
242
|
+
resultStores = matchedStores.map((store) => {
|
|
243
|
+
const stockStore = stockMap.get(store.storeCode);
|
|
244
|
+
return {
|
|
245
|
+
storeCode: store.storeCode,
|
|
246
|
+
storeName: store.storeName,
|
|
247
|
+
address: store.address,
|
|
248
|
+
latitude: store.latitude,
|
|
249
|
+
longitude: store.longitude,
|
|
250
|
+
stockQuantity: stockStore?.stockQuantity ?? -1,
|
|
251
|
+
isSoldOut: stockStore ? stockStore.isSoldOut : false,
|
|
252
|
+
distanceM: null,
|
|
253
|
+
};
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
stockAvailable = false;
|
|
258
|
+
resultStores = matchedStores.map((store) => ({
|
|
259
|
+
storeCode: store.storeCode,
|
|
260
|
+
storeName: store.storeName,
|
|
261
|
+
address: store.address,
|
|
262
|
+
latitude: store.latitude,
|
|
263
|
+
longitude: store.longitude,
|
|
264
|
+
stockQuantity: -1,
|
|
265
|
+
isSoldOut: false,
|
|
266
|
+
distanceM: null,
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
269
|
+
// 거리 계산: 첫 번째 매장 좌표를 기준으로 정렬
|
|
270
|
+
const refStore = matchedStores.find((s) => s.latitude !== 0 && s.longitude !== 0);
|
|
271
|
+
if (refStore) {
|
|
272
|
+
resultStores = attachDistanceToStockStores(resultStores, refStore.latitude, refStore.longitude);
|
|
273
|
+
resultStores.sort((a, b) => {
|
|
274
|
+
if (a.distanceM === null && b.distanceM === null)
|
|
275
|
+
return 0;
|
|
276
|
+
if (a.distanceM === null)
|
|
277
|
+
return 1;
|
|
278
|
+
if (b.distanceM === null)
|
|
279
|
+
return -1;
|
|
280
|
+
return a.distanceM - b.distanceM;
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
const limited = resultStores.slice(0, storeLimit);
|
|
284
|
+
const inStockCount = stockAvailable ? limited.filter((s) => s.stockQuantity > 0).length : 0;
|
|
285
|
+
return {
|
|
286
|
+
productKeyword,
|
|
287
|
+
product: firstProduct
|
|
288
|
+
? {
|
|
289
|
+
itemCode: firstProduct.itemCode,
|
|
290
|
+
itemName: firstProduct.itemName,
|
|
291
|
+
salePrice: firstProduct.salePrice,
|
|
292
|
+
imageUrl: firstProduct.imageUrl,
|
|
293
|
+
}
|
|
294
|
+
: null,
|
|
295
|
+
stockAvailable,
|
|
296
|
+
stockError,
|
|
297
|
+
totalStoreCount: resultStores.length,
|
|
298
|
+
inStockStoreCount: inStockCount,
|
|
299
|
+
stores: limited,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/* c8 ignore stop */
|