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.
- package/README.md +77 -5
- package/dist/api/actionsProxy.js +3 -0
- package/dist/api/cgvHandlers.js +15 -3
- package/dist/api/configStatus.js +2 -1
- package/dist/api/dtryxHandlers.js +113 -0
- package/dist/api/emart24Handlers.js +6 -2
- package/dist/api/gs25Handlers.js +22 -19
- package/dist/api/handlers.js +13 -5
- package/dist/api/healthCheckDefinitions.js +46 -16
- package/dist/api/healthCheckShape.js +24 -8
- package/dist/api/healthChecks.js +39 -20
- package/dist/api/operationalAuth.js +32 -0
- package/dist/api/routes/dtryxRoutes.js +22 -0
- package/dist/api/routes/healthRoutes.js +19 -11
- package/dist/api/routes/rateLimitStatsRoutes.js +274 -0
- package/dist/api/routes/registerServiceRoutes.js +37 -0
- package/dist/api/sevenelevenHandlers.js +7 -3
- package/dist/cli/commandOptions.js +24 -0
- package/dist/cli/commands/cinema.js +27 -0
- package/dist/cli.js +7 -1
- package/dist/cliHelp.js +3 -337
- package/dist/cliHelpDefinitions.js +369 -0
- package/dist/cliInteractive.js +8 -5
- package/dist/core/errors.js +29 -2
- package/dist/core/registry.js +13 -15
- package/dist/durableObjects/dailyRateLimiter.js +185 -0
- package/dist/durableObjects/rateLimitMetricsStore.js +144 -0
- package/dist/index.js +18 -31
- package/dist/middleware/dailyRateLimit.js +176 -0
- package/dist/pages/openapiFullSpec.js +2 -0
- package/dist/pages/openapiSpecActionParameters.js +429 -0
- package/dist/pages/openapiSpecActions.js +1 -391
- package/dist/pages/openapiSpecComponentsLottecinema.js +2 -2
- package/dist/pages/openapiSpecPathsDtryx.js +148 -0
- package/dist/pages/openapiSpecPathsLotteMart.js +10 -2
- package/dist/pages/promptRetailCinemaText.js +9 -1
- package/dist/services/cgv/errors.js +13 -0
- package/dist/services/cgv/transport.js +5 -22
- package/dist/services/cu/client.js +44 -30
- package/dist/services/cu/index.js +13 -3
- package/dist/services/cu/tools/checkInventory.js +65 -45
- package/dist/services/cu/tools/findNearbyStores.js +4 -3
- package/dist/services/cu/upstreamError.js +11 -0
- package/dist/services/dtryx/api.js +25 -0
- package/dist/services/dtryx/client.js +133 -0
- package/dist/services/dtryx/index.js +24 -0
- package/dist/services/dtryx/location.js +216 -0
- package/dist/services/dtryx/tools/getRemainingSeats.js +117 -0
- package/dist/services/dtryx/tools/listCinemas.js +46 -0
- package/dist/services/dtryx/tools/listNowShowing.js +77 -0
- package/dist/services/dtryx/types.js +4 -0
- package/dist/services/dtryx/validation.js +21 -0
- package/dist/services/feedback/tools/submitDeveloperRequest.js +15 -1
- package/dist/services/gs25/api.js +2 -2
- package/dist/services/gs25/client.js +16 -123
- package/dist/services/gs25/errors.js +12 -0
- package/dist/services/gs25/index.js +2 -2
- package/dist/services/gs25/productSearch.js +2 -14
- package/dist/services/gs25/storeStockTransport.js +30 -0
- package/dist/services/gs25/tools/checkInventory.js +7 -2
- package/dist/services/gs25/tools/findNearbyStores.js +20 -5
- package/dist/services/lottecinema/client.js +4 -2
- package/dist/services/lottemart/config.js +1 -1
- package/dist/services/lottemart/session.js +33 -201
- package/dist/services/lottemart/socketTransport.js +149 -0
- package/dist/services/oliveyoung/client.js +7 -39
- package/dist/services/oliveyoung/index.js +4 -4
- package/dist/services/oliveyoung/tools/checkInventory.js +6 -3
- package/dist/services/oliveyoung/tools/findNearbyStores.js +4 -3
- package/dist/services/oliveyoung/tools/searchProducts.js +4 -3
- package/dist/services/oliveyoung/transport.js +46 -0
- package/dist/services/seveneleven/client.js +20 -9
- package/dist/services/seveneleven/index.js +11 -7
- package/dist/services/seveneleven/inventory.js +5 -2
- package/dist/services/seveneleven/productKeyword.js +3 -2
- package/dist/services/seveneleven/readCache.js +49 -0
- package/dist/services/seveneleven/tools/checkInventory.js +4 -3
- package/dist/services/seveneleven/tools/getCatalogSnapshot.js +4 -3
- package/dist/services/seveneleven/tools/getSearchPopwords.js +4 -3
- package/dist/services/seveneleven/tools/searchProducts.js +4 -3
- package/dist/services/seveneleven/tools/searchStores.js +4 -3
- package/dist/utils/format.js +10 -3
- package/dist/utils/http.js +40 -12
- package/dist/utils/zyte.js +4 -101
- package/dist/utils/zyteJsonFallback.js +28 -0
- package/openapi.json +51 -3
- package/openapi.yaml +36 -3
- package/package.json +28 -27
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
* 롯데마트 세션 쿠키 관리
|
|
3
3
|
*/
|
|
4
4
|
import { HttpError, fetchWithTimeout } from '../../utils/http.js';
|
|
5
|
-
import {
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
* 올리브영
|
|
2
|
+
* 올리브영 무료 API 클라이언트
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 공식 API 또는 운영자가 설정한 브라우저 릴레이를 호출합니다.
|
|
5
5
|
*/
|
|
6
6
|
import { OLIVEYOUNG_API } from './api.js';
|
|
7
7
|
import { resolveOliveyoungImageUrl, resolveOliveyoungInStock, resolveOliveyoungStoreStock, } from './normalize.js';
|
|
8
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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: '올리브영 상품 검색, 주변 매장 탐색 및 재고 파악 서비스 (
|
|
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
|
}
|