daiso 1.0.9 → 1.1.0

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 (86) hide show
  1. package/README.md +88 -9
  2. package/dist/api/actionsProxy.js +3 -0
  3. package/dist/api/cgvHandlers.js +15 -3
  4. package/dist/api/configStatus.js +1 -1
  5. package/dist/api/dtryxHandlers.js +113 -0
  6. package/dist/api/emart24Handlers.js +6 -2
  7. package/dist/api/gs25Handlers.js +22 -19
  8. package/dist/api/handlers.js +3 -0
  9. package/dist/api/healthCheckDefinitions.js +56 -8
  10. package/dist/api/healthCheckShape.js +24 -8
  11. package/dist/api/healthChecks.js +48 -20
  12. package/dist/api/operationalAuth.js +32 -0
  13. package/dist/api/routes/dtryxRoutes.js +22 -0
  14. package/dist/api/routes/healthRoutes.js +19 -11
  15. package/dist/api/routes/rateLimitStatsRoutes.js +274 -0
  16. package/dist/api/routes/registerServiceRoutes.js +37 -0
  17. package/dist/api/sevenelevenHandlers.js +7 -3
  18. package/dist/cli/commandOptions.js +24 -0
  19. package/dist/cli/commands/cinema.js +27 -0
  20. package/dist/cli/commands/convenience.js +2 -2
  21. package/dist/cli.js +7 -1
  22. package/dist/cliHelp.js +3 -337
  23. package/dist/cliHelpDefinitions.js +369 -0
  24. package/dist/cliInteractive.js +8 -5
  25. package/dist/core/errors.js +17 -2
  26. package/dist/core/registry.js +13 -15
  27. package/dist/durableObjects/dailyRateLimiter.js +185 -0
  28. package/dist/durableObjects/rateLimitMetricsStore.js +144 -0
  29. package/dist/index.js +16 -30
  30. package/dist/middleware/dailyRateLimit.js +176 -0
  31. package/dist/pages/openapiFullSpec.js +2 -0
  32. package/dist/pages/openapiSpecActionParameters.js +429 -0
  33. package/dist/pages/openapiSpecActions.js +1 -391
  34. package/dist/pages/openapiSpecComponentsLottecinema.js +2 -2
  35. package/dist/pages/openapiSpecPathsDtryx.js +148 -0
  36. package/dist/pages/openapiSpecPathsLotteMart.js +10 -2
  37. package/dist/pages/promptRetailCinemaText.js +9 -1
  38. package/dist/services/cgv/client.js +7 -4
  39. package/dist/services/cgv/errors.js +12 -0
  40. package/dist/services/cgv/transport.js +19 -2
  41. package/dist/services/cu/client.js +42 -12
  42. package/dist/services/cu/index.js +13 -3
  43. package/dist/services/cu/tools/checkInventory.js +65 -45
  44. package/dist/services/cu/tools/findNearbyStores.js +4 -3
  45. package/dist/services/cu/upstreamError.js +15 -0
  46. package/dist/services/dtryx/api.js +25 -0
  47. package/dist/services/dtryx/client.js +133 -0
  48. package/dist/services/dtryx/index.js +24 -0
  49. package/dist/services/dtryx/location.js +216 -0
  50. package/dist/services/dtryx/tools/getRemainingSeats.js +117 -0
  51. package/dist/services/dtryx/tools/listCinemas.js +46 -0
  52. package/dist/services/dtryx/tools/listNowShowing.js +77 -0
  53. package/dist/services/dtryx/types.js +4 -0
  54. package/dist/services/dtryx/validation.js +21 -0
  55. package/dist/services/emart24/tools/searchProducts.js +37 -19
  56. package/dist/services/feedback/tools/submitDeveloperRequest.js +15 -1
  57. package/dist/services/gs25/api.js +2 -2
  58. package/dist/services/gs25/client.js +16 -123
  59. package/dist/services/gs25/errors.js +12 -0
  60. package/dist/services/gs25/index.js +2 -2
  61. package/dist/services/gs25/productSearch.js +1 -0
  62. package/dist/services/gs25/storeStockTransport.js +55 -0
  63. package/dist/services/gs25/tools/checkInventory.js +7 -2
  64. package/dist/services/gs25/tools/findNearbyStores.js +20 -5
  65. package/dist/services/lottecinema/client.js +4 -2
  66. package/dist/services/lottemart/config.js +1 -1
  67. package/dist/services/lottemart/session.js +29 -160
  68. package/dist/services/lottemart/socketTransport.js +149 -0
  69. package/dist/services/oliveyoung/client.js +1 -0
  70. package/dist/services/places/client.js +29 -8
  71. package/dist/services/seveneleven/client.js +18 -8
  72. package/dist/services/seveneleven/index.js +11 -7
  73. package/dist/services/seveneleven/inventory.js +5 -2
  74. package/dist/services/seveneleven/productKeyword.js +3 -2
  75. package/dist/services/seveneleven/tools/checkInventory.js +4 -3
  76. package/dist/services/seveneleven/tools/getCatalogSnapshot.js +4 -3
  77. package/dist/services/seveneleven/tools/getSearchPopwords.js +4 -3
  78. package/dist/services/seveneleven/tools/searchProducts.js +40 -20
  79. package/dist/services/seveneleven/tools/searchStores.js +4 -3
  80. package/dist/utils/format.js +10 -3
  81. package/dist/utils/http.js +38 -10
  82. package/dist/utils/zyte.js +13 -2
  83. package/dist/utils/zyteJsonFallback.js +67 -0
  84. package/openapi.json +51 -3
  85. package/openapi.yaml +36 -3
  86. package/package.json +28 -24
@@ -4,7 +4,10 @@
4
4
  import { HttpError, fetchWithTimeout } from '../../utils/http.js';
5
5
  import { decodeBase64, requestByZyte } from '../../utils/zyte.js';
6
6
  import { LOTTEMART_API } from './api.js';
7
+ import { fetchLotteMartSocketResponse, toLotteMartBodyText, withLotteMartSessionCookie, } from './socketTransport.js';
8
+ export { __testOnlyCreateLotteMartSocketResponse, __testOnlyFetchLotteMartSocketResponse, withLotteMartSessionCookie, } from './socketTransport.js';
7
9
  const SESSION_CACHE_TTL_MS = 5 * 60 * 1000;
10
+ const FALLBACK_TIMEOUT_MS = 5000;
8
11
  let sessionCache = null;
9
12
  function extractSessionCookie(response) {
10
13
  const cookieHeaders = response.headers;
@@ -17,135 +20,6 @@ function extractSessionCookie(response) {
17
20
  .find((value) => value.startsWith('ASPSESSIONID'));
18
21
  return matched ? matched.split(';')[0].trim() : '';
19
22
  }
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
23
  function cacheSessionCookie(cookie) {
150
24
  const normalized = cookie.trim();
151
25
  if (normalized.length === 0) {
@@ -173,39 +47,29 @@ export async function getCachedLotteMartSessionCookie(_timeout, forceRefresh = f
173
47
  export async function getFreshLotteMartSessionCookie(timeout) {
174
48
  return getCachedLotteMartSessionCookie(timeout, true);
175
49
  }
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
50
  async function fetchLotteMartResponse(url, init, timeout, sessionCookie) {
184
- const socketResponse = await fetchLotteMartSocketResponse(url, init, sessionCookie, timeout);
185
- if (socketResponse) {
186
- return socketResponse;
51
+ const headers = withLotteMartSessionCookie({
52
+ Accept: 'text/html, */*; q=0.01',
53
+ ...init.headers,
54
+ }, sessionCookie);
55
+ try {
56
+ return await fetchWithTimeout(url, {
57
+ ...init,
58
+ timeout,
59
+ headers,
60
+ });
61
+ }
62
+ catch (directError) {
63
+ const socketResponse = await fetchLotteMartSocketResponse(url, init, sessionCookie, Math.min(timeout, FALLBACK_TIMEOUT_MS)).catch(() => null);
64
+ if (socketResponse) {
65
+ return socketResponse;
66
+ }
67
+ throw directError;
187
68
  }
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
69
  }
197
70
  function toZyteHeaders(headers) {
198
71
  return Array.from(headers.entries()).map(([name, value]) => ({ name, value }));
199
72
  }
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
73
  async function fetchLotteMartHtmlByZyte(url, init, timeout, sessionCookie, zyteApiKey) {
210
74
  const headers = withLotteMartSessionCookie({
211
75
  Accept: 'text/html, */*; q=0.01',
@@ -217,7 +81,8 @@ async function fetchLotteMartHtmlByZyte(url, init, timeout, sessionCookie, zyteA
217
81
  timeout,
218
82
  method: init.method || 'GET',
219
83
  headers: toZyteHeaders(headers),
220
- bodyText: toBodyText(init.body),
84
+ bodyText: toLotteMartBodyText(init.body),
85
+ tags: { service: 'lottemart' },
221
86
  });
222
87
  if (!result.httpResponseBody) {
223
88
  throw new Error('Zyte HTTP 응답 본문이 비어 있습니다.');
@@ -238,7 +103,9 @@ export async function probeLotteMartRequest(url, init, timeout, sessionCookie, z
238
103
  success: response.ok,
239
104
  status: response.status,
240
105
  statusText: response.statusText,
241
- error: response.ok ? null : new HttpError(response.status, response.statusText, bodyText).message,
106
+ error: response.ok
107
+ ? null
108
+ : new HttpError(response.status, response.statusText, bodyText).message,
242
109
  bodyPreview: toBodyPreview(bodyText),
243
110
  sessionCookie: extractSessionCookie(response) || null,
244
111
  });
@@ -304,7 +171,9 @@ export async function fetchLotteMartHtml(url, init, timeout, sessionCookie, zyte
304
171
  if (!response.ok) {
305
172
  throw new HttpError(response.status, response.statusText, bodyText);
306
173
  }
307
- if (bodyText.trim().length === 0 && sessionCookie.trim().length === 0 && cachedCookie.length > 0) {
174
+ if (bodyText.trim().length === 0 &&
175
+ sessionCookie.trim().length === 0 &&
176
+ cachedCookie.length > 0) {
308
177
  const retried = await fetchLotteMartResponse(url, init, timeout, cachedCookie);
309
178
  cacheSessionCookieFromResponse(retried);
310
179
  const retriedBodyText = await retried.text();
@@ -318,7 +187,7 @@ export async function fetchLotteMartHtml(url, init, timeout, sessionCookie, zyte
318
187
  catch (error) {
319
188
  if (zyteApiKey && error instanceof Error && !error.message.includes('Zyte')) {
320
189
  const fallbackCookie = await getCachedLotteMartSessionCookie(timeout);
321
- return fetchLotteMartHtmlByZyte(url, init, timeout, fallbackCookie || sessionCookie, zyteApiKey);
190
+ return fetchLotteMartHtmlByZyte(url, init, Math.min(timeout, FALLBACK_TIMEOUT_MS), fallbackCookie || sessionCookie, zyteApiKey);
322
191
  }
323
192
  throw error;
324
193
  }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * 롯데마트 HTTP 소켓 전송
3
+ */
4
+ import { LOTTEMART_API } from './api.js';
5
+ function withSocketTimeout(operation, timeout) {
6
+ const timeoutMs = Math.max(1, Math.trunc(timeout));
7
+ return new Promise((resolve, reject) => {
8
+ const timeoutId = setTimeout(() => resolve(null), timeoutMs);
9
+ operation.then((value) => {
10
+ clearTimeout(timeoutId);
11
+ resolve(value);
12
+ }, (error) => {
13
+ clearTimeout(timeoutId);
14
+ reject(error);
15
+ });
16
+ });
17
+ }
18
+ export function withLotteMartSessionCookie(headers, sessionCookie) {
19
+ const result = new Headers(headers);
20
+ if (sessionCookie.trim().length > 0) {
21
+ result.set('Cookie', sessionCookie);
22
+ }
23
+ return result;
24
+ }
25
+ export function toLotteMartBodyText(body) {
26
+ if (typeof body === 'string') {
27
+ return body;
28
+ }
29
+ if (body instanceof URLSearchParams) {
30
+ return body.toString();
31
+ }
32
+ return undefined;
33
+ }
34
+ export function __testOnlyCreateLotteMartSocketResponse(raw) {
35
+ const delimiter = new TextEncoder().encode('\r\n\r\n');
36
+ let boundary = -1;
37
+ for (let index = 0; index <= raw.length - delimiter.length; index += 1) {
38
+ let matched = true;
39
+ for (let inner = 0; inner < delimiter.length; inner += 1) {
40
+ if (raw[index + inner] !== delimiter[inner]) {
41
+ matched = false;
42
+ break;
43
+ }
44
+ }
45
+ if (matched) {
46
+ boundary = index;
47
+ break;
48
+ }
49
+ }
50
+ if (boundary < 0) {
51
+ return null;
52
+ }
53
+ const headerText = new TextDecoder().decode(raw.slice(0, boundary));
54
+ if (!headerText.startsWith('HTTP/')) {
55
+ return null;
56
+ }
57
+ const bodyBytes = raw.slice(boundary + delimiter.length);
58
+ const headerLinesRaw = headerText.split('\r\n');
59
+ const statusLine = headerLinesRaw.shift();
60
+ const [, statusCodeText = '500', ...statusTextParts] = statusLine.split(' ');
61
+ const responseHeaders = new Headers();
62
+ for (const line of headerLinesRaw) {
63
+ const separatorIndex = line.indexOf(':');
64
+ if (separatorIndex < 0) {
65
+ continue;
66
+ }
67
+ responseHeaders.append(line.slice(0, separatorIndex).trim(), line.slice(separatorIndex + 1).trim());
68
+ }
69
+ return new Response(bodyBytes, {
70
+ status: parseInt(statusCodeText, 10) || 500,
71
+ statusText: statusTextParts.join(' ').trim(),
72
+ headers: responseHeaders,
73
+ });
74
+ }
75
+ export async function __testOnlyFetchLotteMartSocketResponse(url, init, sessionCookie, connectFn, timeout) {
76
+ const requestUrl = new URL(url);
77
+ const headers = withLotteMartSessionCookie({
78
+ Accept: 'text/html, */*; q=0.01',
79
+ Host: LOTTEMART_API.ORIGIN_HOST,
80
+ Origin: 'https://company.lottemart.com',
81
+ Referer: LOTTEMART_API.ORIGIN_REFERER,
82
+ Connection: 'close',
83
+ ...init.headers,
84
+ }, sessionCookie);
85
+ const bodyText = toLotteMartBodyText(init.body) || '';
86
+ if (bodyText.length > 0 && !headers.has('Content-Length')) {
87
+ headers.set('Content-Length', String(new TextEncoder().encode(bodyText).length));
88
+ }
89
+ const headerLines = Array.from(headers.entries()).map(([name, value]) => `${name}: ${value}`);
90
+ const path = `${requestUrl.pathname}${requestUrl.search}`;
91
+ const requestText = [`${init.method || 'GET'} ${path} HTTP/1.1`, ...headerLines, '', bodyText].join('\r\n');
92
+ const socket = connectFn({
93
+ hostname: LOTTEMART_API.SOCKET_HOST,
94
+ port: 80,
95
+ }, { allowHalfOpen: true });
96
+ const writer = socket.writable.getWriter();
97
+ const encodedRequest = new TextEncoder().encode(requestText);
98
+ if ((await withSocketTimeout(writer.write(encodedRequest), timeout)) === null) {
99
+ void writer.abort().catch(Boolean);
100
+ return null;
101
+ }
102
+ if ((await withSocketTimeout(writer.close(), timeout)) === null) {
103
+ void writer.abort().catch(Boolean);
104
+ return null;
105
+ }
106
+ const reader = socket.readable.getReader();
107
+ const chunks = [];
108
+ try {
109
+ while (true) {
110
+ const readResult = await withSocketTimeout(reader.read(), timeout);
111
+ if (readResult === null) {
112
+ await reader.cancel().catch(() => undefined);
113
+ return null;
114
+ }
115
+ const { value, done } = readResult;
116
+ if (done) {
117
+ break;
118
+ }
119
+ chunks.push(value);
120
+ }
121
+ }
122
+ finally {
123
+ reader.releaseLock();
124
+ }
125
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
126
+ const raw = new Uint8Array(totalLength);
127
+ let offset = 0;
128
+ for (const chunk of chunks) {
129
+ raw.set(chunk, offset);
130
+ offset += chunk.length;
131
+ }
132
+ return __testOnlyCreateLotteMartSocketResponse(raw);
133
+ }
134
+ /* c8 ignore start */
135
+ export async function fetchLotteMartSocketResponse(url, init, sessionCookie, timeout) {
136
+ let connectFn = null;
137
+ try {
138
+ const socketsModule = await import('cloudflare:sockets');
139
+ connectFn = socketsModule.connect;
140
+ }
141
+ catch {
142
+ return null;
143
+ }
144
+ if (!connectFn) {
145
+ return null;
146
+ }
147
+ return __testOnlyFetchLotteMartSocketResponse(url, init, sessionCookie, connectFn, timeout);
148
+ }
149
+ /* c8 ignore end */
@@ -70,6 +70,7 @@ async function zyteExtract(targetPath, requestBody, options = {}) {
70
70
  { name: 'X-Requested-With', value: 'XMLHttpRequest' },
71
71
  ],
72
72
  bodyText: JSON.stringify(requestBody),
73
+ tags: { service: 'oliveyoung' },
73
74
  });
74
75
  if (result.statusCode !== 200 || !result.httpResponseBody) {
75
76
  throw new Error(`올리브영 API 응답 실패: ${result.statusCode || 'unknown'}`);
@@ -5,22 +5,43 @@ const CATEGORY_KEYWORDS = {
5
5
  dessert: '디저트',
6
6
  all: '',
7
7
  };
8
+ const HTML_ENTITY_REPLACEMENTS = {
9
+ '&amp;': '&',
10
+ '&quot;': '"',
11
+ '&#39;': "'",
12
+ '&#x27;': "'",
13
+ };
8
14
  function getProcessEnvValue(name) {
9
15
  /* c8 ignore next -- Node 테스트와 CLI 런타임은 process를 제공하며, Worker에는 API에서 키를 주입합니다. */
10
16
  return typeof process !== 'undefined' ? process.env[name] : undefined;
11
17
  }
18
+ function stripHtmlTags(value) {
19
+ let result = '';
20
+ let insideTag = false;
21
+ for (const character of value) {
22
+ if (character === '<') {
23
+ insideTag = true;
24
+ continue;
25
+ }
26
+ if (character === '>' && insideTag) {
27
+ insideTag = false;
28
+ continue;
29
+ }
30
+ if (!insideTag) {
31
+ result += character;
32
+ }
33
+ }
34
+ return result;
35
+ }
12
36
  function cleanHtml(value) {
13
37
  if (!value) {
14
38
  return '';
15
39
  }
16
- return value
17
- .replace(/<[^>]*>/g, '')
18
- .replace(/&amp;/g, '&')
19
- .replace(/&lt;/g, '<')
20
- .replace(/&gt;/g, '>')
21
- .replace(/&quot;/g, '"')
22
- .replace(/&#39;/g, "'")
23
- .trim();
40
+ let result = stripHtmlTags(value);
41
+ for (const [entity, replacement] of Object.entries(HTML_ENTITY_REPLACEMENTS)) {
42
+ result = result.split(entity).join(replacement);
43
+ }
44
+ return result.trim();
24
45
  }
25
46
  function toCoordinate(value) {
26
47
  if (!value) {
@@ -2,7 +2,7 @@
2
2
  * 세븐일레븐 API 클라이언트
3
3
  */
4
4
  /* c8 ignore start */
5
- import { fetchJson } from '../../utils/http.js';
5
+ import { fetchJsonWithZyteFallback } from '../../utils/zyteJsonFallback.js';
6
6
  import { SEVENELEVEN_API } from './api.js';
7
7
  const SEVENELEVEN_DEFAULT_HEADERS = {
8
8
  Accept: 'application/json, text/plain, */*',
@@ -109,15 +109,17 @@ function normalizeStores(items) {
109
109
  .filter((store) => store.storeCode.length > 0 || store.storeName.length > 0);
110
110
  }
111
111
  async function requestSevenElevenJson(path, method, body, options = {}) {
112
- const { timeout = 15000 } = options;
112
+ const { timeout = 15000, zyteApiKey } = options;
113
113
  const url = `${SEVENELEVEN_API.BASE_URL}${path}`;
114
- return fetchJson(url, {
114
+ return fetchJsonWithZyteFallback(url, {
115
115
  ...SEVENELEVEN_DEFAULT_FETCH_OPTIONS,
116
116
  method,
117
117
  retryUnsafeMethods: method === 'POST',
118
118
  timeout,
119
119
  headers: SEVENELEVEN_DEFAULT_HEADERS,
120
120
  body: method === 'POST' ? JSON.stringify(body || {}) : undefined,
121
+ zyteApiKey,
122
+ zyteTags: { service: 'seveneleven' },
121
123
  });
122
124
  }
123
125
  export async function searchSevenElevenProducts(params, options = {}) {
@@ -216,7 +218,7 @@ export async function fetchSevenElevenStockProductMeta(itemCode, options = {}) {
216
218
  const { timeout = 15000 } = options;
217
219
  const encodedItemCode = encodeURIComponent(itemCode.trim());
218
220
  const url = `${SEVENELEVEN_API.BASE_URL}${SEVENELEVEN_API.PRODUCT_SEARCH_STOCK_PATH}?itemCd=${encodedItemCode}`;
219
- const response = await fetchJson(url, {
221
+ const response = await fetchJsonWithZyteFallback(url, {
220
222
  ...SEVENELEVEN_DEFAULT_FETCH_OPTIONS,
221
223
  method: 'GET',
222
224
  timeout,
@@ -224,6 +226,8 @@ export async function fetchSevenElevenStockProductMeta(itemCode, options = {}) {
224
226
  Accept: SEVENELEVEN_DEFAULT_HEADERS.Accept,
225
227
  'User-Agent': SEVENELEVEN_DEFAULT_HEADERS['User-Agent'],
226
228
  },
229
+ zyteApiKey: options.zyteApiKey,
230
+ zyteTags: { service: 'seveneleven' },
227
231
  });
228
232
  const productMeta = normalizeStockProductMeta(response);
229
233
  if (productMeta.itemCode.length === 0 || productMeta.smCode.length === 0) {
@@ -263,14 +267,20 @@ function extractContentArray(data) {
263
267
  return [];
264
268
  }
265
269
  export async function fetchSevenElevenCatalogSnapshot(options = {}) {
266
- const { includeIssues = true, includeExhibition = true, timeout = 15000 } = options;
270
+ const { includeIssues = true, includeExhibition = true, timeout = 15000, zyteApiKey } = options;
267
271
  const tasks = [
268
- requestSevenElevenJson(SEVENELEVEN_API.PRODUCT_PAGES_PATH, 'GET', null, { timeout }),
272
+ requestSevenElevenJson(SEVENELEVEN_API.PRODUCT_PAGES_PATH, 'GET', null, { timeout, zyteApiKey }),
269
273
  includeIssues
270
- ? requestSevenElevenJson(SEVENELEVEN_API.PRODUCT_ISSUES_PATH, 'GET', null, { timeout })
274
+ ? requestSevenElevenJson(SEVENELEVEN_API.PRODUCT_ISSUES_PATH, 'GET', null, {
275
+ timeout,
276
+ zyteApiKey,
277
+ })
271
278
  : Promise.resolve({ data: { content: [] } }),
272
279
  includeExhibition
273
- ? requestSevenElevenJson(SEVENELEVEN_API.EXHIBITION_MAIN_PATH, 'GET', null, { timeout })
280
+ ? requestSevenElevenJson(SEVENELEVEN_API.EXHIBITION_MAIN_PATH, 'GET', null, {
281
+ timeout,
282
+ zyteApiKey,
283
+ })
274
284
  : Promise.resolve({ data: [] }),
275
285
  ];
276
286
  const [pagesResult, issuesResult, exhibitionsResult] = await Promise.allSettled(tasks);
@@ -13,18 +13,22 @@ const SEVENELEVEN_METADATA = {
13
13
  description: '세븐일레븐 공개 상품/검색/카탈로그 조회 서비스',
14
14
  };
15
15
  class SevenElevenService {
16
+ options;
16
17
  metadata = SEVENELEVEN_METADATA;
18
+ constructor(options = {}) {
19
+ this.options = options;
20
+ }
17
21
  getTools() {
18
22
  return [
19
- createSearchProductsTool(),
20
- createSearchStoresTool(),
21
- createCheckInventoryTool(),
22
- createGetSearchPopwordsTool(),
23
- createGetCatalogSnapshotTool(),
23
+ createSearchProductsTool(this.options.zyteApiKey),
24
+ createSearchStoresTool(this.options.zyteApiKey),
25
+ createCheckInventoryTool(this.options.zyteApiKey),
26
+ createGetSearchPopwordsTool(this.options.zyteApiKey),
27
+ createGetCatalogSnapshotTool(this.options.zyteApiKey),
24
28
  ];
25
29
  }
26
30
  }
27
- export function createSevenElevenService() {
28
- return new SevenElevenService();
31
+ export function createSevenElevenService(options = {}) {
32
+ return new SevenElevenService(options);
29
33
  }
30
34
  export * from './types.js';
@@ -5,7 +5,8 @@
5
5
  * 재고 API가 암호화로 차단된 경우에도 상품/매장 정보를 graceful하게 반환합니다.
6
6
  */
7
7
  /* c8 ignore start */
8
- import { fetchJson, HttpError } from '../../utils/http.js';
8
+ import { HttpError } from '../../utils/http.js';
9
+ import { fetchJsonWithZyteFallback } from '../../utils/zyteJsonFallback.js';
9
10
  import { SEVENELEVEN_API } from './api.js';
10
11
  import { fetchSevenElevenStockProductMeta, fetchSevenElevenStoresByKeyword, } from './client.js';
11
12
  import { pickBestSevenElevenProduct, searchSevenElevenProductsWithVariants } from './productKeyword.js';
@@ -180,11 +181,13 @@ async function tryStockApi(stockProduct, stores, options = {}) {
180
181
  return { stores: null, error: null };
181
182
  }
182
183
  try {
183
- const response = await fetchJson(url, {
184
+ const response = await fetchJsonWithZyteFallback(url, {
184
185
  method: 'POST',
185
186
  timeout,
186
187
  headers: SEVENELEVEN_DEFAULT_HEADERS,
187
188
  body: JSON.stringify(payload),
189
+ zyteApiKey: options.zyteApiKey,
190
+ zyteTags: { service: 'seveneleven', operation: 'inventory' },
188
191
  });
189
192
  const responseError = normalizeEnvelopeError(response);
190
193
  if (responseError) {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 세븐일레븐 상품 검색어 보정
3
3
  */
4
- import { searchSevenElevenProducts } from './client.js';
4
+ import { searchSevenElevenProducts, } from './client.js';
5
5
  const SANDWICH_SUFFIX_FAMILY = ['샌드위치', '샌드', '산도'];
6
6
  function normalizeProductText(value) {
7
7
  return value.replace(/[^0-9A-Za-z가-힣]+/g, '').toLowerCase();
@@ -95,7 +95,7 @@ function scoreSevenElevenProduct(product, rawKeyword) {
95
95
  * 보정 질의를 순차 적용해 세븐일레븐 상품을 찾습니다.
96
96
  */
97
97
  export async function searchSevenElevenProductsWithVariants(query, options = {}) {
98
- const { page = 1, size = 20, sort = 'recommend', timeout } = options;
98
+ const { page = 1, size = 20, sort = 'recommend', timeout, zyteApiKey } = options;
99
99
  const appliedQueries = buildSevenElevenProductKeywordVariants(query);
100
100
  const seenKeys = new Set();
101
101
  const collectionIds = new Set();
@@ -108,6 +108,7 @@ export async function searchSevenElevenProductsWithVariants(query, options = {})
108
108
  sort,
109
109
  }, {
110
110
  timeout,
111
+ zyteApiKey,
111
112
  });
112
113
  for (const collectionId of result.collectionIds) {
113
114
  collectionIds.add(collectionId);
@@ -16,7 +16,7 @@ function isEncryptedStockFailure(stockError) {
16
16
  const messageMatched = /RSA|복호화|암호화|서비스를 사용할 수 없습니다/i.test(messageSource);
17
17
  return codeMatched || messageMatched;
18
18
  }
19
- async function checkInventory(args) {
19
+ async function checkInventory(args, zyteApiKey) {
20
20
  const { keyword, storeKeyword = '', storeLimit = 20, timeoutMs = 20000, } = args;
21
21
  if (!keyword || keyword.trim().length === 0) {
22
22
  throw new Error('상품 검색어(keyword)를 입력해주세요.');
@@ -27,6 +27,7 @@ async function checkInventory(args) {
27
27
  storeLimit,
28
28
  }, {
29
29
  timeout: timeoutMs,
30
+ zyteApiKey,
30
31
  });
31
32
  const note = !result.product
32
33
  ? '상품 검색 결과가 없어 재고를 조회하지 못했습니다. 다른 상품명이나 더 짧은 키워드로 다시 시도해주세요.'
@@ -59,7 +60,7 @@ async function checkInventory(args) {
59
60
  ],
60
61
  };
61
62
  }
62
- export function createCheckInventoryTool() {
63
+ export function createCheckInventoryTool(zyteApiKey) {
63
64
  return {
64
65
  name: 'seveneleven_check_inventory',
65
66
  metadata: {
@@ -76,7 +77,7 @@ export function createCheckInventoryTool() {
76
77
  timeoutMs: z.number().optional().default(20000).describe('요청 제한 시간(ms, 기본값: 20000)'),
77
78
  },
78
79
  },
79
- handler: checkInventory,
80
+ handler: (args) => checkInventory(args, zyteApiKey),
80
81
  };
81
82
  }
82
83
  /* c8 ignore stop */
@@ -3,12 +3,13 @@
3
3
  */
4
4
  import * as z from 'zod';
5
5
  import { fetchSevenElevenCatalogSnapshot } from '../client.js';
6
- async function getCatalogSnapshot(args) {
6
+ async function getCatalogSnapshot(args, zyteApiKey) {
7
7
  const { includeIssues = true, includeExhibition = true, limit = 20, timeoutMs = 15000 } = args;
8
8
  const result = await fetchSevenElevenCatalogSnapshot({
9
9
  includeIssues,
10
10
  includeExhibition,
11
11
  timeout: timeoutMs,
12
+ zyteApiKey,
12
13
  });
13
14
  return {
14
15
  content: [
@@ -37,7 +38,7 @@ async function getCatalogSnapshot(args) {
37
38
  ],
38
39
  };
39
40
  }
40
- export function createGetCatalogSnapshotTool() {
41
+ export function createGetCatalogSnapshotTool(zyteApiKey) {
41
42
  return {
42
43
  name: 'seveneleven_get_catalog_snapshot',
43
44
  metadata: {
@@ -50,6 +51,6 @@ export function createGetCatalogSnapshotTool() {
50
51
  timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
51
52
  },
52
53
  },
53
- handler: getCatalogSnapshot,
54
+ handler: (args) => getCatalogSnapshot(args, zyteApiKey),
54
55
  };
55
56
  }