daiso 1.0.8 → 1.0.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.
- package/README.md +83 -7
- 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 +170 -0
- package/dist/api/healthCheckShape.js +106 -0
- package/dist/api/healthCheckTypes.js +4 -0
- package/dist/api/healthChecks.js +3 -271
- 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/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/openapi.json +82 -0
- package/openapi.yaml +55 -0
- package/package.json +18 -17
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
# Daiso MCP 및 Skill
|
|
9
9
|
|
|
10
|
-
다이소(제품/매장/재고), 키 없는 통합 가격 후보 비교, 주변 음식점/카페, 롯데마트(매장/상품), GS25(매장/상품/재고), 세븐일레븐(상품/매장/재고/인기검색어/카탈로그), CU(매장/재고), 이마트24(매장/상품/재고), 올리브영(매장/재고), 메가박스(지점/영화/시간표/좌석), 롯데시네마(지점/영화/좌석), CGV(극장/영화/시간표) 조회 기능을 MCP, CLI, Codex Skill로 AI에 연결합니다.
|
|
10
|
+
다이소(제품/매장/재고), 키 없는 통합 가격 후보 비교, 주변 음식점/카페, 개발자 요청 제출, 롯데마트(매장/상품), GS25(매장/상품/재고), 세븐일레븐(상품/매장/재고/인기검색어/카탈로그), CU(매장/재고), 이마트24(매장/상품/재고), 올리브영(매장/재고), 메가박스(지점/영화/시간표/좌석), 롯데시네마(지점/영화/좌석), CGV(극장/영화/시간표) 조회 기능을 MCP, CLI, Codex Skill로 AI에 연결합니다.
|
|
11
11
|
|
|
12
12
|
<br>
|
|
13
13
|
|
|
@@ -40,6 +40,11 @@
|
|
|
40
40
|
<td>다이소, GS25, 세븐일레븐, 이마트24</td>
|
|
41
41
|
<td>새 외부 키 없는 상품 가격 후보 비교</td>
|
|
42
42
|
</tr>
|
|
43
|
+
<tr>
|
|
44
|
+
<td>운영</td>
|
|
45
|
+
<td>Supabase</td>
|
|
46
|
+
<td>MCP 오류, 개선 요청, 신규 기능 요청 저장</td>
|
|
47
|
+
</tr>
|
|
43
48
|
<tr>
|
|
44
49
|
<td>리테일</td>
|
|
45
50
|
<td>다이소, 올리브영, 롯데마트</td>
|
|
@@ -70,11 +75,11 @@
|
|
|
70
75
|
**[실시간 서비스 상태 보기](https://aka-page.betteruptime.com/)**
|
|
71
76
|
|
|
72
77
|
<!-- WORKERS_INVOCATIONS_CHART:START -->
|
|
73
|
-
<h3>Cloudflare Workers 호출량 (2026-03-01 ~ 2026-05-
|
|
78
|
+
<h3>Cloudflare Workers 호출량 (2026-03-01 ~ 2026-05-27, 88일)</h3>
|
|
74
79
|
|
|
75
|
-
<img src="./assets/analytics/workers-invocations.png?v=2026-05-
|
|
80
|
+
<img src="./assets/analytics/workers-invocations.png?v=2026-05-27T22:46:35.568Z" alt="Cloudflare Workers 호출량 그래프 (2026-03-01 ~ 2026-05-27)" width="100%">
|
|
76
81
|
|
|
77
|
-
<sub>기준 워커: <code>daiso-mcp</code> · 마지막 갱신: 2026-05-
|
|
82
|
+
<sub>기준 워커: <code>daiso-mcp</code> · 마지막 갱신: 2026-05-28 07:46 KST</sub>
|
|
78
83
|
|
|
79
84
|
<!-- WORKERS_INVOCATIONS_CHART:END -->
|
|
80
85
|
|
|
@@ -128,6 +133,15 @@ GS25 mcp로 강남 근처 매장과 오감자 재고 알려줘
|
|
|
128
133
|
CGV mcp로 강남 상영 영화와 시간표 알려줘
|
|
129
134
|
```
|
|
130
135
|
|
|
136
|
+
에이전트가 고르면 좋은 대표 흐름:
|
|
137
|
+
|
|
138
|
+
- 가격 비교: `콜라 어디가 싸?` → `compare_products`
|
|
139
|
+
- 주변 장소: `강남역 근처 카페 찾아줘` → `places_search_nearby`
|
|
140
|
+
- 브랜드 명시 재고: `다이소 핫식스 재고 찾아줘` → 먼저 다이소에서 검색 후 결과 없을 때만 대안 제안
|
|
141
|
+
- 편의점 재고: `GS25 강남 오감자 재고` → 상품 후보 확인 후 재고 조회
|
|
142
|
+
- 영화 시간표: `오늘 강남 CGV 시간표` → KST 오늘 날짜로 극장 검색 후 시간표 조회
|
|
143
|
+
- 개발자 요청: `올리브영 재고 도구가 계속 실패한다고 개발자에게 알려줘` → `submit_developer_request`
|
|
144
|
+
|
|
131
145
|
<br>
|
|
132
146
|
|
|
133
147
|
### 
|
|
@@ -223,7 +237,7 @@ AI 앱 연동 없이 터미널에서 바로 확인할 수 있습니다.
|
|
|
223
237
|
npx daiso products 수납박스 --json
|
|
224
238
|
npx daiso places 강남역 --category cafe --limit 5 --json
|
|
225
239
|
npx daiso gs25-inventory 오감자 --storeKeyword 강남 --storeLimit 5 --json
|
|
226
|
-
npx daiso
|
|
240
|
+
npx daiso cgv-movies --playDate 20260520 --theaterCode 0056 --json
|
|
227
241
|
```
|
|
228
242
|
|
|
229
243
|
<br>
|
|
@@ -263,8 +277,13 @@ npx daiso display-location 1034604 04515
|
|
|
263
277
|
npx daiso compare 콜라 --limit 3
|
|
264
278
|
npx daiso places 강남역 --category cafe --limit 5
|
|
265
279
|
npx daiso places 성수동 --keyword 브런치 --limit 5
|
|
280
|
+
npx daiso get /api/opinet/lowest --fuelCode B027 --areaCode 0113 --count 5 --json
|
|
281
|
+
npx daiso get /api/opinet/average --json
|
|
266
282
|
npx daiso cu-stores 강남
|
|
267
283
|
npx daiso cu-inventory 과자 --storeKeyword 강남
|
|
284
|
+
npx daiso cgv-theaters 강남 --limit 10
|
|
285
|
+
npx daiso cgv-movies --playDate 20260307 --theaterCode 0056
|
|
286
|
+
npx daiso cgv-timetable --playDate 20260307 --theaterCode 0056
|
|
268
287
|
npx daiso lottemart-stores 잠실 --area 서울 --limit 10
|
|
269
288
|
npx daiso lottemart-products 콜라 --storeName 강변점 --area 서울
|
|
270
289
|
npx daiso emart24-stores 강남 --service24h true
|
|
@@ -275,10 +294,9 @@ npx daiso gs25-products 오감자 --limit 20
|
|
|
275
294
|
npx daiso gs25-inventory 오감자 --storeKeyword 강남 --storeLimit 10
|
|
276
295
|
npx daiso seveneleven-products 삼각김밥 --size 20
|
|
277
296
|
npx daiso seveneleven-stores 안산 중앙역 --limit 10
|
|
278
|
-
npx daiso
|
|
297
|
+
npx daiso seveneleven-inventory 핫식스 --storeKeyword "안산 중앙역" --storeLimit 10
|
|
279
298
|
npx daiso seveneleven-popwords --label home
|
|
280
299
|
npx daiso seveneleven-catalog --includeIssues true --includeExhibition true --limit 10
|
|
281
|
-
npx daiso get /api/cgv/movies --playDate 20260307 --theaterCode 0056
|
|
282
300
|
|
|
283
301
|
# 원본 JSON 필요 시
|
|
284
302
|
npx daiso products 수납박스 --json
|
|
@@ -294,6 +312,7 @@ npx daiso products 수납박스 --json
|
|
|
294
312
|
- 명령 맵: `skills/daiso-cli/references/cli-command-map.md`
|
|
295
313
|
- 기본 원칙: 쉘 실행이 가능한 환경에서는 CLI를 우선 사용하고, AI 앱 연결이 필요할 때는 MCP 서버 URL `https://mcp.aka.page`를 사용합니다.
|
|
296
314
|
- 구조화 결과가 필요하면 스킬은 `npx daiso ... --json` 형태를 선택합니다.
|
|
315
|
+
- 상황별 레시피는 `콜라 어디가 싸?`, `강남역 근처 카페`, `다이소 핫식스 재고`, `오늘 강남 CGV 시간표`처럼 실제 사용자 문장 기준으로 정리되어 있습니다.
|
|
297
316
|
|
|
298
317
|
### OpenAPI 스펙
|
|
299
318
|
|
|
@@ -327,6 +346,63 @@ GET /api/compare/products?keyword=콜라&limit=3
|
|
|
327
346
|
|
|
328
347
|
지원 서비스는 `daiso`, `gs25`, `seveneleven`, `emart24`입니다. GS25처럼 상품 검색 응답에 가격이 없는 서비스는 후보에는 포함되지만 최저가 계산에서는 제외됩니다.
|
|
329
348
|
|
|
349
|
+
### 오피넷 유가 정보
|
|
350
|
+
|
|
351
|
+
한국석유공사 오피넷 유가정보 API로 전국 평균 유가, 지역별 최저가 주유소, 반경 내 주유소, 주유소 상세정보를 조회합니다.
|
|
352
|
+
|
|
353
|
+
MCP 도구:
|
|
354
|
+
|
|
355
|
+
```text
|
|
356
|
+
opinet_get_average_prices
|
|
357
|
+
opinet_get_lowest_price_stations
|
|
358
|
+
opinet_search_stations_around
|
|
359
|
+
opinet_get_station_detail
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
REST:
|
|
363
|
+
|
|
364
|
+
```text
|
|
365
|
+
GET /api/opinet/average
|
|
366
|
+
GET /api/opinet/lowest?fuelCode=B027&areaCode=0113&count=5
|
|
367
|
+
GET /api/opinet/stations/around?lat=37.4979&lng=127.0276&radiusMeters=3000&fuelCode=B027&sort=price
|
|
368
|
+
GET /api/opinet/stations/around?location=강남역&radiusMeters=3000&fuelCode=B027&sort=price
|
|
369
|
+
GET /api/opinet/stations/around?x=314681.8&y=544837&radiusMeters=3000&fuelCode=B027&sort=price
|
|
370
|
+
GET /api/opinet/station?id=A0010207
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
오피넷 API 키는 `OPINET_API_KEY` 환경 변수 또는 Cloudflare Worker Secret으로 설정합니다. 키 발급은 오피넷 웹사이트의 `유가관련정보 > 유가정보 API > 인증키 발급`에서 진행합니다. 반경 검색은 `lat/lng`, `location`, KATEC `x/y`를 모두 지원하며, `location` 검색에는 `GOOGLE_MAPS_API_KEY`가 필요합니다.
|
|
374
|
+
|
|
375
|
+
운영 제약 및 캐싱 정책:
|
|
376
|
+
|
|
377
|
+
- 오피넷 무료 API는 한국석유공사 공공데이터 활용 가이드 기준 **1일 1,500 call** 한도를 기준으로 운영합니다.
|
|
378
|
+
- 모든 오피넷 응답에는 `source: "한국석유공사 오피넷"`과 `fetchedAt`을 포함합니다.
|
|
379
|
+
- Cloudflare Edge Cache로 동일 GET 요청의 원본 오피넷 호출을 줄입니다.
|
|
380
|
+
- `GET /api/opinet/average`: 60분 캐시, 30분 stale-while-revalidate
|
|
381
|
+
- `GET /api/opinet/lowest`: 30분 캐시, 10분 stale-while-revalidate
|
|
382
|
+
- `GET /api/opinet/stations/around`: 20분 캐시, 5분 stale-while-revalidate
|
|
383
|
+
- `GET /api/opinet/station`: 60분 캐시, 10분 stale-while-revalidate
|
|
384
|
+
- `location` 키워드의 Google Geocoding 결과는 Worker 인스턴스 메모리에서 24시간 캐시합니다.
|
|
385
|
+
- 호출 한도에 자주 도달하면 운영자가 담당 기관인 한국석유공사/오피넷에 문의해 추가 할당량 또는 별도 이용 조건을 협의할 예정입니다.
|
|
386
|
+
|
|
387
|
+
### 개발자 요청 제출
|
|
388
|
+
|
|
389
|
+
AI 에이전트가 MCP 기능 오류, 개선 요청, 신규 기능 요청, 문서 문제를 바로 개발자에게 전달할 수 있습니다. 요청은 Supabase `agent_requests` 테이블에 저장됩니다.
|
|
390
|
+
|
|
391
|
+
MCP 도구:
|
|
392
|
+
|
|
393
|
+
```text
|
|
394
|
+
submit_developer_request
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
REST:
|
|
398
|
+
|
|
399
|
+
```text
|
|
400
|
+
POST /api/feedback/requests
|
|
401
|
+
GET /api/feedback/requests?type=bug&title=제목&description=설명
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
로컬 실행이나 배포 환경에는 `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`를 설정하세요. 테이블 스키마는 [supabase-agent-requests.sql](./docs/supabase-agent-requests.sql)에 있습니다.
|
|
405
|
+
|
|
330
406
|
### 운영 헬스 체크
|
|
331
407
|
|
|
332
408
|
공개 상태 페이지: **[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,170 @@
|
|
|
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 HEALTH_CHECKS = [
|
|
10
|
+
{
|
|
11
|
+
id: 'cli.contract',
|
|
12
|
+
service: 'cli',
|
|
13
|
+
target: 'api-contract',
|
|
14
|
+
mode: 'deep',
|
|
15
|
+
kind: 'cli-contract',
|
|
16
|
+
path: '/health',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'daiso.products',
|
|
20
|
+
service: 'daiso',
|
|
21
|
+
target: 'products',
|
|
22
|
+
mode: 'quick',
|
|
23
|
+
path: '/api/daiso/products?q=%ED%85%8C%EC%9D%B4%ED%94%84&pageSize=1',
|
|
24
|
+
collectionKey: 'products',
|
|
25
|
+
requiredFields: ['id', 'name', 'productName', 'itemName', 'goodsName'],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'cu.stores',
|
|
29
|
+
service: 'cu',
|
|
30
|
+
target: 'stores',
|
|
31
|
+
mode: 'quick',
|
|
32
|
+
path: '/api/cu/stores?keyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
33
|
+
collectionKey: 'stores',
|
|
34
|
+
requiredFields: ['storeCode', 'storeName', 'name'],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'emart24.products',
|
|
38
|
+
service: 'emart24',
|
|
39
|
+
target: 'products',
|
|
40
|
+
mode: 'quick',
|
|
41
|
+
path: '/api/emart24/products?keyword=%EC%BB%A4%ED%94%BC&pageSize=1',
|
|
42
|
+
collectionKey: 'products',
|
|
43
|
+
requiredFields: ['pluCd', 'goodsName', 'itemName', 'name'],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'gs25.products',
|
|
47
|
+
service: 'gs25',
|
|
48
|
+
target: 'products',
|
|
49
|
+
mode: 'quick',
|
|
50
|
+
path: '/api/gs25/products?keyword=%EC%BD%9C%EB%9D%BC&limit=1',
|
|
51
|
+
collectionKey: 'products',
|
|
52
|
+
requiredFields: ['itemCode', 'itemName', 'name'],
|
|
53
|
+
degradedFailurePatterns: GS25_CLOUDFRONT_403_PATTERNS,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'gs25.stores',
|
|
57
|
+
service: 'gs25',
|
|
58
|
+
target: 'stores',
|
|
59
|
+
mode: 'quick',
|
|
60
|
+
path: '/api/gs25/stores?keyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
61
|
+
collectionKey: 'stores',
|
|
62
|
+
requiredFields: ['storeCode', 'storeName', 'name'],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: 'seveneleven.products',
|
|
66
|
+
service: 'seveneleven',
|
|
67
|
+
target: 'products',
|
|
68
|
+
mode: 'quick',
|
|
69
|
+
path: '/api/seveneleven/products?query=%EC%BB%A4%ED%94%BC&size=1',
|
|
70
|
+
collectionKey: 'products',
|
|
71
|
+
requiredFields: ['itemCode', 'itemName', 'productNo', 'name'],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'lottemart.products',
|
|
75
|
+
service: 'lottemart',
|
|
76
|
+
target: 'products',
|
|
77
|
+
mode: 'quick',
|
|
78
|
+
path: '/api/lottemart/products?keyword=%EC%BD%9C%EB%9D%BC&storeCode=2301&area=%EC%84%9C%EC%9A%B8&pageLimit=1&source=zetta',
|
|
79
|
+
collectionKey: 'products',
|
|
80
|
+
requiredFields: ['productCode', 'name', 'productName'],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 'oliveyoung.products',
|
|
84
|
+
service: 'oliveyoung',
|
|
85
|
+
target: 'products',
|
|
86
|
+
mode: 'quick',
|
|
87
|
+
path: '/api/oliveyoung/products?keyword=%EB%A6%BD%EB%B0%A4&size=1',
|
|
88
|
+
collectionKey: 'products',
|
|
89
|
+
requiredFields: ['goodsNumber', 'goodsName', 'productNo', 'productName', 'name'],
|
|
90
|
+
timeoutMs: 5000,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'megabox.theaters',
|
|
94
|
+
service: 'megabox',
|
|
95
|
+
target: 'theaters',
|
|
96
|
+
mode: 'quick',
|
|
97
|
+
path: '/api/megabox/theaters?keyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
98
|
+
collectionKey: 'theaters',
|
|
99
|
+
requiredFields: ['theaterCode', 'theaterName', 'name'],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: 'lottecinema.theaters',
|
|
103
|
+
service: 'lottecinema',
|
|
104
|
+
target: 'theaters',
|
|
105
|
+
mode: 'quick',
|
|
106
|
+
path: '/api/lottecinema/theaters?keyword=%EC%9E%A0%EC%8B%A4&limit=1',
|
|
107
|
+
collectionKey: 'theaters',
|
|
108
|
+
requiredFields: ['theaterCode', 'theaterName', 'name'],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: 'cgv.theaters',
|
|
112
|
+
service: 'cgv',
|
|
113
|
+
target: 'theaters',
|
|
114
|
+
mode: 'quick',
|
|
115
|
+
path: '/api/cgv/theaters?keyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
116
|
+
collectionKey: 'theaters',
|
|
117
|
+
requiredFields: ['theaterCode', 'theaterName', 'name'],
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: 'cu.inventory',
|
|
121
|
+
service: 'cu',
|
|
122
|
+
target: 'inventory',
|
|
123
|
+
mode: 'deep',
|
|
124
|
+
path: '/api/cu/inventory?keyword=%EC%BB%A4%ED%94%BC&size=1&storeLimit=0&storeCheck=false',
|
|
125
|
+
collectionKey: 'inventoryItems',
|
|
126
|
+
requiredFields: ['itemCode', 'itemName', 'name'],
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: 'emart24.inventory',
|
|
130
|
+
service: 'emart24',
|
|
131
|
+
target: 'inventory',
|
|
132
|
+
mode: 'deep',
|
|
133
|
+
path: '/api/emart24/inventory?keyword=%EC%BB%A4%ED%94%BC&storeKeyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
134
|
+
collectionKey: 'inventoryItems',
|
|
135
|
+
requiredFields: ['pluCd', 'goodsName', 'itemName', 'name'],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: 'gs25.inventory',
|
|
139
|
+
service: 'gs25',
|
|
140
|
+
target: 'inventory',
|
|
141
|
+
mode: 'deep',
|
|
142
|
+
path: '/api/gs25/inventory?keyword=%EC%BD%9C%EB%9D%BC&storeKeyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
143
|
+
collectionKey: 'inventoryItems',
|
|
144
|
+
requiredFields: ['itemCode', 'itemName', 'name'],
|
|
145
|
+
degradedFailurePatterns: [
|
|
146
|
+
'401 Unauthorized',
|
|
147
|
+
'인증키가 제공되지 않음',
|
|
148
|
+
...GS25_CLOUDFRONT_403_PATTERNS,
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: 'seveneleven.inventory',
|
|
153
|
+
service: 'seveneleven',
|
|
154
|
+
target: 'inventory',
|
|
155
|
+
mode: 'deep',
|
|
156
|
+
path: '/api/seveneleven/inventory?keyword=%EC%BB%A4%ED%94%BC&storeKeyword=%EA%B0%95%EB%82%A8&size=1',
|
|
157
|
+
collectionKey: 'inventoryItems',
|
|
158
|
+
requiredFields: ['itemCode', 'itemName', 'productNo', 'name'],
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: 'oliveyoung.inventory',
|
|
162
|
+
service: 'oliveyoung',
|
|
163
|
+
target: 'inventory',
|
|
164
|
+
mode: 'deep',
|
|
165
|
+
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',
|
|
166
|
+
collectionKey: 'inventoryProducts',
|
|
167
|
+
requiredFields: ['goodsNumber', 'goodsName', 'productNo', 'productName', 'name'],
|
|
168
|
+
timeoutMs: 5000,
|
|
169
|
+
},
|
|
170
|
+
];
|
|
@@ -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
|
+
}
|