daiso 1.0.10 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +75 -5
  2. package/dist/api/actionsProxy.js +3 -0
  3. package/dist/api/cgvHandlers.js +15 -3
  4. package/dist/api/configStatus.js +1 -1
  5. package/dist/api/dtryxHandlers.js +113 -0
  6. package/dist/api/emart24Handlers.js +6 -2
  7. package/dist/api/gs25Handlers.js +22 -19
  8. package/dist/api/handlers.js +3 -0
  9. package/dist/api/healthCheckDefinitions.js +46 -16
  10. package/dist/api/healthCheckShape.js +24 -8
  11. package/dist/api/healthChecks.js +39 -20
  12. package/dist/api/operationalAuth.js +32 -0
  13. package/dist/api/routes/dtryxRoutes.js +22 -0
  14. package/dist/api/routes/healthRoutes.js +19 -11
  15. package/dist/api/routes/rateLimitStatsRoutes.js +274 -0
  16. package/dist/api/routes/registerServiceRoutes.js +37 -0
  17. package/dist/api/sevenelevenHandlers.js +7 -3
  18. package/dist/cli/commandOptions.js +24 -0
  19. package/dist/cli/commands/cinema.js +27 -0
  20. package/dist/cli.js +7 -1
  21. package/dist/cliHelp.js +3 -337
  22. package/dist/cliHelpDefinitions.js +369 -0
  23. package/dist/cliInteractive.js +8 -5
  24. package/dist/core/errors.js +17 -2
  25. package/dist/core/registry.js +13 -15
  26. package/dist/durableObjects/dailyRateLimiter.js +185 -0
  27. package/dist/durableObjects/rateLimitMetricsStore.js +144 -0
  28. package/dist/index.js +16 -30
  29. package/dist/middleware/dailyRateLimit.js +176 -0
  30. package/dist/pages/openapiFullSpec.js +2 -0
  31. package/dist/pages/openapiSpecActionParameters.js +429 -0
  32. package/dist/pages/openapiSpecActions.js +1 -391
  33. package/dist/pages/openapiSpecComponentsLottecinema.js +2 -2
  34. package/dist/pages/openapiSpecPathsDtryx.js +148 -0
  35. package/dist/pages/openapiSpecPathsLotteMart.js +10 -2
  36. package/dist/pages/promptRetailCinemaText.js +9 -1
  37. package/dist/services/cgv/errors.js +12 -0
  38. package/dist/services/cgv/transport.js +19 -2
  39. package/dist/services/cu/client.js +42 -12
  40. package/dist/services/cu/index.js +13 -3
  41. package/dist/services/cu/tools/checkInventory.js +65 -45
  42. package/dist/services/cu/tools/findNearbyStores.js +4 -3
  43. package/dist/services/cu/upstreamError.js +15 -0
  44. package/dist/services/dtryx/api.js +25 -0
  45. package/dist/services/dtryx/client.js +133 -0
  46. package/dist/services/dtryx/index.js +24 -0
  47. package/dist/services/dtryx/location.js +216 -0
  48. package/dist/services/dtryx/tools/getRemainingSeats.js +117 -0
  49. package/dist/services/dtryx/tools/listCinemas.js +46 -0
  50. package/dist/services/dtryx/tools/listNowShowing.js +77 -0
  51. package/dist/services/dtryx/types.js +4 -0
  52. package/dist/services/dtryx/validation.js +21 -0
  53. package/dist/services/feedback/tools/submitDeveloperRequest.js +15 -1
  54. package/dist/services/gs25/api.js +2 -2
  55. package/dist/services/gs25/client.js +16 -123
  56. package/dist/services/gs25/errors.js +12 -0
  57. package/dist/services/gs25/index.js +2 -2
  58. package/dist/services/gs25/productSearch.js +1 -0
  59. package/dist/services/gs25/storeStockTransport.js +55 -0
  60. package/dist/services/gs25/tools/checkInventory.js +7 -2
  61. package/dist/services/gs25/tools/findNearbyStores.js +20 -5
  62. package/dist/services/lottecinema/client.js +4 -2
  63. package/dist/services/lottemart/config.js +1 -1
  64. package/dist/services/lottemart/session.js +29 -160
  65. package/dist/services/lottemart/socketTransport.js +149 -0
  66. package/dist/services/oliveyoung/client.js +1 -0
  67. package/dist/services/seveneleven/client.js +18 -8
  68. package/dist/services/seveneleven/index.js +11 -7
  69. package/dist/services/seveneleven/inventory.js +5 -2
  70. package/dist/services/seveneleven/productKeyword.js +3 -2
  71. package/dist/services/seveneleven/tools/checkInventory.js +4 -3
  72. package/dist/services/seveneleven/tools/getCatalogSnapshot.js +4 -3
  73. package/dist/services/seveneleven/tools/getSearchPopwords.js +4 -3
  74. package/dist/services/seveneleven/tools/searchProducts.js +4 -3
  75. package/dist/services/seveneleven/tools/searchStores.js +4 -3
  76. package/dist/utils/format.js +10 -3
  77. package/dist/utils/http.js +38 -10
  78. package/dist/utils/zyte.js +13 -2
  79. package/dist/utils/zyteJsonFallback.js +67 -0
  80. package/openapi.json +51 -3
  81. package/openapi.yaml +36 -3
  82. package/package.json +28 -27
@@ -1,398 +1,8 @@
1
1
  /**
2
2
  * OpenAI Actions용 축약 OpenAPI 스펙
3
3
  */
4
- import { ACTION_QUERY_ACTIONS } from '../api/actionsProxy.js';
4
+ import { ACTION_QUERY_PARAMETERS } from './openapiSpecActionParameters.js';
5
5
  import { OPENAPI_COMPONENTS } from './openapiSpecComponents.js';
6
- const ACTION_QUERY_PARAMETERS = [
7
- {
8
- name: 'action',
9
- in: 'query',
10
- required: true,
11
- description: '실행할 액션 이름입니다. 사용 가능한 값은 enum 목록을 참고하세요.',
12
- schema: {
13
- type: 'string',
14
- enum: ACTION_QUERY_ACTIONS,
15
- },
16
- },
17
- {
18
- name: 'q',
19
- in: 'query',
20
- required: false,
21
- description: '다이소 제품 검색어',
22
- schema: { type: 'string' },
23
- },
24
- {
25
- name: 'query',
26
- in: 'query',
27
- required: false,
28
- description: '세븐일레븐 상품 검색어',
29
- schema: { type: 'string' },
30
- },
31
- {
32
- name: 'itemCode',
33
- in: 'query',
34
- required: false,
35
- description: 'GS25 상품 코드',
36
- schema: { type: 'string' },
37
- },
38
- {
39
- name: 'productId',
40
- in: 'query',
41
- required: false,
42
- description: '제품 ID 또는 상세 조회용 path 파라미터. 상품명만 알면 먼저 daisoSearchProducts 또는 /api/daiso/products로 productId를 확인하세요.',
43
- schema: { type: 'string' },
44
- },
45
- {
46
- name: 'keyword',
47
- in: 'query',
48
- required: false,
49
- description: '공통 키워드 파라미터',
50
- schema: { type: 'string' },
51
- },
52
- {
53
- name: 'location',
54
- in: 'query',
55
- required: false,
56
- description: '주변 장소 검색 지역/역/주소 키워드',
57
- schema: { type: 'string' },
58
- },
59
- {
60
- name: 'category',
61
- in: 'query',
62
- required: false,
63
- description: '주변 장소 카테고리(restaurant, cafe, food, dessert, all)',
64
- schema: { type: 'string' },
65
- },
66
- {
67
- name: 'services',
68
- in: 'query',
69
- required: false,
70
- description: '통합 비교 서비스 목록(daiso,gs25,seveneleven,emart24)',
71
- schema: { type: 'string' },
72
- },
73
- {
74
- name: 'type',
75
- in: 'query',
76
- required: false,
77
- description: '개발자 요청 유형(bug, improvement, feature, docs, other)',
78
- schema: { type: 'string' },
79
- },
80
- {
81
- name: 'severity',
82
- in: 'query',
83
- required: false,
84
- description: '개발자 요청 영향도(low, medium, high, critical)',
85
- schema: { type: 'string' },
86
- },
87
- {
88
- name: 'title',
89
- in: 'query',
90
- required: false,
91
- description: '개발자 요청 제목',
92
- schema: { type: 'string' },
93
- },
94
- {
95
- name: 'description',
96
- in: 'query',
97
- required: false,
98
- description: '개발자 요청 상세 설명',
99
- schema: { type: 'string' },
100
- },
101
- {
102
- name: 'toolName',
103
- in: 'query',
104
- required: false,
105
- description: '문제가 발생했거나 개선이 필요한 MCP 도구 이름',
106
- schema: { type: 'string' },
107
- },
108
- {
109
- name: 'reproduction',
110
- in: 'query',
111
- required: false,
112
- description: '오류 재현 방법',
113
- schema: { type: 'string' },
114
- },
115
- {
116
- name: 'expected',
117
- in: 'query',
118
- required: false,
119
- description: '기대 결과',
120
- schema: { type: 'string' },
121
- },
122
- {
123
- name: 'actual',
124
- in: 'query',
125
- required: false,
126
- description: '실제 결과',
127
- schema: { type: 'string' },
128
- },
129
- {
130
- name: 'source',
131
- in: 'query',
132
- required: false,
133
- description: '요청 출처(mcp, chatgpt, claude, cli, api, other)',
134
- schema: { type: 'string' },
135
- },
136
- {
137
- name: 'sido',
138
- in: 'query',
139
- required: false,
140
- description: '다이소 시/도',
141
- schema: { type: 'string' },
142
- },
143
- {
144
- name: 'gugun',
145
- in: 'query',
146
- required: false,
147
- description: '다이소 구/군',
148
- schema: { type: 'string' },
149
- },
150
- {
151
- name: 'dong',
152
- in: 'query',
153
- required: false,
154
- description: '다이소 동',
155
- schema: { type: 'string' },
156
- },
157
- {
158
- name: 'area',
159
- in: 'query',
160
- required: false,
161
- description: '롯데마트 지역',
162
- schema: { type: 'string' },
163
- },
164
- {
165
- name: 'area1',
166
- in: 'query',
167
- required: false,
168
- description: '이마트24 1차 지역',
169
- schema: { type: 'string' },
170
- },
171
- {
172
- name: 'area2',
173
- in: 'query',
174
- required: false,
175
- description: '이마트24 2차 지역',
176
- schema: { type: 'string' },
177
- },
178
- {
179
- name: 'areaCode',
180
- in: 'query',
181
- required: false,
182
- description: '메가박스 지역 코드',
183
- schema: { type: 'string' },
184
- },
185
- {
186
- name: 'brandVariant',
187
- in: 'query',
188
- required: false,
189
- description: '롯데마트 브랜드 변형',
190
- schema: { type: 'string' },
191
- },
192
- {
193
- name: 'lat',
194
- in: 'query',
195
- required: false,
196
- description: '위도',
197
- schema: { type: 'number', format: 'float' },
198
- },
199
- {
200
- name: 'lng',
201
- in: 'query',
202
- required: false,
203
- description: '경도',
204
- schema: { type: 'number', format: 'float' },
205
- },
206
- {
207
- name: 'page',
208
- in: 'query',
209
- required: false,
210
- description: '페이지 번호',
211
- schema: { type: 'integer' },
212
- },
213
- {
214
- name: 'pageIdx',
215
- in: 'query',
216
- required: false,
217
- description: '올리브영 페이지 번호',
218
- schema: { type: 'integer' },
219
- },
220
- {
221
- name: 'pageLimit',
222
- in: 'query',
223
- required: false,
224
- description: '롯데마트 추가 페이지 수',
225
- schema: { type: 'integer' },
226
- },
227
- {
228
- name: 'pageSize',
229
- in: 'query',
230
- required: false,
231
- description: '페이지 크기',
232
- schema: { type: 'integer' },
233
- },
234
- {
235
- name: 'size',
236
- in: 'query',
237
- required: false,
238
- description: '페이지 크기 또는 검색 크기',
239
- schema: { type: 'integer' },
240
- },
241
- {
242
- name: 'offset',
243
- in: 'query',
244
- required: false,
245
- description: 'CU 검색 오프셋',
246
- schema: { type: 'integer' },
247
- },
248
- {
249
- name: 'limit',
250
- in: 'query',
251
- required: false,
252
- description: '최대 결과 수',
253
- schema: { type: 'integer' },
254
- },
255
- {
256
- name: 'storeLimit',
257
- in: 'query',
258
- required: false,
259
- description: '매장 결과 제한 수',
260
- schema: { type: 'integer' },
261
- },
262
- {
263
- name: 'storeCode',
264
- in: 'query',
265
- required: false,
266
- description: '매장 코드. 다이소 진열 위치는 /api/daiso/inventory 응답의 storeInventory.stores[].storeCode에서 확인하세요.',
267
- schema: { type: 'string' },
268
- },
269
- {
270
- name: 'storeName',
271
- in: 'query',
272
- required: false,
273
- description: '매장명. 롯데마트 상품 조회에서 storeCode가 없을 때 사용하며, 모르면 먼저 lottemartFindNearbyStores 또는 /api/lottemart/stores로 확인하세요.',
274
- schema: { type: 'string' },
275
- },
276
- {
277
- name: 'storeKeyword',
278
- in: 'query',
279
- required: false,
280
- description: '매장 키워드',
281
- schema: { type: 'string' },
282
- },
283
- {
284
- name: 'playDate',
285
- in: 'query',
286
- required: false,
287
- description: '상영일(YYYYMMDD)',
288
- schema: { type: 'string' },
289
- },
290
- {
291
- name: 'regionCode',
292
- in: 'query',
293
- required: false,
294
- description: 'CGV 지역 코드',
295
- schema: { type: 'string' },
296
- },
297
- {
298
- name: 'theaterCode',
299
- in: 'query',
300
- required: false,
301
- description: 'CGV 극장 코드',
302
- schema: { type: 'string' },
303
- },
304
- {
305
- name: 'theaterId',
306
- in: 'query',
307
- required: false,
308
- description: '메가박스/롯데시네마 지점 ID',
309
- schema: { type: 'string' },
310
- },
311
- {
312
- name: 'movieCode',
313
- in: 'query',
314
- required: false,
315
- description: 'CGV 영화 코드',
316
- schema: { type: 'string' },
317
- },
318
- {
319
- name: 'movieId',
320
- in: 'query',
321
- required: false,
322
- description: '메가박스/롯데시네마 영화 ID',
323
- schema: { type: 'string' },
324
- },
325
- {
326
- name: 'pluCd',
327
- in: 'query',
328
- required: false,
329
- description: '이마트24 상품 PLU 코드',
330
- schema: { type: 'string' },
331
- },
332
- {
333
- name: 'bizNoArr',
334
- in: 'query',
335
- required: false,
336
- description: '이마트24 사업자번호 목록',
337
- schema: { type: 'string' },
338
- },
339
- {
340
- name: 'label',
341
- in: 'query',
342
- required: false,
343
- description: '세븐일레븐 인기 검색어 라벨',
344
- schema: { type: 'string' },
345
- },
346
- {
347
- name: 'sort',
348
- in: 'query',
349
- required: false,
350
- description: '정렬 기준',
351
- schema: { type: 'string' },
352
- },
353
- {
354
- name: 'searchSort',
355
- in: 'query',
356
- required: false,
357
- description: 'CU 검색 정렬 기준',
358
- schema: { type: 'string' },
359
- },
360
- {
361
- name: 'service24h',
362
- in: 'query',
363
- required: false,
364
- description: '이마트24 24시간 필터',
365
- schema: { type: 'boolean' },
366
- },
367
- {
368
- name: 'includeSoldOut',
369
- in: 'query',
370
- required: false,
371
- description: '올리브영 품절 포함 여부',
372
- schema: { type: 'boolean' },
373
- },
374
- {
375
- name: 'includeIssues',
376
- in: 'query',
377
- required: false,
378
- description: '세븐일레븐 이달의 행사지 포함 여부',
379
- schema: { type: 'boolean' },
380
- },
381
- {
382
- name: 'includeExhibition',
383
- in: 'query',
384
- required: false,
385
- description: '세븐일레븐 전시상품 포함 여부',
386
- schema: { type: 'boolean' },
387
- },
388
- {
389
- name: 'timeoutMs',
390
- in: 'query',
391
- required: false,
392
- description: '세븐일레븐 inventory 타임아웃',
393
- schema: { type: 'integer' },
394
- },
395
- ];
396
6
  export function generateOpenApiSpec(baseUrl) {
397
7
  return {
398
8
  openapi: '3.1.0',
@@ -49,8 +49,8 @@ export const OPENAPI_LOTTECINEMA_COMPONENT_SCHEMAS = {
49
49
  startTime: { type: 'string', example: '10:40' },
50
50
  endTime: { type: 'string', example: '12:47' },
51
51
  totalSeats: { type: 'integer', example: 32 },
52
- bookedSeats: { type: 'integer', example: 28 },
53
- remainingSeats: { type: 'integer', example: 4 },
52
+ bookedSeats: { type: 'integer', example: 4 },
53
+ remainingSeats: { type: 'integer', example: 28 },
54
54
  },
55
55
  },
56
56
  LotteCinemaTheaterSearchResponse: {
@@ -0,0 +1,148 @@
1
+ /** 디트릭스 공개 API 경로와 응답 스키마입니다. */
2
+ const text = { type: 'string' };
3
+ const integer = { type: 'integer' };
4
+ const cinema = {
5
+ type: 'object',
6
+ properties: {
7
+ brandCode: text,
8
+ cinemaCode: text,
9
+ cinemaName: text,
10
+ region: text,
11
+ address: text,
12
+ },
13
+ };
14
+ const movie = {
15
+ type: 'object',
16
+ properties: { movieCode: text, movieName: text, rating: text, runningMinutes: integer },
17
+ };
18
+ const showtime = {
19
+ type: 'object',
20
+ properties: {
21
+ scheduleId: text,
22
+ brandCode: text,
23
+ cinemaCode: text,
24
+ cinemaName: text,
25
+ screenCode: text,
26
+ screenName: text,
27
+ movieCode: text,
28
+ movieName: text,
29
+ playDate: text,
30
+ startTime: text,
31
+ endTime: text,
32
+ runningMinutes: integer,
33
+ rating: text,
34
+ totalSeats: integer,
35
+ remainingSeats: integer,
36
+ bookedSeats: integer,
37
+ planStatus: text,
38
+ },
39
+ };
40
+ const query = (name, description, schema = text) => ({
41
+ name,
42
+ in: 'query',
43
+ required: false,
44
+ description,
45
+ schema,
46
+ });
47
+ const filters = [
48
+ query('keyword', '극장명 부분 검색'),
49
+ query('brandCode', '브랜드 코드. 미등록 극장을 조회할 때 cinemaCode와 함께 지정합니다.'),
50
+ ];
51
+ const selector = [query('cinemaCode', '극장 코드 (예: 000067)'), ...filters];
52
+ const region = query('region', '광역 지역명. 극장명과 함께 지정하면 두 조건을 모두 적용합니다.');
53
+ const limit = query('limit', '최대 결과 수', { ...integer, minimum: 1, maximum: 200, default: 50 });
54
+ const timeout = query('timeoutMs', '원본 요청 제한 시간(ms)', {
55
+ ...integer,
56
+ minimum: 1,
57
+ maximum: 60000,
58
+ default: 15000,
59
+ });
60
+ const error = {
61
+ description: '요청 조건 오류 또는 원본 조회 실패',
62
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } },
63
+ };
64
+ function responses(properties) {
65
+ return {
66
+ '200': {
67
+ description: '조회 성공. 일부 극장 조회 실패는 failedCinemas에 표시합니다.',
68
+ content: {
69
+ 'application/json': {
70
+ schema: {
71
+ type: 'object',
72
+ properties: {
73
+ success: { type: 'boolean', const: true },
74
+ data: { type: 'object', properties },
75
+ meta: { type: 'object', properties: { total: integer, pageSize: integer } },
76
+ },
77
+ },
78
+ },
79
+ },
80
+ },
81
+ '400': error,
82
+ '404': error,
83
+ '500': error,
84
+ '503': error,
85
+ };
86
+ }
87
+ export const OPENAPI_PATHS_DTRYX = {
88
+ '/api/dtryx/cinemas': {
89
+ get: {
90
+ operationId: 'dtryxListCinemas',
91
+ summary: '디트릭스 독립·예술영화관 목록',
92
+ description: '확인된 극장 카탈로그를 검색합니다. 전체 제휴 극장 목록이나 실시간 위치 정보는 아닙니다.',
93
+ parameters: [...filters, region, limit],
94
+ responses: responses({
95
+ filters: { type: 'object' },
96
+ count: integer,
97
+ cinemas: { type: 'array', items: cinema },
98
+ }),
99
+ },
100
+ },
101
+ '/api/dtryx/movies': {
102
+ get: {
103
+ operationId: 'dtryxListNowShowing',
104
+ summary: '디트릭스 극장 상영작 조회',
105
+ description: 'cinemaCode 또는 keyword가 필요합니다. 극장명 검색은 첫 번째 일치 극장을 선택합니다.',
106
+ parameters: [
107
+ ...selector,
108
+ query('includePlayDates', '상영 가능한 날짜 포함', { type: 'boolean', default: false }),
109
+ timeout,
110
+ ],
111
+ responses: responses({
112
+ cinema,
113
+ count: integer,
114
+ movies: { type: 'array', items: movie },
115
+ playDates: {
116
+ type: 'array',
117
+ items: {
118
+ type: 'object',
119
+ properties: { playDate: text, hidden: { type: 'boolean' }, rest: { type: 'boolean' } },
120
+ },
121
+ },
122
+ }),
123
+ },
124
+ },
125
+ '/api/dtryx/seats': {
126
+ get: {
127
+ operationId: 'dtryxGetRemainingSeats',
128
+ summary: '디트릭스 상영시간표와 잔여 좌석 조회',
129
+ description: '극장 조건이 없으면 카탈로그 전체를 조회합니다. 일부 실패는 failedCinemas에 표시하고 전체 실패는 503을 반환합니다.',
130
+ parameters: [
131
+ ...selector,
132
+ region,
133
+ query('movieName', '영화명 부분 일치'),
134
+ query('playDate', 'YYYYMMDD 또는 YYYY-MM-DD. 기본값은 한국 날짜 기준 오늘입니다.'),
135
+ limit,
136
+ timeout,
137
+ ],
138
+ responses: responses({
139
+ playDate: text,
140
+ filters: { type: 'object' },
141
+ searchedCinemaCount: integer,
142
+ failedCinemas: { type: 'array', items: text },
143
+ count: integer,
144
+ showtimes: { type: 'array', items: showtime },
145
+ }),
146
+ },
147
+ },
148
+ };
@@ -29,7 +29,15 @@ export const OPENAPI_PATHS_LOTTEMART = {
29
29
  description: '브랜드 변형 필터',
30
30
  schema: {
31
31
  type: 'string',
32
- enum: ['lottemart', 'toysrus', 'max', 'bottlebunker', 'mealguru', 'grandgrocery', 'other'],
32
+ enum: [
33
+ 'lottemart',
34
+ 'toysrus',
35
+ 'max',
36
+ 'bottlebunker',
37
+ 'mealguru',
38
+ 'grandgrocery',
39
+ 'other',
40
+ ],
33
41
  },
34
42
  },
35
43
  {
@@ -123,7 +131,7 @@ export const OPENAPI_PATHS_LOTTEMART = {
123
131
  in: 'query',
124
132
  required: false,
125
133
  description: '요청 제한 시간(ms)',
126
- schema: { type: 'integer', default: 45000, minimum: 1 },
134
+ schema: { type: 'integer', default: 15000, minimum: 1 },
127
135
  },
128
136
  ],
129
137
  responses: {
@@ -2,7 +2,15 @@
2
2
  * 프롬프트 리테일/영화 API 섹션
3
3
  */
4
4
  export function buildPromptRetailCinemaText(baseUrl) {
5
- return `### 6-0. 키 없는 통합 상품 가격 후보 비교
5
+ return `### 디트릭스 독립·예술영화관
6
+
7
+ - 극장 카탈로그: ${baseUrl}/api/dtryx/cinemas?region=서울
8
+ - 상영작: ${baseUrl}/api/dtryx/movies?keyword=모모&includePlayDates=true
9
+ - 회차와 잔여 좌석: ${baseUrl}/api/dtryx/seats?region=서울
10
+
11
+ 상영작 조회는 cinemaCode 또는 keyword가 필요합니다. 날짜 기본값은 한국 날짜 기준 오늘이며 YYYYMMDD 또는 YYYY-MM-DD를 받습니다. 지역·극장명·브랜드 필터는 함께 적용합니다. 카탈로그는 확인된 22곳이며 전체 제휴 극장이 아닙니다. 일부 실패는 failedCinemas로 표시하고 전체 조회 실패는 오류입니다.
12
+
13
+ ### 6-0. 키 없는 통합 상품 가격 후보 비교
6
14
 
7
15
  **설명**: 새 외부 API 키 없이 기존 다이소, GS25, 세븐일레븐, 이마트24 상품 검색을 묶어 가격 후보를 비교합니다. 실제 매장 재고와 행사가까지 확정하는 기능이 아니라 "어디가 싸 보이는지"를 빠르게 고르는 1차 검색입니다.
8
16
 
@@ -0,0 +1,12 @@
1
+ /**
2
+ * CGV 원본 및 대체 전송 경로를 사용할 수 없는 상태
3
+ */
4
+ export class CgvUpstreamUnavailableError extends Error {
5
+ constructor() {
6
+ super('CGV 원본 서비스에 연결할 수 없습니다. 잠시 후 다시 시도해주세요.');
7
+ this.name = 'CgvUpstreamUnavailableError';
8
+ }
9
+ }
10
+ export function isCgvUpstreamUnavailableError(error) {
11
+ return error instanceof CgvUpstreamUnavailableError;
12
+ }
@@ -6,6 +6,7 @@
6
6
  import { createTimeoutController } from '../../utils/http.js';
7
7
  import { decodeZyteHttpBody, requestByZyte } from '../../utils/zyte.js';
8
8
  import { CGV_API } from './api.js';
9
+ import { CgvUpstreamUnavailableError } from './errors.js';
9
10
  function toBase64(bytes) {
10
11
  if (typeof Buffer !== 'undefined') {
11
12
  return Buffer.from(bytes).toString('base64');
@@ -49,7 +50,11 @@ async function requestByZyteCgv(path, searchParams, timeout, apiKey) {
49
50
  { name: 'X-TIMESTAMP', value: timestamp },
50
51
  { name: 'X-SIGNATURE', value: signature },
51
52
  ],
53
+ tags: { service: 'cgv' },
52
54
  });
55
+ if (result.statusCode === 401 || result.statusCode === 403) {
56
+ throw new CgvUpstreamUnavailableError();
57
+ }
53
58
  return decodeZyteHttpBody(result);
54
59
  }
55
60
  export async function requestCgv(path, searchParams, timeout = 15000, zyteApiKey) {
@@ -71,8 +76,20 @@ export async function requestCgv(path, searchParams, timeout = 15000, zyteApiKey
71
76
  if (response.ok) {
72
77
  return await parseJsonResponse(response);
73
78
  }
74
- if (response.status === 403 && zyteApiKey) {
75
- return await requestByZyteCgv(path, searchParams, timeout, zyteApiKey);
79
+ 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
+ }
76
93
  }
77
94
  throw new Error(`CGV API 호출 실패: ${response.status}`);
78
95
  }