daiso 1.0.8 → 1.0.10
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 +96 -11
- package/dist/api/actionsProxy.js +5 -0
- package/dist/api/configStatus.js +8 -0
- package/dist/api/feedbackHandlers.js +60 -0
- package/dist/api/healthCheckDefinitions.js +188 -0
- package/dist/api/healthCheckShape.js +106 -0
- package/dist/api/healthCheckTypes.js +4 -0
- package/dist/api/healthChecks.js +13 -272
- package/dist/api/opinetHandlers.js +91 -0
- package/dist/api/routes/feedbackRoutes.js +5 -0
- package/dist/api/routes/healthRoutes.js +17 -1
- package/dist/api/routes/opinetRoutes.js +24 -0
- package/dist/bin.js +0 -0
- package/dist/cli/commandOptions.js +23 -2
- package/dist/cli/commands/apiCommand.js +24 -0
- package/dist/cli/commands/cinema.js +65 -0
- package/dist/cli/commands/convenience.js +57 -100
- package/dist/cli/http.js +1 -1
- package/dist/cli.js +10 -1
- package/dist/cliHelp.js +44 -8
- package/dist/core/outputSchema.js +141 -0
- package/dist/core/registry.js +1 -138
- package/dist/index.js +92 -29
- package/dist/pages/openapiFullSpec.js +5 -0
- package/dist/pages/openapiSpecActions.js +63 -0
- package/dist/pages/openapiSpecPathsFeedback.js +95 -0
- package/dist/pages/openapiSpecPathsOpinet.js +81 -0
- package/dist/pages/promptReferenceText.js +15 -0
- package/dist/services/cgv/client.js +7 -4
- package/dist/services/emart24/tools/searchProducts.js +37 -19
- package/dist/services/feedback/client.js +109 -0
- package/dist/services/feedback/index.js +22 -0
- package/dist/services/feedback/tools/submitDeveloperRequest.js +54 -0
- package/dist/services/feedback/types.js +3 -0
- package/dist/services/gs25/client.js +1 -84
- package/dist/services/gs25/productSearch.js +97 -0
- package/dist/services/oliveyoung/client.js +1 -55
- package/dist/services/oliveyoung/normalize.js +58 -0
- package/dist/services/opinet/client.js +231 -0
- package/dist/services/opinet/index.js +26 -0
- package/dist/services/opinet/location.js +119 -0
- package/dist/services/opinet/tools.js +80 -0
- package/dist/services/opinet/types.js +1 -0
- package/dist/services/places/client.js +29 -8
- package/dist/services/seveneleven/tools/searchProducts.js +36 -17
- package/openapi.json +82 -0
- package/openapi.yaml +55 -0
- package/package.json +24 -20
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
# Daiso MCP 및 Skill
|
|
9
9
|
|
|
10
|
-
다이소(제품/매장/재고),
|
|
10
|
+
다이소(제품/매장/재고), 상품 가격 비교, 주변 음식점/카페, 주유소/유가, 개발자 요청 제출, 롯데마트(매장/상품), GS25(매장/상품/재고), 세븐일레븐(상품/매장/재고/인기검색어/카탈로그), CU(매장/재고), 이마트24(매장/상품/재고), 올리브영(매장/재고), 메가박스(지점/영화/시간표/좌석), 롯데시네마(지점/영화/좌석), CGV(극장/영화/시간표) 조회 기능을 MCP, CLI, Codex Skill로 AI에 연결합니다.
|
|
11
11
|
|
|
12
12
|
<br>
|
|
13
13
|
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
**ClawHub:** [clawhub.ai/hmmhmmhm/daiso-cli](https://clawhub.ai/hmmhmmhm/daiso-cli)
|
|
17
17
|
|
|
18
|
-
한국 로컬
|
|
18
|
+
한국 로컬 리테일, 생활 정보, 영화관 조회를 MCP, CLI, Codex Skill로 연결하는 도구입니다.
|
|
19
|
+
사용자는 별도 API 키를 준비하지 않고 바로 사용할 수 있습니다.
|
|
19
20
|
|
|
20
21
|
<br>
|
|
21
22
|
|
|
@@ -35,10 +36,20 @@
|
|
|
35
36
|
<td>네이버 지역 검색</td>
|
|
36
37
|
<td>음식점, 카페, 디저트, 주변 장소</td>
|
|
37
38
|
</tr>
|
|
39
|
+
<tr>
|
|
40
|
+
<td>교통</td>
|
|
41
|
+
<td>오피넷</td>
|
|
42
|
+
<td>전국 평균 유가, 최저가 주유소, 위치 기반 주유소, 주유소 상세정보</td>
|
|
43
|
+
</tr>
|
|
38
44
|
<tr>
|
|
39
45
|
<td>비교</td>
|
|
40
46
|
<td>다이소, GS25, 세븐일레븐, 이마트24</td>
|
|
41
|
-
<td
|
|
47
|
+
<td>같은 상품의 판매처별 가격 후보 비교</td>
|
|
48
|
+
</tr>
|
|
49
|
+
<tr>
|
|
50
|
+
<td>운영</td>
|
|
51
|
+
<td>Supabase</td>
|
|
52
|
+
<td>MCP 오류, 개선 요청, 신규 기능 요청 저장</td>
|
|
42
53
|
</tr>
|
|
43
54
|
<tr>
|
|
44
55
|
<td>리테일</td>
|
|
@@ -70,11 +81,11 @@
|
|
|
70
81
|
**[실시간 서비스 상태 보기](https://aka-page.betteruptime.com/)**
|
|
71
82
|
|
|
72
83
|
<!-- WORKERS_INVOCATIONS_CHART:START -->
|
|
73
|
-
<h3>Cloudflare Workers 호출량 (2026-
|
|
84
|
+
<h3>Cloudflare Workers 호출량 (2026-06-14 ~ 2026-07-13, 30일)</h3>
|
|
74
85
|
|
|
75
|
-
<img src="./assets/analytics/workers-invocations.png?v=2026-
|
|
86
|
+
<img src="./assets/analytics/workers-invocations.png?v=2026-07-13T16:10:36.883Z" alt="Cloudflare Workers 호출량 그래프 (2026-06-14 ~ 2026-07-13)" width="100%">
|
|
76
87
|
|
|
77
|
-
<sub>기준 워커: <code>daiso-mcp</code> · 마지막 갱신: 2026-
|
|
88
|
+
<sub>기준 워커: <code>daiso-mcp</code> · 마지막 갱신: 2026-07-14 01:10 KST</sub>
|
|
78
89
|
|
|
79
90
|
<!-- WORKERS_INVOCATIONS_CHART:END -->
|
|
80
91
|
|
|
@@ -122,12 +133,24 @@ GS25 mcp로 강남 근처 매장과 오감자 재고 알려줘
|
|
|
122
133
|
세븐일레븐 mcp로 인기 검색어와 카탈로그 요약 알려줘
|
|
123
134
|
강남역 근처 카페 찾아줘
|
|
124
135
|
성수동 근처 브런치 음식점 찾아줘
|
|
136
|
+
강남역 근처 제일 싼 주유소 찾아줘
|
|
137
|
+
오늘 전국 평균 휘발유 가격 알려줘
|
|
125
138
|
메가박스 mcp로 강남점 영화와 잔여 좌석 알려줘
|
|
126
139
|
롯데시네마 mcp로 월드타워 근처 지점과 상영 영화 알려줘
|
|
127
140
|
롯데시네마 mcp로 월드타워 잔여 좌석 알려줘
|
|
128
141
|
CGV mcp로 강남 상영 영화와 시간표 알려줘
|
|
129
142
|
```
|
|
130
143
|
|
|
144
|
+
에이전트가 고르면 좋은 대표 흐름:
|
|
145
|
+
|
|
146
|
+
- 가격 비교: `콜라 어디가 싸?` → `compare_products`
|
|
147
|
+
- 주변 장소: `강남역 근처 카페 찾아줘` → `places_search_nearby`
|
|
148
|
+
- 주유소/유가: `강남역 근처 제일 싼 주유소 찾아줘` → `opinet_search_stations_around`
|
|
149
|
+
- 브랜드 명시 재고: `다이소 핫식스 재고 찾아줘` → 먼저 다이소에서 검색 후 결과 없을 때만 대안 제안
|
|
150
|
+
- 편의점 재고: `GS25 강남 오감자 재고` → 상품 후보 확인 후 재고 조회
|
|
151
|
+
- 영화 시간표: `오늘 강남 CGV 시간표` → KST 오늘 날짜로 극장 검색 후 시간표 조회
|
|
152
|
+
- 개발자 요청: `올리브영 재고 도구가 계속 실패한다고 개발자에게 알려줘` → `submit_developer_request`
|
|
153
|
+
|
|
131
154
|
<br>
|
|
132
155
|
|
|
133
156
|
### 
|
|
@@ -223,7 +246,7 @@ AI 앱 연동 없이 터미널에서 바로 확인할 수 있습니다.
|
|
|
223
246
|
npx daiso products 수납박스 --json
|
|
224
247
|
npx daiso places 강남역 --category cafe --limit 5 --json
|
|
225
248
|
npx daiso gs25-inventory 오감자 --storeKeyword 강남 --storeLimit 5 --json
|
|
226
|
-
npx daiso
|
|
249
|
+
npx daiso cgv-movies --playDate 20260520 --theaterCode 0056 --json
|
|
227
250
|
```
|
|
228
251
|
|
|
229
252
|
<br>
|
|
@@ -263,8 +286,13 @@ npx daiso display-location 1034604 04515
|
|
|
263
286
|
npx daiso compare 콜라 --limit 3
|
|
264
287
|
npx daiso places 강남역 --category cafe --limit 5
|
|
265
288
|
npx daiso places 성수동 --keyword 브런치 --limit 5
|
|
289
|
+
npx daiso get /api/opinet/lowest --fuelCode B027 --areaCode 0113 --count 5 --json
|
|
290
|
+
npx daiso get /api/opinet/average --json
|
|
266
291
|
npx daiso cu-stores 강남
|
|
267
292
|
npx daiso cu-inventory 과자 --storeKeyword 강남
|
|
293
|
+
npx daiso cgv-theaters 강남 --limit 10
|
|
294
|
+
npx daiso cgv-movies --playDate 20260307 --theaterCode 0056
|
|
295
|
+
npx daiso cgv-timetable --playDate 20260307 --theaterCode 0056
|
|
268
296
|
npx daiso lottemart-stores 잠실 --area 서울 --limit 10
|
|
269
297
|
npx daiso lottemart-products 콜라 --storeName 강변점 --area 서울
|
|
270
298
|
npx daiso emart24-stores 강남 --service24h true
|
|
@@ -275,10 +303,9 @@ npx daiso gs25-products 오감자 --limit 20
|
|
|
275
303
|
npx daiso gs25-inventory 오감자 --storeKeyword 강남 --storeLimit 10
|
|
276
304
|
npx daiso seveneleven-products 삼각김밥 --size 20
|
|
277
305
|
npx daiso seveneleven-stores 안산 중앙역 --limit 10
|
|
278
|
-
npx daiso
|
|
306
|
+
npx daiso seveneleven-inventory 핫식스 --storeKeyword "안산 중앙역" --storeLimit 10
|
|
279
307
|
npx daiso seveneleven-popwords --label home
|
|
280
308
|
npx daiso seveneleven-catalog --includeIssues true --includeExhibition true --limit 10
|
|
281
|
-
npx daiso get /api/cgv/movies --playDate 20260307 --theaterCode 0056
|
|
282
309
|
|
|
283
310
|
# 원본 JSON 필요 시
|
|
284
311
|
npx daiso products 수납박스 --json
|
|
@@ -294,6 +321,7 @@ npx daiso products 수납박스 --json
|
|
|
294
321
|
- 명령 맵: `skills/daiso-cli/references/cli-command-map.md`
|
|
295
322
|
- 기본 원칙: 쉘 실행이 가능한 환경에서는 CLI를 우선 사용하고, AI 앱 연결이 필요할 때는 MCP 서버 URL `https://mcp.aka.page`를 사용합니다.
|
|
296
323
|
- 구조화 결과가 필요하면 스킬은 `npx daiso ... --json` 형태를 선택합니다.
|
|
324
|
+
- 상황별 레시피는 `콜라 어디가 싸?`, `강남역 근처 카페`, `다이소 핫식스 재고`, `오늘 강남 CGV 시간표`처럼 실제 사용자 문장 기준으로 정리되어 있습니다.
|
|
297
325
|
|
|
298
326
|
### OpenAPI 스펙
|
|
299
327
|
|
|
@@ -311,9 +339,9 @@ MCP 도구 응답은 각 서비스의 원본 필드를 유지하면서, 클라
|
|
|
311
339
|
- `standard.theaters`: 영화관 목록을 `code`, `name`, `address`, `distanceMeters`, `raw` 형태로 정규화합니다.
|
|
312
340
|
- `standard.places`: 음식점/카페 같은 장소 목록을 `name`, `address`, `raw` 중심으로 정규화합니다.
|
|
313
341
|
|
|
314
|
-
###
|
|
342
|
+
### 통합 상품 가격 비교
|
|
315
343
|
|
|
316
|
-
|
|
344
|
+
여러 리테일/편의점 상품 검색 결과를 묶어 같은 상품의 판매처별 가격 후보를 비교합니다.
|
|
317
345
|
|
|
318
346
|
```bash
|
|
319
347
|
npx daiso compare 콜라 --limit 3 --json
|
|
@@ -327,6 +355,63 @@ GET /api/compare/products?keyword=콜라&limit=3
|
|
|
327
355
|
|
|
328
356
|
지원 서비스는 `daiso`, `gs25`, `seveneleven`, `emart24`입니다. GS25처럼 상품 검색 응답에 가격이 없는 서비스는 후보에는 포함되지만 최저가 계산에서는 제외됩니다.
|
|
329
357
|
|
|
358
|
+
### 오피넷 유가 정보
|
|
359
|
+
|
|
360
|
+
한국석유공사 오피넷 유가정보 API로 전국 평균 유가, 지역별 최저가 주유소, 반경 내 주유소, 주유소 상세정보를 조회합니다.
|
|
361
|
+
|
|
362
|
+
MCP 도구:
|
|
363
|
+
|
|
364
|
+
```text
|
|
365
|
+
opinet_get_average_prices
|
|
366
|
+
opinet_get_lowest_price_stations
|
|
367
|
+
opinet_search_stations_around
|
|
368
|
+
opinet_get_station_detail
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
REST:
|
|
372
|
+
|
|
373
|
+
```text
|
|
374
|
+
GET /api/opinet/average
|
|
375
|
+
GET /api/opinet/lowest?fuelCode=B027&areaCode=0113&count=5
|
|
376
|
+
GET /api/opinet/stations/around?lat=37.4979&lng=127.0276&radiusMeters=3000&fuelCode=B027&sort=price
|
|
377
|
+
GET /api/opinet/stations/around?location=강남역&radiusMeters=3000&fuelCode=B027&sort=price
|
|
378
|
+
GET /api/opinet/stations/around?x=314681.8&y=544837&radiusMeters=3000&fuelCode=B027&sort=price
|
|
379
|
+
GET /api/opinet/station?id=A0010207
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
오피넷 API 키는 `OPINET_API_KEY` 환경 변수 또는 Cloudflare Worker Secret으로 설정합니다. 키 발급은 오피넷 웹사이트의 `유가관련정보 > 유가정보 API > 인증키 발급`에서 진행합니다. 반경 검색은 `lat/lng`, `location`, KATEC `x/y`를 모두 지원하며, `location` 검색에는 `GOOGLE_MAPS_API_KEY`가 필요합니다.
|
|
383
|
+
|
|
384
|
+
운영 제약 및 캐싱 정책:
|
|
385
|
+
|
|
386
|
+
- 오피넷 무료 API는 한국석유공사 공공데이터 활용 가이드 기준 **1일 1,500 call** 한도를 기준으로 운영합니다.
|
|
387
|
+
- 모든 오피넷 응답에는 `source: "한국석유공사 오피넷"`과 `fetchedAt`을 포함합니다.
|
|
388
|
+
- Cloudflare Edge Cache로 동일 GET 요청의 원본 오피넷 호출을 줄입니다.
|
|
389
|
+
- `GET /api/opinet/average`: 60분 캐시, 30분 stale-while-revalidate
|
|
390
|
+
- `GET /api/opinet/lowest`: 30분 캐시, 10분 stale-while-revalidate
|
|
391
|
+
- `GET /api/opinet/stations/around`: 20분 캐시, 5분 stale-while-revalidate
|
|
392
|
+
- `GET /api/opinet/station`: 60분 캐시, 10분 stale-while-revalidate
|
|
393
|
+
- `location` 키워드의 Google Geocoding 결과는 Worker 인스턴스 메모리에서 24시간 캐시합니다.
|
|
394
|
+
- 호출 한도에 자주 도달하면 운영자가 담당 기관인 한국석유공사/오피넷에 문의해 추가 할당량 또는 별도 이용 조건을 협의할 예정입니다.
|
|
395
|
+
|
|
396
|
+
### 개발자 요청 제출
|
|
397
|
+
|
|
398
|
+
AI 에이전트가 MCP 기능 오류, 개선 요청, 신규 기능 요청, 문서 문제를 바로 개발자에게 전달할 수 있습니다. 요청은 Supabase `agent_requests` 테이블에 저장됩니다.
|
|
399
|
+
|
|
400
|
+
MCP 도구:
|
|
401
|
+
|
|
402
|
+
```text
|
|
403
|
+
submit_developer_request
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
REST:
|
|
407
|
+
|
|
408
|
+
```text
|
|
409
|
+
POST /api/feedback/requests
|
|
410
|
+
GET /api/feedback/requests?type=bug&title=제목&description=설명
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
로컬 실행이나 배포 환경에는 `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`를 설정하세요. 테이블 스키마는 [supabase-agent-requests.sql](./docs/supabase-agent-requests.sql)에 있습니다.
|
|
414
|
+
|
|
330
415
|
### 운영 헬스 체크
|
|
331
416
|
|
|
332
417
|
공개 상태 페이지: **[Daiso MCP Status](https://aka-page.betteruptime.com/)**
|
package/dist/api/actionsProxy.js
CHANGED
|
@@ -96,6 +96,11 @@ export const ACTION_QUERY_DEFINITIONS = [
|
|
|
96
96
|
summary: '음식점/카페 등 주변 장소 검색',
|
|
97
97
|
targetPath: '/api/places/search',
|
|
98
98
|
},
|
|
99
|
+
{
|
|
100
|
+
action: 'submitDeveloperRequest',
|
|
101
|
+
summary: '개발자 요청 제출',
|
|
102
|
+
targetPath: '/api/feedback/requests',
|
|
103
|
+
},
|
|
99
104
|
{
|
|
100
105
|
action: 'megaboxFindTheaters',
|
|
101
106
|
summary: '메가박스 지점 검색',
|
package/dist/api/configStatus.js
CHANGED
|
@@ -15,6 +15,14 @@ export function buildConfigStatus(bindings) {
|
|
|
15
15
|
configured: isConfigured(bindings?.NAVER_CLIENT_ID) && isConfigured(bindings?.NAVER_CLIENT_SECRET),
|
|
16
16
|
usedBy: ['places'],
|
|
17
17
|
},
|
|
18
|
+
opinetApiKey: {
|
|
19
|
+
configured: isConfigured(bindings?.OPINET_API_KEY),
|
|
20
|
+
usedBy: ['opinet'],
|
|
21
|
+
},
|
|
22
|
+
supabaseFeedback: {
|
|
23
|
+
configured: isConfigured(bindings?.SUPABASE_URL) && isConfigured(bindings?.SUPABASE_SERVICE_ROLE_KEY),
|
|
24
|
+
usedBy: ['feedback'],
|
|
25
|
+
},
|
|
18
26
|
healthCheckSecret: {
|
|
19
27
|
configured: isConfigured(bindings?.HEALTH_CHECK_SECRET),
|
|
20
28
|
usedBy: ['health-checks'],
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { errorResponse, successResponse } from './response.js';
|
|
2
|
+
import { submitDeveloperRequest } from '../services/feedback/index.js';
|
|
3
|
+
function getErrorMessage(error) {
|
|
4
|
+
return error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
|
|
5
|
+
}
|
|
6
|
+
async function readJsonBody(c) {
|
|
7
|
+
if (c.req.method !== 'POST') {
|
|
8
|
+
return {};
|
|
9
|
+
}
|
|
10
|
+
return c.req.json().catch(() => ({}));
|
|
11
|
+
}
|
|
12
|
+
function queryInput(c) {
|
|
13
|
+
return {
|
|
14
|
+
type: c.req.query('type'),
|
|
15
|
+
severity: c.req.query('severity'),
|
|
16
|
+
title: c.req.query('title') || '',
|
|
17
|
+
description: c.req.query('description') || '',
|
|
18
|
+
service: c.req.query('service'),
|
|
19
|
+
toolName: c.req.query('toolName'),
|
|
20
|
+
reproduction: c.req.query('reproduction'),
|
|
21
|
+
expected: c.req.query('expected'),
|
|
22
|
+
actual: c.req.query('actual'),
|
|
23
|
+
source: c.req.query('source') || 'api',
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function normalizeInput(raw) {
|
|
27
|
+
return {
|
|
28
|
+
type: typeof raw.type === 'string' ? raw.type : undefined,
|
|
29
|
+
severity: typeof raw.severity === 'string' ? raw.severity : undefined,
|
|
30
|
+
title: typeof raw.title === 'string' ? raw.title : '',
|
|
31
|
+
description: typeof raw.description === 'string' ? raw.description : '',
|
|
32
|
+
service: typeof raw.service === 'string' ? raw.service : undefined,
|
|
33
|
+
toolName: typeof raw.toolName === 'string' ? raw.toolName : undefined,
|
|
34
|
+
reproduction: typeof raw.reproduction === 'string' ? raw.reproduction : undefined,
|
|
35
|
+
expected: typeof raw.expected === 'string' ? raw.expected : undefined,
|
|
36
|
+
actual: typeof raw.actual === 'string' ? raw.actual : undefined,
|
|
37
|
+
source: typeof raw.source === 'string' ? raw.source : undefined,
|
|
38
|
+
userContext: raw.userContext,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export async function handleSubmitDeveloperRequest(c) {
|
|
42
|
+
try {
|
|
43
|
+
const body = await readJsonBody(c);
|
|
44
|
+
const input = normalizeInput(c.req.method === 'POST' ? body : queryInput(c));
|
|
45
|
+
if (!input.title.trim()) {
|
|
46
|
+
return errorResponse(c, 'MISSING_FEEDBACK_TITLE', 'title을 입력해주세요.', 400);
|
|
47
|
+
}
|
|
48
|
+
if (!input.description.trim()) {
|
|
49
|
+
return errorResponse(c, 'MISSING_FEEDBACK_DESCRIPTION', 'description을 입력해주세요.', 400);
|
|
50
|
+
}
|
|
51
|
+
const result = await submitDeveloperRequest(input, {
|
|
52
|
+
supabaseUrl: c.env?.SUPABASE_URL,
|
|
53
|
+
supabaseServiceRoleKey: c.env?.SUPABASE_SERVICE_ROLE_KEY,
|
|
54
|
+
});
|
|
55
|
+
return successResponse(c, result);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return errorResponse(c, 'DEVELOPER_REQUEST_SUBMIT_FAILED', getErrorMessage(error), 500);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 서비스별 헬스 체크 정의
|
|
3
|
+
*/
|
|
4
|
+
export const GS25_CLOUDFRONT_403_PATTERNS = [
|
|
5
|
+
'403 Forbidden',
|
|
6
|
+
'The request could not be satisfied',
|
|
7
|
+
'403 ERROR',
|
|
8
|
+
];
|
|
9
|
+
export const EMART24_UPSTREAM_403_PATTERNS = [
|
|
10
|
+
'403 Forbidden',
|
|
11
|
+
'<title>403 Forbidden</title>',
|
|
12
|
+
];
|
|
13
|
+
export const CU_UPSTREAM_BLOCK_PATTERNS = [
|
|
14
|
+
'400 Bad Request',
|
|
15
|
+
'Request Blocked',
|
|
16
|
+
];
|
|
17
|
+
export const SEVENELEVEN_UPSTREAM_403_PATTERNS = [
|
|
18
|
+
'403 Forbidden',
|
|
19
|
+
'_Incapsula_Resource',
|
|
20
|
+
'NOINDEX, NOFOLLOW',
|
|
21
|
+
];
|
|
22
|
+
export const HEALTH_CHECKS = [
|
|
23
|
+
{
|
|
24
|
+
id: 'cli.contract',
|
|
25
|
+
service: 'cli',
|
|
26
|
+
target: 'api-contract',
|
|
27
|
+
mode: 'deep',
|
|
28
|
+
kind: 'cli-contract',
|
|
29
|
+
path: '/health',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'daiso.products',
|
|
33
|
+
service: 'daiso',
|
|
34
|
+
target: 'products',
|
|
35
|
+
mode: 'quick',
|
|
36
|
+
path: '/api/daiso/products?q=%ED%85%8C%EC%9D%B4%ED%94%84&pageSize=1',
|
|
37
|
+
collectionKey: 'products',
|
|
38
|
+
requiredFields: ['id', 'name', 'productName', 'itemName', 'goodsName'],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'cu.stores',
|
|
42
|
+
service: 'cu',
|
|
43
|
+
target: 'stores',
|
|
44
|
+
mode: 'quick',
|
|
45
|
+
path: '/api/cu/stores?keyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
46
|
+
collectionKey: 'stores',
|
|
47
|
+
requiredFields: ['storeCode', 'storeName', 'name'],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'emart24.products',
|
|
51
|
+
service: 'emart24',
|
|
52
|
+
target: 'products',
|
|
53
|
+
mode: 'quick',
|
|
54
|
+
path: '/api/emart24/products?keyword=%EC%BB%A4%ED%94%BC&pageSize=1',
|
|
55
|
+
collectionKey: 'products',
|
|
56
|
+
requiredFields: ['pluCd', 'goodsName', 'itemName', 'name'],
|
|
57
|
+
degradedFailurePatterns: EMART24_UPSTREAM_403_PATTERNS,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'gs25.products',
|
|
61
|
+
service: 'gs25',
|
|
62
|
+
target: 'products',
|
|
63
|
+
mode: 'quick',
|
|
64
|
+
path: '/api/gs25/products?keyword=%EC%BD%9C%EB%9D%BC&limit=1',
|
|
65
|
+
collectionKey: 'products',
|
|
66
|
+
requiredFields: ['itemCode', 'itemName', 'name'],
|
|
67
|
+
degradedFailurePatterns: GS25_CLOUDFRONT_403_PATTERNS,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'gs25.stores',
|
|
71
|
+
service: 'gs25',
|
|
72
|
+
target: 'stores',
|
|
73
|
+
mode: 'quick',
|
|
74
|
+
path: '/api/gs25/stores?keyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
75
|
+
collectionKey: 'stores',
|
|
76
|
+
requiredFields: ['storeCode', 'storeName', 'name'],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 'seveneleven.products',
|
|
80
|
+
service: 'seveneleven',
|
|
81
|
+
target: 'products',
|
|
82
|
+
mode: 'quick',
|
|
83
|
+
path: '/api/seveneleven/products?query=%EC%BB%A4%ED%94%BC&size=1',
|
|
84
|
+
collectionKey: 'products',
|
|
85
|
+
requiredFields: ['itemCode', 'itemName', 'productNo', 'name'],
|
|
86
|
+
degradedFailurePatterns: SEVENELEVEN_UPSTREAM_403_PATTERNS,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'lottemart.products',
|
|
90
|
+
service: 'lottemart',
|
|
91
|
+
target: 'products',
|
|
92
|
+
mode: 'quick',
|
|
93
|
+
path: '/api/lottemart/products?keyword=%EC%BD%9C%EB%9D%BC&storeCode=2301&area=%EC%84%9C%EC%9A%B8&pageLimit=1&source=zetta',
|
|
94
|
+
collectionKey: 'products',
|
|
95
|
+
requiredFields: ['productCode', 'name', 'productName'],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: 'oliveyoung.products',
|
|
99
|
+
service: 'oliveyoung',
|
|
100
|
+
target: 'products',
|
|
101
|
+
mode: 'quick',
|
|
102
|
+
path: '/api/oliveyoung/products?keyword=%EB%A6%BD%EB%B0%A4&size=1',
|
|
103
|
+
collectionKey: 'products',
|
|
104
|
+
requiredFields: ['goodsNumber', 'goodsName', 'productNo', 'productName', 'name'],
|
|
105
|
+
timeoutMs: 5000,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'megabox.theaters',
|
|
109
|
+
service: 'megabox',
|
|
110
|
+
target: 'theaters',
|
|
111
|
+
mode: 'quick',
|
|
112
|
+
path: '/api/megabox/theaters?keyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
113
|
+
collectionKey: 'theaters',
|
|
114
|
+
requiredFields: ['theaterCode', 'theaterName', 'name'],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: 'lottecinema.theaters',
|
|
118
|
+
service: 'lottecinema',
|
|
119
|
+
target: 'theaters',
|
|
120
|
+
mode: 'quick',
|
|
121
|
+
path: '/api/lottecinema/theaters?keyword=%EC%9E%A0%EC%8B%A4&limit=1',
|
|
122
|
+
collectionKey: 'theaters',
|
|
123
|
+
requiredFields: ['theaterCode', 'theaterName', 'name'],
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: 'cgv.theaters',
|
|
127
|
+
service: 'cgv',
|
|
128
|
+
target: 'theaters',
|
|
129
|
+
mode: 'quick',
|
|
130
|
+
path: '/api/cgv/theaters?keyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
131
|
+
collectionKey: 'theaters',
|
|
132
|
+
requiredFields: ['theaterCode', 'theaterName', 'name'],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: 'cu.inventory',
|
|
136
|
+
service: 'cu',
|
|
137
|
+
target: 'inventory',
|
|
138
|
+
mode: 'deep',
|
|
139
|
+
path: '/api/cu/inventory?keyword=%EC%BB%A4%ED%94%BC&size=1&storeLimit=0&storeCheck=false',
|
|
140
|
+
collectionKey: 'inventoryItems',
|
|
141
|
+
requiredFields: ['itemCode', 'itemName', 'name'],
|
|
142
|
+
degradedFailurePatterns: CU_UPSTREAM_BLOCK_PATTERNS,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: 'emart24.inventory',
|
|
146
|
+
service: 'emart24',
|
|
147
|
+
target: 'inventory',
|
|
148
|
+
mode: 'deep',
|
|
149
|
+
path: '/api/emart24/inventory?keyword=%EC%BB%A4%ED%94%BC&storeKeyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
150
|
+
collectionKey: 'inventoryItems',
|
|
151
|
+
requiredFields: ['pluCd', 'goodsName', 'itemName', 'name'],
|
|
152
|
+
degradedFailurePatterns: EMART24_UPSTREAM_403_PATTERNS,
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: 'gs25.inventory',
|
|
156
|
+
service: 'gs25',
|
|
157
|
+
target: 'inventory',
|
|
158
|
+
mode: 'deep',
|
|
159
|
+
path: '/api/gs25/inventory?keyword=%EC%BD%9C%EB%9D%BC&storeKeyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
160
|
+
collectionKey: 'inventoryItems',
|
|
161
|
+
requiredFields: ['itemCode', 'itemName', 'name'],
|
|
162
|
+
degradedFailurePatterns: [
|
|
163
|
+
'401 Unauthorized',
|
|
164
|
+
'인증키가 제공되지 않음',
|
|
165
|
+
...GS25_CLOUDFRONT_403_PATTERNS,
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: 'seveneleven.inventory',
|
|
170
|
+
service: 'seveneleven',
|
|
171
|
+
target: 'inventory',
|
|
172
|
+
mode: 'deep',
|
|
173
|
+
path: '/api/seveneleven/inventory?keyword=%EC%BB%A4%ED%94%BC&storeKeyword=%EA%B0%95%EB%82%A8&size=1',
|
|
174
|
+
collectionKey: 'inventoryItems',
|
|
175
|
+
requiredFields: ['itemCode', 'itemName', 'productNo', 'name'],
|
|
176
|
+
degradedFailurePatterns: SEVENELEVEN_UPSTREAM_403_PATTERNS,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: 'oliveyoung.inventory',
|
|
180
|
+
service: 'oliveyoung',
|
|
181
|
+
target: 'inventory',
|
|
182
|
+
mode: 'deep',
|
|
183
|
+
path: '/api/oliveyoung/inventory?keyword=%EC%84%A0%ED%81%AC%EB%A6%BC&storeKeyword=%EB%AA%85%EB%8F%99&size=1&storeLimit=1&stockCheckLimit=0',
|
|
184
|
+
collectionKey: 'inventoryProducts',
|
|
185
|
+
requiredFields: ['goodsNumber', 'goodsName', 'productNo', 'productName', 'name'],
|
|
186
|
+
timeoutMs: 5000,
|
|
187
|
+
},
|
|
188
|
+
];
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 헬스 체크 응답 shape 검사 유틸리티
|
|
3
|
+
*/
|
|
4
|
+
export function toCount(data) {
|
|
5
|
+
if (!data || typeof data !== 'object') {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
const record = data;
|
|
9
|
+
if (typeof record.count === 'number') {
|
|
10
|
+
return record.count;
|
|
11
|
+
}
|
|
12
|
+
for (const key of ['products', 'stores', 'theaters', 'movies', 'showtimes']) {
|
|
13
|
+
const value = record[key];
|
|
14
|
+
if (Array.isArray(value)) {
|
|
15
|
+
return value.length;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (record.inventory && typeof record.inventory === 'object') {
|
|
19
|
+
const inventory = record.inventory;
|
|
20
|
+
for (const key of ['products', 'items']) {
|
|
21
|
+
const value = inventory[key];
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
return value.length;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
export function toFirstName(data) {
|
|
30
|
+
if (!data || typeof data !== 'object') {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
const record = data;
|
|
34
|
+
for (const key of ['products', 'stores', 'theaters', 'movies', 'showtimes']) {
|
|
35
|
+
const value = record[key];
|
|
36
|
+
if (!Array.isArray(value) || !value[0] || typeof value[0] !== 'object') {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const item = value[0];
|
|
40
|
+
for (const nameKey of ['productName', 'itemName', 'goodsName', 'name', 'storeName', 'theaterName', 'movieName']) {
|
|
41
|
+
if (typeof item[nameKey] === 'string' && item[nameKey].trim().length > 0) {
|
|
42
|
+
return item[nameKey].trim();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (record.inventory && typeof record.inventory === 'object') {
|
|
47
|
+
const inventory = record.inventory;
|
|
48
|
+
for (const key of ['products', 'items']) {
|
|
49
|
+
const value = inventory[key];
|
|
50
|
+
if (!Array.isArray(value) || !value[0] || typeof value[0] !== 'object') {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const item = value[0];
|
|
54
|
+
for (const nameKey of ['productName', 'itemName', 'goodsName', 'name']) {
|
|
55
|
+
if (typeof item[nameKey] === 'string' && item[nameKey].trim().length > 0) {
|
|
56
|
+
return item[nameKey].trim();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
function getCollectionItems(data, collectionKey) {
|
|
64
|
+
if (!data || typeof data !== 'object') {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
const record = data;
|
|
68
|
+
if (collectionKey === 'inventoryProducts' || collectionKey === 'inventoryItems') {
|
|
69
|
+
const inventory = record.inventory;
|
|
70
|
+
if (!inventory || typeof inventory !== 'object') {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
const key = collectionKey === 'inventoryProducts' ? 'products' : 'items';
|
|
74
|
+
const value = inventory[key];
|
|
75
|
+
return Array.isArray(value) ? value : [];
|
|
76
|
+
}
|
|
77
|
+
if (collectionKey && Array.isArray(record[collectionKey])) {
|
|
78
|
+
return record[collectionKey];
|
|
79
|
+
}
|
|
80
|
+
for (const key of ['products', 'stores', 'theaters', 'movies', 'showtimes']) {
|
|
81
|
+
/* c8 ignore next -- 현재 정의된 체크는 collectionKey를 명시한다. */
|
|
82
|
+
if (Array.isArray(record[key])) {
|
|
83
|
+
return record[key];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
export function hasRequiredRepresentativeFields(data, collectionKey, requiredFields = []) {
|
|
89
|
+
/* c8 ignore next -- 현재 정의된 API 체크는 requiredFields를 명시한다. */
|
|
90
|
+
if (requiredFields.length === 0) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
const items = getCollectionItems(data, collectionKey);
|
|
94
|
+
if (items.length === 0) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
const first = items[0];
|
|
98
|
+
if (!first || typeof first !== 'object' || Array.isArray(first)) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
const record = first;
|
|
102
|
+
return requiredFields.some((field) => {
|
|
103
|
+
const value = record[field];
|
|
104
|
+
return typeof value === 'string' ? value.trim().length > 0 : value !== undefined && value !== null;
|
|
105
|
+
});
|
|
106
|
+
}
|