daiso 1.0.10 → 1.2.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 (88) hide show
  1. package/README.md +77 -5
  2. package/dist/api/actionsProxy.js +3 -0
  3. package/dist/api/cgvHandlers.js +15 -3
  4. package/dist/api/configStatus.js +2 -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 +13 -5
  9. package/dist/api/healthCheckDefinitions.js +46 -16
  10. package/dist/api/healthCheckShape.js +24 -8
  11. package/dist/api/healthChecks.js +39 -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.js +7 -1
  21. package/dist/cliHelp.js +3 -337
  22. package/dist/cliHelpDefinitions.js +369 -0
  23. package/dist/cliInteractive.js +8 -5
  24. package/dist/core/errors.js +29 -2
  25. package/dist/core/registry.js +13 -15
  26. package/dist/durableObjects/dailyRateLimiter.js +185 -0
  27. package/dist/durableObjects/rateLimitMetricsStore.js +144 -0
  28. package/dist/index.js +18 -31
  29. package/dist/middleware/dailyRateLimit.js +176 -0
  30. package/dist/pages/openapiFullSpec.js +2 -0
  31. package/dist/pages/openapiSpecActionParameters.js +429 -0
  32. package/dist/pages/openapiSpecActions.js +1 -391
  33. package/dist/pages/openapiSpecComponentsLottecinema.js +2 -2
  34. package/dist/pages/openapiSpecPathsDtryx.js +148 -0
  35. package/dist/pages/openapiSpecPathsLotteMart.js +10 -2
  36. package/dist/pages/promptRetailCinemaText.js +9 -1
  37. package/dist/services/cgv/errors.js +13 -0
  38. package/dist/services/cgv/transport.js +5 -22
  39. package/dist/services/cu/client.js +44 -30
  40. package/dist/services/cu/index.js +13 -3
  41. package/dist/services/cu/tools/checkInventory.js +65 -45
  42. package/dist/services/cu/tools/findNearbyStores.js +4 -3
  43. package/dist/services/cu/upstreamError.js +11 -0
  44. package/dist/services/dtryx/api.js +25 -0
  45. package/dist/services/dtryx/client.js +133 -0
  46. package/dist/services/dtryx/index.js +24 -0
  47. package/dist/services/dtryx/location.js +216 -0
  48. package/dist/services/dtryx/tools/getRemainingSeats.js +117 -0
  49. package/dist/services/dtryx/tools/listCinemas.js +46 -0
  50. package/dist/services/dtryx/tools/listNowShowing.js +77 -0
  51. package/dist/services/dtryx/types.js +4 -0
  52. package/dist/services/dtryx/validation.js +21 -0
  53. package/dist/services/feedback/tools/submitDeveloperRequest.js +15 -1
  54. package/dist/services/gs25/api.js +2 -2
  55. package/dist/services/gs25/client.js +16 -123
  56. package/dist/services/gs25/errors.js +12 -0
  57. package/dist/services/gs25/index.js +2 -2
  58. package/dist/services/gs25/productSearch.js +2 -14
  59. package/dist/services/gs25/storeStockTransport.js +30 -0
  60. package/dist/services/gs25/tools/checkInventory.js +7 -2
  61. package/dist/services/gs25/tools/findNearbyStores.js +20 -5
  62. package/dist/services/lottecinema/client.js +4 -2
  63. package/dist/services/lottemart/config.js +1 -1
  64. package/dist/services/lottemart/session.js +33 -201
  65. package/dist/services/lottemart/socketTransport.js +149 -0
  66. package/dist/services/oliveyoung/client.js +7 -39
  67. package/dist/services/oliveyoung/index.js +4 -4
  68. package/dist/services/oliveyoung/tools/checkInventory.js +6 -3
  69. package/dist/services/oliveyoung/tools/findNearbyStores.js +4 -3
  70. package/dist/services/oliveyoung/tools/searchProducts.js +4 -3
  71. package/dist/services/oliveyoung/transport.js +46 -0
  72. package/dist/services/seveneleven/client.js +20 -9
  73. package/dist/services/seveneleven/index.js +11 -7
  74. package/dist/services/seveneleven/inventory.js +5 -2
  75. package/dist/services/seveneleven/productKeyword.js +3 -2
  76. package/dist/services/seveneleven/readCache.js +49 -0
  77. package/dist/services/seveneleven/tools/checkInventory.js +4 -3
  78. package/dist/services/seveneleven/tools/getCatalogSnapshot.js +4 -3
  79. package/dist/services/seveneleven/tools/getSearchPopwords.js +4 -3
  80. package/dist/services/seveneleven/tools/searchProducts.js +4 -3
  81. package/dist/services/seveneleven/tools/searchStores.js +4 -3
  82. package/dist/utils/format.js +10 -3
  83. package/dist/utils/http.js +40 -12
  84. package/dist/utils/zyte.js +4 -101
  85. package/dist/utils/zyteJsonFallback.js +28 -0
  86. package/openapi.json +51 -3
  87. package/openapi.yaml +36 -3
  88. package/package.json +28 -27
@@ -2,9 +2,12 @@
2
2
  * 롯데마트 세션 쿠키 관리
3
3
  */
4
4
  import { HttpError, fetchWithTimeout } from '../../utils/http.js';
5
- import { decodeBase64, requestByZyte } from '../../utils/zyte.js';
5
+ import { ZYTE_COST_POLICY_MESSAGE } from '../../core/errors.js';
6
6
  import { LOTTEMART_API } from './api.js';
7
+ import { fetchLotteMartSocketResponse, 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,56 +47,25 @@ 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;
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
51
  const headers = withLotteMartSessionCookie({
211
52
  Accept: 'text/html, */*; q=0.01',
212
53
  ...init.headers,
213
54
  }, 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 응답 본문이 비어 있습니다.');
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;
224
68
  }
225
- return decodeBase64(result.httpResponseBody);
226
69
  }
227
70
  function toBodyPreview(bodyText) {
228
71
  const normalized = bodyText.trim().replace(/\s+/g, ' ');
@@ -238,7 +81,9 @@ export async function probeLotteMartRequest(url, init, timeout, sessionCookie, z
238
81
  success: response.ok,
239
82
  status: response.status,
240
83
  statusText: response.statusText,
241
- error: response.ok ? null : new HttpError(response.status, response.statusText, bodyText).message,
84
+ error: response.ok
85
+ ? null
86
+ : new HttpError(response.status, response.statusText, bodyText).message,
242
87
  bodyPreview: toBodyPreview(bodyText),
243
88
  sessionCookie: extractSessionCookie(response) || null,
244
89
  });
@@ -255,29 +100,15 @@ export async function probeLotteMartRequest(url, init, timeout, sessionCookie, z
255
100
  });
256
101
  }
257
102
  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
- }
103
+ attempts.push({
104
+ used: 'zyte',
105
+ success: false,
106
+ status: null,
107
+ statusText: null,
108
+ error: ZYTE_COST_POLICY_MESSAGE,
109
+ bodyPreview: null,
110
+ sessionCookie: sessionCookie || null,
111
+ });
281
112
  }
282
113
  return attempts;
283
114
  }
@@ -304,7 +135,9 @@ export async function fetchLotteMartHtml(url, init, timeout, sessionCookie, zyte
304
135
  if (!response.ok) {
305
136
  throw new HttpError(response.status, response.statusText, bodyText);
306
137
  }
307
- if (bodyText.trim().length === 0 && sessionCookie.trim().length === 0 && cachedCookie.length > 0) {
138
+ if (bodyText.trim().length === 0 &&
139
+ sessionCookie.trim().length === 0 &&
140
+ cachedCookie.length > 0) {
308
141
  const retried = await fetchLotteMartResponse(url, init, timeout, cachedCookie);
309
142
  cacheSessionCookieFromResponse(retried);
310
143
  const retriedBodyText = await retried.text();
@@ -317,8 +150,7 @@ export async function fetchLotteMartHtml(url, init, timeout, sessionCookie, zyte
317
150
  }
318
151
  catch (error) {
319
152
  if (zyteApiKey && error instanceof Error && !error.message.includes('Zyte')) {
320
- const fallbackCookie = await getCachedLotteMartSessionCookie(timeout);
321
- return fetchLotteMartHtmlByZyte(url, init, timeout, fallbackCookie || sessionCookie, zyteApiKey);
153
+ throw new Error(ZYTE_COST_POLICY_MESSAGE);
322
154
  }
323
155
  throw error;
324
156
  }
@@ -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 */
@@ -1,11 +1,11 @@
1
1
  /**
2
- * 올리브영 Zyte 클라이언트
2
+ * 올리브영 무료 API 클라이언트
3
3
  *
4
- * Zyte extract API를 통해 올리브영 내부 API를 우회 호출합니다.
4
+ * 공식 API 또는 운영자가 설정한 브라우저 릴레이를 호출합니다.
5
5
  */
6
6
  import { OLIVEYOUNG_API } from './api.js';
7
7
  import { resolveOliveyoungImageUrl, resolveOliveyoungInStock, resolveOliveyoungStoreStock, } from './normalize.js';
8
- import { decodeBase64, requestByZyte } from '../../utils/zyte.js';
8
+ import { requestOliveyoung } from './transport.js';
9
9
  const OLIVEYOUNG_PRODUCT_ID_CACHE_TTL_MS = 10 * 60 * 1000;
10
10
  const OLIVEYOUNG_PRODUCT_SEARCH_STALE_CACHE_TTL_MS = 30 * 60 * 1000;
11
11
  const oliveyoungProductIdCache = new Map();
@@ -56,38 +56,6 @@ function writeStaleResult(cache, cacheKey, result, cloneResult) {
56
56
  result: cloneResult(result),
57
57
  });
58
58
  }
59
- async function zyteExtract(targetPath, requestBody, options = {}) {
60
- const { timeout = 15000, apiKey } = options;
61
- try {
62
- const result = await requestByZyte({
63
- apiKey,
64
- timeout,
65
- url: `${OLIVEYOUNG_API.BASE_URL}${targetPath}`,
66
- method: 'POST',
67
- headers: [
68
- { name: 'Content-Type', value: 'application/json' },
69
- { name: 'Accept', value: 'application/json' },
70
- { name: 'X-Requested-With', value: 'XMLHttpRequest' },
71
- ],
72
- bodyText: JSON.stringify(requestBody),
73
- });
74
- if (result.statusCode !== 200 || !result.httpResponseBody) {
75
- throw new Error(`올리브영 API 응답 실패: ${result.statusCode || 'unknown'}`);
76
- }
77
- const decodedBody = decodeBase64(result.httpResponseBody);
78
- const parsedBody = JSON.parse(decodedBody);
79
- if (parsedBody.status !== 'SUCCESS') {
80
- throw new Error(`올리브영 API 상태 오류: ${parsedBody.status || 'UNKNOWN'}`);
81
- }
82
- return parsedBody;
83
- }
84
- catch (error) {
85
- if (error instanceof Error && error.name === 'AbortError') {
86
- throw new Error('올리브영 API 요청 시간 초과');
87
- }
88
- throw error;
89
- }
90
- }
91
59
  export async function fetchOliveyoungStores(params, options = {}) {
92
60
  const payload = {
93
61
  lat: params.latitude,
@@ -101,7 +69,7 @@ export async function fetchOliveyoungStores(params, options = {}) {
101
69
  };
102
70
  const cacheKey = createOliveyoungStoreSearchCacheKey(params);
103
71
  try {
104
- const body = await zyteExtract(OLIVEYOUNG_API.STORE_FINDER_PATH, payload, options);
72
+ const body = await requestOliveyoung(OLIVEYOUNG_API.STORE_FINDER_PATH, payload, options);
105
73
  const stores = (body.data?.storeList || []).map((store) => ({
106
74
  storeCode: store.storeCode || '',
107
75
  storeName: store.storeName || '',
@@ -136,7 +104,7 @@ export async function fetchOliveyoungProducts(params, options = {}) {
136
104
  };
137
105
  const cacheKey = createOliveyoungProductSearchCacheKey(params);
138
106
  try {
139
- const body = await zyteExtract(OLIVEYOUNG_API.PRODUCT_SEARCH_PATH, payload, options);
107
+ const body = await requestOliveyoung(OLIVEYOUNG_API.PRODUCT_SEARCH_PATH, payload, options);
140
108
  const list = body.data?.serachList || body.data?.searchList || [];
141
109
  const products = list.map((product) => {
142
110
  const o2oStockFlag = Boolean(product.o2oStockFlag);
@@ -184,7 +152,7 @@ async function fetchOliveyoungProductId(goodsNumber, options = {}) {
184
152
  if (cached && cached.expiresAt > now) {
185
153
  return cached.productId;
186
154
  }
187
- const body = await zyteExtract(OLIVEYOUNG_API.STOCK_GOODS_INFO_PATH, { goodsNo: normalizedGoodsNumber }, options);
155
+ const body = await requestOliveyoung(OLIVEYOUNG_API.STOCK_GOODS_INFO_PATH, { goodsNo: normalizedGoodsNumber }, options);
188
156
  const productId = body.data?.goodsInfo?.masterGoodsNumber || '';
189
157
  if (productId) {
190
158
  oliveyoungProductIdCache.set(normalizedGoodsNumber, {
@@ -197,7 +165,7 @@ async function fetchOliveyoungProductId(goodsNumber, options = {}) {
197
165
  async function fetchOliveyoungStockStores(params, options = {}) {
198
166
  const cacheKey = createOliveyoungStockStoresCacheKey(params);
199
167
  try {
200
- const body = await zyteExtract(OLIVEYOUNG_API.STOCK_STORES_PATH, {
168
+ const body = await requestOliveyoung(OLIVEYOUNG_API.STOCK_STORES_PATH, {
201
169
  productId: params.productId,
202
170
  lat: params.latitude,
203
171
  lon: params.longitude,
@@ -8,7 +8,7 @@ const OLIVEYOUNG_METADATA = {
8
8
  id: 'oliveyoung',
9
9
  name: '올리브영',
10
10
  version: '1.0.0',
11
- description: '올리브영 상품 검색, 주변 매장 탐색 및 재고 파악 서비스 (Zyte API 기반)',
11
+ description: '올리브영 상품 검색, 주변 매장 탐색 및 재고 파악 서비스 (무료 직접 요청 및 브라우저 릴레이)',
12
12
  };
13
13
  class OliveyoungService {
14
14
  options;
@@ -18,9 +18,9 @@ class OliveyoungService {
18
18
  }
19
19
  getTools() {
20
20
  return [
21
- createSearchProductsTool(this.options.zyteApiKey),
22
- createFindNearbyStoresTool(this.options.zyteApiKey),
23
- createCheckInventoryTool(this.options.zyteApiKey),
21
+ createSearchProductsTool(this.options.zyteApiKey, this.options),
22
+ createFindNearbyStoresTool(this.options.zyteApiKey, this.options),
23
+ createCheckInventoryTool(this.options.zyteApiKey, this.options),
24
24
  ];
25
25
  }
26
26
  }
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import * as z from 'zod';
5
5
  import { enrichOliveyoungProductsWithNearbyStoreInventory, fetchOliveyoungProducts, fetchOliveyoungStores, } from '../client.js';
6
- async function checkInventory(args) {
6
+ async function checkInventory(args, transport) {
7
7
  const { keyword, latitude = 37.5665, longitude = 126.978, storeKeyword = '', page = 1, size = 20, sort = '01', includeSoldOut = false, storeLimit = 10, stockCheckLimit = 5, timeoutMs = 15000, zyteApiKey, } = args;
8
8
  if (!keyword || keyword.trim().length === 0) {
9
9
  throw new Error('상품 검색어(keyword)를 입력해주세요.');
@@ -17,6 +17,7 @@ async function checkInventory(args) {
17
17
  }, {
18
18
  timeout: timeoutMs,
19
19
  apiKey: zyteApiKey,
20
+ ...transport,
20
21
  }),
21
22
  fetchOliveyoungProducts({
22
23
  keyword,
@@ -27,6 +28,7 @@ async function checkInventory(args) {
27
28
  }, {
28
29
  timeout: timeoutMs,
29
30
  apiKey: zyteApiKey,
31
+ ...transport,
30
32
  }),
31
33
  ]);
32
34
  const enrichedInventory = await enrichOliveyoungProductsWithNearbyStoreInventory(productResult.products, {
@@ -37,6 +39,7 @@ async function checkInventory(args) {
37
39
  }, {
38
40
  timeout: timeoutMs,
39
41
  apiKey: zyteApiKey,
42
+ ...transport,
40
43
  });
41
44
  const inStockProducts = enrichedInventory.products.filter((product) => product.inStock);
42
45
  const result = {
@@ -71,7 +74,7 @@ async function checkInventory(args) {
71
74
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
72
75
  };
73
76
  }
74
- export function createCheckInventoryTool(apiKey) {
77
+ export function createCheckInventoryTool(apiKey, transport = {}) {
75
78
  return {
76
79
  name: 'oliveyoung_check_inventory',
77
80
  metadata: {
@@ -98,6 +101,6 @@ export function createCheckInventoryTool(apiKey) {
98
101
  timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
99
102
  },
100
103
  },
101
- handler: ((args) => checkInventory({ ...args, zyteApiKey: apiKey })),
104
+ handler: ((args) => checkInventory({ ...args, zyteApiKey: apiKey }, transport)),
102
105
  };
103
106
  }
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import * as z from 'zod';
5
5
  import { fetchOliveyoungStores } from '../client.js';
6
- async function findNearbyStores(args) {
6
+ async function findNearbyStores(args, transport) {
7
7
  const { latitude = 37.5665, longitude = 126.978, keyword = '', pageIdx = 1, limit = 20, timeoutMs = 15000, zyteApiKey, } = args;
8
8
  const { totalCount, stores } = await fetchOliveyoungStores({
9
9
  latitude,
@@ -13,6 +13,7 @@ async function findNearbyStores(args) {
13
13
  }, {
14
14
  timeout: timeoutMs,
15
15
  apiKey: zyteApiKey,
16
+ ...transport,
16
17
  });
17
18
  const limitedStores = stores.slice(0, limit);
18
19
  const result = {
@@ -30,7 +31,7 @@ async function findNearbyStores(args) {
30
31
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
31
32
  };
32
33
  }
33
- export function createFindNearbyStoresTool(apiKey) {
34
+ export function createFindNearbyStoresTool(apiKey, transport = {}) {
34
35
  return {
35
36
  name: 'oliveyoung_find_nearby_stores',
36
37
  metadata: {
@@ -48,6 +49,6 @@ export function createFindNearbyStoresTool(apiKey) {
48
49
  timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
49
50
  },
50
51
  },
51
- handler: ((args) => findNearbyStores({ ...args, zyteApiKey: apiKey })),
52
+ handler: ((args) => findNearbyStores({ ...args, zyteApiKey: apiKey }, transport)),
52
53
  };
53
54
  }
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import * as z from 'zod';
5
5
  import { fetchOliveyoungProducts } from '../client.js';
6
- async function searchProducts(args) {
6
+ async function searchProducts(args, transport) {
7
7
  const { keyword, page = 1, size = 20, sort = '01', includeSoldOut = false, timeoutMs = 15000, zyteApiKey, } = args;
8
8
  if (!keyword || keyword.trim().length === 0) {
9
9
  throw new Error('상품 검색어(keyword)를 입력해주세요.');
@@ -17,6 +17,7 @@ async function searchProducts(args) {
17
17
  }, {
18
18
  timeout: timeoutMs,
19
19
  apiKey: zyteApiKey,
20
+ ...transport,
20
21
  });
21
22
  return {
22
23
  content: [
@@ -37,7 +38,7 @@ async function searchProducts(args) {
37
38
  ],
38
39
  };
39
40
  }
40
- export function createSearchProductsTool(apiKey) {
41
+ export function createSearchProductsTool(apiKey, transport = {}) {
41
42
  return {
42
43
  name: 'oliveyoung_search_products',
43
44
  metadata: {
@@ -52,6 +53,6 @@ export function createSearchProductsTool(apiKey) {
52
53
  timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
53
54
  },
54
55
  },
55
- handler: ((args) => searchProducts({ ...args, zyteApiKey: apiKey })),
56
+ handler: ((args) => searchProducts({ ...args, zyteApiKey: apiKey }, transport)),
56
57
  };
57
58
  }