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,331 @@
1
+ /**
2
+ * 롯데마트 세션 쿠키 관리
3
+ */
4
+ import { HttpError, fetchWithTimeout } from '../../utils/http.js';
5
+ import { decodeBase64, requestByZyte } from '../../utils/zyte.js';
6
+ import { LOTTEMART_API } from './api.js';
7
+ const SESSION_CACHE_TTL_MS = 5 * 60 * 1000;
8
+ let sessionCache = null;
9
+ function extractSessionCookie(response) {
10
+ const cookieHeaders = response.headers;
11
+ const cookieCandidates = cookieHeaders.getSetCookie
12
+ ? cookieHeaders.getSetCookie()
13
+ : [response.headers.get('set-cookie') || ''];
14
+ const matched = cookieCandidates
15
+ .flatMap((value) => value.split(','))
16
+ .map((value) => value.trim())
17
+ .find((value) => value.startsWith('ASPSESSIONID'));
18
+ return matched ? matched.split(';')[0].trim() : '';
19
+ }
20
+ function withSocketTimeout(operation, timeout) {
21
+ const timeoutMs = Math.max(1, Math.trunc(timeout));
22
+ return new Promise((resolve, reject) => {
23
+ const timeoutId = setTimeout(() => resolve(null), timeoutMs);
24
+ operation.then((value) => {
25
+ clearTimeout(timeoutId);
26
+ resolve(value);
27
+ }, (error) => {
28
+ clearTimeout(timeoutId);
29
+ reject(error);
30
+ });
31
+ });
32
+ }
33
+ export function __testOnlyCreateLotteMartSocketResponse(raw) {
34
+ const delimiter = new TextEncoder().encode('\r\n\r\n');
35
+ let boundary = -1;
36
+ for (let index = 0; index <= raw.length - delimiter.length; index += 1) {
37
+ let matched = true;
38
+ for (let inner = 0; inner < delimiter.length; inner += 1) {
39
+ if (raw[index + inner] !== delimiter[inner]) {
40
+ matched = false;
41
+ break;
42
+ }
43
+ }
44
+ if (matched) {
45
+ boundary = index;
46
+ break;
47
+ }
48
+ }
49
+ if (boundary < 0) {
50
+ return null;
51
+ }
52
+ const headerText = new TextDecoder().decode(raw.slice(0, boundary));
53
+ if (!headerText.startsWith('HTTP/')) {
54
+ return null;
55
+ }
56
+ const bodyBytes = raw.slice(boundary + delimiter.length);
57
+ const headerLinesRaw = headerText.split('\r\n');
58
+ const statusLine = headerLinesRaw.shift();
59
+ const [, statusCodeText = '500', ...statusTextParts] = statusLine.split(' ');
60
+ const responseHeaders = new Headers();
61
+ for (const line of headerLinesRaw) {
62
+ const separatorIndex = line.indexOf(':');
63
+ if (separatorIndex < 0) {
64
+ continue;
65
+ }
66
+ responseHeaders.append(line.slice(0, separatorIndex).trim(), line.slice(separatorIndex + 1).trim());
67
+ }
68
+ return new Response(bodyBytes, {
69
+ status: parseInt(statusCodeText, 10) || 500,
70
+ statusText: statusTextParts.join(' ').trim(),
71
+ headers: responseHeaders,
72
+ });
73
+ }
74
+ export async function __testOnlyFetchLotteMartSocketResponse(url, init, sessionCookie, connectFn, timeout) {
75
+ const requestUrl = new URL(url);
76
+ const headers = withLotteMartSessionCookie({
77
+ Accept: 'text/html, */*; q=0.01',
78
+ Host: LOTTEMART_API.ORIGIN_HOST,
79
+ Origin: 'https://company.lottemart.com',
80
+ Referer: LOTTEMART_API.ORIGIN_REFERER,
81
+ Connection: 'close',
82
+ ...init.headers,
83
+ }, sessionCookie);
84
+ const bodyText = toBodyText(init.body) || '';
85
+ if (bodyText.length > 0 && !headers.has('Content-Length')) {
86
+ headers.set('Content-Length', String(new TextEncoder().encode(bodyText).length));
87
+ }
88
+ const headerLines = Array.from(headers.entries()).map(([name, value]) => `${name}: ${value}`);
89
+ const path = `${requestUrl.pathname}${requestUrl.search}`;
90
+ const requestText = [`${init.method || 'GET'} ${path} HTTP/1.1`, ...headerLines, '', bodyText].join('\r\n');
91
+ const socket = connectFn({
92
+ hostname: LOTTEMART_API.SOCKET_HOST,
93
+ port: 80,
94
+ }, { allowHalfOpen: true });
95
+ const writer = socket.writable.getWriter();
96
+ const encodedRequest = new TextEncoder().encode(requestText);
97
+ if ((await withSocketTimeout(writer.write(encodedRequest), timeout)) === null) {
98
+ void writer.abort().catch(Boolean);
99
+ return null;
100
+ }
101
+ if ((await withSocketTimeout(writer.close(), timeout)) === null) {
102
+ void writer.abort().catch(Boolean);
103
+ return null;
104
+ }
105
+ const reader = socket.readable.getReader();
106
+ const chunks = [];
107
+ try {
108
+ while (true) {
109
+ const readResult = await withSocketTimeout(reader.read(), timeout);
110
+ if (readResult === null) {
111
+ await reader.cancel().catch(() => undefined);
112
+ return null;
113
+ }
114
+ const { value, done } = readResult;
115
+ if (done) {
116
+ break;
117
+ }
118
+ chunks.push(value);
119
+ }
120
+ }
121
+ finally {
122
+ reader.releaseLock();
123
+ }
124
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
125
+ const raw = new Uint8Array(totalLength);
126
+ let offset = 0;
127
+ for (const chunk of chunks) {
128
+ raw.set(chunk, offset);
129
+ offset += chunk.length;
130
+ }
131
+ return __testOnlyCreateLotteMartSocketResponse(raw);
132
+ }
133
+ /* c8 ignore start */
134
+ async function fetchLotteMartSocketResponse(url, init, sessionCookie, timeout) {
135
+ let connectFn = null;
136
+ try {
137
+ const socketsModule = await import('cloudflare:sockets');
138
+ connectFn = socketsModule.connect;
139
+ }
140
+ catch {
141
+ return null;
142
+ }
143
+ if (!connectFn) {
144
+ return null;
145
+ }
146
+ return __testOnlyFetchLotteMartSocketResponse(url, init, sessionCookie, connectFn, timeout);
147
+ }
148
+ /* c8 ignore end */
149
+ function cacheSessionCookie(cookie) {
150
+ const normalized = cookie.trim();
151
+ if (normalized.length === 0) {
152
+ return '';
153
+ }
154
+ sessionCache = {
155
+ expiresAt: Date.now() + SESSION_CACHE_TTL_MS,
156
+ cookie: normalized,
157
+ };
158
+ return normalized;
159
+ }
160
+ function cacheSessionCookieFromResponse(response) {
161
+ const cookie = extractSessionCookie(response);
162
+ return cacheSessionCookie(cookie);
163
+ }
164
+ export async function getCachedLotteMartSessionCookie(_timeout, forceRefresh = false) {
165
+ if (forceRefresh) {
166
+ sessionCache = null;
167
+ }
168
+ if (sessionCache && sessionCache.expiresAt > Date.now()) {
169
+ return sessionCache.cookie;
170
+ }
171
+ return '';
172
+ }
173
+ export async function getFreshLotteMartSessionCookie(timeout) {
174
+ return getCachedLotteMartSessionCookie(timeout, true);
175
+ }
176
+ export function withLotteMartSessionCookie(headers, sessionCookie) {
177
+ const result = new Headers(headers);
178
+ if (sessionCookie.trim().length > 0) {
179
+ result.set('Cookie', sessionCookie);
180
+ }
181
+ return result;
182
+ }
183
+ async function fetchLotteMartResponse(url, init, timeout, sessionCookie) {
184
+ const socketResponse = await fetchLotteMartSocketResponse(url, init, sessionCookie, timeout);
185
+ if (socketResponse) {
186
+ return socketResponse;
187
+ }
188
+ return fetchWithTimeout(url, {
189
+ ...init,
190
+ timeout,
191
+ headers: withLotteMartSessionCookie({
192
+ Accept: 'text/html, */*; q=0.01',
193
+ ...init.headers,
194
+ }, sessionCookie),
195
+ });
196
+ }
197
+ function toZyteHeaders(headers) {
198
+ return Array.from(headers.entries()).map(([name, value]) => ({ name, value }));
199
+ }
200
+ function toBodyText(body) {
201
+ if (typeof body === 'string') {
202
+ return body;
203
+ }
204
+ if (body instanceof URLSearchParams) {
205
+ return body.toString();
206
+ }
207
+ return undefined;
208
+ }
209
+ async function fetchLotteMartHtmlByZyte(url, init, timeout, sessionCookie, zyteApiKey) {
210
+ const headers = withLotteMartSessionCookie({
211
+ Accept: 'text/html, */*; q=0.01',
212
+ ...init.headers,
213
+ }, sessionCookie);
214
+ const result = await requestByZyte({
215
+ apiKey: zyteApiKey,
216
+ url,
217
+ timeout,
218
+ method: init.method || 'GET',
219
+ headers: toZyteHeaders(headers),
220
+ bodyText: toBodyText(init.body),
221
+ });
222
+ if (!result.httpResponseBody) {
223
+ throw new Error('Zyte HTTP 응답 본문이 비어 있습니다.');
224
+ }
225
+ return decodeBase64(result.httpResponseBody);
226
+ }
227
+ function toBodyPreview(bodyText) {
228
+ const normalized = bodyText.trim().replace(/\s+/g, ' ');
229
+ return normalized.length > 0 ? normalized.slice(0, 300) : null;
230
+ }
231
+ export async function probeLotteMartRequest(url, init, timeout, sessionCookie, zyteApiKey) {
232
+ const attempts = [];
233
+ try {
234
+ const response = await fetchLotteMartResponse(url, init, timeout, sessionCookie);
235
+ const bodyText = await response.text();
236
+ attempts.push({
237
+ used: 'direct',
238
+ success: response.ok,
239
+ status: response.status,
240
+ statusText: response.statusText,
241
+ error: response.ok ? null : new HttpError(response.status, response.statusText, bodyText).message,
242
+ bodyPreview: toBodyPreview(bodyText),
243
+ sessionCookie: extractSessionCookie(response) || null,
244
+ });
245
+ }
246
+ catch (error) {
247
+ attempts.push({
248
+ used: 'direct',
249
+ success: false,
250
+ status: null,
251
+ statusText: null,
252
+ error: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.',
253
+ bodyPreview: null,
254
+ sessionCookie: null,
255
+ });
256
+ }
257
+ if (zyteApiKey) {
258
+ try {
259
+ const bodyText = await fetchLotteMartHtmlByZyte(url, init, timeout, sessionCookie, zyteApiKey);
260
+ attempts.push({
261
+ used: 'zyte',
262
+ success: true,
263
+ status: 200,
264
+ statusText: 'OK',
265
+ error: null,
266
+ bodyPreview: toBodyPreview(bodyText),
267
+ sessionCookie: sessionCookie || null,
268
+ });
269
+ }
270
+ catch (error) {
271
+ attempts.push({
272
+ used: 'zyte',
273
+ success: false,
274
+ status: null,
275
+ statusText: null,
276
+ error: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.',
277
+ bodyPreview: null,
278
+ sessionCookie: sessionCookie || null,
279
+ });
280
+ }
281
+ }
282
+ return attempts;
283
+ }
284
+ export async function fetchLotteMartHtml(url, init, timeout, sessionCookie, zyteApiKey) {
285
+ try {
286
+ const response = await fetchLotteMartResponse(url, init, timeout, sessionCookie);
287
+ const cachedCookie = cacheSessionCookieFromResponse(response);
288
+ let bodyText;
289
+ try {
290
+ bodyText = await response.text();
291
+ }
292
+ catch (error) {
293
+ if (sessionCookie.trim().length === 0 && cachedCookie.length > 0) {
294
+ const retried = await fetchLotteMartResponse(url, init, timeout, cachedCookie);
295
+ cacheSessionCookieFromResponse(retried);
296
+ const retriedBodyText = await retried.text();
297
+ if (!retried.ok) {
298
+ throw new HttpError(retried.status, retried.statusText, retriedBodyText);
299
+ }
300
+ return retriedBodyText;
301
+ }
302
+ throw error;
303
+ }
304
+ if (!response.ok) {
305
+ throw new HttpError(response.status, response.statusText, bodyText);
306
+ }
307
+ if (bodyText.trim().length === 0 && sessionCookie.trim().length === 0 && cachedCookie.length > 0) {
308
+ const retried = await fetchLotteMartResponse(url, init, timeout, cachedCookie);
309
+ cacheSessionCookieFromResponse(retried);
310
+ const retriedBodyText = await retried.text();
311
+ if (!retried.ok) {
312
+ throw new HttpError(retried.status, retried.statusText, retriedBodyText);
313
+ }
314
+ return retriedBodyText;
315
+ }
316
+ return bodyText;
317
+ }
318
+ catch (error) {
319
+ if (zyteApiKey && error instanceof Error && !error.message.includes('Zyte')) {
320
+ const fallbackCookie = await getCachedLotteMartSessionCookie(timeout);
321
+ return fetchLotteMartHtmlByZyte(url, init, timeout, fallbackCookie || sessionCookie, zyteApiKey);
322
+ }
323
+ throw error;
324
+ }
325
+ }
326
+ export async function fetchLotteMartPageWithSession(path, init, timeout, sessionCookie, zyteApiKey) {
327
+ return fetchLotteMartHtml(new URL(path, LOTTEMART_API.BASE_URL).toString(), init, timeout, sessionCookie, zyteApiKey);
328
+ }
329
+ export function __testOnlyClearLotteMartSessionCache() {
330
+ sessionCache = null;
331
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * 롯데마트 매장 검색 보조 로직
3
+ */
4
+ import { LOTTEMART_AREAS } from './api.js';
5
+ import { calculateDistanceM, matchesBrandVariant, matchesKeyword } from './parser.js';
6
+ export function normalizeArea(area) {
7
+ const trimmed = (area || '').trim();
8
+ if (trimmed.length === 0) {
9
+ return undefined;
10
+ }
11
+ if (trimmed === '제주') {
12
+ return '기타';
13
+ }
14
+ return LOTTEMART_AREAS.find((value) => value === trimmed);
15
+ }
16
+ export function toDisplayArea(area) {
17
+ return area === '기타' ? '제주' : area;
18
+ }
19
+ export function getTargetAreas(area) {
20
+ return area ? [area] : [...LOTTEMART_AREAS];
21
+ }
22
+ export function attachDistance(stores, latitude, longitude) {
23
+ return stores.map((store) => {
24
+ if (typeof latitude !== 'number' || typeof longitude !== 'number') {
25
+ return store;
26
+ }
27
+ if (store.latitude === 0 || store.longitude === 0) {
28
+ return store;
29
+ }
30
+ return {
31
+ ...store,
32
+ distanceM: calculateDistanceM(latitude, longitude, store.latitude, store.longitude),
33
+ };
34
+ });
35
+ }
36
+ export async function fetchAllStoresForAreaList(areas, fetcher) {
37
+ return (await Promise.all(areas.map((value) => fetcher(value)))).flat();
38
+ }
39
+ export async function fetchKeywordMatchedStores(areas, keyword, brandVariant, limit, fetcher) {
40
+ const stores = [];
41
+ for (const currentArea of areas) {
42
+ const areaStores = await fetcher(currentArea);
43
+ const matched = areaStores
44
+ .filter((store) => matchesKeyword(store, keyword))
45
+ .filter((store) => matchesBrandVariant(store, brandVariant));
46
+ stores.push(...matched);
47
+ if (stores.length >= limit) {
48
+ break;
49
+ }
50
+ }
51
+ return stores;
52
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * 롯데마트 주변 매장 탐색 도구
3
+ */
4
+ import * as z from 'zod';
5
+ import { fetchLotteMartStores } from '../client.js';
6
+ import { DEFAULT_LOTTEMART_TIMEOUT_MS } from '../config.js';
7
+ async function findNearbyStores(args) {
8
+ const { area, keyword = '', brandVariant = '', latitude, longitude, limit = 20, timeoutMs = DEFAULT_LOTTEMART_TIMEOUT_MS, googleMapsApiKey, zyteApiKey, } = args;
9
+ const result = await fetchLotteMartStores({
10
+ area,
11
+ keyword,
12
+ brandVariant,
13
+ latitude,
14
+ longitude,
15
+ limit,
16
+ }, {
17
+ timeout: timeoutMs,
18
+ googleMapsApiKey,
19
+ zyteApiKey,
20
+ });
21
+ return {
22
+ content: [
23
+ {
24
+ type: 'text',
25
+ text: JSON.stringify({
26
+ area: area || null,
27
+ keyword,
28
+ brandVariant: brandVariant || null,
29
+ geocodeUsed: result.geocodeUsed,
30
+ location: result.location,
31
+ count: result.stores.length,
32
+ stores: result.stores,
33
+ }, null, 2),
34
+ },
35
+ ],
36
+ };
37
+ }
38
+ export function createFindNearbyStoresTool(googleMapsApiKey, zyteApiKey) {
39
+ return {
40
+ name: 'lottemart_find_nearby_stores',
41
+ metadata: {
42
+ title: '롯데마트 주변 매장 탐색',
43
+ description: '지역/키워드와 좌표 기준으로 롯데마트 계열 매장을 조회하고 거리순으로 정렬합니다.',
44
+ inputSchema: {
45
+ area: z.string().optional().describe('지역 (예: 서울, 경기, 제주)'),
46
+ keyword: z.string().optional().describe('매장명/주소 키워드 (예: 강변, 잠실, 서울역)'),
47
+ brandVariant: z
48
+ .enum(['lottemart', 'toysrus', 'max', 'bottlebunker', 'mealguru', 'grandgrocery', 'other'])
49
+ .optional()
50
+ .describe('브랜드 변형 필터'),
51
+ latitude: z.number().optional().describe('위도 (미입력 시 keyword 지오코딩 시도)'),
52
+ longitude: z.number().optional().describe('경도 (미입력 시 keyword 지오코딩 시도)'),
53
+ limit: z.number().optional().default(20).describe('반환할 최대 매장 수 (기본값: 20)'),
54
+ timeoutMs: z
55
+ .number()
56
+ .optional()
57
+ .default(DEFAULT_LOTTEMART_TIMEOUT_MS)
58
+ .describe(`요청 제한 시간(ms, 기본값: ${DEFAULT_LOTTEMART_TIMEOUT_MS})`),
59
+ },
60
+ },
61
+ handler: ((args) => findNearbyStores({
62
+ ...args,
63
+ googleMapsApiKey: args.googleMapsApiKey || googleMapsApiKey,
64
+ zyteApiKey: args.zyteApiKey || zyteApiKey,
65
+ })),
66
+ };
67
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * 롯데마트 상품 검색 도구
3
+ */
4
+ import * as z from 'zod';
5
+ import { searchLotteMartProducts } from '../client.js';
6
+ import { DEFAULT_LOTTEMART_TIMEOUT_MS } from '../config.js';
7
+ async function searchProducts(args) {
8
+ const { area, storeCode, storeName, keyword, pageLimit = 3, timeoutMs = DEFAULT_LOTTEMART_TIMEOUT_MS, zyteApiKey, } = args;
9
+ const result = await searchLotteMartProducts({
10
+ area,
11
+ storeCode,
12
+ storeName,
13
+ keyword,
14
+ pageLimit,
15
+ }, {
16
+ timeout: timeoutMs,
17
+ zyteApiKey,
18
+ });
19
+ return {
20
+ content: [
21
+ {
22
+ type: 'text',
23
+ text: JSON.stringify({
24
+ area: result.area,
25
+ storeCode: result.storeCode,
26
+ storeName: result.storeName,
27
+ keyword,
28
+ pageLimit,
29
+ totalCount: result.totalCount,
30
+ totalPages: result.totalPages,
31
+ count: result.products.length,
32
+ products: result.products,
33
+ }, null, 2),
34
+ },
35
+ ],
36
+ };
37
+ }
38
+ export function createSearchProductsTool(zyteApiKey) {
39
+ return {
40
+ name: 'lottemart_search_products',
41
+ metadata: {
42
+ title: '롯데마트 상품 검색',
43
+ description: '특정 롯데마트 매장을 기준으로 상품 가격과 재고를 조회합니다.',
44
+ inputSchema: {
45
+ area: z.string().optional().describe('지역 (storeCode 미입력 시 storeName 해석에 사용)'),
46
+ storeCode: z.string().optional().describe('매장 코드 (예: 2301)'),
47
+ storeName: z.string().optional().describe('매장명 (예: 강변점)'),
48
+ keyword: z.string().describe('상품 검색어 (예: 콜라, 우유, 과자)'),
49
+ pageLimit: z.number().optional().default(3).describe('추가 조회할 최대 페이지 수 (기본값: 3)'),
50
+ timeoutMs: z
51
+ .number()
52
+ .optional()
53
+ .default(DEFAULT_LOTTEMART_TIMEOUT_MS)
54
+ .describe(`요청 제한 시간(ms, 기본값: ${DEFAULT_LOTTEMART_TIMEOUT_MS})`),
55
+ },
56
+ },
57
+ handler: ((args) => searchProducts({
58
+ ...args,
59
+ zyteApiKey: args.zyteApiKey || zyteApiKey,
60
+ })),
61
+ };
62
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * 롯데마트 타입 정의
3
+ */
4
+ export {};
@@ -0,0 +1,105 @@
1
+ /**
2
+ * 롯데마트 제타 상품 검색 fallback
3
+ */
4
+ import { fetchJson } from '../../utils/http.js';
5
+ import { dedupeProducts } from './parser.js';
6
+ import { normalizeArea } from './storeSearch.js';
7
+ const ZETTA_PRODUCT_SEARCH_URL = 'https://lottemartzetta.com/api/webproductpagews/v6/product-pages/search';
8
+ function toZettaProductPrice(product) {
9
+ const parsed = Number.parseFloat(product.price?.amount || '');
10
+ return Number.isFinite(parsed) ? parsed : 0;
11
+ }
12
+ function toZettaProductBarcode(retailerProductId) {
13
+ return (retailerProductId || '').replace(/^OS/i, '').trim();
14
+ }
15
+ function mapZettaProduct(area, storeCode, storeName, keyword, product) {
16
+ const productName = (product.name || '').trim();
17
+ if (productName.length === 0) {
18
+ return null;
19
+ }
20
+ return {
21
+ area,
22
+ storeCode,
23
+ storeName,
24
+ keyword,
25
+ productName,
26
+ barcode: toZettaProductBarcode(product.retailerProductId),
27
+ spec: (product.packSizeDescription || '').trim(),
28
+ manufacturer: (product.brand || '').trim(),
29
+ price: toZettaProductPrice(product),
30
+ stockQuantity: product.available === false ? 0 : 1,
31
+ };
32
+ }
33
+ function createZettaProductSearchUrl(keyword, pageToken) {
34
+ const endpoint = new URL(ZETTA_PRODUCT_SEARCH_URL);
35
+ endpoint.searchParams.set('q', keyword);
36
+ endpoint.searchParams.set('tag', 'web');
37
+ endpoint.searchParams.set('includeAdditionalPageInfo', pageToken ? 'false' : 'true');
38
+ endpoint.searchParams.set('maxProductsToDecorate', '50');
39
+ endpoint.searchParams.set('maxPageSize', '50');
40
+ if (pageToken) {
41
+ endpoint.searchParams.set('pageToken', pageToken);
42
+ }
43
+ return endpoint.toString();
44
+ }
45
+ export function createZettaFallbackStore(params) {
46
+ const storeCode = (params.storeCode || '').trim();
47
+ const storeName = (params.storeName || '').trim();
48
+ return {
49
+ area: normalizeArea(params.area || '') || '서울',
50
+ storeCode: storeCode || 'zetta',
51
+ storeName: storeName || `롯데마트 ${storeCode}`,
52
+ brandVariant: 'lottemart',
53
+ };
54
+ }
55
+ export async function fetchZettaLotteMartProductsWithPrimaryError(resolvedStore, keyword, pageLimit, timeout, error) {
56
+ try {
57
+ return await fetchZettaLotteMartProducts(resolvedStore, keyword, pageLimit, timeout);
58
+ }
59
+ catch (fallbackError) {
60
+ const primaryMessage = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
61
+ const fallbackMessage = fallbackError instanceof Error ? fallbackError.message : '알 수 없는 오류가 발생했습니다.';
62
+ throw new Error(`롯데마트 상품 조회 실패: 기존 경로(${primaryMessage}), 제타 경로(${fallbackMessage})`);
63
+ }
64
+ }
65
+ function clampZettaPageLimit(pageLimit) {
66
+ if (pageLimit < 1) {
67
+ return 1;
68
+ }
69
+ if (pageLimit > 5) {
70
+ return 5;
71
+ }
72
+ return pageLimit;
73
+ }
74
+ async function fetchZettaLotteMartProducts(resolvedStore, keyword, pageLimit, timeout) {
75
+ const products = [];
76
+ let nextPageToken;
77
+ let fetchedPages = 0;
78
+ const maxPage = clampZettaPageLimit(pageLimit);
79
+ do {
80
+ const response = await fetchJson(createZettaProductSearchUrl(keyword, nextPageToken), {
81
+ timeout,
82
+ headers: {
83
+ Accept: 'application/json,text/plain,*/*',
84
+ 'Accept-Language': 'ko-KR,ko;q=0.9',
85
+ Referer: `https://lottemartzetta.com/products/search?q=${encodeURIComponent(keyword)}`,
86
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125 Safari/537.36',
87
+ },
88
+ });
89
+ fetchedPages += 1;
90
+ products.push(...(response.productGroups || [])
91
+ .flatMap((group) => group.decoratedProducts || [])
92
+ .map((product) => mapZettaProduct(resolvedStore.area, resolvedStore.storeCode, resolvedStore.storeName, keyword, product))
93
+ .filter((product) => product !== null));
94
+ nextPageToken = response.metadata?.nextPageToken;
95
+ } while (nextPageToken && fetchedPages < maxPage);
96
+ const dedupedProducts = dedupeProducts(products);
97
+ return {
98
+ area: resolvedStore.area,
99
+ storeCode: resolvedStore.storeCode,
100
+ storeName: resolvedStore.storeName,
101
+ totalCount: dedupedProducts.length,
102
+ totalPages: nextPageToken ? fetchedPages + 1 : fetchedPages,
103
+ products: dedupedProducts,
104
+ };
105
+ }