daiso 1.2.7 → 1.2.9

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 (70) hide show
  1. package/README.md +17 -17
  2. package/dist/api/actionsProxy.js +0 -10
  3. package/dist/api/cgvHandlers.js +4 -3
  4. package/dist/api/configStatus.js +7 -2
  5. package/dist/api/gs25Handlers.js +4 -3
  6. package/dist/api/handlers.js +11 -7
  7. package/dist/api/healthCheckDefinitions.js +0 -9
  8. package/dist/api/healthChecks.js +0 -1
  9. package/dist/api/lottecinemaHandlers.js +4 -3
  10. package/dist/api/lottemartHandlers.js +5 -142
  11. package/dist/api/megaboxHandlers.js +4 -3
  12. package/dist/api/opinetHandlers.js +2 -1
  13. package/dist/api/routes/cgvRoutes.js +3 -3
  14. package/dist/api/routes/cuRoutes.js +2 -2
  15. package/dist/api/routes/gs25Routes.js +3 -3
  16. package/dist/api/routes/lottecinemaRoutes.js +3 -3
  17. package/dist/api/routes/lottemartRoutes.js +4 -16
  18. package/dist/api/routes/megaboxRoutes.js +3 -3
  19. package/dist/api/routes/opinetRoutes.js +4 -4
  20. package/dist/cli.js +5 -5
  21. package/dist/cliHelp.js +0 -3
  22. package/dist/cliHelpDefinitions.js +2 -19
  23. package/dist/index.js +10 -11
  24. package/dist/middleware/dailyRateLimit.js +0 -1
  25. package/dist/pages/openapiFullSpec.js +0 -2
  26. package/dist/pages/openapiSpecActionParameters.js +4 -4
  27. package/dist/pages/openapiSpecComponents.js +0 -2
  28. package/dist/pages/openapiSpecPathsMegabox.js +1 -1
  29. package/dist/pages/promptReferenceText.js +0 -5
  30. package/dist/pages/promptRetailCinemaText.js +0 -26
  31. package/dist/services/cgv/index.js +3 -6
  32. package/dist/services/cgv/location.js +38 -125
  33. package/dist/services/cgv/tools/findTheaters.js +5 -4
  34. package/dist/services/cgv/tools/getTimetable.js +5 -4
  35. package/dist/services/cgv/tools/searchMovies.js +5 -4
  36. package/dist/services/cu/client.js +3 -35
  37. package/dist/services/cu/index.js +1 -0
  38. package/dist/services/cu/tools/checkInventory.js +3 -1
  39. package/dist/services/gs25/client.js +3 -33
  40. package/dist/services/gs25/index.js +2 -2
  41. package/dist/services/gs25/tools/checkInventory.js +3 -1
  42. package/dist/services/gs25/tools/findNearbyStores.js +3 -1
  43. package/dist/services/lottecinema/index.js +3 -6
  44. package/dist/services/lottecinema/location.js +13 -100
  45. package/dist/services/lottecinema/tools/findNearbyTheaters.js +7 -6
  46. package/dist/services/lottecinema/tools/getRemainingSeats.js +5 -4
  47. package/dist/services/lottecinema/tools/listNowShowing.js +5 -4
  48. package/dist/services/lottemart/client.js +5 -32
  49. package/dist/services/lottemart/geocode.js +3 -46
  50. package/dist/services/megabox/index.js +9 -8
  51. package/dist/services/megabox/location.js +19 -84
  52. package/dist/services/megabox/tools/findNearbyTheaters.js +7 -7
  53. package/dist/services/megabox/tools/getRemainingSeats.js +5 -5
  54. package/dist/services/megabox/tools/listNowShowing.js +5 -5
  55. package/dist/services/oliveyoung/tools/checkInventory.js +5 -5
  56. package/dist/services/oliveyoung/tools/findNearbyStores.js +3 -3
  57. package/dist/services/oliveyoung/tools/searchProducts.js +3 -3
  58. package/dist/services/oliveyoung/transport.js +49 -33
  59. package/dist/services/opinet/client.js +1 -1
  60. package/dist/services/opinet/index.js +1 -1
  61. package/dist/services/opinet/location.js +5 -56
  62. package/dist/services/opinet/tools.js +2 -2
  63. package/dist/utils/geocode.js +155 -0
  64. package/dist/utils/http.js +2 -2
  65. package/dist/utils/relayQuota.js +4 -2
  66. package/openapi.json +4 -183
  67. package/openapi.yaml +4 -131
  68. package/package.json +1 -2
  69. package/dist/pages/openapiSpecComponentsLotteMart.js +0 -85
  70. package/dist/pages/openapiSpecPathsLotteMart.js +0 -157
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  # Daiso MCP 및 Skill
11
11
 
12
- 다이소(제품/매장/재고), 상품 가격 비교, 주변 음식점/카페, 주유소/유가, 개발자 요청 제출, 롯데마트(매장/상품), GS25(매장/상품/재고), 세븐일레븐(상품/매장/재고/인기검색어/카탈로그), CU(매장/재고), 이마트24(매장/상품/재고), 올리브영(매장/재고), 메가박스(지점/영화/시간표/좌석), 롯데시네마(지점/영화/좌석), CGV(극장/영화/시간표), 디트릭스(독립·예술영화관/상영작/잔여 좌석) 조회 기능을 MCP, CLI, Codex Skill로 AI에 연결합니다.
12
+ 다이소(제품/매장/재고), 상품 가격 비교, 주변 음식점/카페, 주유소/유가, 개발자 요청 제출, GS25(매장/상품/재고), 세븐일레븐(상품/매장/재고/인기검색어/카탈로그), CU(매장/재고), 이마트24(매장/상품/재고), 올리브영(매장/재고), 메가박스(지점/영화/시간표/좌석), 롯데시네마(지점/영화/좌석), CGV(극장/영화/시간표), 디트릭스(독립·예술영화관/상영작/잔여 좌석) 조회 기능을 MCP, CLI, Codex Skill로 AI에 연결합니다.
13
13
 
14
14
  디트릭스의 지원 범위와 CLI 예시는 [독립·예술영화관 조회 가이드](docs/dtryx.md)를 참고하세요.
15
15
 
@@ -57,7 +57,7 @@
57
57
  </tr>
58
58
  <tr>
59
59
  <td>리테일</td>
60
- <td>다이소, 올리브영, 롯데마트</td>
60
+ <td>다이소, 올리브영</td>
61
61
  <td>상품, 매장, 재고</td>
62
62
  </tr>
63
63
  <tr>
@@ -85,17 +85,17 @@
85
85
  **[실시간 서비스 상태 보기](https://aka-page.betteruptime.com/)**
86
86
 
87
87
  <!-- WORKERS_INVOCATIONS_CHART:START -->
88
- <h3>Cloudflare 요청 수 (2026-08-30 ~ 2026-09-28, 30일)</h3>
88
+ <h3>Cloudflare 요청 수 (2026-09-05 ~ 2026-10-04, 30일)</h3>
89
89
 
90
- <img src="./assets/analytics/workers-invocations.png?v=2026-09-28T21:15:27.313Z" alt="Cloudflare 요청 수 그래프 (2026-08-30 ~ 2026-09-28)" width="100%">
90
+ <img src="./assets/analytics/workers-invocations.png?v=2026-10-04T18:39:37.051Z" alt="Cloudflare 요청 수 그래프 (2026-09-05 ~ 2026-10-04)" width="100%">
91
91
 
92
- <sub>기준 워커: <code>daiso-mcp</code> · 마지막 갱신: 2026-09-29 06:15 KST</sub>
92
+ <sub>기준 워커: <code>daiso-mcp</code> · 마지막 갱신: 2026-10-05 03:39 KST</sub>
93
93
  <br><sub>집계: Worker 실행 + 루트 GET 리디렉션 요청 · 사용자 수와 다릅니다.</sub>
94
94
 
95
95
  </div>
96
96
 
97
97
  > [!IMPORTANT]
98
- > 최근 공개 서버 사용량이 크게 증가하여 2026년 7월 18일부터 올리브영·CGV·CU·GS25·롯데마트의 검색을 포함한 공개 GET API에 IP당 하루 합산 3,000회(KST 기준)의 호출 제한을 적용합니다. 한도를 초과하는 사용이 필요하다면 Daiso MCP는 오픈 소스이므로 이 저장소를 직접 배포해 이용해 주세요.
98
+ > 최근 공개 서버 사용량이 크게 증가하여 2026년 7월 18일부터 올리브영·CGV·CU·GS25의 검색을 포함한 공개 GET API에 IP당 하루 합산 3,000회(KST 기준)의 호출 제한을 적용합니다. 한도를 초과하는 사용이 필요하다면 Daiso MCP는 오픈 소스이므로 이 저장소를 직접 배포해 이용해 주세요.
99
99
 
100
100
  <div align="center">
101
101
 
@@ -136,8 +136,6 @@ ChatGPT, Claude, Home Assistant, Grok 같은 AI 앱에서 바로 연결해 사
136
136
  콜라 어디가 싼지 비교해줘
137
137
  올리브영 mcp로 명동 근처 매장 찾아줘
138
138
  이마트24 mcp로 강남 근처 매장과 두바이 재고 알려줘
139
- 롯데마트 mcp로 잠실 근처 매장 찾아줘
140
- 롯데마트 mcp로 강변점에서 콜라 재고 알려줘
141
139
  GS25 mcp로 강남 근처 매장과 오감자 재고 알려줘
142
140
  세븐일레븐 mcp로 삼각김밥 검색해줘
143
141
  세븐일레븐 mcp로 안산 중앙역 근처 매장 찾아줘
@@ -183,8 +181,6 @@ CGV mcp로 강남 상영 영화와 시간표 알려줘
183
181
  올리브영 mcp를 사용해서 명동 근처 매장 찾아줘
184
182
  올리브영 mcp를 사용해서 선크림 재고 확인해줘
185
183
  이마트24 mcp를 사용해서 강남 매장 찾고 두바이 재고 확인해줘
186
- 롯데마트 mcp를 사용해서 잠실 근처 매장 찾아줘
187
- 롯데마트 mcp를 사용해서 강변점에서 콜라 검색해줘
188
184
  GS25 mcp를 사용해서 강남 매장 찾고 오감자 재고 확인해줘
189
185
  세븐일레븐 mcp를 사용해서 안산 중앙역 근처 매장 찾고 핫식스 재고 확인해줘
190
186
  메가박스 mcp를 사용해서 강남역 근처 지점 찾아줘
@@ -318,8 +314,6 @@ npx daiso cu-inventory 과자 --storeKeyword 강남
318
314
  npx daiso cgv-theaters 강남 --limit 10
319
315
  npx daiso cgv-movies --playDate 20260307 --theaterCode 0056
320
316
  npx daiso cgv-timetable --playDate 20260307 --theaterCode 0056
321
- npx daiso lottemart-stores 잠실 --area 서울 --limit 10
322
- npx daiso lottemart-products 콜라 --storeName 강변점 --area 서울
323
317
  npx daiso emart24-stores 강남 --service24h true
324
318
  npx daiso emart24-products 두바이 --pageSize 20
325
319
  npx daiso emart24-inventory 8800244010504 --bizNoArr 28339,05015
@@ -404,7 +398,7 @@ GET /api/opinet/stations/around?x=314681.8&y=544837&radiusMeters=3000&fuelCode=B
404
398
  GET /api/opinet/station?id=A0010207
405
399
  ```
406
400
 
407
- 오피넷 API 키는 `OPINET_API_KEY` 환경 변수 또는 Cloudflare Worker Secret으로 설정합니다. 키 발급은 오피넷 웹사이트의 `유가관련정보 > 유가정보 API > 인증키 발급`에서 진행합니다. 반경 검색은 `lat/lng`, `location`, KATEC `x/y`를 모두 지원하며, `location` 검색에는 `GOOGLE_MAPS_API_KEY`가 필요합니다.
401
+ 오피넷 API 키는 `OPINET_API_KEY` 환경 변수 또는 Cloudflare Worker Secret으로 설정합니다. 키 발급은 오피넷 웹사이트의 `유가관련정보 > 유가정보 API > 인증키 발급`에서 진행합니다. 반경 검색은 `lat/lng`, `location`, KATEC `x/y`를 모두 지원하며, `location` 검색에는 `KAKAO_REST_API_KEY`를 설정합니다. 역명·상호명은 네이버 지역 검색으로 보완합니다. 자세한 설정은 [무료 위치 검색 가이드](./docs/free-location-search.md)를 확인하세요.
408
402
 
409
403
  운영 제약 및 캐싱 정책:
410
404
 
@@ -415,7 +409,7 @@ GET /api/opinet/station?id=A0010207
415
409
  - `GET /api/opinet/lowest`: 30분 캐시, 10분 stale-while-revalidate
416
410
  - `GET /api/opinet/stations/around`: 20분 캐시, 5분 stale-while-revalidate
417
411
  - `GET /api/opinet/station`: 60분 캐시, 10분 stale-while-revalidate
418
- - `location` 키워드의 Google Geocoding 결과는 Worker 인스턴스 메모리에서 24시간 캐시합니다.
412
+ - 주소·장소명 변환은 카카오 주소/키워드 검색을 사용합니다. 직접 좌표를 입력하면 위치 변환 API를 호출하지 않습니다.
419
413
  - 호출 한도에 자주 도달하면 운영자가 담당 기관인 한국석유공사/오피넷에 문의해 추가 할당량 또는 별도 이용 조건을 협의할 예정입니다.
420
414
 
421
415
  ### 개발자 요청 제출
@@ -437,6 +431,10 @@ GET /api/feedback/requests?type=bug&title=제목&description=설명
437
431
 
438
432
  로컬 실행이나 배포 환경에는 `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`를 설정하세요. 테이블 스키마는 [supabase-agent-requests.sql](./docs/supabase-agent-requests.sql)에 있습니다.
439
433
 
434
+ ### 무료 위치 검색
435
+
436
+ 주소·역명·상호명을 좌표로 바꾸는 기능은 카카오맵 무료 API를 사용합니다. Google Geocoding과 Zyte는 호출하지 않습니다. 카카오 무료 쿼터 대상 확인, 환경 변수와 실패 시 동작은 [무료 위치 검색 가이드](./docs/free-location-search.md)에 있습니다.
437
+
440
438
  ### 운영 헬스 체크
441
439
 
442
440
  공개 상태 페이지: **[Daiso MCP Status](https://aka-page.betteruptime.com/)**
@@ -453,7 +451,7 @@ curl -H "Authorization: Bearer $HEALTH_CHECK_SECRET" \
453
451
  주요 쿼리:
454
452
 
455
453
  - `service=gs25`: 특정 서비스만 확인
456
- - `check=lottemart.products`: 특정 체크만 확인
454
+ - `check=daiso.products`: 특정 체크만 확인
457
455
  - `mode=quick|deep|full`: 체크 모드 선택
458
456
  - `fresh=true`: 60초 캐시 우회
459
457
  - `includeSamples=true`: 첫 결과 이름 샘플 포함
@@ -484,7 +482,7 @@ curl -H "x-health-check-key: $HEALTH_CHECK_SECRET" \
484
482
  "https://mcp.aka.page/api/rate-limit/stats?service=cgv"
485
483
  ```
486
484
 
487
- 쿼리 필터는 `from`, `to`, `service`입니다. `from`과 `to`는 함께 지정하거나 둘 다 생략해야 하며 날짜 형식은 `YYYY-MM-DD`입니다. `service`에는 `oliveyoung`, `cgv`, `cu`, `gs25`, `lottemart`만 사용할 수 있습니다. 날짜를 생략하면 현재 KST 일자를 포함한 최근 7일을 조회합니다. 조회 가능한 보관 범위는 현재 KST 일자와 그 이전 29일이며, 한 번에 KST 달력 날짜 기준 최대 30일을 요청할 수 있습니다.
485
+ 쿼리 필터는 `from`, `to`, `service`입니다. `from`과 `to`는 함께 지정하거나 둘 다 생략해야 하며 날짜 형식은 `YYYY-MM-DD`입니다. `service`에는 `oliveyoung`, `cgv`, `cu`, `gs25`, `lottemart`만 사용할 수 있습니다. `lottemart` 필터는 지원 중단 전 호출 제한 통계를 조회하기 위해 유지합니다. 날짜를 생략하면 현재 KST 일자를 포함한 최근 7일을 조회합니다. 조회 가능한 보관 범위는 현재 KST 일자와 그 이전 29일이며, 한 번에 KST 달력 날짜 기준 최대 30일을 요청할 수 있습니다.
488
486
 
489
487
  성공 응답은 전체 합계와 일별·서비스별 차단 요청 수와 고유 차단 주체 수를 제공합니다.
490
488
 
@@ -640,7 +638,9 @@ daiso 인터랙티브 모드
640
638
  - [이마트24 앱 스크래핑 준비 가이드](./docs/emart24-app-scraping-preparation-guide.md)
641
639
  - [이마트24 앱 스크래핑 리플레이 가이드](./docs/emart24-app-scraping-replay-guide.md)
642
640
 
643
- ### 롯데마트
641
+ ### 롯데마트 과거 분석
642
+
643
+ 롯데마트 지원은 중단했습니다. 기존 REST 경로는 `410 SERVICE_RETIRED`, 기존 CLI 명령은 지원 중단 오류를 반환합니다.
644
644
 
645
645
  - [롯데마트 모바일 도와센터 스크래핑 리플레이 계획](./docs/lottemart-mobile-scraping-replay-plan.md)
646
646
 
@@ -51,16 +51,6 @@ export const ACTION_QUERY_DEFINITIONS = [
51
51
  summary: '이마트24 재고 조회',
52
52
  targetPath: '/api/emart24/inventory',
53
53
  },
54
- {
55
- action: 'lottemartFindStores',
56
- summary: '롯데마트 매장 검색',
57
- targetPath: '/api/lottemart/stores',
58
- },
59
- {
60
- action: 'lottemartSearchProducts',
61
- summary: '롯데마트 상품 검색',
62
- targetPath: '/api/lottemart/products',
63
- },
64
54
  { action: 'gs25FindStores', summary: 'GS25 매장 검색', targetPath: '/api/gs25/stores' },
65
55
  { action: 'gs25SearchProducts', summary: 'GS25 상품 검색', targetPath: '/api/gs25/products' },
66
56
  { action: 'gs25CheckInventory', summary: 'GS25 재고 조회', targetPath: '/api/gs25/inventory' },
@@ -1,3 +1,4 @@
1
+ import { geocodeBindings } from '../utils/geocode.js';
1
2
  /**
2
3
  * CGV GET API 핸들러
3
4
  */
@@ -38,7 +39,7 @@ export async function handleCgvFindTheaters(c) {
38
39
  }, {
39
40
  timeout: timeoutMs,
40
41
  zyteApiKey: c.env?.ZYTE_API_KEY,
41
- googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
42
+ ...geocodeBindings(c.env),
42
43
  });
43
44
  return successResponse(c, result, { total: result.count, pageSize: limit });
44
45
  }
@@ -92,7 +93,7 @@ export async function handleCgvSearchMovies(c) {
92
93
  }, {
93
94
  timeout: timeoutMs,
94
95
  zyteApiKey: c.env?.ZYTE_API_KEY,
95
- googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
96
+ ...geocodeBindings(c.env),
96
97
  });
97
98
  resolvedTheater = resolved.theater;
98
99
  theaterCode = resolved.theater?.theaterCode;
@@ -152,7 +153,7 @@ export async function handleCgvGetTimetable(c) {
152
153
  }, {
153
154
  timeout: timeoutMs,
154
155
  zyteApiKey: c.env?.ZYTE_API_KEY,
155
- googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
156
+ ...geocodeBindings(c.env),
156
157
  });
157
158
  resolvedTheater = resolved.theater;
158
159
  theaterCode = resolved.theater?.theaterCode;
@@ -49,7 +49,12 @@ export function buildConfigStatus(bindings) {
49
49
  },
50
50
  googleMapsApiKey: {
51
51
  configured: isConfigured(bindings?.GOOGLE_MAPS_API_KEY),
52
- usedBy: ['gs25', 'cu', 'lottemart', 'megabox', 'lottecinema', 'cgv'],
52
+ enabled: false,
53
+ usedBy: [],
54
+ },
55
+ kakaoRestApiKey: {
56
+ configured: isConfigured(bindings?.KAKAO_REST_API_KEY),
57
+ usedBy: ['gs25', 'cu', 'opinet', 'megabox', 'lottecinema', 'cgv'],
53
58
  },
54
59
  zyteApiKey: {
55
60
  configured: isConfigured(bindings?.ZYTE_API_KEY),
@@ -58,7 +63,7 @@ export function buildConfigStatus(bindings) {
58
63
  },
59
64
  naverLocalSearch: {
60
65
  configured: isConfigured(bindings?.NAVER_CLIENT_ID) && isConfigured(bindings?.NAVER_CLIENT_SECRET),
61
- usedBy: ['places'],
66
+ usedBy: ['places', 'gs25', 'cu', 'opinet', 'megabox', 'lottecinema', 'cgv'],
62
67
  },
63
68
  opinetApiKey: {
64
69
  configured: isConfigured(bindings?.OPINET_API_KEY),
@@ -1,3 +1,4 @@
1
+ import { geocodeBindings } from '../utils/geocode.js';
1
2
  /**
2
3
  * GS25 GET API 핸들러
3
4
  */
@@ -26,7 +27,7 @@ export async function handleGs25FindStores(c) {
26
27
  keyword.trim().length > 0) {
27
28
  const geocoded = await geocodeGs25Address(keyword, {
28
29
  timeout: 15000,
29
- googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
30
+ ...geocodeBindings(c.env),
30
31
  });
31
32
  if (geocoded) {
32
33
  latitude = geocoded.latitude;
@@ -168,7 +169,7 @@ export async function handleGs25CheckInventory(c) {
168
169
  storeKeyword.trim().length > 0) {
169
170
  const directGeocoded = await geocodeGs25Address(storeKeyword, {
170
171
  timeout: 15000,
171
- googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
172
+ ...geocodeBindings(c.env),
172
173
  });
173
174
  if (directGeocoded) {
174
175
  latitude = directGeocoded.latitude;
@@ -187,7 +188,7 @@ export async function handleGs25CheckInventory(c) {
187
188
  if (firstAddress.length > 0) {
188
189
  const geocoded = await geocodeGs25Address(firstAddress, {
189
190
  timeout: 15000,
190
- googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
191
+ ...geocodeBindings(c.env),
191
192
  });
192
193
  if (geocoded) {
193
194
  latitude = geocoded.latitude;
@@ -1,3 +1,4 @@
1
+ import { geocodeBindings } from '../utils/geocode.js';
1
2
  /**
2
3
  * GET API 핸들러
3
4
  *
@@ -32,8 +33,9 @@ export async function handleOliveyoungSearchProducts(c) {
32
33
  const size = parseInt(c.req.query('size') || '20');
33
34
  const sort = c.req.query('sort') || '01';
34
35
  const includeSoldOut = c.req.query('includeSoldOut') === 'true';
35
- const parsedTimeoutMs = parseInt(c.req.query('timeoutMs') || '15000', 10);
36
- const timeoutMs = Number.isFinite(parsedTimeoutMs) && parsedTimeoutMs > 0 ? parsedTimeoutMs : 15000;
36
+ const defaultTimeoutMs = c.env?.OY_RELAY_URL ? 60000 : 15000;
37
+ const parsedTimeoutMs = parseInt(c.req.query('timeoutMs') || '', 10);
38
+ const timeoutMs = Number.isFinite(parsedTimeoutMs) && parsedTimeoutMs > 0 ? parsedTimeoutMs : defaultTimeoutMs;
37
39
  if (keyword.trim().length === 0) {
38
40
  return errorResponse(c, 'MISSING_QUERY', '검색어(keyword)를 입력해주세요.');
39
41
  }
@@ -76,8 +78,9 @@ export async function handleOliveyoungFindStores(c) {
76
78
  const lng = parseFloat(c.req.query('lng') || '126.978');
77
79
  const pageIdx = parseInt(c.req.query('pageIdx') || '1');
78
80
  const limit = parseInt(c.req.query('limit') || '20');
79
- const parsedTimeoutMs = parseInt(c.req.query('timeoutMs') || '15000', 10);
80
- const timeoutMs = Number.isFinite(parsedTimeoutMs) && parsedTimeoutMs > 0 ? parsedTimeoutMs : 15000;
81
+ const defaultTimeoutMs = c.env?.OY_RELAY_URL ? 60000 : 15000;
82
+ const parsedTimeoutMs = parseInt(c.req.query('timeoutMs') || '', 10);
83
+ const timeoutMs = Number.isFinite(parsedTimeoutMs) && parsedTimeoutMs > 0 ? parsedTimeoutMs : defaultTimeoutMs;
81
84
  try {
82
85
  const result = await fetchOliveyoungStores({
83
86
  latitude: lat,
@@ -116,8 +119,9 @@ export async function handleOliveyoungCheckInventory(c) {
116
119
  const sort = c.req.query('sort') || '01';
117
120
  const includeSoldOut = c.req.query('includeSoldOut') === 'true';
118
121
  const storeLimit = parseInt(c.req.query('storeLimit') || '10');
119
- const parsedTimeoutMs = parseInt(c.req.query('timeoutMs') || '15000', 10);
120
- const timeoutMs = Number.isFinite(parsedTimeoutMs) && parsedTimeoutMs > 0 ? parsedTimeoutMs : 15000;
122
+ const defaultTimeoutMs = c.env?.OY_RELAY_URL ? 60000 : 15000;
123
+ const parsedTimeoutMs = parseInt(c.req.query('timeoutMs') || '', 10);
124
+ const timeoutMs = Number.isFinite(parsedTimeoutMs) && parsedTimeoutMs > 0 ? parsedTimeoutMs : defaultTimeoutMs;
121
125
  const parsedStockCheckLimit = parseInt(c.req.query('stockCheckLimit') || '5', 10);
122
126
  const stockCheckLimit = Number.isFinite(parsedStockCheckLimit) && parsedStockCheckLimit >= 0 ? parsedStockCheckLimit : 5;
123
127
  if (!keyword || keyword.trim().length === 0) {
@@ -272,7 +276,7 @@ export async function handleCuCheckInventory(c) {
272
276
  if (firstAddress.length > 0) {
273
277
  const geocoded = await geocodeCuAddress(firstAddress, {
274
278
  timeout: 15000,
275
- googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
279
+ ...geocodeBindings(c.env),
276
280
  });
277
281
  if (geocoded) {
278
282
  const hasStockSeed = !!firstStockItem?.itemCode;
@@ -117,15 +117,6 @@ export const HEALTH_CHECKS = [
117
117
  degradedFailurePatterns: SEVENELEVEN_UPSTREAM_403_PATTERNS,
118
118
  allowEmpty: true,
119
119
  },
120
- {
121
- id: 'lottemart.products',
122
- service: 'lottemart',
123
- target: 'products',
124
- mode: 'quick',
125
- path: '/api/lottemart/products?keyword=%EC%BD%9C%EB%9D%BC&storeCode=2301&area=%EC%84%9C%EC%9A%B8&pageLimit=1&source=zetta',
126
- collectionKey: 'products',
127
- requiredFields: ['productCode', 'name', 'productName'],
128
- },
129
120
  {
130
121
  id: 'oliveyoung.products',
131
122
  service: 'oliveyoung',
@@ -106,7 +106,6 @@ const CLI_CONTRACT_PATHS = [
106
106
  '/api/gs25/stores?keyword=%EA%B0%95%EB%82%A8&limit=1',
107
107
  '/api/seveneleven/products?query=%EC%BB%A4%ED%94%BC&size=1',
108
108
  '/api/emart24/products?keyword=%EC%BB%A4%ED%94%BC&pageSize=1',
109
- '/api/lottemart/products?keyword=%EC%BD%9C%EB%9D%BC&storeCode=2301&area=%EC%84%9C%EC%9A%B8&pageLimit=1',
110
109
  '/api/oliveyoung/products?keyword=%EC%84%A0%ED%81%AC%EB%A6%BC&size=1',
111
110
  '/api/megabox/theaters?keyword=%EA%B0%95%EB%82%A8&limit=1',
112
111
  '/api/lottecinema/theaters?keyword=%EC%9E%A0%EC%8B%A4&limit=1',
@@ -1,3 +1,4 @@
1
+ import { geocodeBindings } from '../utils/geocode.js';
1
2
  /**
2
3
  * 롯데시네마 GET API 핸들러
3
4
  */
@@ -29,7 +30,7 @@ export async function handleLotteCinemaFindNearbyTheaters(c) {
29
30
  timeout: timeoutMs,
30
31
  }, {
31
32
  timeout: timeoutMs,
32
- googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
33
+ ...geocodeBindings(c.env),
33
34
  });
34
35
  return successResponse(c, result, { total: result.count, pageSize: limit });
35
36
  }
@@ -57,7 +58,7 @@ export async function handleLotteCinemaListNowShowing(c) {
57
58
  timeout: timeoutMs,
58
59
  }, {
59
60
  timeout: timeoutMs,
60
- googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
61
+ ...geocodeBindings(c.env),
61
62
  });
62
63
  resolvedTheater = resolved.theater;
63
64
  theaterId = resolved.theater?.theaterId;
@@ -111,7 +112,7 @@ export async function handleLotteCinemaGetRemainingSeats(c) {
111
112
  timeout: timeoutMs,
112
113
  }, {
113
114
  timeout: timeoutMs,
114
- googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
115
+ ...geocodeBindings(c.env),
115
116
  });
116
117
  resolvedTheater = resolved.theater;
117
118
  theaterId = resolved.theater?.theaterId;
@@ -1,144 +1,7 @@
1
- /**
2
- * 롯데마트 GET API 핸들러
3
- */
4
- import { errorResponse, successResponse } from './response.js';
5
- import { fetchLotteMartStores, searchLotteMartProducts } from '../services/lottemart/client.js';
6
- import { DEFAULT_LOTTEMART_TIMEOUT_MS } from '../services/lottemart/config.js';
7
- import { probeLotteMartUpstream } from '../services/lottemart/debug.js';
8
- function parseLotteMartProductSource(value) {
9
- if (!value || value === 'auto') {
10
- return 'auto';
11
- }
12
- if (value === 'legacy' || value === 'zetta') {
13
- return value;
14
- }
15
- return null;
16
- }
17
- /**
18
- * 롯데마트 매장 검색 API 핸들러
19
- * GET /api/lottemart/stores?keyword={키워드}&lat={위도}&lng={경도}
20
- */
1
+ /** 롯데마트 지원 종료 응답. 업스트림에는 요청하지 않습니다. */
2
+ import { errorResponse } from './response.js';
21
3
  export async function handleLotteMartFindStores(c) {
22
- const keyword = c.req.query('keyword') || '';
23
- const area = c.req.query('area') || '';
24
- const brandVariant = c.req.query('brandVariant') || '';
25
- const limit = parseInt(c.req.query('limit') || '20', 10);
26
- const rawLat = c.req.query('lat');
27
- const rawLng = c.req.query('lng');
28
- const parsedLat = rawLat ? parseFloat(rawLat) : undefined;
29
- const parsedLng = rawLng ? parseFloat(rawLng) : undefined;
30
- const latitude = typeof parsedLat === 'number' && Number.isFinite(parsedLat) ? parsedLat : undefined;
31
- const longitude = typeof parsedLng === 'number' && Number.isFinite(parsedLng) ? parsedLng : undefined;
32
- try {
33
- const result = await fetchLotteMartStores({
34
- area,
35
- keyword,
36
- brandVariant,
37
- latitude,
38
- longitude,
39
- limit,
40
- }, {
41
- timeout: DEFAULT_LOTTEMART_TIMEOUT_MS,
42
- googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
43
- zyteApiKey: c.env?.ZYTE_API_KEY,
44
- });
45
- return successResponse(c, {
46
- area: area || null,
47
- keyword,
48
- brandVariant: brandVariant || null,
49
- geocodeUsed: result.geocodeUsed,
50
- location: result.location,
51
- count: result.stores.length,
52
- stores: result.stores,
53
- }, {
54
- total: result.stores.length,
55
- page: 1,
56
- pageSize: limit,
57
- });
58
- }
59
- catch (error) {
60
- const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
61
- return errorResponse(c, 'LOTTEMART_STORE_SEARCH_FAILED', message, 500);
62
- }
63
- }
64
- /**
65
- * 롯데마트 상품 검색 API 핸들러
66
- * GET /api/lottemart/products?keyword={검색어}&storeCode={매장코드}
67
- */
68
- export async function handleLotteMartSearchProducts(c) {
69
- const keyword = c.req.query('keyword') || '';
70
- const area = c.req.query('area') || '';
71
- const storeCode = c.req.query('storeCode') || '';
72
- const storeName = c.req.query('storeName') || '';
73
- const pageLimit = parseInt(c.req.query('pageLimit') || '3', 10);
74
- const parsedTimeoutMs = parseInt(c.req.query('timeoutMs') || String(DEFAULT_LOTTEMART_TIMEOUT_MS), 10);
75
- const timeoutMs = Number.isFinite(parsedTimeoutMs) && parsedTimeoutMs > 0 ? parsedTimeoutMs : DEFAULT_LOTTEMART_TIMEOUT_MS;
76
- const source = parseLotteMartProductSource(c.req.query('source'));
77
- if (keyword.trim().length === 0) {
78
- return errorResponse(c, 'MISSING_QUERY', '검색어(keyword)를 입력해주세요.');
79
- }
80
- if (!source) {
81
- return errorResponse(c, 'INVALID_LOTTEMART_SOURCE', 'source는 auto, legacy, zetta 중 하나여야 합니다.');
82
- }
83
- if (storeCode.trim().length === 0 && storeName.trim().length === 0) {
84
- return errorResponse(c, 'MISSING_STORE', 'storeCode 또는 storeName 중 하나를 입력해주세요.');
85
- }
86
- try {
87
- const result = await searchLotteMartProducts({
88
- area,
89
- storeCode,
90
- storeName,
91
- keyword,
92
- pageLimit,
93
- source,
94
- }, {
95
- timeout: timeoutMs,
96
- zyteApiKey: c.env?.ZYTE_API_KEY,
97
- });
98
- return successResponse(c, {
99
- area: result.area,
100
- storeCode: result.storeCode,
101
- storeName: result.storeName,
102
- keyword,
103
- pageLimit,
104
- totalPages: result.totalPages,
105
- products: result.products,
106
- }, {
107
- total: result.totalCount,
108
- page: 1,
109
- pageSize: result.products.length,
110
- });
111
- }
112
- catch (error) {
113
- const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
114
- return errorResponse(c, 'LOTTEMART_PRODUCT_SEARCH_FAILED', message, 500);
115
- }
116
- }
117
- /**
118
- * 롯데마트 업스트림 단독 진단 API 핸들러
119
- * GET /api/lottemart/debug?target=market-options|stores|products|product-page
120
- */
121
- export async function handleLotteMartDebug(c) {
122
- const target = (c.req.query('target') || '');
123
- const allowedTargets = ['market-options', 'stores', 'products', 'product-page'];
124
- if (!allowedTargets.includes(target)) {
125
- return errorResponse(c, 'INVALID_TARGET', 'target은 market-options, stores, products, product-page 중 하나여야 합니다.');
126
- }
127
- try {
128
- const result = await probeLotteMartUpstream({
129
- target,
130
- area: c.req.query('area') || undefined,
131
- type: (c.req.query('type') || undefined),
132
- storeCode: c.req.query('storeCode') || undefined,
133
- keyword: c.req.query('keyword') || undefined,
134
- page: c.req.query('page') ? parseInt(c.req.query('page'), 10) : undefined,
135
- timeout: c.req.query('timeoutMs') ? parseInt(c.req.query('timeoutMs'), 10) : DEFAULT_LOTTEMART_TIMEOUT_MS,
136
- zyteApiKey: c.env?.ZYTE_API_KEY,
137
- });
138
- return successResponse(c, result);
139
- }
140
- catch (error) {
141
- const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
142
- return errorResponse(c, 'LOTTEMART_DEBUG_FAILED', message, 500);
143
- }
4
+ return errorResponse(c, 'SERVICE_RETIRED', '롯데마트 서비스 지원이 종료되었습니다.', 410);
144
5
  }
6
+ export const handleLotteMartSearchProducts = handleLotteMartFindStores;
7
+ export const handleLotteMartDebug = handleLotteMartFindStores;
@@ -1,3 +1,4 @@
1
+ import { geocodeBindings } from '../utils/geocode.js';
1
2
  /**
2
3
  * 메가박스 GET API 핸들러
3
4
  */
@@ -33,7 +34,7 @@ export async function handleMegaboxFindNearbyTheaters(c) {
33
34
  limit,
34
35
  timeout: timeoutMs,
35
36
  }, {
36
- googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
37
+ ...geocodeBindings(c.env),
37
38
  timeout: timeoutMs,
38
39
  });
39
40
  return successResponse(c, result, { total: result.count, pageSize: limit });
@@ -68,7 +69,7 @@ export async function handleMegaboxListNowShowing(c) {
68
69
  playDate,
69
70
  timeout: timeoutMs,
70
71
  }, {
71
- googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
72
+ ...geocodeBindings(c.env),
72
73
  timeout: timeoutMs,
73
74
  });
74
75
  theaterId = resolved.theater?.theaterId;
@@ -130,7 +131,7 @@ export async function handleMegaboxGetRemainingSeats(c) {
130
131
  playDate,
131
132
  timeout: timeoutMs,
132
133
  }, {
133
- googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
134
+ ...geocodeBindings(c.env),
134
135
  timeout: timeoutMs,
135
136
  });
136
137
  theaterId = resolved.theater?.theaterId;
@@ -1,3 +1,4 @@
1
+ import { geocodeBindings } from '../utils/geocode.js';
1
2
  import { getErrorMessage } from '../core/errors.js';
2
3
  import { fetchOpinetAveragePrices, fetchOpinetLowestStations, fetchOpinetStationDetail, fetchOpinetStationsAround, } from '../services/opinet/client.js';
3
4
  import { errorResponse, successResponse } from './response.js';
@@ -64,7 +65,7 @@ export async function handleOpinetStationsAround(c) {
64
65
  sort: c.req.query('sort'),
65
66
  }, {
66
67
  apiKey: c.env?.OPINET_API_KEY,
67
- googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
68
+ ...geocodeBindings(c.env),
68
69
  timeoutMs: parseInteger(c.req.query('timeoutMs')),
69
70
  });
70
71
  return successResponse(c, result, { total: result.count, page: 1, pageSize: result.count });
@@ -7,16 +7,16 @@ export function registerCgvRoutes(app) {
7
7
  app.get('/api/cgv/theaters', async (c) => withEdgeCache(c.req.url, {
8
8
  ttlSeconds: 60 * 60 * 24,
9
9
  staleWhileRevalidateSeconds: 60 * 5,
10
- keyPrefix: 'cgv-theaters-v1',
10
+ keyPrefix: 'cgv-theaters-v2',
11
11
  }, () => handleCgvFindTheaters(c)));
12
12
  app.get('/api/cgv/movies', async (c) => withEdgeCache(c.req.url, {
13
13
  ttlSeconds: 60 * 10,
14
14
  staleWhileRevalidateSeconds: 60,
15
- keyPrefix: 'cgv-movies-v1',
15
+ keyPrefix: 'cgv-movies-v2',
16
16
  }, () => handleCgvSearchMovies(c)));
17
17
  app.get('/api/cgv/timetable', async (c) => withEdgeCache(c.req.url, {
18
18
  ttlSeconds: 60 * 3,
19
19
  staleWhileRevalidateSeconds: 30,
20
- keyPrefix: 'cgv-timetable-v1',
20
+ keyPrefix: 'cgv-timetable-v2',
21
21
  }, () => handleCgvGetTimetable(c)));
22
22
  }
@@ -7,11 +7,11 @@ export function registerCuRoutes(app) {
7
7
  app.get('/api/cu/stores', async (c) => withEdgeCache(c.req.url, {
8
8
  ttlSeconds: 60 * 60 * 24,
9
9
  staleWhileRevalidateSeconds: 60 * 5,
10
- keyPrefix: 'cu-stores-v1',
10
+ keyPrefix: 'cu-stores-v2',
11
11
  }, () => handleCuFindStores(c)));
12
12
  app.get('/api/cu/inventory', async (c) => withEdgeCache(c.req.url, {
13
13
  ttlSeconds: 60 * 10,
14
14
  staleWhileRevalidateSeconds: 60,
15
- keyPrefix: 'cu-inventory-v1',
15
+ keyPrefix: 'cu-inventory-v2',
16
16
  }, () => handleCuCheckInventory(c)));
17
17
  }
@@ -7,16 +7,16 @@ export function registerGs25Routes(app) {
7
7
  app.get('/api/gs25/stores', async (c) => withEdgeCache(c.req.url, {
8
8
  ttlSeconds: 60 * 10,
9
9
  staleWhileRevalidateSeconds: 60,
10
- keyPrefix: 'gs25-stores-v1',
10
+ keyPrefix: 'gs25-stores-v2',
11
11
  }, () => handleGs25FindStores(c)));
12
12
  app.get('/api/gs25/products', async (c) => withEdgeCache(c.req.url, {
13
13
  ttlSeconds: 60 * 3,
14
14
  staleWhileRevalidateSeconds: 60,
15
- keyPrefix: 'gs25-products-v1',
15
+ keyPrefix: 'gs25-products-v2',
16
16
  }, () => handleGs25SearchProducts(c)));
17
17
  app.get('/api/gs25/inventory', async (c) => withEdgeCache(c.req.url, {
18
18
  ttlSeconds: 60 * 2,
19
19
  staleWhileRevalidateSeconds: 30,
20
- keyPrefix: 'gs25-inventory-v1',
20
+ keyPrefix: 'gs25-inventory-v2',
21
21
  }, () => handleGs25CheckInventory(c)));
22
22
  }
@@ -7,16 +7,16 @@ export function registerLotteCinemaRoutes(app) {
7
7
  app.get('/api/lottecinema/theaters', async (c) => withEdgeCache(c.req.url, {
8
8
  ttlSeconds: 60 * 60 * 24,
9
9
  staleWhileRevalidateSeconds: 60 * 5,
10
- keyPrefix: 'lottecinema-theaters-v1',
10
+ keyPrefix: 'lottecinema-theaters-v2',
11
11
  }, () => handleLotteCinemaFindNearbyTheaters(c)));
12
12
  app.get('/api/lottecinema/movies', async (c) => withEdgeCache(c.req.url, {
13
13
  ttlSeconds: 60 * 10,
14
14
  staleWhileRevalidateSeconds: 60,
15
- keyPrefix: 'lottecinema-movies-v1',
15
+ keyPrefix: 'lottecinema-movies-v2',
16
16
  }, () => handleLotteCinemaListNowShowing(c)));
17
17
  app.get('/api/lottecinema/seats', async (c) => withEdgeCache(c.req.url, {
18
18
  ttlSeconds: 60 * 3,
19
19
  staleWhileRevalidateSeconds: 30,
20
- keyPrefix: 'lottecinema-seats-v1',
20
+ keyPrefix: 'lottecinema-seats-v2',
21
21
  }, () => handleLotteCinemaGetRemainingSeats(c)));
22
22
  }
@@ -1,18 +1,6 @@
1
- /**
2
- * 롯데마트 GET API 라우트 등록
3
- */
4
- import { withEdgeCache } from '../../utils/cache.js';
5
- import { handleLotteMartDebug, handleLotteMartFindStores, handleLotteMartSearchProducts } from '../lottemartHandlers.js';
1
+ import { handleLotteMartFindStores } from '../lottemartHandlers.js';
6
2
  export function registerLotteMartRoutes(app) {
7
- app.get('/api/lottemart/stores', async (c) => withEdgeCache(c.req.url, {
8
- ttlSeconds: 60 * 30,
9
- staleWhileRevalidateSeconds: 60 * 3,
10
- keyPrefix: 'lottemart-stores-v1',
11
- }, () => handleLotteMartFindStores(c)));
12
- app.get('/api/lottemart/products', async (c) => withEdgeCache(c.req.url, {
13
- ttlSeconds: 60 * 5,
14
- staleWhileRevalidateSeconds: 60,
15
- keyPrefix: 'lottemart-products-v1',
16
- }, () => handleLotteMartSearchProducts(c)));
17
- app.get('/api/lottemart/debug', (c) => handleLotteMartDebug(c));
3
+ for (const endpoint of ['stores', 'products', 'debug']) {
4
+ app.get(`/api/lottemart/${endpoint}`, handleLotteMartFindStores);
5
+ }
18
6
  }