daiso 1.1.0 → 1.2.1

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 CHANGED
@@ -94,6 +94,8 @@
94
94
 
95
95
  </div>
96
96
 
97
+ > Zyte 유료 호출을 중지했습니다. 무료 원본 조회를 우선하며 일부 서비스는 별도 실행 환경이 필요합니다. [비용 정책과 서비스별 상태](docs/zyte-free-access.md)를 확인하세요.
98
+
97
99
  > [!IMPORTANT]
98
100
  > 최근 공개 서버 사용량이 크게 증가하여 2026년 7월 18일부터 올리브영·CGV·CU·GS25·롯데마트의 검색을 포함한 공개 GET API에 IP당 하루 합산 3,000회(KST 기준)의 호출 제한을 적용합니다. 한도를 초과하는 사용이 필요하다면 Daiso MCP는 오픈 소스이므로 이 저장소를 직접 배포해 이용해 주세요.
99
101
 
@@ -9,7 +9,8 @@ export function buildConfigStatus(bindings) {
9
9
  },
10
10
  zyteApiKey: {
11
11
  configured: isConfigured(bindings?.ZYTE_API_KEY),
12
- usedBy: ['oliveyoung', 'gs25', 'cu', 'seveneleven', 'lottemart', 'cgv'],
12
+ enabled: false,
13
+ usedBy: [],
13
14
  },
14
15
  naverLocalSearch: {
15
16
  configured: isConfigured(bindings?.NAVER_CLIENT_ID) && isConfigured(bindings?.NAVER_CLIENT_SECRET),
@@ -44,7 +44,10 @@ export async function handleOliveyoungSearchProducts(c) {
44
44
  sort,
45
45
  includeSoldOut,
46
46
  }, {
47
- apiKey: c.env?.ZYTE_API_KEY,
47
+ relayUrl: c.env?.OY_RELAY_URL,
48
+ relayToken: c.env?.OY_RELAY_TOKEN,
49
+ accessClientId: c.env?.OY_ACCESS_CLIENT_ID,
50
+ accessClientSecret: c.env?.OY_ACCESS_CLIENT_SECRET,
48
51
  timeout: timeoutMs,
49
52
  });
50
53
  return successResponse(c, {
@@ -79,7 +82,10 @@ export async function handleOliveyoungFindStores(c) {
79
82
  pageIdx,
80
83
  searchWords: keyword,
81
84
  }, {
82
- apiKey: c.env?.ZYTE_API_KEY,
85
+ relayUrl: c.env?.OY_RELAY_URL,
86
+ relayToken: c.env?.OY_RELAY_TOKEN,
87
+ accessClientId: c.env?.OY_ACCESS_CLIENT_ID,
88
+ accessClientSecret: c.env?.OY_ACCESS_CLIENT_SECRET,
83
89
  timeout: timeoutMs,
84
90
  });
85
91
  return successResponse(c, {
@@ -120,7 +126,10 @@ export async function handleOliveyoungCheckInventory(c) {
120
126
  pageIdx: 1,
121
127
  searchWords: storeKeyword,
122
128
  }, {
123
- apiKey: c.env.ZYTE_API_KEY,
129
+ relayUrl: c.env?.OY_RELAY_URL,
130
+ relayToken: c.env?.OY_RELAY_TOKEN,
131
+ accessClientId: c.env?.OY_ACCESS_CLIENT_ID,
132
+ accessClientSecret: c.env?.OY_ACCESS_CLIENT_SECRET,
124
133
  timeout: timeoutMs,
125
134
  }),
126
135
  fetchOliveyoungProducts({
@@ -130,7 +139,10 @@ export async function handleOliveyoungCheckInventory(c) {
130
139
  sort,
131
140
  includeSoldOut,
132
141
  }, {
133
- apiKey: c.env.ZYTE_API_KEY,
142
+ relayUrl: c.env?.OY_RELAY_URL,
143
+ relayToken: c.env?.OY_RELAY_TOKEN,
144
+ accessClientId: c.env?.OY_ACCESS_CLIENT_ID,
145
+ accessClientSecret: c.env?.OY_ACCESS_CLIENT_SECRET,
134
146
  timeout: timeoutMs,
135
147
  }),
136
148
  ]);
@@ -140,7 +152,10 @@ export async function handleOliveyoungCheckInventory(c) {
140
152
  storeKeyword,
141
153
  maxProducts: Math.min(productResult.products.length, stockCheckLimit),
142
154
  }, {
143
- apiKey: c.env.ZYTE_API_KEY,
155
+ relayUrl: c.env?.OY_RELAY_URL,
156
+ relayToken: c.env?.OY_RELAY_TOKEN,
157
+ accessClientId: c.env?.OY_ACCESS_CLIENT_ID,
158
+ accessClientSecret: c.env?.OY_ACCESS_CLIENT_SECRET,
144
159
  timeout: timeoutMs,
145
160
  });
146
161
  const inStockCount = enrichedInventory.products.filter((product) => product.inStock).length;
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * MCP/API 공통 에러 진단 구조
3
3
  */
4
+ export const ZYTE_COST_POLICY_MESSAGE = 'Zyte 유료 호출은 비용 정책에 따라 비활성화되어 있습니다.';
4
5
  const SERVICE_PREFIXES = [
5
6
  'daiso',
6
7
  'gs25',
@@ -50,15 +51,23 @@ function buildHint(retryable) {
50
51
  }
51
52
  export function toStandardErrorDiagnostics(code, message, options = {}) {
52
53
  const inferred = inferServiceAndOperation(code);
54
+ const costPolicyError = message.includes(ZYTE_COST_POLICY_MESSAGE);
53
55
  // 실제 Zyte 진단 문구만 설정 오류로 취급해 일반적인 접근 차단과 구분합니다.
54
56
  const configurationError = [
55
57
  'Zyte API 호출 실패: 403 Your account has been suspended.',
56
58
  'Zyte API 호출 실패: 403 account suspended',
57
59
  'ZYTE_API_KEY가 설정되지 않았습니다.',
58
60
  ].some((diagnostic) => message.includes(diagnostic));
59
- const gs25AuthenticationError = code === 'GS25_UPSTREAM_UNAVAILABLE' &&
60
- message.includes('GS25 재고 서비스 인증을 사용할 수 없습니다.');
61
- const retryable = !configurationError &&
61
+ const gs25AuthenticationError = message.includes('GS25 재고 서비스 인증을 사용할 수 없습니다.');
62
+ const oliveyoungRelayConfigurationError = [
63
+ '올리브영 릴레이 URL은 HTTPS 또는 로컬 HTTP 주소여야 합니다.',
64
+ 'OY_RELAY_TOKEN이 필요합니다.',
65
+ '올리브영 Access 서비스 토큰 설정이 필요합니다.',
66
+ '올리브영 직접 요청 실패. 운영자는 OY_RELAY_URL과 OY_RELAY_TOKEN으로 브라우저 릴레이를 설정해주세요.',
67
+ ].includes(message);
68
+ const retryable = !oliveyoungRelayConfigurationError &&
69
+ !costPolicyError &&
70
+ !configurationError &&
62
71
  !gs25AuthenticationError &&
63
72
  isRetryable(code, options.status || options.upstreamStatus);
64
73
  return {
@@ -69,11 +78,15 @@ export function toStandardErrorDiagnostics(code, message, options = {}) {
69
78
  service: options.service || inferred.service,
70
79
  operation: options.operation || inferred.operation,
71
80
  upstreamStatus: options.upstreamStatus,
72
- hint: gs25AuthenticationError
73
- ? '운영자는 GS25_API_KEY 설정을 확인하세요.'
74
- : configurationError
75
- ? '운영자는 ZYTE_API_KEY 설정과 Zyte 계정 상태를 확인하세요.'
76
- : buildHint(retryable),
81
+ hint: oliveyoungRelayConfigurationError
82
+ ? '운영자는 OY_RELAY_URL과 OY_RELAY_TOKEN 및 브라우저 릴레이 실행 상태를 확인하세요.'
83
+ : costPolicyError
84
+ ? ZYTE_COST_POLICY_MESSAGE
85
+ : gs25AuthenticationError
86
+ ? '운영자는 GS25_API_KEY 설정을 확인하세요.'
87
+ : configurationError
88
+ ? '운영자는 ZYTE_API_KEY 설정과 Zyte 계정 상태를 확인하세요.'
89
+ : buildHint(retryable),
77
90
  };
78
91
  }
79
92
  export function getErrorMessage(error) {
package/dist/index.js CHANGED
@@ -85,7 +85,10 @@ const createRegistry = (bindings) => {
85
85
  googleMapsApiKey: bindings?.GOOGLE_MAPS_API_KEY,
86
86
  }),
87
87
  () => createOliveyoungService({
88
- zyteApiKey: bindings?.ZYTE_API_KEY,
88
+ relayUrl: bindings?.OY_RELAY_URL,
89
+ relayToken: bindings?.OY_RELAY_TOKEN,
90
+ accessClientId: bindings?.OY_ACCESS_CLIENT_ID,
91
+ accessClientSecret: bindings?.OY_ACCESS_CLIENT_SECRET,
89
92
  }),
90
93
  ]);
91
94
  return registry;
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * CGV 원본 및 대체 전송 경로를 사용할 수 없는 상태
3
3
  */
4
+ import { ZYTE_COST_POLICY_MESSAGE } from '../../core/errors.js';
4
5
  export class CgvUpstreamUnavailableError extends Error {
5
6
  constructor() {
6
- super('CGV 원본 서비스에 연결할 수 없습니다. 잠시 후 다시 시도해주세요.');
7
+ super(`CGV 원본 서비스에 연결할 수 없습니다. ${ZYTE_COST_POLICY_MESSAGE}`);
7
8
  this.name = 'CgvUpstreamUnavailableError';
8
9
  }
9
10
  }
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * CGV 전송 계층
3
3
  *
4
- * 서명 헤더 생성, 직접 호출, Zyte fallback을 담당합니다.
4
+ * 서명 헤더를 생성하고 직접 호출합니다. 유료 대체 경로는 사용하지 않습니다.
5
5
  */
6
6
  import { createTimeoutController } from '../../utils/http.js';
7
- import { decodeZyteHttpBody, requestByZyte } from '../../utils/zyte.js';
8
7
  import { CGV_API } from './api.js';
9
8
  import { CgvUpstreamUnavailableError } from './errors.js';
10
9
  function toBase64(bytes) {
@@ -36,28 +35,7 @@ async function parseJsonResponse(response) {
36
35
  throw new Error(text.slice(0, 120) || 'CGV API 응답 파싱 실패');
37
36
  }
38
37
  }
39
- async function requestByZyteCgv(path, searchParams, timeout, apiKey) {
40
- const url = `${CGV_API.BASE_URL}${path}?${searchParams.toString()}`;
41
- const timestamp = Math.floor(Date.now() / 1000).toString();
42
- const signature = await createSignature(path, '', timestamp);
43
- const result = await requestByZyte({
44
- apiKey,
45
- timeout,
46
- url,
47
- headers: [
48
- { name: 'Accept', value: 'application/json' },
49
- { name: 'Accept-Language', value: 'ko-KR' },
50
- { name: 'X-TIMESTAMP', value: timestamp },
51
- { name: 'X-SIGNATURE', value: signature },
52
- ],
53
- tags: { service: 'cgv' },
54
- });
55
- if (result.statusCode === 401 || result.statusCode === 403) {
56
- throw new CgvUpstreamUnavailableError();
57
- }
58
- return decodeZyteHttpBody(result);
59
- }
60
- export async function requestCgv(path, searchParams, timeout = 15000, zyteApiKey) {
38
+ export async function requestCgv(path, searchParams, timeout = 15000, _zyteApiKey) {
61
39
  const url = `${CGV_API.BASE_URL}${path}?${searchParams.toString()}`;
62
40
  const timestamp = Math.floor(Date.now() / 1000).toString();
63
41
  const signature = await createSignature(path, '', timestamp);
@@ -77,19 +55,7 @@ export async function requestCgv(path, searchParams, timeout = 15000, zyteApiKey
77
55
  return await parseJsonResponse(response);
78
56
  }
79
57
  if (response.status === 401 || response.status === 403) {
80
- const normalizedZyteApiKey = zyteApiKey?.trim();
81
- if (!normalizedZyteApiKey) {
82
- throw new CgvUpstreamUnavailableError();
83
- }
84
- try {
85
- return await requestByZyteCgv(path, searchParams, timeout, normalizedZyteApiKey);
86
- }
87
- catch (fallbackError) {
88
- if (fallbackError instanceof CgvUpstreamUnavailableError) {
89
- throw fallbackError;
90
- }
91
- throw new CgvUpstreamUnavailableError();
92
- }
58
+ throw new CgvUpstreamUnavailableError();
93
59
  }
94
60
  throw new Error(`CGV API 호출 실패: ${response.status}`);
95
61
  }
@@ -2,7 +2,7 @@
2
2
  * CU API 클라이언트
3
3
  */
4
4
  import { fetchJson } 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 { fetchJsonWithZyteFallback } from '../../utils/zyteJsonFallback.js';
7
7
  import { CU_API } from './api.js';
8
8
  import { cuStockUnavailableReason } from './upstreamError.js';
@@ -57,7 +57,7 @@ async function requestCuJson(path, body, options = {}) {
57
57
  zyteTags: { service: 'cu' },
58
58
  });
59
59
  }
60
- async function requestCuWebHtml(path, body, apiKey, timeout = 15000) {
60
+ async function requestCuWebHtml(path, body, _apiKey, timeout = 15000) {
61
61
  const form = new URLSearchParams(body);
62
62
  const formText = form.toString();
63
63
  const targetUrl = `${CU_API.WEB_BASE_URL}${path}`;
@@ -71,23 +71,7 @@ async function requestCuWebHtml(path, body, apiKey, timeout = 15000) {
71
71
  return response.text();
72
72
  }
73
73
  if (response.status === 400 || response.status === 403 || response.status === 429) {
74
- try {
75
- const result = await requestByZyte({
76
- apiKey,
77
- url: targetUrl,
78
- timeout,
79
- method: 'POST',
80
- headers: Object.entries(CU_WEB_DEFAULT_HEADERS).map(([name, value]) => ({ name, value })),
81
- bodyText: formText,
82
- tags: { service: 'cu' },
83
- });
84
- if (result.statusCode === 200 && result.httpResponseBody) {
85
- return decodeBase64(result.httpResponseBody);
86
- }
87
- }
88
- catch {
89
- // 직접 호출 실패 시 Zyte 재시도도 실패하면 원본 에러를 반환합니다.
90
- }
74
+ throw new Error(ZYTE_COST_POLICY_MESSAGE);
91
75
  }
92
76
  throw new Error(`API 요청 실패: ${response.status} ${response.statusText}`);
93
77
  }
@@ -7,9 +7,5 @@ export function cuStockUnavailableReason(error) {
7
7
  if (error instanceof HttpError && [400, 403, 429].includes(error.status)) {
8
8
  return `CU 재고 API가 차단되었습니다 (${error.status} Request Blocked).`;
9
9
  }
10
- const message = error instanceof Error ? error.message : '';
11
- if (message.includes('Zyte API 호출 실패: 520') || message.includes('Zyte 대상 응답 실패: 520')) {
12
- return 'CU 재고 API가 차단되었습니다 (Zyte Website Ban 520).';
13
- }
14
10
  return null;
15
11
  }
@@ -3,7 +3,7 @@
3
3
  */
4
4
  /* c8 ignore start */
5
5
  import { fetchJson, HttpError } from '../../utils/http.js';
6
- import { decodeZyteHttpBody, requestByZyte } from '../../utils/zyte.js';
6
+ import { requestByZyte } from '../../utils/zyte.js';
7
7
  import { GS25_API } from './api.js';
8
8
  import { toNumber } from './storeUtils.js';
9
9
  const GS25_TOTAL_SEARCH_HEADERS = {
@@ -37,20 +37,7 @@ async function fetchGs25TotalSearchResponse(query, options) {
37
37
  if (!(error instanceof HttpError) || error.status !== 403 || !zyteApiKey) {
38
38
  throw error;
39
39
  }
40
- const result = await requestByZyte({
41
- apiKey: zyteApiKey,
42
- url: endpoint.toString(),
43
- method: 'POST',
44
- timeout,
45
- retries: 1,
46
- headers: Object.entries(GS25_TOTAL_SEARCH_HEADERS).map(([name, value]) => ({
47
- name,
48
- value,
49
- })),
50
- bodyText,
51
- tags: { service: 'gs25' },
52
- });
53
- return decodeZyteHttpBody(result);
40
+ return requestByZyte({ apiKey: zyteApiKey, url: endpoint.toString() });
54
41
  }
55
42
  }
56
43
  export async function fetchGs25NormalizedKeyword(keyword, options = {}) {
@@ -2,7 +2,6 @@
2
2
  * GS25 재고 원본 요청과 Zyte 대체 경로
3
3
  */
4
4
  import { fetchJson, HttpError } from '../../utils/http.js';
5
- import { decodeZyteHttpBody, requestByZyte } from '../../utils/zyte.js';
6
5
  import { Gs25UpstreamUnavailableError } from './errors.js';
7
6
  function withApiKey(headers, apiKey) {
8
7
  const normalizedApiKey = apiKey?.trim();
@@ -26,30 +25,6 @@ export async function fetchGs25StoreStockResponse(url, options, headers) {
26
25
  if (!(error instanceof HttpError) || !isAuthenticationStatus(error.status)) {
27
26
  throw error;
28
27
  }
29
- const zyteApiKey = options.zyteApiKey?.trim();
30
- if (error.status === 401 || !zyteApiKey) {
31
- throw new Gs25UpstreamUnavailableError();
32
- }
33
- try {
34
- const result = await requestByZyte({
35
- apiKey: zyteApiKey,
36
- url,
37
- method: 'GET',
38
- timeout: options.timeout,
39
- retries: 1,
40
- headers: Object.entries(requestHeaders).map(([name, value]) => ({ name, value })),
41
- tags: { service: 'gs25' },
42
- });
43
- if (isAuthenticationStatus(result.statusCode)) {
44
- throw new Gs25UpstreamUnavailableError();
45
- }
46
- return decodeZyteHttpBody(result);
47
- }
48
- catch (fallbackError) {
49
- if (fallbackError instanceof Gs25UpstreamUnavailableError) {
50
- throw fallbackError;
51
- }
52
- throw new Gs25UpstreamUnavailableError();
53
- }
28
+ throw new Gs25UpstreamUnavailableError();
54
29
  }
55
30
  }
@@ -2,9 +2,9 @@
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, toLotteMartBodyText, withLotteMartSessionCookie, } from './socketTransport.js';
7
+ import { fetchLotteMartSocketResponse, withLotteMartSessionCookie, } from './socketTransport.js';
8
8
  export { __testOnlyCreateLotteMartSocketResponse, __testOnlyFetchLotteMartSocketResponse, withLotteMartSessionCookie, } from './socketTransport.js';
9
9
  const SESSION_CACHE_TTL_MS = 5 * 60 * 1000;
10
10
  const FALLBACK_TIMEOUT_MS = 5000;
@@ -67,28 +67,6 @@ async function fetchLotteMartResponse(url, init, timeout, sessionCookie) {
67
67
  throw directError;
68
68
  }
69
69
  }
70
- function toZyteHeaders(headers) {
71
- return Array.from(headers.entries()).map(([name, value]) => ({ name, value }));
72
- }
73
- async function fetchLotteMartHtmlByZyte(url, init, timeout, sessionCookie, zyteApiKey) {
74
- const headers = withLotteMartSessionCookie({
75
- Accept: 'text/html, */*; q=0.01',
76
- ...init.headers,
77
- }, sessionCookie);
78
- const result = await requestByZyte({
79
- apiKey: zyteApiKey,
80
- url,
81
- timeout,
82
- method: init.method || 'GET',
83
- headers: toZyteHeaders(headers),
84
- bodyText: toLotteMartBodyText(init.body),
85
- tags: { service: 'lottemart' },
86
- });
87
- if (!result.httpResponseBody) {
88
- throw new Error('Zyte HTTP 응답 본문이 비어 있습니다.');
89
- }
90
- return decodeBase64(result.httpResponseBody);
91
- }
92
70
  function toBodyPreview(bodyText) {
93
71
  const normalized = bodyText.trim().replace(/\s+/g, ' ');
94
72
  return normalized.length > 0 ? normalized.slice(0, 300) : null;
@@ -122,29 +100,15 @@ export async function probeLotteMartRequest(url, init, timeout, sessionCookie, z
122
100
  });
123
101
  }
124
102
  if (zyteApiKey) {
125
- try {
126
- const bodyText = await fetchLotteMartHtmlByZyte(url, init, timeout, sessionCookie, zyteApiKey);
127
- attempts.push({
128
- used: 'zyte',
129
- success: true,
130
- status: 200,
131
- statusText: 'OK',
132
- error: null,
133
- bodyPreview: toBodyPreview(bodyText),
134
- sessionCookie: sessionCookie || null,
135
- });
136
- }
137
- catch (error) {
138
- attempts.push({
139
- used: 'zyte',
140
- success: false,
141
- status: null,
142
- statusText: null,
143
- error: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.',
144
- bodyPreview: null,
145
- sessionCookie: sessionCookie || null,
146
- });
147
- }
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
+ });
148
112
  }
149
113
  return attempts;
150
114
  }
@@ -186,8 +150,7 @@ export async function fetchLotteMartHtml(url, init, timeout, sessionCookie, zyte
186
150
  }
187
151
  catch (error) {
188
152
  if (zyteApiKey && error instanceof Error && !error.message.includes('Zyte')) {
189
- const fallbackCookie = await getCachedLotteMartSessionCookie(timeout);
190
- return fetchLotteMartHtmlByZyte(url, init, Math.min(timeout, FALLBACK_TIMEOUT_MS), fallbackCookie || sessionCookie, zyteApiKey);
153
+ throw new Error(ZYTE_COST_POLICY_MESSAGE);
191
154
  }
192
155
  throw error;
193
156
  }
@@ -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,39 +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
- tags: { service: 'oliveyoung' },
74
- });
75
- if (result.statusCode !== 200 || !result.httpResponseBody) {
76
- throw new Error(`올리브영 API 응답 실패: ${result.statusCode || 'unknown'}`);
77
- }
78
- const decodedBody = decodeBase64(result.httpResponseBody);
79
- const parsedBody = JSON.parse(decodedBody);
80
- if (parsedBody.status !== 'SUCCESS') {
81
- throw new Error(`올리브영 API 상태 오류: ${parsedBody.status || 'UNKNOWN'}`);
82
- }
83
- return parsedBody;
84
- }
85
- catch (error) {
86
- if (error instanceof Error && error.name === 'AbortError') {
87
- throw new Error('올리브영 API 요청 시간 초과');
88
- }
89
- throw error;
90
- }
91
- }
92
59
  export async function fetchOliveyoungStores(params, options = {}) {
93
60
  const payload = {
94
61
  lat: params.latitude,
@@ -102,7 +69,7 @@ export async function fetchOliveyoungStores(params, options = {}) {
102
69
  };
103
70
  const cacheKey = createOliveyoungStoreSearchCacheKey(params);
104
71
  try {
105
- const body = await zyteExtract(OLIVEYOUNG_API.STORE_FINDER_PATH, payload, options);
72
+ const body = await requestOliveyoung(OLIVEYOUNG_API.STORE_FINDER_PATH, payload, options);
106
73
  const stores = (body.data?.storeList || []).map((store) => ({
107
74
  storeCode: store.storeCode || '',
108
75
  storeName: store.storeName || '',
@@ -137,7 +104,7 @@ export async function fetchOliveyoungProducts(params, options = {}) {
137
104
  };
138
105
  const cacheKey = createOliveyoungProductSearchCacheKey(params);
139
106
  try {
140
- const body = await zyteExtract(OLIVEYOUNG_API.PRODUCT_SEARCH_PATH, payload, options);
107
+ const body = await requestOliveyoung(OLIVEYOUNG_API.PRODUCT_SEARCH_PATH, payload, options);
141
108
  const list = body.data?.serachList || body.data?.searchList || [];
142
109
  const products = list.map((product) => {
143
110
  const o2oStockFlag = Boolean(product.o2oStockFlag);
@@ -185,7 +152,7 @@ async function fetchOliveyoungProductId(goodsNumber, options = {}) {
185
152
  if (cached && cached.expiresAt > now) {
186
153
  return cached.productId;
187
154
  }
188
- 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);
189
156
  const productId = body.data?.goodsInfo?.masterGoodsNumber || '';
190
157
  if (productId) {
191
158
  oliveyoungProductIdCache.set(normalizedGoodsNumber, {
@@ -198,7 +165,7 @@ async function fetchOliveyoungProductId(goodsNumber, options = {}) {
198
165
  async function fetchOliveyoungStockStores(params, options = {}) {
199
166
  const cacheKey = createOliveyoungStockStoresCacheKey(params);
200
167
  try {
201
- const body = await zyteExtract(OLIVEYOUNG_API.STOCK_STORES_PATH, {
168
+ const body = await requestOliveyoung(OLIVEYOUNG_API.STOCK_STORES_PATH, {
202
169
  productId: params.productId,
203
170
  lat: params.latitude,
204
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
  }
@@ -0,0 +1,65 @@
1
+ /** 올리브영 무료 직접 요청 및 운영자가 설정한 브라우저 릴레이 전송. */
2
+ import { fetchJson } from '../../utils/http.js';
3
+ import { OLIVEYOUNG_API } from './api.js';
4
+ export async function requestOliveyoung(path, body, options = {}) {
5
+ const { timeout = 15000, relayUrl, relayToken } = options;
6
+ const headers = {
7
+ 'Content-Type': 'application/json',
8
+ Accept: 'application/json',
9
+ 'X-Requested-With': 'XMLHttpRequest',
10
+ };
11
+ let url = `${OLIVEYOUNG_API.BASE_URL}${path}`;
12
+ if (relayUrl) {
13
+ let relay;
14
+ try {
15
+ relay = new URL(relayUrl);
16
+ }
17
+ catch {
18
+ throw new Error('올리브영 릴레이 URL은 HTTPS 또는 로컬 HTTP 주소여야 합니다.');
19
+ }
20
+ if (relay.username ||
21
+ relay.password ||
22
+ relay.search ||
23
+ relay.hash ||
24
+ (relay.protocol !== 'https:' &&
25
+ !(relay.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(relay.hostname)))) {
26
+ throw new Error('올리브영 릴레이 URL은 HTTPS 또는 로컬 HTTP 주소여야 합니다.');
27
+ }
28
+ if (!relayToken?.trim())
29
+ throw new Error('OY_RELAY_TOKEN이 필요합니다.');
30
+ url = `${relayUrl.replace(/\/$/, '')}/v1/oliveyoung/${path.split('/').pop()}`;
31
+ headers.Authorization = `Bearer ${relayToken}`;
32
+ const { accessClientId, accessClientSecret } = options;
33
+ if (accessClientId !== undefined || accessClientSecret !== undefined) {
34
+ if (!accessClientId?.trim() || !accessClientSecret?.trim()) {
35
+ throw new Error('올리브영 Access 서비스 토큰 설정이 필요합니다.');
36
+ }
37
+ headers['CF-Access-Client-Id'] = accessClientId;
38
+ headers['CF-Access-Client-Secret'] = accessClientSecret;
39
+ }
40
+ }
41
+ let result;
42
+ try {
43
+ result = await fetchJson(url, {
44
+ method: 'POST',
45
+ redirect: 'error',
46
+ headers,
47
+ body: JSON.stringify(body),
48
+ timeout,
49
+ retries: 0,
50
+ expectedStatus: 200,
51
+ });
52
+ }
53
+ catch (error) {
54
+ if (error instanceof Error && error.name === 'AbortError') {
55
+ throw new Error('올리브영 API 요청 시간 초과');
56
+ }
57
+ if (relayUrl)
58
+ throw new Error('올리브영 브라우저 릴레이 요청 실패');
59
+ throw new Error('올리브영 직접 요청 실패. 운영자는 OY_RELAY_URL과 OY_RELAY_TOKEN으로 브라우저 릴레이를 설정해주세요.', { cause: error });
60
+ }
61
+ if (result?.status !== 'SUCCESS') {
62
+ throw new Error(`올리브영 API 상태 오류: ${result?.status || 'UNKNOWN'}`);
63
+ }
64
+ return result;
65
+ }
@@ -2,6 +2,7 @@
2
2
  * 세븐일레븐 API 클라이언트
3
3
  */
4
4
  /* c8 ignore start */
5
+ import { withSevenElevenReadCache } from './readCache.js';
5
6
  import { fetchJsonWithZyteFallback } from '../../utils/zyteJsonFallback.js';
6
7
  import { SEVENELEVEN_API } from './api.js';
7
8
  const SEVENELEVEN_DEFAULT_HEADERS = {
@@ -111,7 +112,7 @@ function normalizeStores(items) {
111
112
  async function requestSevenElevenJson(path, method, body, options = {}) {
112
113
  const { timeout = 15000, zyteApiKey } = options;
113
114
  const url = `${SEVENELEVEN_API.BASE_URL}${path}`;
114
- return fetchJsonWithZyteFallback(url, {
115
+ return withSevenElevenReadCache(path, body, timeout, () => fetchJsonWithZyteFallback(url, {
115
116
  ...SEVENELEVEN_DEFAULT_FETCH_OPTIONS,
116
117
  method,
117
118
  retryUnsafeMethods: method === 'POST',
@@ -120,7 +121,7 @@ async function requestSevenElevenJson(path, method, body, options = {}) {
120
121
  body: method === 'POST' ? JSON.stringify(body || {}) : undefined,
121
122
  zyteApiKey,
122
123
  zyteTags: { service: 'seveneleven' },
123
- });
124
+ }));
124
125
  }
125
126
  export async function searchSevenElevenProducts(params, options = {}) {
126
127
  const { query, page = 1, size = 20 } = params;
@@ -0,0 +1,49 @@
1
+ /** 공개 상품·매장 조회를 REST와 MCP에서 함께 재사용합니다. 재고는 저장하지 않습니다. */
2
+ import { SEVENELEVEN_API } from './api.js';
3
+ const MAX_ENTRIES = 128;
4
+ const TTL_BY_PATH = new Map([
5
+ [SEVENELEVEN_API.SEARCH_GOODS_PATH, 5 * 60_000],
6
+ [SEVENELEVEN_API.SEARCH_STORE_PATH, 30 * 60_000],
7
+ [SEVENELEVEN_API.SEARCH_POPWORD_PATH, 5 * 60_000],
8
+ [SEVENELEVEN_API.PRODUCT_PAGES_PATH, 5 * 60_000],
9
+ [SEVENELEVEN_API.PRODUCT_ISSUES_PATH, 5 * 60_000],
10
+ [SEVENELEVEN_API.EXHIBITION_MAIN_PATH, 5 * 60_000],
11
+ ]);
12
+ const values = new Map();
13
+ const pending = new Map();
14
+ export function clearSevenElevenReadCache() {
15
+ values.clear();
16
+ pending.clear();
17
+ }
18
+ export async function withSevenElevenReadCache(path, body, timeout, request) {
19
+ const ttl = TTL_BY_PATH.get(path.split('?')[0]);
20
+ if (!ttl)
21
+ return request();
22
+ const key = JSON.stringify([path, body, timeout]);
23
+ const cached = values.get(key);
24
+ if (cached && cached.expiresAt > Date.now()) {
25
+ return structuredClone(cached.value);
26
+ }
27
+ values.delete(key);
28
+ const existing = pending.get(key);
29
+ if (existing)
30
+ return structuredClone(await existing);
31
+ // 부하가 높아져도 진행 중인 조회 키를 무제한으로 보관하지 않습니다.
32
+ if (pending.size >= MAX_ENTRIES)
33
+ return request();
34
+ const promise = request().then((value) => {
35
+ if (value.success === true && value.data !== undefined) {
36
+ if (values.size >= MAX_ENTRIES)
37
+ values.delete(values.keys().next().value);
38
+ values.set(key, { expiresAt: Date.now() + ttl, value: structuredClone(value) });
39
+ }
40
+ return value;
41
+ });
42
+ pending.set(key, promise);
43
+ try {
44
+ return structuredClone(await promise);
45
+ }
46
+ finally {
47
+ pending.delete(key);
48
+ }
49
+ }
@@ -66,7 +66,7 @@ function parseRetryAfterDelayMs(response) {
66
66
  return null;
67
67
  }
68
68
  async function requestWithTimeout(url, options, readResponse) {
69
- const { timeout = 10000, retries = 0, retryDelayMs = 250, retryStatusCodes = DEFAULT_RETRY_STATUS_CODES, retryUnsafeMethods = false, retryMethods = DEFAULT_RETRY_METHODS, onRetry, ...restOptions } = options;
69
+ const { timeout = 10000, retries = 0, retryDelayMs = 250, retryStatusCodes = DEFAULT_RETRY_STATUS_CODES, retryUnsafeMethods = false, retryMethods = DEFAULT_RETRY_METHODS, onRetry, expectedStatus: _expectedStatus, ...restOptions } = options;
70
70
  const maxAttempts = Math.max(1, Math.trunc(retries) + 1);
71
71
  const method = normalizeMethod(restOptions.method);
72
72
  const retryAllowed = canRetryMethod(method, retryMethods, retryUnsafeMethods);
@@ -132,7 +132,7 @@ async function readTextResponse(url, options) {
132
132
  }
133
133
  export async function fetchJson(url, options = {}) {
134
134
  const { response, body } = await readTextResponse(url, options);
135
- if (!response.ok) {
135
+ if (!response.ok || (options.expectedStatus !== undefined && response.status !== options.expectedStatus)) {
136
136
  throw new HttpError(response.status, response.statusText, body);
137
137
  }
138
138
  if (/^\s*(?:<!doctype\s+html\b|<html\b)/i.test(body)) {
@@ -1,29 +1,7 @@
1
1
  /**
2
2
  * Zyte Extract API 공통 유틸리티
3
3
  */
4
- import { createTimeoutController } from './http.js';
5
- export function resolveZyteApiKey(apiKey) {
6
- if (apiKey && apiKey.trim().length > 0) {
7
- return apiKey;
8
- }
9
- /* c8 ignore start */
10
- if (typeof process !== 'undefined' && process.env?.ZYTE_API_KEY) {
11
- return process.env.ZYTE_API_KEY;
12
- }
13
- /* c8 ignore end */
14
- throw new Error('ZYTE_API_KEY가 설정되지 않았습니다. .env 또는 Cloudflare Worker Secret을 확인해주세요.');
15
- }
16
- export function encodeBasicAuth(apiKey) {
17
- if (typeof btoa === 'function') {
18
- return btoa(`${apiKey}:`);
19
- }
20
- /* c8 ignore start */
21
- if (typeof Buffer !== 'undefined') {
22
- return Buffer.from(`${apiKey}:`).toString('base64');
23
- }
24
- /* c8 ignore end */
25
- throw new Error('Basic 인증 인코딩을 지원하지 않는 런타임입니다.');
26
- }
4
+ import { ZYTE_COST_POLICY_MESSAGE } from '../core/errors.js';
27
5
  export function decodeBase64(value) {
28
6
  if (typeof atob === 'function') {
29
7
  const binary = atob(value);
@@ -37,95 +15,9 @@ export function decodeBase64(value) {
37
15
  /* c8 ignore end */
38
16
  throw new Error('Base64 디코딩을 지원하지 않는 런타임입니다.');
39
17
  }
40
- function isAbortError(error) {
41
- return error instanceof Error && error.name === 'AbortError';
42
- }
43
- class RetryableZyteError extends Error {
44
- }
45
- function isRetryableZyteStatus(status) {
46
- return status === 408 || status === 429 || status >= 500;
47
- }
48
- function isRetryableTargetStatus(statusCode) {
49
- return typeof statusCode === 'number' && statusCode >= 500;
50
- }
51
- function isRetryableError(error) {
52
- return isAbortError(error) || error instanceof RetryableZyteError;
53
- }
54
- function parseZyteResponseBody(text) {
55
- try {
56
- return JSON.parse(text);
57
- }
58
- catch {
59
- return {
60
- detail: text.trim().replace(/\s+/g, ' ').slice(0, 300),
61
- };
62
- }
63
- }
64
- function wait(ms) {
65
- if (ms <= 0) {
66
- return Promise.resolve();
67
- }
68
- return new Promise((resolve) => {
69
- setTimeout(resolve, ms);
70
- });
71
- }
72
- export async function requestByZyte(options) {
73
- const { timeout = 15000, retries = 1, retryDelayMs = 250, url, method = 'GET', headers = [], bodyText, apiKey, tags, } = options;
74
- const auth = encodeBasicAuth(resolveZyteApiKey(apiKey));
75
- const maxAttempts = Math.max(1, Math.trunc(retries) + 1);
76
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
77
- const { controller, timeoutId } = createTimeoutController(timeout);
78
- try {
79
- let response;
80
- try {
81
- response = await fetch('https://api.zyte.com/v1/extract', {
82
- method: 'POST',
83
- headers: {
84
- Authorization: `Basic ${auth}`,
85
- 'Content-Type': 'application/json',
86
- Accept: 'application/json',
87
- },
88
- body: JSON.stringify({
89
- url,
90
- httpRequestMethod: method,
91
- customHttpRequestHeaders: headers,
92
- httpRequestText: bodyText,
93
- httpResponseBody: true,
94
- ...(tags ? { tags } : {}),
95
- }),
96
- signal: controller.signal,
97
- });
98
- }
99
- catch (error) {
100
- if (error instanceof TypeError) {
101
- throw new RetryableZyteError(error.message);
102
- }
103
- throw error;
104
- }
105
- const result = parseZyteResponseBody(await response.text());
106
- if (!response.ok) {
107
- const message = `Zyte API 호출 실패: ${response.status} ${result.detail || result.title || ''}`.trim();
108
- if (isRetryableZyteStatus(response.status)) {
109
- throw new RetryableZyteError(message);
110
- }
111
- throw new Error(message);
112
- }
113
- if (isRetryableTargetStatus(result.statusCode) && attempt < maxAttempts) {
114
- throw new RetryableZyteError(`Zyte target response failed: ${result.statusCode}`);
115
- }
116
- return result;
117
- }
118
- catch (error) {
119
- if (!isRetryableError(error) || attempt >= maxAttempts) {
120
- throw error;
121
- }
122
- await wait(retryDelayMs);
123
- }
124
- finally {
125
- clearTimeout(timeoutId);
126
- }
127
- }
128
- throw new Error('Zyte API 호출 실패');
18
+ /** 키와 재시도 설정에 관계없이 유료 네트워크 경로를 차단합니다. */
19
+ export async function requestByZyte(_options) {
20
+ throw new Error(ZYTE_COST_POLICY_MESSAGE);
129
21
  }
130
22
  export function decodeZyteHttpBody(result) {
131
23
  if (!result.httpResponseBody) {
@@ -1,37 +1,9 @@
1
1
  /**
2
- * 차단 응답에만 Zyte를 사용하는 JSON 전송 유틸리티
2
+ * 직접 JSON 조회와 유료 대체 경로 중지 안내
3
3
  */
4
4
  import { fetchJson, HttpError } from './http.js';
5
- import { decodeZyteHttpBody, requestByZyte, } from './zyte.js';
5
+ import { requestByZyte } from './zyte.js';
6
6
  const ZYTE_FALLBACK_STATUSES = new Set([400, 403, 429]);
7
- const ZYTE_METHODS = new Set([
8
- 'GET',
9
- 'POST',
10
- 'PUT',
11
- 'PATCH',
12
- 'DELETE',
13
- ]);
14
- function normalizeMethod(method) {
15
- const normalized = (method || 'GET').toUpperCase();
16
- if (!ZYTE_METHODS.has(normalized)) {
17
- throw new Error(`Zyte에서 지원하지 않는 HTTP 메서드입니다: ${normalized}`);
18
- }
19
- return normalized;
20
- }
21
- function toZyteHeaders(headers) {
22
- const normalized = new Headers(headers);
23
- const result = [];
24
- normalized.forEach((value, name) => {
25
- result.push({ name, value });
26
- });
27
- return result;
28
- }
29
- function assertSuccessfulTarget(result) {
30
- const status = result.statusCode;
31
- if (typeof status !== 'number' || status < 200 || status >= 300) {
32
- throw new Error(`Zyte 대상 응답 실패: ${status ?? '알 수 없음'}`);
33
- }
34
- }
35
7
  function hasZyteApiKey(apiKey) {
36
8
  if (apiKey?.trim()) {
37
9
  return true;
@@ -52,16 +24,5 @@ export async function fetchJsonWithZyteFallback(url, options = {}) {
52
24
  throw error;
53
25
  }
54
26
  }
55
- const result = await requestByZyte({
56
- apiKey: zyteApiKey,
57
- url,
58
- timeout: directOptions.timeout,
59
- retries: 0,
60
- method: normalizeMethod(directOptions.method),
61
- headers: toZyteHeaders(directOptions.headers),
62
- bodyText: typeof directOptions.body === 'string' ? directOptions.body : undefined,
63
- tags: zyteTags,
64
- });
65
- assertSuccessfulTarget(result);
66
- return decodeZyteHttpBody(result);
27
+ return requestByZyte({ apiKey: zyteApiKey, url, tags: zyteTags });
67
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "daiso",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Korean retail and cinema MCP server, CLI, and Codex Skill for Daiso, convenience stores, marts, Olive Young, and theaters",
5
5
  "main": "dist/index.js",
6
6
  "bin": {