daiso 1.0.5 → 1.0.7
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 +101 -7
- package/dist/api/actionsProxy.js +111 -21
- package/dist/api/configStatus.js +23 -0
- package/dist/api/gs25Handlers.js +68 -22
- package/dist/api/handlers.js +32 -3
- package/dist/api/healthChecks.js +233 -20
- package/dist/api/lottemartHandlers.js +17 -1
- package/dist/api/placesHandlers.js +44 -0
- package/dist/api/response.js +2 -0
- package/dist/api/routes/healthRoutes.js +20 -3
- package/dist/api/routes/placesRoutes.js +9 -0
- package/dist/bin.js +0 -0
- package/dist/cli/args.js +21 -0
- package/dist/cli/commandOptions.js +96 -0
- package/dist/cli/commands/convenience.js +73 -1
- package/dist/cli/commands/daiso.js +18 -2
- package/dist/cli/http.js +58 -0
- package/dist/cli/interactiveItemSearch.js +4 -2
- package/dist/cli/interactivePrompt.js +3 -2
- package/dist/cli.js +25 -1
- package/dist/cliHelp.js +20 -6
- package/dist/cliInteractive.js +75 -6
- package/dist/cliPicker.js +1 -0
- package/dist/cliRenderer.js +93 -3
- package/dist/core/errors.js +66 -0
- package/dist/core/registry.js +246 -3
- package/dist/index.js +19 -4
- package/dist/pages/openapiFullSpec.js +3 -0
- package/dist/pages/openapiSpecActions.js +308 -42
- package/dist/pages/openapiSpecPathsCu.js +7 -0
- package/dist/pages/openapiSpecPathsLotteMart.js +14 -0
- package/dist/pages/openapiSpecPathsOliveyoung.js +35 -0
- package/dist/pages/openapiSpecPathsPlaces.js +85 -0
- package/dist/pages/promptReferenceText.js +16 -7
- package/dist/pages/promptRetailCinemaText.js +24 -2
- package/dist/services/daiso/client.js +8 -0
- package/dist/services/daiso/index.js +2 -0
- package/dist/services/daiso/tools/checkInventory.js +4 -2
- package/dist/services/daiso/tools/findInventoryByName.js +222 -0
- package/dist/services/daiso/tools/getDisplayLocation.js +2 -2
- package/dist/services/emart24/client.js +9 -0
- package/dist/services/emart24/tools/searchProducts.js +27 -17
- package/dist/services/gs25/api.js +3 -0
- package/dist/services/gs25/client.js +216 -30
- package/dist/services/gs25/index.js +11 -3
- package/dist/services/gs25/tools/checkInventory.js +39 -12
- package/dist/services/gs25/tools/findNearbyStores.js +19 -5
- package/dist/services/gs25/tools/searchProducts.js +39 -25
- package/dist/services/lottemart/client.js +10 -1
- package/dist/services/lottemart/tools/searchProducts.js +8 -1
- package/dist/services/lottemart/zetta.js +1 -1
- package/dist/services/oliveyoung/client.js +206 -86
- package/dist/services/oliveyoung/tools/checkInventory.js +7 -2
- package/dist/services/places/client.js +116 -0
- package/dist/services/places/index.js +23 -0
- package/dist/services/places/tools/searchNearby.js +56 -0
- package/dist/services/places/types.js +1 -0
- package/dist/services/seveneleven/client.js +7 -0
- package/dist/services/seveneleven/tools/searchProducts.js +29 -18
- package/dist/utils/http.js +82 -10
- package/dist/utils/zyte.js +74 -26
- package/openapi.json +22 -3
- package/openapi.yaml +16 -3
- package/package.json +28 -9
package/README.md
CHANGED
|
@@ -5,9 +5,53 @@
|
|
|
5
5
|
<br>
|
|
6
6
|
<br>
|
|
7
7
|
|
|
8
|
-
# Daiso MCP
|
|
8
|
+
# Daiso MCP 및 Skill
|
|
9
9
|
|
|
10
|
-
다이소(제품/매장/재고), 롯데마트(매장/상품), GS25(매장/상품/재고), 세븐일레븐(상품/매장/재고/인기검색어/카탈로그), CU(매장/재고), 이마트24(매장/상품/재고), 올리브영(매장/재고), 메가박스(지점/영화/시간표/좌석), 롯데시네마(지점/영화/좌석), CGV(극장/영화/시간표) 조회 기능을 AI에 연결합니다.
|
|
10
|
+
다이소(제품/매장/재고), 주변 음식점/카페, 롯데마트(매장/상품), GS25(매장/상품/재고), 세븐일레븐(상품/매장/재고/인기검색어/카탈로그), CU(매장/재고), 이마트24(매장/상품/재고), 올리브영(매장/재고), 메가박스(지점/영화/시간표/좌석), 롯데시네마(지점/영화/좌석), CGV(극장/영화/시간표) 조회 기능을 MCP, CLI, Codex Skill로 AI에 연결합니다.
|
|
11
|
+
|
|
12
|
+
<br>
|
|
13
|
+
|
|
14
|
+
**MCP:** `https://mcp.aka.page` · **CLI:** `npx daiso` · **Skill:** `clawhub install daiso-cli`
|
|
15
|
+
|
|
16
|
+
**ClawHub:** [clawhub.ai/hmmhmmhm/daiso-cli](https://clawhub.ai/hmmhmmhm/daiso-cli)
|
|
17
|
+
|
|
18
|
+
한국 로컬 리테일과 영화관 조회를 MCP, CLI, Codex Skill로 연결하는 도구입니다.
|
|
19
|
+
|
|
20
|
+
<br>
|
|
21
|
+
|
|
22
|
+
<h3>지원 서비스</h3>
|
|
23
|
+
|
|
24
|
+
<table>
|
|
25
|
+
<thead>
|
|
26
|
+
<tr>
|
|
27
|
+
<th>분류</th>
|
|
28
|
+
<th>서비스</th>
|
|
29
|
+
<th>조회 기능</th>
|
|
30
|
+
</tr>
|
|
31
|
+
</thead>
|
|
32
|
+
<tbody>
|
|
33
|
+
<tr>
|
|
34
|
+
<td>장소</td>
|
|
35
|
+
<td>네이버 지역 검색</td>
|
|
36
|
+
<td>음식점, 카페, 디저트, 주변 장소</td>
|
|
37
|
+
</tr>
|
|
38
|
+
<tr>
|
|
39
|
+
<td>리테일</td>
|
|
40
|
+
<td>다이소, 올리브영, 롯데마트</td>
|
|
41
|
+
<td>상품, 매장, 재고</td>
|
|
42
|
+
</tr>
|
|
43
|
+
<tr>
|
|
44
|
+
<td>편의점</td>
|
|
45
|
+
<td>GS25, 세븐일레븐, CU, 이마트24</td>
|
|
46
|
+
<td>상품, 매장, 재고, 인기검색어, 카탈로그</td>
|
|
47
|
+
</tr>
|
|
48
|
+
<tr>
|
|
49
|
+
<td>영화관</td>
|
|
50
|
+
<td>CGV, 메가박스, 롯데시네마</td>
|
|
51
|
+
<td>극장, 영화, 시간표, 잔여 좌석</td>
|
|
52
|
+
</tr>
|
|
53
|
+
</tbody>
|
|
54
|
+
</table>
|
|
11
55
|
|
|
12
56
|
<br>
|
|
13
57
|
|
|
@@ -21,11 +65,11 @@
|
|
|
21
65
|
**[실시간 서비스 상태 보기](https://aka-page.betteruptime.com/)**
|
|
22
66
|
|
|
23
67
|
<!-- WORKERS_INVOCATIONS_CHART:START -->
|
|
24
|
-
<h3>Cloudflare Workers 호출량 (2026-03-01 ~ 2026-05-
|
|
68
|
+
<h3>Cloudflare Workers 호출량 (2026-03-01 ~ 2026-05-21, 82일)</h3>
|
|
25
69
|
|
|
26
|
-
<img src="./assets/analytics/workers-invocations.png?v=2026-05-
|
|
70
|
+
<img src="./assets/analytics/workers-invocations.png?v=2026-05-23T04:50:41.937Z" alt="Cloudflare Workers 호출량 그래프 (2026-03-01 ~ 2026-05-21)" width="100%">
|
|
27
71
|
|
|
28
|
-
<sub>기준 워커: <code>daiso-mcp</code> · 마지막 갱신: 2026-05-
|
|
72
|
+
<sub>기준 워커: <code>daiso-mcp</code> · 마지막 갱신: 2026-05-23 13:50 KST</sub>
|
|
29
73
|
|
|
30
74
|
<!-- WORKERS_INVOCATIONS_CHART:END -->
|
|
31
75
|
|
|
@@ -70,6 +114,8 @@ GS25 mcp로 강남 근처 매장과 오감자 재고 알려줘
|
|
|
70
114
|
세븐일레븐 mcp로 안산 중앙역 근처 매장 찾아줘
|
|
71
115
|
세븐일레븐 mcp로 안산 중앙역 근처 세븐일레븐에서 핫식스 재고 알려줘
|
|
72
116
|
세븐일레븐 mcp로 인기 검색어와 카탈로그 요약 알려줘
|
|
117
|
+
강남역 근처 카페 찾아줘
|
|
118
|
+
성수동 근처 브런치 음식점 찾아줘
|
|
73
119
|
메가박스 mcp로 강남점 영화와 잔여 좌석 알려줘
|
|
74
120
|
롯데시네마 mcp로 월드타워 근처 지점과 상영 영화 알려줘
|
|
75
121
|
롯데시네마 mcp로 월드타워 잔여 좌석 알려줘
|
|
@@ -163,6 +209,19 @@ AI: (https://mcp.aka.page/api/cgv/timetable?playDate=20260315&keyword=안산%20
|
|
|
163
209
|
|
|
164
210
|
<br>
|
|
165
211
|
|
|
212
|
+
### 바로 실행해보기
|
|
213
|
+
|
|
214
|
+
AI 앱 연동 없이 터미널에서 바로 확인할 수 있습니다.
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
npx daiso products 수납박스 --json
|
|
218
|
+
npx daiso places 강남역 --category cafe --limit 5 --json
|
|
219
|
+
npx daiso gs25-inventory 오감자 --storeKeyword 강남 --storeLimit 5 --json
|
|
220
|
+
npx daiso get /api/cgv/movies --playDate 20260520 --theaterCode 0056 --json
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
<br>
|
|
224
|
+
|
|
166
225
|
### MCP 서버 URL / CLI (고급)
|
|
167
226
|
|
|
168
227
|
AI 앱 대신 직접 연결하거나 스크립트에서 사용할 때만 참고하세요.
|
|
@@ -195,6 +254,8 @@ npx daiso product 1034604
|
|
|
195
254
|
npx daiso stores 강남역
|
|
196
255
|
npx daiso inventory 1034604 --keyword 강남역
|
|
197
256
|
npx daiso display-location 1034604 04515
|
|
257
|
+
npx daiso places 강남역 --category cafe --limit 5
|
|
258
|
+
npx daiso places 성수동 --keyword 브런치 --limit 5
|
|
198
259
|
npx daiso cu-stores 강남
|
|
199
260
|
npx daiso cu-inventory 과자 --storeKeyword 강남
|
|
200
261
|
npx daiso lottemart-stores 잠실 --area 서울 --limit 10
|
|
@@ -216,6 +277,17 @@ npx daiso get /api/cgv/movies --playDate 20260307 --theaterCode 0056
|
|
|
216
277
|
npx daiso products 수납박스 --json
|
|
217
278
|
```
|
|
218
279
|
|
|
280
|
+
### Codex Skill
|
|
281
|
+
|
|
282
|
+
이 저장소는 MCP 서버뿐 아니라 에이전트가 `npx daiso` CLI를 직접 고를 수 있게 하는 Codex Skill도 제공합니다.
|
|
283
|
+
|
|
284
|
+
- ClawHub: `clawhub install daiso-cli`
|
|
285
|
+
- 공개 페이지: `https://clawhub.ai/hmmhmmhm/daiso-cli`
|
|
286
|
+
- 스킬 파일: `skills/daiso-cli/SKILL.md`
|
|
287
|
+
- 명령 맵: `skills/daiso-cli/references/cli-command-map.md`
|
|
288
|
+
- 기본 원칙: 쉘 실행이 가능한 환경에서는 CLI를 우선 사용하고, AI 앱 연결이 필요할 때는 MCP 서버 URL `https://mcp.aka.page`를 사용합니다.
|
|
289
|
+
- 구조화 결과가 필요하면 스킬은 `npx daiso ... --json` 형태를 선택합니다.
|
|
290
|
+
|
|
219
291
|
### OpenAPI 스펙
|
|
220
292
|
|
|
221
293
|
- OpenAI 챗봇 등록용 축약 스펙: `https://mcp.aka.page/openapi.json`
|
|
@@ -223,24 +295,45 @@ npx daiso products 수납박스 --json
|
|
|
223
295
|
- 전체 개별 엔드포인트 스펙(JSON): `https://mcp.aka.page/openapi-full.json`
|
|
224
296
|
- 전체 개별 엔드포인트 스펙(YAML): `https://mcp.aka.page/openapi-full.yaml`
|
|
225
297
|
|
|
298
|
+
### MCP 표준 응답 모델
|
|
299
|
+
|
|
300
|
+
MCP 도구 응답은 각 서비스의 원본 필드를 유지하면서, 클라이언트가 공통 UI를 만들 수 있도록 `standard` 필드를 함께 제공합니다.
|
|
301
|
+
|
|
302
|
+
- `standard.products`: 상품/영화 목록을 `code`, `name`, `price`, `imageUrl`, `raw` 형태로 정규화합니다.
|
|
303
|
+
- `standard.stores`: 매장 목록을 `code`, `name`, `address`, `distanceMeters`, `raw` 형태로 정규화합니다.
|
|
304
|
+
- `standard.theaters`: 영화관 목록을 `code`, `name`, `address`, `distanceMeters`, `raw` 형태로 정규화합니다.
|
|
305
|
+
- `standard.places`: 음식점/카페 같은 장소 목록을 `name`, `address`, `raw` 중심으로 정규화합니다.
|
|
306
|
+
|
|
226
307
|
### 운영 헬스 체크
|
|
227
308
|
|
|
228
309
|
공개 상태 페이지: **[Daiso MCP Status](https://aka-page.betteruptime.com/)**
|
|
229
310
|
|
|
311
|
+
주변 음식점/카페 검색은 네이버 지역 검색 API를 사용합니다. 로컬 실행이나 배포 환경에는 `NAVER_CLIENT_ID`, `NAVER_CLIENT_SECRET`을 설정하세요.
|
|
312
|
+
|
|
230
313
|
서비스별 API 상태를 즉시 확인할 때는 `GET /api/health/checks`를 사용합니다. 이 엔드포인트는 `HEALTH_CHECK_SECRET` 환경 변수가 설정되어 있어야 하며, 요청에는 `Authorization: Bearer <secret>` 또는 `x-health-check-key: <secret>` 헤더가 필요합니다. 내부 체크 요청의 기준 URL은 `HEALTH_CHECK_BASE_URL`로 지정할 수 있습니다.
|
|
231
314
|
|
|
232
315
|
```bash
|
|
233
316
|
curl -H "Authorization: Bearer $HEALTH_CHECK_SECRET" \
|
|
234
|
-
"https://mcp.aka.page/api/health/checks?
|
|
317
|
+
"https://mcp.aka.page/api/health/checks?mode=full&fresh=true&includeSamples=true&timeoutMs=20000&slowThresholdMs=9000"
|
|
235
318
|
```
|
|
236
319
|
|
|
237
320
|
주요 쿼리:
|
|
238
321
|
|
|
239
322
|
- `service=gs25`: 특정 서비스만 확인
|
|
240
323
|
- `check=lottemart.products`: 특정 체크만 확인
|
|
241
|
-
- `mode=quick|deep`: 체크 모드 선택
|
|
324
|
+
- `mode=quick|deep|full`: 체크 모드 선택
|
|
242
325
|
- `fresh=true`: 60초 캐시 우회
|
|
243
326
|
- `includeSamples=true`: 첫 결과 이름 샘플 포함
|
|
327
|
+
- `timeoutMs=20000`: 체크별 요청 제한 시간
|
|
328
|
+
- `slowThresholdMs=9000`: 지정 시간보다 느린 성공 응답을 degraded로 표시
|
|
329
|
+
|
|
330
|
+
상태 기준:
|
|
331
|
+
|
|
332
|
+
- `ok`: 필수 응답 구조와 최소 결과가 정상입니다.
|
|
333
|
+
- `degraded`: 기능은 살아 있지만 빈 결과, 느린 응답, 응답 구조 변화 등 확인이 필요합니다.
|
|
334
|
+
- `fail`: 외부 API 오류, 타임아웃, 인증 문제처럼 실제 장애로 봐야 합니다.
|
|
335
|
+
|
|
336
|
+
Better Stack 같은 외부 모니터링에서는 `fail`을 장애 알림 기준으로 보고, `degraded`는 느린 외부 API나 응답 품질 저하를 추적하는 경고 신호로 봅니다.
|
|
244
337
|
|
|
245
338
|
배포 전 로컬에서 CLI 모드까지 확인할 때는 아래 명령을 사용합니다.
|
|
246
339
|
|
|
@@ -315,6 +408,7 @@ daiso 인터랙티브 모드
|
|
|
315
408
|
|
|
316
409
|
- [@thecats1105](https://github.com/thecats1105): 다이소 진열 위치 조회 도구(`daiso_get_display_location`) 구현 및 API/테스트 연동
|
|
317
410
|
- [@betterthanhajin](https://github.com/betterthanhajin): CGV 서비스 프로바이더 구현(극장/영화/시간표 도구, 라우트·스펙·테스트 추가)
|
|
411
|
+
- [@LLagoon3](https://github.com/LLagoon3): 다이소 매장 재고 조회 신규 인증 흐름 전환 및 관련 테스트 보강
|
|
318
412
|
- [제로초님](https://youtube.com/shorts/ZgIqA1NCEp0?si=UW0pKsSpqmEi7lXG): 프로젝트 홍보 도움
|
|
319
413
|
|
|
320
414
|
<br>
|
package/dist/api/actionsProxy.js
CHANGED
|
@@ -3,34 +3,124 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const ACTION_QUERY_DEFINITIONS = [
|
|
5
5
|
{ action: 'daisoSearchProducts', summary: '다이소 제품 검색', targetPath: '/api/daiso/products' },
|
|
6
|
-
{
|
|
6
|
+
{
|
|
7
|
+
action: 'daisoGetProduct',
|
|
8
|
+
summary: '다이소 제품 상세 조회',
|
|
9
|
+
targetPath: '/api/daiso/products/:productId',
|
|
10
|
+
pathParam: 'productId',
|
|
11
|
+
},
|
|
7
12
|
{ action: 'daisoFindStores', summary: '다이소 매장 검색', targetPath: '/api/daiso/stores' },
|
|
8
|
-
{
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
{
|
|
14
|
+
action: 'daisoCheckInventory',
|
|
15
|
+
summary: '다이소 재고 조회',
|
|
16
|
+
targetPath: '/api/daiso/inventory',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
action: 'daisoGetDisplayLocation',
|
|
20
|
+
summary: '다이소 진열 위치 조회',
|
|
21
|
+
targetPath: '/api/daiso/display-location',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
action: 'oliveyoungSearchProducts',
|
|
25
|
+
summary: '올리브영 상품 검색',
|
|
26
|
+
targetPath: '/api/oliveyoung/products',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
action: 'oliveyoungFindStores',
|
|
30
|
+
summary: '올리브영 매장 검색',
|
|
31
|
+
targetPath: '/api/oliveyoung/stores',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
action: 'oliveyoungCheckInventory',
|
|
35
|
+
summary: '올리브영 재고 조회',
|
|
36
|
+
targetPath: '/api/oliveyoung/inventory',
|
|
37
|
+
},
|
|
13
38
|
{ action: 'cuFindStores', summary: 'CU 매장 검색', targetPath: '/api/cu/stores' },
|
|
14
39
|
{ action: 'cuCheckInventory', summary: 'CU 재고 조회', targetPath: '/api/cu/inventory' },
|
|
15
40
|
{ action: 'emart24FindStores', summary: '이마트24 매장 검색', targetPath: '/api/emart24/stores' },
|
|
16
|
-
{
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
41
|
+
{
|
|
42
|
+
action: 'emart24SearchProducts',
|
|
43
|
+
summary: '이마트24 상품 검색',
|
|
44
|
+
targetPath: '/api/emart24/products',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
action: 'emart24CheckInventory',
|
|
48
|
+
summary: '이마트24 재고 조회',
|
|
49
|
+
targetPath: '/api/emart24/inventory',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
action: 'lottemartFindStores',
|
|
53
|
+
summary: '롯데마트 매장 검색',
|
|
54
|
+
targetPath: '/api/lottemart/stores',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
action: 'lottemartSearchProducts',
|
|
58
|
+
summary: '롯데마트 상품 검색',
|
|
59
|
+
targetPath: '/api/lottemart/products',
|
|
60
|
+
},
|
|
20
61
|
{ action: 'gs25FindStores', summary: 'GS25 매장 검색', targetPath: '/api/gs25/stores' },
|
|
21
62
|
{ action: 'gs25SearchProducts', summary: 'GS25 상품 검색', targetPath: '/api/gs25/products' },
|
|
22
63
|
{ action: 'gs25CheckInventory', summary: 'GS25 재고 조회', targetPath: '/api/gs25/inventory' },
|
|
23
|
-
{
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
{
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
{
|
|
64
|
+
{
|
|
65
|
+
action: 'sevenelevenSearchProducts',
|
|
66
|
+
summary: '세븐일레븐 상품 검색',
|
|
67
|
+
targetPath: '/api/seveneleven/products',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
action: 'sevenelevenSearchStores',
|
|
71
|
+
summary: '세븐일레븐 매장 검색',
|
|
72
|
+
targetPath: '/api/seveneleven/stores',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
action: 'sevenelevenCheckInventory',
|
|
76
|
+
summary: '세븐일레븐 재고 조회',
|
|
77
|
+
targetPath: '/api/seveneleven/inventory',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
action: 'sevenelevenGetPopwords',
|
|
81
|
+
summary: '세븐일레븐 인기 검색어 조회',
|
|
82
|
+
targetPath: '/api/seveneleven/popwords',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
action: 'sevenelevenGetCatalog',
|
|
86
|
+
summary: '세븐일레븐 카탈로그 조회',
|
|
87
|
+
targetPath: '/api/seveneleven/catalog',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
action: 'placesSearchNearby',
|
|
91
|
+
summary: '음식점/카페 등 주변 장소 검색',
|
|
92
|
+
targetPath: '/api/places/search',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
action: 'megaboxFindTheaters',
|
|
96
|
+
summary: '메가박스 지점 검색',
|
|
97
|
+
targetPath: '/api/megabox/theaters',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
action: 'megaboxListMovies',
|
|
101
|
+
summary: '메가박스 영화 목록 조회',
|
|
102
|
+
targetPath: '/api/megabox/movies',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
action: 'megaboxGetSeats',
|
|
106
|
+
summary: '메가박스 잔여 좌석 조회',
|
|
107
|
+
targetPath: '/api/megabox/seats',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
action: 'lottecinemaFindTheaters',
|
|
111
|
+
summary: '롯데시네마 지점 검색',
|
|
112
|
+
targetPath: '/api/lottecinema/theaters',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
action: 'lottecinemaListMovies',
|
|
116
|
+
summary: '롯데시네마 영화 목록 조회',
|
|
117
|
+
targetPath: '/api/lottecinema/movies',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
action: 'lottecinemaGetSeats',
|
|
121
|
+
summary: '롯데시네마 잔여 좌석 조회',
|
|
122
|
+
targetPath: '/api/lottecinema/seats',
|
|
123
|
+
},
|
|
34
124
|
{ action: 'cgvFindTheaters', summary: 'CGV 극장 검색', targetPath: '/api/cgv/theaters' },
|
|
35
125
|
{ action: 'cgvSearchMovies', summary: 'CGV 영화 목록 조회', targetPath: '/api/cgv/movies' },
|
|
36
126
|
{ action: 'cgvGetTimetable', summary: 'CGV 시간표 조회', targetPath: '/api/cgv/timetable' },
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
function isConfigured(value) {
|
|
2
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
3
|
+
}
|
|
4
|
+
export function buildConfigStatus(bindings) {
|
|
5
|
+
return {
|
|
6
|
+
googleMapsApiKey: {
|
|
7
|
+
configured: isConfigured(bindings?.GOOGLE_MAPS_API_KEY),
|
|
8
|
+
usedBy: ['gs25', 'cu', 'lottemart', 'megabox', 'lottecinema', 'cgv'],
|
|
9
|
+
},
|
|
10
|
+
zyteApiKey: {
|
|
11
|
+
configured: isConfigured(bindings?.ZYTE_API_KEY),
|
|
12
|
+
usedBy: ['oliveyoung', 'gs25', 'lottemart', 'cgv'],
|
|
13
|
+
},
|
|
14
|
+
naverLocalSearch: {
|
|
15
|
+
configured: isConfigured(bindings?.NAVER_CLIENT_ID) && isConfigured(bindings?.NAVER_CLIENT_SECRET),
|
|
16
|
+
usedBy: ['places'],
|
|
17
|
+
},
|
|
18
|
+
healthCheckSecret: {
|
|
19
|
+
configured: isConfigured(bindings?.HEALTH_CHECK_SECRET),
|
|
20
|
+
usedBy: ['health-checks'],
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
package/dist/api/gs25Handlers.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
/* c8 ignore start */
|
|
5
5
|
import { errorResponse, successResponse } from './response.js';
|
|
6
|
-
import { attachDistanceToGs25Stores, fetchGs25SearchProducts, fetchGs25Stores, filterGs25StoresByKeyword, geocodeGs25Address, selectGs25StoresForKeyword, sortGs25Stores, } from '../services/gs25/client.js';
|
|
6
|
+
import { attachDistanceToGs25Stores, fetchGs25SearchProducts, fetchGs25Stores, fetchGs25WebStores, filterGs25StoresByKeyword, geocodeGs25Address, selectGs25StoresForKeyword, sortGs25Stores, } from '../services/gs25/client.js';
|
|
7
7
|
const GS25_FALLBACK_STORE_LOOKUP_ITEM_CODE = '8801117752804';
|
|
8
8
|
/**
|
|
9
9
|
* GS25 매장 검색 API 핸들러
|
|
@@ -21,7 +21,8 @@ export async function handleGs25FindStores(c) {
|
|
|
21
21
|
let longitude = typeof parsedLng === 'number' && Number.isFinite(parsedLng) ? parsedLng : undefined;
|
|
22
22
|
let geocodeUsed = false;
|
|
23
23
|
try {
|
|
24
|
-
if ((typeof latitude !== 'number' || typeof longitude !== 'number') &&
|
|
24
|
+
if ((typeof latitude !== 'number' || typeof longitude !== 'number') &&
|
|
25
|
+
keyword.trim().length > 0) {
|
|
25
26
|
const geocoded = await geocodeGs25Address(keyword, {
|
|
26
27
|
timeout: 15000,
|
|
27
28
|
googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
|
|
@@ -38,9 +39,12 @@ export async function handleGs25FindStores(c) {
|
|
|
38
39
|
longitude,
|
|
39
40
|
}, {
|
|
40
41
|
timeout: 20000,
|
|
42
|
+
zyteApiKey: c.env?.ZYTE_API_KEY,
|
|
41
43
|
});
|
|
42
44
|
let fallbackUsed = false;
|
|
43
|
-
if (storeResult.stores.length === 0 &&
|
|
45
|
+
if (storeResult.stores.length === 0 &&
|
|
46
|
+
typeof latitude === 'number' &&
|
|
47
|
+
typeof longitude === 'number') {
|
|
44
48
|
try {
|
|
45
49
|
const fallbackResult = await fetchGs25Stores({
|
|
46
50
|
serviceCode,
|
|
@@ -51,6 +55,7 @@ export async function handleGs25FindStores(c) {
|
|
|
51
55
|
useCache: false,
|
|
52
56
|
}, {
|
|
53
57
|
timeout: 20000,
|
|
58
|
+
zyteApiKey: c.env?.ZYTE_API_KEY,
|
|
54
59
|
});
|
|
55
60
|
if (fallbackResult.stores.length > 0) {
|
|
56
61
|
storeResult = fallbackResult;
|
|
@@ -61,6 +66,24 @@ export async function handleGs25FindStores(c) {
|
|
|
61
66
|
fallbackUsed = false;
|
|
62
67
|
}
|
|
63
68
|
}
|
|
69
|
+
if (storeResult.stores.length === 0 && keyword.trim().length > 0) {
|
|
70
|
+
try {
|
|
71
|
+
const webStoreResult = await fetchGs25WebStores(keyword, {
|
|
72
|
+
timeout: 20000,
|
|
73
|
+
});
|
|
74
|
+
if (webStoreResult.stores.length > 0) {
|
|
75
|
+
storeResult = {
|
|
76
|
+
totalCount: webStoreResult.totalCount,
|
|
77
|
+
stores: webStoreResult.stores,
|
|
78
|
+
cacheHit: false,
|
|
79
|
+
};
|
|
80
|
+
fallbackUsed = true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
fallbackUsed = false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
64
87
|
const selected = selectGs25StoresForKeyword(storeResult.stores, keyword, {
|
|
65
88
|
relaxWhenEmpty: typeof latitude === 'number' && typeof longitude === 'number',
|
|
66
89
|
});
|
|
@@ -98,7 +121,10 @@ export async function handleGs25SearchProducts(c) {
|
|
|
98
121
|
return errorResponse(c, 'MISSING_QUERY', '검색어(keyword)를 입력해주세요.');
|
|
99
122
|
}
|
|
100
123
|
try {
|
|
101
|
-
const products = await fetchGs25SearchProducts(keyword, {
|
|
124
|
+
const products = await fetchGs25SearchProducts(keyword, {
|
|
125
|
+
timeout: 20000,
|
|
126
|
+
zyteApiKey: c.env?.ZYTE_API_KEY,
|
|
127
|
+
});
|
|
102
128
|
const limitedProducts = products.slice(0, limit);
|
|
103
129
|
return successResponse(c, {
|
|
104
130
|
keyword,
|
|
@@ -142,23 +168,35 @@ export async function handleGs25CheckInventory(c) {
|
|
|
142
168
|
return errorResponse(c, 'MISSING_QUERY', '검색어(keyword) 또는 상품 코드(itemCode)를 입력해주세요.');
|
|
143
169
|
}
|
|
144
170
|
try {
|
|
145
|
-
if ((typeof latitude !== 'number' || typeof longitude !== 'number') &&
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
171
|
+
if ((typeof latitude !== 'number' || typeof longitude !== 'number') &&
|
|
172
|
+
storeKeyword.trim().length > 0) {
|
|
173
|
+
const directGeocoded = await geocodeGs25Address(storeKeyword, {
|
|
174
|
+
timeout: 15000,
|
|
175
|
+
googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
|
|
150
176
|
});
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
177
|
+
if (directGeocoded) {
|
|
178
|
+
latitude = directGeocoded.latitude;
|
|
179
|
+
longitude = directGeocoded.longitude;
|
|
180
|
+
geocodeUsed = true;
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
const baseStores = await fetchGs25Stores({
|
|
184
|
+
serviceCode,
|
|
185
|
+
}, {
|
|
186
|
+
timeout: 20000,
|
|
187
|
+
zyteApiKey: c.env?.ZYTE_API_KEY,
|
|
157
188
|
});
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
189
|
+
const firstAddress = filterGs25StoresByKeyword(baseStores.stores, storeKeyword).find((store) => store.address.trim().length > 0)?.address || '';
|
|
190
|
+
if (firstAddress.length > 0) {
|
|
191
|
+
const geocoded = await geocodeGs25Address(firstAddress, {
|
|
192
|
+
timeout: 15000,
|
|
193
|
+
googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
|
|
194
|
+
});
|
|
195
|
+
if (geocoded) {
|
|
196
|
+
latitude = geocoded.latitude;
|
|
197
|
+
longitude = geocoded.longitude;
|
|
198
|
+
geocodeUsed = true;
|
|
199
|
+
}
|
|
162
200
|
}
|
|
163
201
|
}
|
|
164
202
|
}
|
|
@@ -177,10 +215,14 @@ export async function handleGs25CheckInventory(c) {
|
|
|
177
215
|
useCache: false,
|
|
178
216
|
}, {
|
|
179
217
|
timeout: 20000,
|
|
218
|
+
zyteApiKey: c.env?.ZYTE_API_KEY,
|
|
180
219
|
});
|
|
181
220
|
}
|
|
182
221
|
else {
|
|
183
|
-
const searchProducts = await fetchGs25SearchProducts(keyword, {
|
|
222
|
+
const searchProducts = await fetchGs25SearchProducts(keyword, {
|
|
223
|
+
timeout: 20000,
|
|
224
|
+
zyteApiKey: c.env?.ZYTE_API_KEY,
|
|
225
|
+
});
|
|
184
226
|
firstProduct = searchProducts.find((p) => p.itemCode.length > 0);
|
|
185
227
|
if (firstProduct) {
|
|
186
228
|
resolvedItemCode = firstProduct.itemCode;
|
|
@@ -194,6 +236,7 @@ export async function handleGs25CheckInventory(c) {
|
|
|
194
236
|
useCache: false,
|
|
195
237
|
}, {
|
|
196
238
|
timeout: 20000,
|
|
239
|
+
zyteApiKey: c.env?.ZYTE_API_KEY,
|
|
197
240
|
});
|
|
198
241
|
}
|
|
199
242
|
else {
|
|
@@ -206,6 +249,7 @@ export async function handleGs25CheckInventory(c) {
|
|
|
206
249
|
useCache: false,
|
|
207
250
|
}, {
|
|
208
251
|
timeout: 20000,
|
|
252
|
+
zyteApiKey: c.env?.ZYTE_API_KEY,
|
|
209
253
|
});
|
|
210
254
|
}
|
|
211
255
|
}
|
|
@@ -216,8 +260,10 @@ export async function handleGs25CheckInventory(c) {
|
|
|
216
260
|
const withDistance = attachDistanceToGs25Stores(filtered, latitude, longitude);
|
|
217
261
|
const stores = sortGs25Stores(withDistance).slice(0, storeLimit);
|
|
218
262
|
// 상품 정보: searchProducts에서 먼저 가져오고, 없으면 stores에서
|
|
219
|
-
const productName = firstProduct?.itemName ||
|
|
220
|
-
|
|
263
|
+
const productName = firstProduct?.itemName ||
|
|
264
|
+
firstProduct?.shortItemName ||
|
|
265
|
+
filtered.find((item) => item.searchItemName.length > 0)?.searchItemName ||
|
|
266
|
+
null;
|
|
221
267
|
const productPrice = filtered.find((item) => item.searchItemSellPrice !== null)?.searchItemSellPrice ?? null;
|
|
222
268
|
const inStockStoreCount = filtered.filter((item) => item.realStockQuantity > 0).length;
|
|
223
269
|
const totalStockQuantity = filtered.reduce((sum, item) => sum + Math.max(item.realStockQuantity, 0), 0);
|
package/dist/api/handlers.js
CHANGED
|
@@ -8,6 +8,19 @@ import { enrichOliveyoungProductsWithNearbyStoreInventory, fetchOliveyoungProduc
|
|
|
8
8
|
import { fetchCuStock, fetchCuStores, geocodeCuAddress } from '../services/cu/client.js';
|
|
9
9
|
import { errorResponse, successResponse } from './response.js';
|
|
10
10
|
export { handleCheckInventory, handleFindStores, handleGetDisplayLocation, handleGetProduct, handleSearchProducts, } from './daisoHandlers.js';
|
|
11
|
+
function parseBooleanQuery(value, defaultValue = false) {
|
|
12
|
+
if (!value) {
|
|
13
|
+
return defaultValue;
|
|
14
|
+
}
|
|
15
|
+
const normalized = value.trim().toLowerCase();
|
|
16
|
+
if (normalized === 'true' || normalized === '1' || normalized === 'y') {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
if (normalized === 'false' || normalized === '0' || normalized === 'n') {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return defaultValue;
|
|
23
|
+
}
|
|
11
24
|
/**
|
|
12
25
|
* 올리브영 상품 검색 API 핸들러
|
|
13
26
|
* GET /api/oliveyoung/products?keyword={검색어}
|
|
@@ -18,6 +31,8 @@ export async function handleOliveyoungSearchProducts(c) {
|
|
|
18
31
|
const size = parseInt(c.req.query('size') || '20');
|
|
19
32
|
const sort = c.req.query('sort') || '01';
|
|
20
33
|
const includeSoldOut = c.req.query('includeSoldOut') === 'true';
|
|
34
|
+
const parsedTimeoutMs = parseInt(c.req.query('timeoutMs') || '15000', 10);
|
|
35
|
+
const timeoutMs = Number.isFinite(parsedTimeoutMs) && parsedTimeoutMs > 0 ? parsedTimeoutMs : 15000;
|
|
21
36
|
if (keyword.trim().length === 0) {
|
|
22
37
|
return errorResponse(c, 'MISSING_QUERY', '검색어(keyword)를 입력해주세요.');
|
|
23
38
|
}
|
|
@@ -30,6 +45,7 @@ export async function handleOliveyoungSearchProducts(c) {
|
|
|
30
45
|
includeSoldOut,
|
|
31
46
|
}, {
|
|
32
47
|
apiKey: c.env?.ZYTE_API_KEY,
|
|
48
|
+
timeout: timeoutMs,
|
|
33
49
|
});
|
|
34
50
|
return successResponse(c, {
|
|
35
51
|
keyword,
|
|
@@ -54,6 +70,8 @@ export async function handleOliveyoungFindStores(c) {
|
|
|
54
70
|
const lng = parseFloat(c.req.query('lng') || '126.978');
|
|
55
71
|
const pageIdx = parseInt(c.req.query('pageIdx') || '1');
|
|
56
72
|
const limit = parseInt(c.req.query('limit') || '20');
|
|
73
|
+
const parsedTimeoutMs = parseInt(c.req.query('timeoutMs') || '15000', 10);
|
|
74
|
+
const timeoutMs = Number.isFinite(parsedTimeoutMs) && parsedTimeoutMs > 0 ? parsedTimeoutMs : 15000;
|
|
57
75
|
try {
|
|
58
76
|
const result = await fetchOliveyoungStores({
|
|
59
77
|
latitude: lat,
|
|
@@ -62,6 +80,7 @@ export async function handleOliveyoungFindStores(c) {
|
|
|
62
80
|
searchWords: keyword,
|
|
63
81
|
}, {
|
|
64
82
|
apiKey: c.env?.ZYTE_API_KEY,
|
|
83
|
+
timeout: timeoutMs,
|
|
65
84
|
});
|
|
66
85
|
return successResponse(c, {
|
|
67
86
|
stores: result.stores.slice(0, limit),
|
|
@@ -86,6 +105,10 @@ export async function handleOliveyoungCheckInventory(c) {
|
|
|
86
105
|
const sort = c.req.query('sort') || '01';
|
|
87
106
|
const includeSoldOut = c.req.query('includeSoldOut') === 'true';
|
|
88
107
|
const storeLimit = parseInt(c.req.query('storeLimit') || '10');
|
|
108
|
+
const parsedTimeoutMs = parseInt(c.req.query('timeoutMs') || '15000', 10);
|
|
109
|
+
const timeoutMs = Number.isFinite(parsedTimeoutMs) && parsedTimeoutMs > 0 ? parsedTimeoutMs : 15000;
|
|
110
|
+
const parsedStockCheckLimit = parseInt(c.req.query('stockCheckLimit') || '5', 10);
|
|
111
|
+
const stockCheckLimit = Number.isFinite(parsedStockCheckLimit) && parsedStockCheckLimit >= 0 ? parsedStockCheckLimit : 5;
|
|
89
112
|
if (!keyword || keyword.trim().length === 0) {
|
|
90
113
|
return errorResponse(c, 'MISSING_QUERY', '검색어(keyword)를 입력해주세요.');
|
|
91
114
|
}
|
|
@@ -98,6 +121,7 @@ export async function handleOliveyoungCheckInventory(c) {
|
|
|
98
121
|
searchWords: storeKeyword,
|
|
99
122
|
}, {
|
|
100
123
|
apiKey: c.env.ZYTE_API_KEY,
|
|
124
|
+
timeout: timeoutMs,
|
|
101
125
|
}),
|
|
102
126
|
fetchOliveyoungProducts({
|
|
103
127
|
keyword,
|
|
@@ -107,15 +131,17 @@ export async function handleOliveyoungCheckInventory(c) {
|
|
|
107
131
|
includeSoldOut,
|
|
108
132
|
}, {
|
|
109
133
|
apiKey: c.env.ZYTE_API_KEY,
|
|
134
|
+
timeout: timeoutMs,
|
|
110
135
|
}),
|
|
111
136
|
]);
|
|
112
137
|
const enrichedInventory = await enrichOliveyoungProductsWithNearbyStoreInventory(productResult.products, {
|
|
113
138
|
latitude: lat,
|
|
114
139
|
longitude: lng,
|
|
115
140
|
storeKeyword,
|
|
116
|
-
maxProducts: Math.min(productResult.products.length,
|
|
141
|
+
maxProducts: Math.min(productResult.products.length, stockCheckLimit),
|
|
117
142
|
}, {
|
|
118
143
|
apiKey: c.env.ZYTE_API_KEY,
|
|
144
|
+
timeout: timeoutMs,
|
|
119
145
|
});
|
|
120
146
|
const inStockCount = enrichedInventory.products.filter((product) => product.inStock).length;
|
|
121
147
|
return successResponse(c, {
|
|
@@ -191,6 +217,7 @@ export async function handleCuCheckInventory(c) {
|
|
|
191
217
|
const offset = parseInt(c.req.query('offset') || '0');
|
|
192
218
|
const searchSort = c.req.query('searchSort') || 'recom';
|
|
193
219
|
const storeLimit = parseInt(c.req.query('storeLimit') || '10');
|
|
220
|
+
const storeCheck = parseBooleanQuery(c.req.query('storeCheck'), true);
|
|
194
221
|
if (!keyword || keyword.trim().length === 0) {
|
|
195
222
|
return errorResponse(c, 'MISSING_QUERY', '검색어(keyword)를 입력해주세요.');
|
|
196
223
|
}
|
|
@@ -207,9 +234,11 @@ export async function handleCuCheckInventory(c) {
|
|
|
207
234
|
const hasInputLocation = typeof lat === 'number' && typeof lng === 'number';
|
|
208
235
|
const resolvedLat = hasInputLocation ? lat : undefined;
|
|
209
236
|
const resolvedLng = hasInputLocation ? lng : undefined;
|
|
210
|
-
let storeResult =
|
|
237
|
+
let storeResult = storeCheck
|
|
238
|
+
? null
|
|
239
|
+
: { totalCount: 0, stores: [] };
|
|
211
240
|
// 좌표 미입력 + 매장 키워드 입력 시, 키워드 기반 매장 검색 결과를 우선 사용합니다.
|
|
212
|
-
if (!hasInputLocation && storeKeyword.trim().length > 0) {
|
|
241
|
+
if (storeCheck && !hasInputLocation && storeKeyword.trim().length > 0) {
|
|
213
242
|
const keywordStoreResult = await fetchCuStores({
|
|
214
243
|
searchWord: storeKeyword,
|
|
215
244
|
}, {
|