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.
Files changed (158) hide show
  1. package/README.md +218 -540
  2. package/dist/api/actionsProxy.js +82 -0
  3. package/dist/api/cgvHandlers.js +103 -16
  4. package/dist/api/daisoHandlers.js +127 -0
  5. package/dist/api/emart24Handlers.js +176 -0
  6. package/dist/api/gs25Handlers.js +256 -0
  7. package/dist/api/handlers.js +184 -108
  8. package/dist/api/healthChecks.js +332 -0
  9. package/dist/api/lottecinemaHandlers.js +155 -0
  10. package/dist/api/lottemartHandlers.js +128 -0
  11. package/dist/api/megaboxHandlers.js +81 -50
  12. package/dist/api/routes/cuRoutes.js +17 -0
  13. package/dist/api/routes/daisoRoutes.js +6 -1
  14. package/dist/api/routes/emart24Routes.js +22 -0
  15. package/dist/api/routes/gs25Routes.js +22 -0
  16. package/dist/api/routes/healthRoutes.js +44 -0
  17. package/dist/api/routes/lottecinemaRoutes.js +22 -0
  18. package/dist/api/routes/lottemartRoutes.js +18 -0
  19. package/dist/api/routes/oliveyoungRoutes.js +6 -1
  20. package/dist/api/routes/sevenelevenRoutes.js +32 -0
  21. package/dist/api/sevenelevenHandlers.js +200 -0
  22. package/dist/cli/args.js +54 -0
  23. package/dist/cli/commands/convenience.js +241 -0
  24. package/dist/cli/commands/daiso.js +96 -0
  25. package/dist/cli/constants.js +5 -0
  26. package/dist/cli/deps.js +48 -0
  27. package/dist/cli/http.js +98 -0
  28. package/dist/cli/interactiveFetch.js +31 -0
  29. package/dist/cli/interactiveItemSearch.js +259 -0
  30. package/dist/cli/interactivePrompt.js +49 -0
  31. package/dist/cli/interactiveTypes.js +4 -0
  32. package/dist/cli/types.js +4 -0
  33. package/dist/cli.js +66 -197
  34. package/dist/cliHelp.js +206 -1
  35. package/dist/cliInteractive.js +174 -0
  36. package/dist/cliPicker.js +56 -0
  37. package/dist/cliRenderer.js +4 -0
  38. package/dist/index.js +147 -8
  39. package/dist/pages/openapi.js +2 -1
  40. package/dist/pages/openapiFullSpec.js +58 -0
  41. package/dist/pages/openapiResponse.js +28 -0
  42. package/dist/pages/openapiSpec.js +2 -41
  43. package/dist/pages/openapiSpecActions.js +109 -0
  44. package/dist/pages/openapiSpecComponents.js +30 -391
  45. package/dist/pages/openapiSpecComponentsCgv.js +24 -0
  46. package/dist/pages/openapiSpecComponentsCu.js +112 -0
  47. package/dist/pages/openapiSpecComponentsDaisoOliveyoung.js +342 -0
  48. package/dist/pages/openapiSpecComponentsEmart24.js +141 -0
  49. package/dist/pages/openapiSpecComponentsGs25.js +109 -0
  50. package/dist/pages/openapiSpecComponentsLotteMart.js +85 -0
  51. package/dist/pages/openapiSpecComponentsLottecinema.js +163 -0
  52. package/dist/pages/openapiSpecComponentsMegabox.js +144 -0
  53. package/dist/pages/openapiSpecComponentsSeveneleven.js +82 -0
  54. package/dist/pages/openapiSpecPathsCgv.js +66 -3
  55. package/dist/pages/openapiSpecPathsCu.js +138 -0
  56. package/dist/pages/openapiSpecPathsDaiso.js +302 -0
  57. package/dist/pages/openapiSpecPathsDaisoOliveyoung.js +4 -376
  58. package/dist/pages/openapiSpecPathsEmart24.js +223 -0
  59. package/dist/pages/openapiSpecPathsGs25.js +166 -0
  60. package/dist/pages/openapiSpecPathsLotteMart.js +135 -0
  61. package/dist/pages/openapiSpecPathsLottecinema.js +212 -0
  62. package/dist/pages/openapiSpecPathsMegabox.js +52 -3
  63. package/dist/pages/openapiSpecPathsOliveyoung.js +231 -0
  64. package/dist/pages/openapiSpecPathsSeveneleven.js +234 -0
  65. package/dist/pages/prompt.js +8 -402
  66. package/dist/pages/promptCoreText.js +278 -0
  67. package/dist/pages/promptReferenceText.js +131 -0
  68. package/dist/pages/promptRetailCinemaText.js +273 -0
  69. package/dist/services/cgv/client.js +6 -1
  70. package/dist/services/cgv/index.js +3 -3
  71. package/dist/services/cgv/location.js +235 -0
  72. package/dist/services/cgv/tools/findTheaters.js +30 -5
  73. package/dist/services/cgv/tools/getTimetable.js +42 -14
  74. package/dist/services/cgv/tools/searchMovies.js +40 -12
  75. package/dist/services/cgv/transport.js +0 -3
  76. package/dist/services/cu/api.js +11 -0
  77. package/dist/services/cu/client.js +297 -0
  78. package/dist/services/cu/index.js +21 -0
  79. package/dist/services/cu/tools/checkInventory.js +123 -0
  80. package/dist/services/cu/tools/findNearbyStores.js +49 -0
  81. package/dist/services/cu/types.js +4 -0
  82. package/dist/services/daiso/api.js +22 -2
  83. package/dist/services/daiso/client.js +59 -0
  84. package/dist/services/daiso/index.js +2 -0
  85. package/dist/services/daiso/product.js +17 -0
  86. package/dist/services/daiso/tools/checkInventory.js +76 -41
  87. package/dist/services/daiso/tools/findStores.js +17 -7
  88. package/dist/services/daiso/tools/getDisplayLocation.js +75 -0
  89. package/dist/services/emart24/api.js +11 -0
  90. package/dist/services/emart24/client.js +173 -0
  91. package/dist/services/emart24/index.js +22 -0
  92. package/dist/services/emart24/inventoryLookup.js +134 -0
  93. package/dist/services/emart24/tools/checkInventory.js +54 -0
  94. package/dist/services/emart24/tools/findNearbyStores.js +72 -0
  95. package/dist/services/emart24/tools/searchProducts.js +63 -0
  96. package/dist/services/emart24/types.js +4 -0
  97. package/dist/services/gs25/api.js +9 -0
  98. package/dist/services/gs25/client.js +198 -0
  99. package/dist/services/gs25/index.js +22 -0
  100. package/dist/services/gs25/storeUtils.js +168 -0
  101. package/dist/services/gs25/tools/checkInventory.js +163 -0
  102. package/dist/services/gs25/tools/findNearbyStores.js +103 -0
  103. package/dist/services/gs25/tools/searchProducts.js +50 -0
  104. package/dist/services/gs25/types.js +4 -0
  105. package/dist/services/lottecinema/api.js +12 -0
  106. package/dist/services/lottecinema/client.js +161 -0
  107. package/dist/services/lottecinema/index.js +30 -0
  108. package/dist/services/lottecinema/location.js +191 -0
  109. package/dist/services/lottecinema/tools/findNearbyTheaters.js +47 -0
  110. package/dist/services/lottecinema/tools/getRemainingSeats.js +85 -0
  111. package/dist/services/lottecinema/tools/listNowShowing.js +79 -0
  112. package/dist/services/lottecinema/types.js +4 -0
  113. package/dist/services/lottemart/api.js +31 -0
  114. package/dist/services/lottemart/client.js +245 -0
  115. package/dist/services/lottemart/clientTypes.js +4 -0
  116. package/dist/services/lottemart/config.js +4 -0
  117. package/dist/services/lottemart/debug.js +92 -0
  118. package/dist/services/lottemart/geocode.js +49 -0
  119. package/dist/services/lottemart/index.js +30 -0
  120. package/dist/services/lottemart/keyword.js +29 -0
  121. package/dist/services/lottemart/parser.js +185 -0
  122. package/dist/services/lottemart/session.js +331 -0
  123. package/dist/services/lottemart/storeSearch.js +52 -0
  124. package/dist/services/lottemart/tools/findNearbyStores.js +67 -0
  125. package/dist/services/lottemart/tools/searchProducts.js +62 -0
  126. package/dist/services/lottemart/types.js +4 -0
  127. package/dist/services/lottemart/zetta.js +105 -0
  128. package/dist/services/megabox/location.js +219 -0
  129. package/dist/services/megabox/tools/findNearbyTheaters.js +13 -44
  130. package/dist/services/megabox/tools/getRemainingSeats.js +31 -3
  131. package/dist/services/megabox/tools/listNowShowing.js +31 -3
  132. package/dist/services/oliveyoung/api.js +2 -0
  133. package/dist/services/oliveyoung/client.js +150 -9
  134. package/dist/services/oliveyoung/index.js +3 -1
  135. package/dist/services/oliveyoung/tools/checkInventory.js +15 -4
  136. package/dist/services/oliveyoung/tools/searchProducts.js +57 -0
  137. package/dist/services/seveneleven/api.js +15 -0
  138. package/dist/services/seveneleven/client.js +279 -0
  139. package/dist/services/seveneleven/index.js +30 -0
  140. package/dist/services/seveneleven/inventory.js +302 -0
  141. package/dist/services/seveneleven/productKeyword.js +141 -0
  142. package/dist/services/seveneleven/tools/checkInventory.js +82 -0
  143. package/dist/services/seveneleven/tools/getCatalogSnapshot.js +55 -0
  144. package/dist/services/seveneleven/tools/getSearchPopwords.js +41 -0
  145. package/dist/services/seveneleven/tools/searchProducts.js +56 -0
  146. package/dist/services/seveneleven/tools/searchStores.js +45 -0
  147. package/dist/services/seveneleven/types.js +4 -0
  148. package/dist/utils/cliInteractiveHelpers.js +63 -0
  149. package/dist/utils/daisoKeyword.js +30 -0
  150. package/dist/utils/http.js +16 -2
  151. package/openapi.json +1977 -734
  152. package/openapi.yaml +1430 -543
  153. package/package.json +28 -15
  154. package/dist/tools/checkInventory.js +0 -98
  155. package/dist/tools/findStores.js +0 -125
  156. package/dist/tools/getPriceInfo.js +0 -77
  157. package/dist/tools/searchProducts.js +0 -61
  158. package/dist/types/index.js +0 -4
@@ -0,0 +1,4 @@
1
+ /**
2
+ * 롯데마트 클라이언트 입력 타입
3
+ */
4
+ export {};
@@ -0,0 +1,4 @@
1
+ /**
2
+ * 롯데마트 서비스 공통 설정
3
+ */
4
+ export const DEFAULT_LOTTEMART_TIMEOUT_MS = 45000;
@@ -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(/&nbsp;/gi, ' ')
26
+ .replace(/&lt;/gi, '<')
27
+ .replace(/&gt;/gi, '>')
28
+ .replace(/&quot;/gi, '"')
29
+ .replace(/&#39;/gi, "'")
30
+ .replace(/&#x2F;/gi, '/')
31
+ .replace(/&amp;/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
+ }