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.
- package/README.md +88 -9
- package/dist/api/actionsProxy.js +3 -0
- package/dist/api/cgvHandlers.js +15 -3
- package/dist/api/configStatus.js +1 -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 +3 -0
- package/dist/api/healthCheckDefinitions.js +56 -8
- package/dist/api/healthCheckShape.js +24 -8
- package/dist/api/healthChecks.js +48 -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/commands/convenience.js +2 -2
- 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 +17 -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 +16 -30
- 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/client.js +7 -4
- package/dist/services/cgv/errors.js +12 -0
- package/dist/services/cgv/transport.js +19 -2
- package/dist/services/cu/client.js +42 -12
- 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 +15 -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/emart24/tools/searchProducts.js +37 -19
- 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 +1 -0
- package/dist/services/gs25/storeStockTransport.js +55 -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 +29 -160
- package/dist/services/lottemart/socketTransport.js +149 -0
- package/dist/services/oliveyoung/client.js +1 -0
- package/dist/services/places/client.js +29 -8
- package/dist/services/seveneleven/client.js +18 -8
- 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/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 +40 -20
- package/dist/services/seveneleven/tools/searchStores.js +4 -3
- package/dist/utils/format.js +10 -3
- package/dist/utils/http.js +38 -10
- package/dist/utils/zyte.js +13 -2
- package/dist/utils/zyteJsonFallback.js +67 -0
- package/openapi.json +51 -3
- package/openapi.yaml +36 -3
- 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
|
|
185
|
-
|
|
186
|
-
|
|
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:
|
|
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
|
|
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 &&
|
|
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
|
+
'&': '&',
|
|
10
|
+
'"': '"',
|
|
11
|
+
''': "'",
|
|
12
|
+
''': "'",
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.replace(/"/g, '"')
|
|
22
|
-
.replace(/'/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 {
|
|
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
|
|
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
|
|
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, {
|
|
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, {
|
|
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 {
|
|
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
|
|
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
|
}
|