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
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 롯데마트 업스트림 단독 진단
|
|
3
|
+
*/
|
|
4
|
+
import { LOTTEMART_API } from './api.js';
|
|
5
|
+
import { DEFAULT_LOTTEMART_TIMEOUT_MS } from './config.js';
|
|
6
|
+
import { probeLotteMartRequest } from './session.js';
|
|
7
|
+
import { normalizeArea } from './storeSearch.js';
|
|
8
|
+
function resolveArea(area) {
|
|
9
|
+
const normalized = normalizeArea(area || '경기');
|
|
10
|
+
return normalized || '경기';
|
|
11
|
+
}
|
|
12
|
+
export function buildLotteMartDebugRequest(params) {
|
|
13
|
+
const target = params.target;
|
|
14
|
+
const area = resolveArea(params.area);
|
|
15
|
+
switch (target) {
|
|
16
|
+
case 'market-options': {
|
|
17
|
+
const endpoint = new URL(LOTTEMART_API.MARKET_OPTIONS_PATH, LOTTEMART_API.BASE_URL);
|
|
18
|
+
endpoint.searchParams.set('p_area', area);
|
|
19
|
+
endpoint.searchParams.set('p_type', params.type || '1');
|
|
20
|
+
if ((params.storeCode || '').trim().length > 0) {
|
|
21
|
+
endpoint.searchParams.set('p_werks', params.storeCode.trim());
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
target,
|
|
25
|
+
method: 'GET',
|
|
26
|
+
url: endpoint.toString(),
|
|
27
|
+
bodyText: null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
case 'stores':
|
|
31
|
+
return {
|
|
32
|
+
target,
|
|
33
|
+
method: 'POST',
|
|
34
|
+
url: new URL(LOTTEMART_API.STORE_SEARCH_PATH, LOTTEMART_API.BASE_URL).toString(),
|
|
35
|
+
bodyText: new URLSearchParams({
|
|
36
|
+
m_area: area,
|
|
37
|
+
...(params.storeCode ? { m_market: params.storeCode.trim() } : {}),
|
|
38
|
+
...(params.keyword ? { m_schWord: params.keyword.trim() } : {}),
|
|
39
|
+
}).toString(),
|
|
40
|
+
};
|
|
41
|
+
case 'products':
|
|
42
|
+
return {
|
|
43
|
+
target,
|
|
44
|
+
method: 'POST',
|
|
45
|
+
url: new URL(LOTTEMART_API.PRODUCT_SEARCH_PATH, LOTTEMART_API.BASE_URL).toString(),
|
|
46
|
+
bodyText: new URLSearchParams({
|
|
47
|
+
p_area: area,
|
|
48
|
+
p_market: (params.storeCode || '2415').trim(),
|
|
49
|
+
p_schWord: (params.keyword || '핫식스').trim(),
|
|
50
|
+
}).toString(),
|
|
51
|
+
};
|
|
52
|
+
case 'product-page': {
|
|
53
|
+
const endpoint = new URL(LOTTEMART_API.PRODUCT_PAGE_PATH, LOTTEMART_API.BASE_URL);
|
|
54
|
+
endpoint.searchParams.set('p_market', (params.storeCode || '2415').trim());
|
|
55
|
+
endpoint.searchParams.set('p_schWord', (params.keyword || '핫식스').trim());
|
|
56
|
+
endpoint.searchParams.set('page', String(Math.max(params.page || 2, 1)));
|
|
57
|
+
return {
|
|
58
|
+
target,
|
|
59
|
+
method: 'GET',
|
|
60
|
+
url: endpoint.toString(),
|
|
61
|
+
bodyText: null,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export async function probeLotteMartUpstream(params) {
|
|
67
|
+
const request = buildLotteMartDebugRequest(params);
|
|
68
|
+
const timeout = params.timeout || DEFAULT_LOTTEMART_TIMEOUT_MS;
|
|
69
|
+
const init = {
|
|
70
|
+
method: request.method,
|
|
71
|
+
headers: request.method === 'POST'
|
|
72
|
+
? {
|
|
73
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
74
|
+
}
|
|
75
|
+
: {
|
|
76
|
+
Accept: 'text/html, */*; q=0.01',
|
|
77
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
78
|
+
},
|
|
79
|
+
body: request.bodyText || undefined,
|
|
80
|
+
};
|
|
81
|
+
return {
|
|
82
|
+
request: {
|
|
83
|
+
target: request.target,
|
|
84
|
+
method: request.method,
|
|
85
|
+
url: request.url,
|
|
86
|
+
bodyText: request.bodyText,
|
|
87
|
+
timeout,
|
|
88
|
+
hasZyteApiKey: Boolean(params.zyteApiKey),
|
|
89
|
+
},
|
|
90
|
+
attempts: await probeLotteMartRequest(request.url, init, timeout, '', params.zyteApiKey),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 롯데마트 매장 주소 지오코딩
|
|
3
|
+
*/
|
|
4
|
+
import { fetchJson } from '../../utils/http.js';
|
|
5
|
+
import { DEFAULT_LOTTEMART_TIMEOUT_MS } from './config.js';
|
|
6
|
+
const GEOCODE_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
7
|
+
const geocodeCache = new Map();
|
|
8
|
+
export async function geocodeLotteMartAddress(address, options = {}) {
|
|
9
|
+
const keyword = address.trim();
|
|
10
|
+
if (keyword.length === 0) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const cached = geocodeCache.get(keyword);
|
|
14
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
15
|
+
return cached.value;
|
|
16
|
+
}
|
|
17
|
+
const apiKey = (options.googleMapsApiKey || '').trim();
|
|
18
|
+
if (apiKey.length === 0) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const endpoint = new URL('https://maps.googleapis.com/maps/api/geocode/json');
|
|
22
|
+
endpoint.searchParams.set('address', keyword);
|
|
23
|
+
endpoint.searchParams.set('key', apiKey);
|
|
24
|
+
const body = await fetchJson(endpoint.toString(), {
|
|
25
|
+
method: 'GET',
|
|
26
|
+
timeout: options.timeout || DEFAULT_LOTTEMART_TIMEOUT_MS,
|
|
27
|
+
headers: {
|
|
28
|
+
Accept: 'application/json',
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
if (body.status !== 'OK') {
|
|
32
|
+
geocodeCache.set(keyword, {
|
|
33
|
+
expiresAt: Date.now() + GEOCODE_CACHE_TTL_MS,
|
|
34
|
+
value: null,
|
|
35
|
+
});
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const latitude = body.results?.[0]?.geometry?.location?.lat;
|
|
39
|
+
const longitude = body.results?.[0]?.geometry?.location?.lng;
|
|
40
|
+
const value = typeof latitude === 'number' && typeof longitude === 'number' ? { latitude, longitude } : null;
|
|
41
|
+
geocodeCache.set(keyword, {
|
|
42
|
+
expiresAt: Date.now() + GEOCODE_CACHE_TTL_MS,
|
|
43
|
+
value,
|
|
44
|
+
});
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
export function __testOnlyClearLotteMartGeocodeCache() {
|
|
48
|
+
geocodeCache.clear();
|
|
49
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 롯데마트 서비스 프로바이더
|
|
3
|
+
*/
|
|
4
|
+
import { createFindNearbyStoresTool } from './tools/findNearbyStores.js';
|
|
5
|
+
import { createSearchProductsTool } from './tools/searchProducts.js';
|
|
6
|
+
const LOTTEMART_METADATA = {
|
|
7
|
+
id: 'lottemart',
|
|
8
|
+
name: '롯데마트',
|
|
9
|
+
version: '1.0.0',
|
|
10
|
+
description: '롯데마트 계열 매장 탐색 및 매장별 상품 검색 서비스',
|
|
11
|
+
};
|
|
12
|
+
class LotteMartService {
|
|
13
|
+
googleMapsApiKey;
|
|
14
|
+
zyteApiKey;
|
|
15
|
+
metadata = LOTTEMART_METADATA;
|
|
16
|
+
constructor(googleMapsApiKey, zyteApiKey) {
|
|
17
|
+
this.googleMapsApiKey = googleMapsApiKey;
|
|
18
|
+
this.zyteApiKey = zyteApiKey;
|
|
19
|
+
}
|
|
20
|
+
getTools() {
|
|
21
|
+
return [
|
|
22
|
+
createFindNearbyStoresTool(this.googleMapsApiKey, this.zyteApiKey),
|
|
23
|
+
createSearchProductsTool(this.zyteApiKey),
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function createLotteMartService(options = {}) {
|
|
28
|
+
return new LotteMartService(options.googleMapsApiKey, options.zyteApiKey);
|
|
29
|
+
}
|
|
30
|
+
export * from './types.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 롯데마트 매장 검색 키워드 보정
|
|
3
|
+
*/
|
|
4
|
+
export function buildLotteMartKeywordVariants(rawKeyword) {
|
|
5
|
+
const base = rawKeyword.trim();
|
|
6
|
+
if (!base) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
const compact = base.replace(/\s+/g, '');
|
|
10
|
+
const withoutNoise = compact.replace(/(롯데마트|근처|주변)/g, '');
|
|
11
|
+
const tokens = base
|
|
12
|
+
.split(/\s+/)
|
|
13
|
+
.map((value) => value.trim())
|
|
14
|
+
.filter((value) => value.length > 0)
|
|
15
|
+
.filter((value) => !['롯데마트', '근처', '주변'].includes(value));
|
|
16
|
+
const normalizedTokens = tokens
|
|
17
|
+
.map((value) => value.replace(/역$/g, '').trim())
|
|
18
|
+
.filter((value) => value.length > 0);
|
|
19
|
+
const primaryToken = normalizedTokens[0] || tokens[0] || '';
|
|
20
|
+
const candidates = [
|
|
21
|
+
base,
|
|
22
|
+
compact,
|
|
23
|
+
withoutNoise,
|
|
24
|
+
primaryToken,
|
|
25
|
+
...(tokens.length <= 1 ? tokens : []),
|
|
26
|
+
...(normalizedTokens.length <= 1 ? normalizedTokens : []),
|
|
27
|
+
];
|
|
28
|
+
return [...new Set(candidates.map((value) => value.trim()).filter((value) => value.length > 0))];
|
|
29
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 롯데마트 HTML 파싱 유틸리티
|
|
3
|
+
*/
|
|
4
|
+
import { buildLotteMartKeywordVariants } from './keyword.js';
|
|
5
|
+
export function detectBrandVariant(storeName) {
|
|
6
|
+
if (storeName.startsWith('토이저러스')) {
|
|
7
|
+
return 'toysrus';
|
|
8
|
+
}
|
|
9
|
+
if (storeName.startsWith('맥스')) {
|
|
10
|
+
return 'max';
|
|
11
|
+
}
|
|
12
|
+
if (storeName.startsWith('보틀벙커')) {
|
|
13
|
+
return 'bottlebunker';
|
|
14
|
+
}
|
|
15
|
+
if (storeName.startsWith('Mealguru')) {
|
|
16
|
+
return 'mealguru';
|
|
17
|
+
}
|
|
18
|
+
if (storeName.startsWith('그랑그로서리')) {
|
|
19
|
+
return 'grandgrocery';
|
|
20
|
+
}
|
|
21
|
+
return 'lottemart';
|
|
22
|
+
}
|
|
23
|
+
function decodeHtmlEntities(value) {
|
|
24
|
+
return value
|
|
25
|
+
.replace(/ /gi, ' ')
|
|
26
|
+
.replace(/</gi, '<')
|
|
27
|
+
.replace(/>/gi, '>')
|
|
28
|
+
.replace(/"/gi, '"')
|
|
29
|
+
.replace(/'/gi, "'")
|
|
30
|
+
.replace(///gi, '/')
|
|
31
|
+
.replace(/&/gi, '&');
|
|
32
|
+
}
|
|
33
|
+
function stripHtmlComments(value) {
|
|
34
|
+
let sanitized = '';
|
|
35
|
+
let index = 0;
|
|
36
|
+
while (index < value.length) {
|
|
37
|
+
if (value.startsWith('<!--', index)) {
|
|
38
|
+
const commentEnd = value.indexOf('-->', index + 4);
|
|
39
|
+
if (commentEnd < 0) {
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
index = commentEnd + 3;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
sanitized += value[index];
|
|
46
|
+
index += 1;
|
|
47
|
+
}
|
|
48
|
+
return sanitized.replaceAll('<!--', '').replaceAll('-->', '');
|
|
49
|
+
}
|
|
50
|
+
function stripTags(value) {
|
|
51
|
+
return stripHtmlComments(value).replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]+>/g, ' ');
|
|
52
|
+
}
|
|
53
|
+
function normalizeWhitespace(value) {
|
|
54
|
+
return decodeHtmlEntities(stripTags(value)).replace(/\s+/g, ' ').trim();
|
|
55
|
+
}
|
|
56
|
+
export function normalizeHtml(html) {
|
|
57
|
+
const trimmed = html.trim();
|
|
58
|
+
const htmlStart = trimmed.search(/<(?:!doctype|html|option|li|ul|section)\b/i);
|
|
59
|
+
return htmlStart >= 0 ? trimmed.slice(htmlStart) : trimmed;
|
|
60
|
+
}
|
|
61
|
+
function toNumber(value) {
|
|
62
|
+
const parsed = Number.parseFloat(value.replace(/[^\d.-]/g, ''));
|
|
63
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
64
|
+
}
|
|
65
|
+
export function calculateDistanceM(lat1, lng1, lat2, lng2) {
|
|
66
|
+
const toRad = (deg) => (deg * Math.PI) / 180;
|
|
67
|
+
const dLat = toRad(lat2 - lat1);
|
|
68
|
+
const dLng = toRad(lng2 - lng1);
|
|
69
|
+
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
70
|
+
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
|
71
|
+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
72
|
+
return Math.round(6371000 * c);
|
|
73
|
+
}
|
|
74
|
+
function splitTopLevelBlocks(html, markerClass) {
|
|
75
|
+
const pattern = new RegExp(`<li>\\s*<div class="${markerClass}">`, 'g');
|
|
76
|
+
const normalized = html.replace(/\r/g, '');
|
|
77
|
+
const indexes = Array.from(normalized.matchAll(pattern)).map((match) => match.index || 0);
|
|
78
|
+
return indexes.map((start, index) => normalized.slice(start, indexes[index + 1] || normalized.length));
|
|
79
|
+
}
|
|
80
|
+
function extractFirstMatch(pattern, value) {
|
|
81
|
+
return value.match(pattern)?.[1]?.trim() || '';
|
|
82
|
+
}
|
|
83
|
+
export function parseMarketOptions(area, html) {
|
|
84
|
+
return Array.from(normalizeHtml(html).matchAll(/<option value="([^"]*)"[^>]*>([^<]*)<\/option>/g))
|
|
85
|
+
.map((match) => ({
|
|
86
|
+
area,
|
|
87
|
+
storeCode: (match[1] || '').trim(),
|
|
88
|
+
storeName: normalizeWhitespace(match[2] || ''),
|
|
89
|
+
}))
|
|
90
|
+
.filter((item) => item.storeCode.length > 0 && item.storeName.length > 0)
|
|
91
|
+
.map((item) => ({
|
|
92
|
+
...item,
|
|
93
|
+
brandVariant: detectBrandVariant(item.storeName),
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
export function parseStores(area, html) {
|
|
97
|
+
return splitTopLevelBlocks(normalizeHtml(html), 'shop-tit')
|
|
98
|
+
.map((block) => {
|
|
99
|
+
const storeName = normalizeWhitespace(extractFirstMatch(/<div class="shop-tit">([\s\S]*?)<\/div>/, block));
|
|
100
|
+
const detailUrl = extractFirstMatch(/<a class="link" href="([^"]+)"/, block);
|
|
101
|
+
const storeCode = extractFirstMatch(/werks=([^'"]+)/, block) || extractFirstMatch(/goClick\('([^']+)'\)/, block);
|
|
102
|
+
return {
|
|
103
|
+
area,
|
|
104
|
+
storeCode,
|
|
105
|
+
storeName,
|
|
106
|
+
brandVariant: detectBrandVariant(storeName),
|
|
107
|
+
address: normalizeWhitespace(extractFirstMatch(/주소\s*:\s*<\/span>([\s\S]*?)<\/li>/, block)),
|
|
108
|
+
phone: normalizeWhitespace(extractFirstMatch(/상담전화\s*:\s*<\/span>([\s\S]*?)<\/li>/, block)),
|
|
109
|
+
openTime: normalizeWhitespace(extractFirstMatch(/영업시간\s*:\s*<\/span>([\s\S]*?)<\/li>/, block)),
|
|
110
|
+
closedDays: normalizeWhitespace(extractFirstMatch(/휴점일\s*:\s*<\/span>([\s\S]*?)<\/li>/, block)),
|
|
111
|
+
parkingType: normalizeWhitespace(extractFirstMatch(/주차정보\s*:\s*([^<]*)<\/span>/, block)),
|
|
112
|
+
parkingDetails: normalizeWhitespace(extractFirstMatch(/<div class="park-info">([\s\S]*?)<\/div>/, block)),
|
|
113
|
+
detailUrl: detailUrl.startsWith('.') ? `/mobiledowa/market/${detailUrl.replace(/^\.\//, '')}` : detailUrl,
|
|
114
|
+
latitude: 0,
|
|
115
|
+
longitude: 0,
|
|
116
|
+
distanceM: null,
|
|
117
|
+
};
|
|
118
|
+
})
|
|
119
|
+
.filter((store) => store.storeCode.length > 0 && store.storeName.length > 0);
|
|
120
|
+
}
|
|
121
|
+
export function parseProducts(area, storeCode, storeName, keyword, html) {
|
|
122
|
+
return splitTopLevelBlocks(normalizeHtml(html), 'prod-box')
|
|
123
|
+
.map((block) => {
|
|
124
|
+
const rawSpec = extractFirstMatch(/<div class="prod-count">([\s\S]*?)<\/div>/, block);
|
|
125
|
+
return {
|
|
126
|
+
area,
|
|
127
|
+
storeCode,
|
|
128
|
+
storeName,
|
|
129
|
+
keyword,
|
|
130
|
+
productName: normalizeWhitespace(extractFirstMatch(/<div class="prod-name">([\s\S]*?)<\/div>/, block)) ||
|
|
131
|
+
normalizeWhitespace(extractFirstMatch(/<div class="layer-head">([\s\S]*?)<\/div>/, block)),
|
|
132
|
+
barcode: normalizeWhitespace(extractFirstMatch(/<!--\s*([^>]+?)\s*-->/, rawSpec)),
|
|
133
|
+
spec: normalizeWhitespace(stripHtmlComments(rawSpec)),
|
|
134
|
+
manufacturer: normalizeWhitespace(extractFirstMatch(/제조사\s*:\s*<\/th>\s*<td>([\s\S]*?)<\/td>/, block)),
|
|
135
|
+
price: toNumber(extractFirstMatch(/가격\s*:\s*<\/th>\s*<td>([\s\S]*?)<\/td>/, block)),
|
|
136
|
+
stockQuantity: toNumber(extractFirstMatch(/재고\s*:\s*<\/th>\s*<td>([\s\S]*?)<\/td>/, block)),
|
|
137
|
+
};
|
|
138
|
+
})
|
|
139
|
+
.filter((product) => product.productName.length > 0);
|
|
140
|
+
}
|
|
141
|
+
export function parseProductSummary(html) {
|
|
142
|
+
const normalized = normalizeHtml(html);
|
|
143
|
+
const totalCount = toNumber(extractFirstMatch(/검색결과\s*:\s*<span>([\d,]+)<\/span>건/, normalized));
|
|
144
|
+
const totalPages = toNumber(extractFirstMatch(/var\s+totalPage\s*=\s*"(\d+)"/, normalized));
|
|
145
|
+
return {
|
|
146
|
+
totalCount,
|
|
147
|
+
totalPages: totalPages > 0 ? totalPages : 1,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
export function dedupeProducts(products) {
|
|
151
|
+
return Array.from(new Map(products.map((product) => [
|
|
152
|
+
[product.productName, product.spec, product.manufacturer, product.price, product.stockQuantity].join('|'),
|
|
153
|
+
product,
|
|
154
|
+
])).values());
|
|
155
|
+
}
|
|
156
|
+
export function matchesKeyword(store, keyword) {
|
|
157
|
+
const variants = buildLotteMartKeywordVariants(keyword).map((value) => value.toLowerCase());
|
|
158
|
+
if (variants.length === 0) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
const haystacks = [store.storeName, store.address, store.phone]
|
|
162
|
+
.map((value) => value.toLowerCase().replace(/\s+/g, ''))
|
|
163
|
+
.filter((value) => value.length > 0);
|
|
164
|
+
return variants.some((variant) => {
|
|
165
|
+
const normalizedVariant = variant.replace(/\s+/g, '');
|
|
166
|
+
return haystacks.some((value) => value.includes(normalizedVariant));
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
export function matchesBrandVariant(store, brandVariant) {
|
|
170
|
+
const normalized = (brandVariant || '').trim().toLowerCase();
|
|
171
|
+
return normalized.length === 0 ? true : store.brandVariant === normalized;
|
|
172
|
+
}
|
|
173
|
+
export function sortStores(stores) {
|
|
174
|
+
return [...stores].sort((a, b) => {
|
|
175
|
+
const distanceDelta = (a.distanceM ?? Number.MAX_SAFE_INTEGER) - (b.distanceM ?? Number.MAX_SAFE_INTEGER);
|
|
176
|
+
if (distanceDelta !== 0) {
|
|
177
|
+
return distanceDelta;
|
|
178
|
+
}
|
|
179
|
+
const areaDelta = a.area.localeCompare(b.area, 'ko');
|
|
180
|
+
if (areaDelta !== 0) {
|
|
181
|
+
return areaDelta;
|
|
182
|
+
}
|
|
183
|
+
return a.storeName.localeCompare(b.storeName, 'ko');
|
|
184
|
+
});
|
|
185
|
+
}
|