daiso 1.0.4 → 1.0.5
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 +218 -540
- package/dist/api/actionsProxy.js +82 -0
- package/dist/api/cgvHandlers.js +103 -16
- package/dist/api/daisoHandlers.js +127 -0
- package/dist/api/emart24Handlers.js +176 -0
- package/dist/api/gs25Handlers.js +256 -0
- package/dist/api/handlers.js +184 -108
- package/dist/api/healthChecks.js +332 -0
- package/dist/api/lottecinemaHandlers.js +155 -0
- package/dist/api/lottemartHandlers.js +128 -0
- package/dist/api/megaboxHandlers.js +81 -50
- package/dist/api/routes/cuRoutes.js +17 -0
- package/dist/api/routes/daisoRoutes.js +6 -1
- package/dist/api/routes/emart24Routes.js +22 -0
- package/dist/api/routes/gs25Routes.js +22 -0
- package/dist/api/routes/healthRoutes.js +44 -0
- package/dist/api/routes/lottecinemaRoutes.js +22 -0
- package/dist/api/routes/lottemartRoutes.js +18 -0
- package/dist/api/routes/oliveyoungRoutes.js +6 -1
- package/dist/api/routes/sevenelevenRoutes.js +32 -0
- package/dist/api/sevenelevenHandlers.js +200 -0
- package/dist/cli/args.js +54 -0
- package/dist/cli/commands/convenience.js +241 -0
- package/dist/cli/commands/daiso.js +96 -0
- package/dist/cli/constants.js +5 -0
- package/dist/cli/deps.js +48 -0
- package/dist/cli/http.js +98 -0
- package/dist/cli/interactiveFetch.js +31 -0
- package/dist/cli/interactiveItemSearch.js +259 -0
- package/dist/cli/interactivePrompt.js +49 -0
- package/dist/cli/interactiveTypes.js +4 -0
- package/dist/cli/types.js +4 -0
- package/dist/cli.js +66 -197
- package/dist/cliHelp.js +206 -1
- package/dist/cliInteractive.js +174 -0
- package/dist/cliPicker.js +56 -0
- package/dist/cliRenderer.js +4 -0
- package/dist/index.js +147 -8
- package/dist/pages/openapi.js +2 -1
- package/dist/pages/openapiFullSpec.js +58 -0
- package/dist/pages/openapiResponse.js +28 -0
- package/dist/pages/openapiSpec.js +2 -41
- package/dist/pages/openapiSpecActions.js +109 -0
- package/dist/pages/openapiSpecComponents.js +30 -391
- package/dist/pages/openapiSpecComponentsCgv.js +24 -0
- package/dist/pages/openapiSpecComponentsCu.js +112 -0
- package/dist/pages/openapiSpecComponentsDaisoOliveyoung.js +342 -0
- package/dist/pages/openapiSpecComponentsEmart24.js +141 -0
- package/dist/pages/openapiSpecComponentsGs25.js +109 -0
- package/dist/pages/openapiSpecComponentsLotteMart.js +85 -0
- package/dist/pages/openapiSpecComponentsLottecinema.js +163 -0
- package/dist/pages/openapiSpecComponentsMegabox.js +144 -0
- package/dist/pages/openapiSpecComponentsSeveneleven.js +82 -0
- package/dist/pages/openapiSpecPathsCgv.js +66 -3
- package/dist/pages/openapiSpecPathsCu.js +138 -0
- package/dist/pages/openapiSpecPathsDaiso.js +302 -0
- package/dist/pages/openapiSpecPathsDaisoOliveyoung.js +4 -376
- package/dist/pages/openapiSpecPathsEmart24.js +223 -0
- package/dist/pages/openapiSpecPathsGs25.js +166 -0
- package/dist/pages/openapiSpecPathsLotteMart.js +135 -0
- package/dist/pages/openapiSpecPathsLottecinema.js +212 -0
- package/dist/pages/openapiSpecPathsMegabox.js +52 -3
- package/dist/pages/openapiSpecPathsOliveyoung.js +231 -0
- package/dist/pages/openapiSpecPathsSeveneleven.js +234 -0
- package/dist/pages/prompt.js +8 -402
- package/dist/pages/promptCoreText.js +278 -0
- package/dist/pages/promptReferenceText.js +131 -0
- package/dist/pages/promptRetailCinemaText.js +273 -0
- package/dist/services/cgv/client.js +6 -1
- package/dist/services/cgv/index.js +3 -3
- package/dist/services/cgv/location.js +235 -0
- package/dist/services/cgv/tools/findTheaters.js +30 -5
- package/dist/services/cgv/tools/getTimetable.js +42 -14
- package/dist/services/cgv/tools/searchMovies.js +40 -12
- package/dist/services/cgv/transport.js +0 -3
- package/dist/services/cu/api.js +11 -0
- package/dist/services/cu/client.js +297 -0
- package/dist/services/cu/index.js +21 -0
- package/dist/services/cu/tools/checkInventory.js +123 -0
- package/dist/services/cu/tools/findNearbyStores.js +49 -0
- package/dist/services/cu/types.js +4 -0
- package/dist/services/daiso/api.js +22 -2
- package/dist/services/daiso/client.js +59 -0
- package/dist/services/daiso/index.js +2 -0
- package/dist/services/daiso/product.js +17 -0
- package/dist/services/daiso/tools/checkInventory.js +76 -41
- package/dist/services/daiso/tools/findStores.js +17 -7
- package/dist/services/daiso/tools/getDisplayLocation.js +75 -0
- package/dist/services/emart24/api.js +11 -0
- package/dist/services/emart24/client.js +173 -0
- package/dist/services/emart24/index.js +22 -0
- package/dist/services/emart24/inventoryLookup.js +134 -0
- package/dist/services/emart24/tools/checkInventory.js +54 -0
- package/dist/services/emart24/tools/findNearbyStores.js +72 -0
- package/dist/services/emart24/tools/searchProducts.js +63 -0
- package/dist/services/emart24/types.js +4 -0
- package/dist/services/gs25/api.js +9 -0
- package/dist/services/gs25/client.js +198 -0
- package/dist/services/gs25/index.js +22 -0
- package/dist/services/gs25/storeUtils.js +168 -0
- package/dist/services/gs25/tools/checkInventory.js +163 -0
- package/dist/services/gs25/tools/findNearbyStores.js +103 -0
- package/dist/services/gs25/tools/searchProducts.js +50 -0
- package/dist/services/gs25/types.js +4 -0
- package/dist/services/lottecinema/api.js +12 -0
- package/dist/services/lottecinema/client.js +161 -0
- package/dist/services/lottecinema/index.js +30 -0
- package/dist/services/lottecinema/location.js +191 -0
- package/dist/services/lottecinema/tools/findNearbyTheaters.js +47 -0
- package/dist/services/lottecinema/tools/getRemainingSeats.js +85 -0
- package/dist/services/lottecinema/tools/listNowShowing.js +79 -0
- package/dist/services/lottecinema/types.js +4 -0
- package/dist/services/lottemart/api.js +31 -0
- package/dist/services/lottemart/client.js +245 -0
- package/dist/services/lottemart/clientTypes.js +4 -0
- package/dist/services/lottemart/config.js +4 -0
- package/dist/services/lottemart/debug.js +92 -0
- package/dist/services/lottemart/geocode.js +49 -0
- package/dist/services/lottemart/index.js +30 -0
- package/dist/services/lottemart/keyword.js +29 -0
- package/dist/services/lottemart/parser.js +185 -0
- package/dist/services/lottemart/session.js +331 -0
- package/dist/services/lottemart/storeSearch.js +52 -0
- package/dist/services/lottemart/tools/findNearbyStores.js +67 -0
- package/dist/services/lottemart/tools/searchProducts.js +62 -0
- package/dist/services/lottemart/types.js +4 -0
- package/dist/services/lottemart/zetta.js +105 -0
- package/dist/services/megabox/location.js +219 -0
- package/dist/services/megabox/tools/findNearbyTheaters.js +13 -44
- package/dist/services/megabox/tools/getRemainingSeats.js +31 -3
- package/dist/services/megabox/tools/listNowShowing.js +31 -3
- package/dist/services/oliveyoung/api.js +2 -0
- package/dist/services/oliveyoung/client.js +150 -9
- package/dist/services/oliveyoung/index.js +3 -1
- package/dist/services/oliveyoung/tools/checkInventory.js +15 -4
- package/dist/services/oliveyoung/tools/searchProducts.js +57 -0
- package/dist/services/seveneleven/api.js +15 -0
- package/dist/services/seveneleven/client.js +279 -0
- package/dist/services/seveneleven/index.js +30 -0
- package/dist/services/seveneleven/inventory.js +302 -0
- package/dist/services/seveneleven/productKeyword.js +141 -0
- package/dist/services/seveneleven/tools/checkInventory.js +82 -0
- package/dist/services/seveneleven/tools/getCatalogSnapshot.js +55 -0
- package/dist/services/seveneleven/tools/getSearchPopwords.js +41 -0
- package/dist/services/seveneleven/tools/searchProducts.js +56 -0
- package/dist/services/seveneleven/tools/searchStores.js +45 -0
- package/dist/services/seveneleven/types.js +4 -0
- package/dist/utils/cliInteractiveHelpers.js +63 -0
- package/dist/utils/daisoKeyword.js +30 -0
- package/dist/utils/http.js +16 -2
- package/openapi.json +1977 -734
- package/openapi.yaml +1430 -543
- package/package.json +28 -15
- package/dist/tools/checkInventory.js +0 -98
- package/dist/tools/findStores.js +0 -125
- package/dist/tools/getPriceInfo.js +0 -77
- package/dist/tools/searchProducts.js +0 -61
- package/dist/types/index.js +0 -4
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GS25 재고 확인 도구
|
|
3
|
+
*/
|
|
4
|
+
/* c8 ignore start */
|
|
5
|
+
import * as z from 'zod';
|
|
6
|
+
import { attachDistanceToGs25Stores, fetchGs25SearchProducts, fetchGs25Stores, filterGs25StoresByKeyword, geocodeGs25Address, selectGs25StoresForKeyword, sortGs25Stores, } from '../client.js';
|
|
7
|
+
async function checkInventory(args) {
|
|
8
|
+
const { keyword, itemCode: providedItemCode, latitude, longitude, storeKeyword = '', serviceCode = '01', storeLimit = 20, timeoutMs = 20000, } = args;
|
|
9
|
+
if (!keyword || keyword.trim().length === 0) {
|
|
10
|
+
throw new Error('상품 검색어(keyword)를 입력해주세요.');
|
|
11
|
+
}
|
|
12
|
+
let resolvedLatitude = latitude;
|
|
13
|
+
let resolvedLongitude = longitude;
|
|
14
|
+
let geocodeUsed = false;
|
|
15
|
+
let itemCodeUsed = false;
|
|
16
|
+
let resolvedItemCode = null;
|
|
17
|
+
// 좌표가 없고 storeKeyword가 있으면 지오코딩 시도
|
|
18
|
+
if ((typeof resolvedLatitude !== 'number' || typeof resolvedLongitude !== 'number') &&
|
|
19
|
+
storeKeyword.trim().length > 0) {
|
|
20
|
+
const baseStores = await fetchGs25Stores({
|
|
21
|
+
serviceCode,
|
|
22
|
+
}, {
|
|
23
|
+
timeout: timeoutMs,
|
|
24
|
+
});
|
|
25
|
+
const firstAddress = filterGs25StoresByKeyword(baseStores.stores, storeKeyword).find((store) => store.address.trim().length > 0)
|
|
26
|
+
?.address || '';
|
|
27
|
+
if (firstAddress.length > 0) {
|
|
28
|
+
const geocoded = await geocodeGs25Address(firstAddress, {
|
|
29
|
+
timeout: timeoutMs,
|
|
30
|
+
googleMapsApiKey: process.env.GOOGLE_MAPS_API_KEY,
|
|
31
|
+
});
|
|
32
|
+
if (geocoded) {
|
|
33
|
+
resolvedLatitude = geocoded.latitude;
|
|
34
|
+
resolvedLongitude = geocoded.longitude;
|
|
35
|
+
geocodeUsed = true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// 좌표가 없으면 기본 좌표 (서울 강남) 사용
|
|
40
|
+
// itemCode 기반 재고 조회는 좌표가 필수
|
|
41
|
+
if (typeof resolvedLatitude !== 'number' || typeof resolvedLongitude !== 'number') {
|
|
42
|
+
resolvedLatitude = 37.4979;
|
|
43
|
+
resolvedLongitude = 127.0276;
|
|
44
|
+
}
|
|
45
|
+
let stockResult;
|
|
46
|
+
let firstProduct;
|
|
47
|
+
// itemCode가 직접 제공된 경우 totalSearch 단계 건너뛰기
|
|
48
|
+
if (providedItemCode && providedItemCode.trim().length > 0) {
|
|
49
|
+
resolvedItemCode = providedItemCode.trim();
|
|
50
|
+
itemCodeUsed = true;
|
|
51
|
+
stockResult = await fetchGs25Stores({
|
|
52
|
+
serviceCode,
|
|
53
|
+
itemCode: resolvedItemCode,
|
|
54
|
+
realTimeStockYn: 'Y',
|
|
55
|
+
latitude: resolvedLatitude,
|
|
56
|
+
longitude: resolvedLongitude,
|
|
57
|
+
useCache: false,
|
|
58
|
+
}, {
|
|
59
|
+
timeout: timeoutMs,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// itemCode가 없으면 totalSearch API로 키워드 → itemCode 변환
|
|
64
|
+
const searchProducts = await fetchGs25SearchProducts(keyword, { timeout: timeoutMs });
|
|
65
|
+
firstProduct = searchProducts.find((p) => p.itemCode.length > 0);
|
|
66
|
+
if (firstProduct) {
|
|
67
|
+
resolvedItemCode = firstProduct.itemCode;
|
|
68
|
+
itemCodeUsed = true;
|
|
69
|
+
stockResult = await fetchGs25Stores({
|
|
70
|
+
serviceCode,
|
|
71
|
+
itemCode: resolvedItemCode,
|
|
72
|
+
realTimeStockYn: 'Y',
|
|
73
|
+
latitude: resolvedLatitude,
|
|
74
|
+
longitude: resolvedLongitude,
|
|
75
|
+
useCache: false,
|
|
76
|
+
}, {
|
|
77
|
+
timeout: timeoutMs,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// itemCode가 없으면 기존 keyword 방식 fallback
|
|
82
|
+
stockResult = await fetchGs25Stores({
|
|
83
|
+
serviceCode,
|
|
84
|
+
keyword,
|
|
85
|
+
realTimeStockYn: 'Y',
|
|
86
|
+
latitude: resolvedLatitude,
|
|
87
|
+
longitude: resolvedLongitude,
|
|
88
|
+
useCache: false,
|
|
89
|
+
}, {
|
|
90
|
+
timeout: timeoutMs,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const selectedByStoreKeyword = selectGs25StoresForKeyword(stockResult.stores, storeKeyword, {
|
|
95
|
+
relaxWhenEmpty: typeof resolvedLatitude === 'number' && typeof resolvedLongitude === 'number',
|
|
96
|
+
});
|
|
97
|
+
const filteredByStoreKeyword = selectedByStoreKeyword.stores;
|
|
98
|
+
const withDistance = attachDistanceToGs25Stores(filteredByStoreKeyword, resolvedLatitude, resolvedLongitude);
|
|
99
|
+
const stores = sortGs25Stores(withDistance).slice(0, storeLimit);
|
|
100
|
+
const totalInStockCount = filteredByStoreKeyword.filter((item) => item.realStockQuantity > 0).length;
|
|
101
|
+
const totalStockQuantity = filteredByStoreKeyword.reduce((sum, item) => sum + Math.max(item.realStockQuantity, 0), 0);
|
|
102
|
+
// searchProducts에서 상품 정보 사용
|
|
103
|
+
const firstProductName = firstProduct?.itemName || firstProduct?.shortItemName || null;
|
|
104
|
+
const firstProductPrice = filteredByStoreKeyword.find((item) => item.searchItemSellPrice !== null)?.searchItemSellPrice ?? null;
|
|
105
|
+
return {
|
|
106
|
+
content: [
|
|
107
|
+
{
|
|
108
|
+
type: 'text',
|
|
109
|
+
text: JSON.stringify({
|
|
110
|
+
serviceCode,
|
|
111
|
+
keyword,
|
|
112
|
+
itemCodeUsed,
|
|
113
|
+
itemCode: resolvedItemCode,
|
|
114
|
+
storeKeyword,
|
|
115
|
+
geocodeUsed,
|
|
116
|
+
filterRelaxed: selectedByStoreKeyword.filterRelaxed,
|
|
117
|
+
location: {
|
|
118
|
+
latitude: resolvedLatitude,
|
|
119
|
+
longitude: resolvedLongitude,
|
|
120
|
+
},
|
|
121
|
+
product: {
|
|
122
|
+
name: firstProductName,
|
|
123
|
+
sellPrice: firstProductPrice,
|
|
124
|
+
imageUrl: firstProduct?.imageUrl || null,
|
|
125
|
+
rating: firstProduct?.rating || null,
|
|
126
|
+
},
|
|
127
|
+
inventory: {
|
|
128
|
+
totalStoreCount: stockResult.totalCount,
|
|
129
|
+
matchedStoreCount: filteredByStoreKeyword.length,
|
|
130
|
+
inStockStoreCount: totalInStockCount,
|
|
131
|
+
totalStockQuantity,
|
|
132
|
+
count: stores.length,
|
|
133
|
+
stores,
|
|
134
|
+
},
|
|
135
|
+
}, null, 2),
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export function createCheckInventoryTool() {
|
|
141
|
+
return {
|
|
142
|
+
name: 'gs25_check_inventory',
|
|
143
|
+
metadata: {
|
|
144
|
+
title: 'GS25 재고 확인',
|
|
145
|
+
description: '상품 키워드 기준으로 GS25 매장별 재고를 조회합니다.',
|
|
146
|
+
inputSchema: {
|
|
147
|
+
keyword: z.string().describe('상품 검색어 (예: 오감자, 삼각김밥, 커피)'),
|
|
148
|
+
itemCode: z
|
|
149
|
+
.string()
|
|
150
|
+
.optional()
|
|
151
|
+
.describe('상품 코드 (gs25_search_products로 조회한 정확한 코드, 제공 시 keyword 검색 건너뜀)'),
|
|
152
|
+
latitude: z.number().optional().describe('위도 (선택, 미입력 시 storeKeyword 지오코딩 시도)'),
|
|
153
|
+
longitude: z.number().optional().describe('경도 (선택, 미입력 시 storeKeyword 지오코딩 시도)'),
|
|
154
|
+
storeKeyword: z.string().optional().describe('매장명/주소 키워드 필터 (예: 강남, 안산 중앙역)'),
|
|
155
|
+
serviceCode: z.string().optional().default('01').describe('서비스 코드 (기본값: 01=GS25)'),
|
|
156
|
+
storeLimit: z.number().optional().default(20).describe('반환할 매장 수 (기본값: 20)'),
|
|
157
|
+
timeoutMs: z.number().optional().default(20000).describe('요청 제한 시간(ms, 기본값: 20000)'),
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
handler: checkInventory,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/* c8 ignore stop */
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GS25 주변 매장 탐색 도구
|
|
3
|
+
*/
|
|
4
|
+
import * as z from 'zod';
|
|
5
|
+
import { attachDistanceToGs25Stores, fetchGs25Stores, geocodeGs25Address, selectGs25StoresForKeyword, sortGs25Stores, } from '../client.js';
|
|
6
|
+
const FALLBACK_STORE_LOOKUP_ITEM_CODE = '8801117752804';
|
|
7
|
+
async function findNearbyStores(args) {
|
|
8
|
+
const { latitude, longitude, keyword = '', serviceCode = '01', limit = 20, timeoutMs = 20000, } = args;
|
|
9
|
+
let resolvedLatitude = latitude;
|
|
10
|
+
let resolvedLongitude = longitude;
|
|
11
|
+
let geocodeUsed = false;
|
|
12
|
+
if ((typeof resolvedLatitude !== 'number' || typeof resolvedLongitude !== 'number') &&
|
|
13
|
+
keyword.trim().length > 0) {
|
|
14
|
+
const geocoded = await geocodeGs25Address(keyword, {
|
|
15
|
+
timeout: timeoutMs,
|
|
16
|
+
googleMapsApiKey: process.env.GOOGLE_MAPS_API_KEY,
|
|
17
|
+
});
|
|
18
|
+
if (geocoded) {
|
|
19
|
+
resolvedLatitude = geocoded.latitude;
|
|
20
|
+
resolvedLongitude = geocoded.longitude;
|
|
21
|
+
geocodeUsed = true;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
let result = await fetchGs25Stores({
|
|
25
|
+
serviceCode,
|
|
26
|
+
latitude: resolvedLatitude,
|
|
27
|
+
longitude: resolvedLongitude,
|
|
28
|
+
}, {
|
|
29
|
+
timeout: timeoutMs,
|
|
30
|
+
});
|
|
31
|
+
let fallbackUsed = false;
|
|
32
|
+
if (result.stores.length === 0 &&
|
|
33
|
+
typeof resolvedLatitude === 'number' &&
|
|
34
|
+
typeof resolvedLongitude === 'number') {
|
|
35
|
+
try {
|
|
36
|
+
const fallbackResult = await fetchGs25Stores({
|
|
37
|
+
serviceCode,
|
|
38
|
+
itemCode: FALLBACK_STORE_LOOKUP_ITEM_CODE,
|
|
39
|
+
realTimeStockYn: 'Y',
|
|
40
|
+
latitude: resolvedLatitude,
|
|
41
|
+
longitude: resolvedLongitude,
|
|
42
|
+
useCache: false,
|
|
43
|
+
}, {
|
|
44
|
+
timeout: timeoutMs,
|
|
45
|
+
});
|
|
46
|
+
if (fallbackResult.stores.length > 0) {
|
|
47
|
+
result = fallbackResult;
|
|
48
|
+
fallbackUsed = true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
fallbackUsed = false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const selected = selectGs25StoresForKeyword(result.stores, keyword, {
|
|
56
|
+
relaxWhenEmpty: typeof resolvedLatitude === 'number' && typeof resolvedLongitude === 'number',
|
|
57
|
+
});
|
|
58
|
+
const withDistance = attachDistanceToGs25Stores(selected.stores, resolvedLatitude, resolvedLongitude);
|
|
59
|
+
const stores = sortGs25Stores(withDistance).slice(0, limit);
|
|
60
|
+
return {
|
|
61
|
+
content: [
|
|
62
|
+
{
|
|
63
|
+
type: 'text',
|
|
64
|
+
text: JSON.stringify({
|
|
65
|
+
serviceCode,
|
|
66
|
+
keyword,
|
|
67
|
+
geocodeUsed,
|
|
68
|
+
location: typeof resolvedLatitude === 'number' && typeof resolvedLongitude === 'number'
|
|
69
|
+
? {
|
|
70
|
+
latitude: resolvedLatitude,
|
|
71
|
+
longitude: resolvedLongitude,
|
|
72
|
+
}
|
|
73
|
+
: null,
|
|
74
|
+
cacheHit: result.cacheHit,
|
|
75
|
+
totalCount: result.totalCount,
|
|
76
|
+
filteredCount: selected.stores.length,
|
|
77
|
+
filterRelaxed: selected.filterRelaxed,
|
|
78
|
+
fallbackUsed,
|
|
79
|
+
count: stores.length,
|
|
80
|
+
stores,
|
|
81
|
+
}, null, 2),
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export function createFindNearbyStoresTool() {
|
|
87
|
+
return {
|
|
88
|
+
name: 'gs25_find_nearby_stores',
|
|
89
|
+
metadata: {
|
|
90
|
+
title: 'GS25 주변 매장 탐색',
|
|
91
|
+
description: '좌표/키워드로 GS25 매장을 조회하고 거리순으로 정렬합니다.',
|
|
92
|
+
inputSchema: {
|
|
93
|
+
latitude: z.number().optional().describe('위도 (선택, 미입력 시 keyword 지오코딩 시도)'),
|
|
94
|
+
longitude: z.number().optional().describe('경도 (선택, 미입력 시 keyword 지오코딩 시도)'),
|
|
95
|
+
keyword: z.string().optional().describe('매장명/주소 키워드 (예: 강남, 안산 중앙역)'),
|
|
96
|
+
serviceCode: z.string().optional().default('01').describe('서비스 코드 (기본값: 01=GS25)'),
|
|
97
|
+
limit: z.number().optional().default(20).describe('반환할 최대 매장 수 (기본값: 20)'),
|
|
98
|
+
timeoutMs: z.number().optional().default(20000).describe('요청 제한 시간(ms, 기본값: 20000)'),
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
handler: findNearbyStores,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GS25 상품 키워드 검색 도구
|
|
3
|
+
*/
|
|
4
|
+
import * as z from 'zod';
|
|
5
|
+
import { fetchGs25SearchProducts } from '../client.js';
|
|
6
|
+
async function searchProducts(args) {
|
|
7
|
+
const { keyword, limit = 20, timeoutMs = 20000 } = args;
|
|
8
|
+
if (!keyword || keyword.trim().length === 0) {
|
|
9
|
+
throw new Error('상품 검색어(keyword)를 입력해주세요.');
|
|
10
|
+
}
|
|
11
|
+
const products = await fetchGs25SearchProducts(keyword, { timeout: timeoutMs });
|
|
12
|
+
const limitedProducts = products.slice(0, limit);
|
|
13
|
+
return {
|
|
14
|
+
content: [
|
|
15
|
+
{
|
|
16
|
+
type: 'text',
|
|
17
|
+
text: JSON.stringify({
|
|
18
|
+
keyword,
|
|
19
|
+
count: limitedProducts.length,
|
|
20
|
+
products: limitedProducts.map((p) => ({
|
|
21
|
+
itemCode: p.itemCode,
|
|
22
|
+
itemName: p.itemName,
|
|
23
|
+
shortItemName: p.shortItemName,
|
|
24
|
+
imageUrl: p.imageUrl,
|
|
25
|
+
rating: p.rating,
|
|
26
|
+
stockCheckEnabled: p.stockCheckEnabled,
|
|
27
|
+
})),
|
|
28
|
+
note: limitedProducts.length === 0
|
|
29
|
+
? '검색 결과가 없습니다. 다른 키워드로 검색해보세요.'
|
|
30
|
+
: '재고 확인 시 itemCode를 gs25_check_inventory에 전달하면 정확한 상품 재고를 조회할 수 있습니다.',
|
|
31
|
+
}, null, 2),
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function createSearchProductsTool() {
|
|
37
|
+
return {
|
|
38
|
+
name: 'gs25_search_products',
|
|
39
|
+
metadata: {
|
|
40
|
+
title: 'GS25 상품 키워드 검색',
|
|
41
|
+
description: '키워드로 GS25 상품을 검색하여 itemCode를 조회합니다. 재고 확인 전에 먼저 이 도구로 정확한 상품을 찾으세요.',
|
|
42
|
+
inputSchema: {
|
|
43
|
+
keyword: z.string().describe('상품 검색어 (예: 핫식스, 삼각김밥, 커피)'),
|
|
44
|
+
limit: z.number().optional().default(20).describe('반환할 최대 상품 수 (기본값: 20)'),
|
|
45
|
+
timeoutMs: z.number().optional().default(20000).describe('요청 제한 시간(ms, 기본값: 20000)'),
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
handler: searchProducts,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 롯데시네마 API 엔드포인트 및 메서드 상수
|
|
3
|
+
*/
|
|
4
|
+
export const LOTTECINEMA_API = {
|
|
5
|
+
BASE_URL: 'https://www.lottecinema.co.kr',
|
|
6
|
+
TICKETING_PATH: '/LCWS/Ticketing/TicketingData.aspx',
|
|
7
|
+
MAIN_PATH: '/LCWS/Common/MainData.aspx',
|
|
8
|
+
METHODS: {
|
|
9
|
+
GET_TICKETING_PAGE: 'GetTicketingPageTOBE',
|
|
10
|
+
GET_PLAY_SEQUENCE: 'GetPlaySequence',
|
|
11
|
+
},
|
|
12
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 롯데시네마 API 클라이언트
|
|
3
|
+
*/
|
|
4
|
+
import { formatTime, toNumber, toYyyymmdd } from '../../utils/format.js';
|
|
5
|
+
import { createTimeoutController } from '../../utils/http.js';
|
|
6
|
+
import { LOTTECINEMA_API } from './api.js';
|
|
7
|
+
const DEFAULT_OS_VERSION = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36';
|
|
8
|
+
function normalizeDateForRequest(playDate) {
|
|
9
|
+
if (/^\d{8}$/.test(playDate)) {
|
|
10
|
+
return `${playDate.slice(0, 4)}-${playDate.slice(4, 6)}-${playDate.slice(6, 8)}`;
|
|
11
|
+
}
|
|
12
|
+
return playDate;
|
|
13
|
+
}
|
|
14
|
+
function normalizeDateForOutput(playDate) {
|
|
15
|
+
return playDate.replace(/-/g, '');
|
|
16
|
+
}
|
|
17
|
+
function toNullableNumber(value) {
|
|
18
|
+
if (value === null || value === undefined || value === '') {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const parsed = Number(value);
|
|
22
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
23
|
+
}
|
|
24
|
+
function buildRequestPayload(methodName, fields) {
|
|
25
|
+
return {
|
|
26
|
+
MethodName: methodName,
|
|
27
|
+
channelType: 'HO',
|
|
28
|
+
osType: 'W',
|
|
29
|
+
osVersion: DEFAULT_OS_VERSION,
|
|
30
|
+
...fields,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
async function requestLotteCinema(methodName, fields, timeout = 15000) {
|
|
34
|
+
const { controller, timeoutId } = createTimeoutController(timeout);
|
|
35
|
+
try {
|
|
36
|
+
const body = new FormData();
|
|
37
|
+
body.append('paramList', JSON.stringify(buildRequestPayload(methodName, fields)));
|
|
38
|
+
const response = await fetch(`${LOTTECINEMA_API.BASE_URL}${LOTTECINEMA_API.TICKETING_PATH}`, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: {
|
|
41
|
+
Accept: 'application/json, text/javascript, */*; q=0.01',
|
|
42
|
+
},
|
|
43
|
+
body,
|
|
44
|
+
signal: controller.signal,
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
throw new Error(`롯데시네마 API 호출 실패: ${response.status}`);
|
|
48
|
+
}
|
|
49
|
+
const parsed = (await response.json());
|
|
50
|
+
if (parsed.IsOK === false) {
|
|
51
|
+
throw new Error(`롯데시네마 API 응답 실패: ${parsed.ResultMessage || 'UNKNOWN'}`);
|
|
52
|
+
}
|
|
53
|
+
return parsed;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
57
|
+
throw new Error('롯데시네마 API 호출 시간 초과');
|
|
58
|
+
}
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
clearTimeout(timeoutId);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function buildCompositeCinemaId(theater) {
|
|
66
|
+
return `${theater.regionCode}|${theater.regionDetailCode}|${theater.theaterId}`;
|
|
67
|
+
}
|
|
68
|
+
function buildScheduleId(playDate, theaterId, screenId, playSequence) {
|
|
69
|
+
return `${playDate}-${theaterId}-${screenId}-${playSequence}`;
|
|
70
|
+
}
|
|
71
|
+
export async function fetchLotteCinemaTicketingPage(timeout = 15000) {
|
|
72
|
+
const response = await requestLotteCinema(LOTTECINEMA_API.METHODS.GET_TICKETING_PAGE, {
|
|
73
|
+
memberOnNo: '0',
|
|
74
|
+
}, timeout);
|
|
75
|
+
const theaters = Array.from(new Map((response.Cinemas?.Cinemas?.Items || [])
|
|
76
|
+
.filter((item) => item.CinemaID && item.CinemaNameKR && item.DivisionCode && item.DetailDivisionCode)
|
|
77
|
+
.map((item) => ({
|
|
78
|
+
theaterId: String(item.CinemaID),
|
|
79
|
+
theaterName: item.CinemaNameKR,
|
|
80
|
+
regionCode: String(item.DivisionCode),
|
|
81
|
+
regionDetailCode: String(item.DetailDivisionCode),
|
|
82
|
+
latitude: toNullableNumber(item.Latitude),
|
|
83
|
+
longitude: toNullableNumber(item.Longitude),
|
|
84
|
+
address: item.CinemaAddrSummary || '',
|
|
85
|
+
}))
|
|
86
|
+
.map((theater) => [theater.theaterId, theater])).values());
|
|
87
|
+
const movies = (response.Movies?.Movies?.Items || [])
|
|
88
|
+
.filter((item) => item.RepresentationMovieCode && item.MovieNameKR)
|
|
89
|
+
.map((item) => ({
|
|
90
|
+
movieId: String(item.RepresentationMovieCode),
|
|
91
|
+
movieName: item.MovieNameKR,
|
|
92
|
+
rating: item.ViewGradeNameKR || undefined,
|
|
93
|
+
durationMinutes: item.PlayTime === null || item.PlayTime === undefined ? undefined : toNumber(item.PlayTime),
|
|
94
|
+
releaseDate: item.ReleaseDate || undefined,
|
|
95
|
+
}));
|
|
96
|
+
return { theaters, movies };
|
|
97
|
+
}
|
|
98
|
+
async function fetchPlaySequenceByPair(playDate, theater, movie, timeout = 15000) {
|
|
99
|
+
const response = await requestLotteCinema(LOTTECINEMA_API.METHODS.GET_PLAY_SEQUENCE, {
|
|
100
|
+
playDate: normalizeDateForRequest(playDate),
|
|
101
|
+
cinemaID: buildCompositeCinemaId(theater),
|
|
102
|
+
representationMovieCode: movie.movieId,
|
|
103
|
+
}, timeout);
|
|
104
|
+
return (response.PlaySeqs?.Items || [])
|
|
105
|
+
.filter((item) => item.CinemaID && item.RepresentationMovieCode && item.ScreenID && item.PlaySequence)
|
|
106
|
+
.map((item) => {
|
|
107
|
+
const totalSeats = toNumber(item.TotalSeatCount);
|
|
108
|
+
const bookedSeats = toNumber(item.BookingSeatCount);
|
|
109
|
+
const normalizedPlayDate = normalizeDateForOutput(item.PlayDt || playDate);
|
|
110
|
+
const theaterId = String(item.CinemaID);
|
|
111
|
+
const screenId = String(item.ScreenID);
|
|
112
|
+
const playSequence = String(item.PlaySequence);
|
|
113
|
+
return {
|
|
114
|
+
scheduleId: buildScheduleId(normalizedPlayDate, theaterId, screenId, playSequence),
|
|
115
|
+
theaterId,
|
|
116
|
+
theaterName: item.CinemaNameKR || theater.theaterName,
|
|
117
|
+
movieId: String(item.RepresentationMovieCode),
|
|
118
|
+
movieName: item.MovieNameKR || movie.movieName,
|
|
119
|
+
screenId,
|
|
120
|
+
screenName: item.ScreenNameKR || '',
|
|
121
|
+
playDate: normalizedPlayDate,
|
|
122
|
+
startTime: formatTime(item.StartTime),
|
|
123
|
+
endTime: formatTime(item.EndTime),
|
|
124
|
+
totalSeats,
|
|
125
|
+
bookedSeats,
|
|
126
|
+
remainingSeats: Math.max(totalSeats - bookedSeats, 0),
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
export async function fetchLotteCinemaNowShowing(params) {
|
|
131
|
+
const playDate = params.playDate || toYyyymmdd();
|
|
132
|
+
const timeout = params.timeout || 15000;
|
|
133
|
+
const ticketingPage = await fetchLotteCinemaTicketingPage(timeout);
|
|
134
|
+
if (!params.theaterId && !params.movieId) {
|
|
135
|
+
return {
|
|
136
|
+
theaters: ticketingPage.theaters,
|
|
137
|
+
movies: ticketingPage.movies,
|
|
138
|
+
showtimes: [],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const theaters = params.theaterId
|
|
142
|
+
? ticketingPage.theaters.filter((theater) => theater.theaterId === params.theaterId)
|
|
143
|
+
: ticketingPage.theaters;
|
|
144
|
+
const movies = params.movieId
|
|
145
|
+
? ticketingPage.movies.filter((movie) => movie.movieId === params.movieId)
|
|
146
|
+
: ticketingPage.movies;
|
|
147
|
+
if (theaters.length === 0 || movies.length === 0) {
|
|
148
|
+
return { theaters, movies, showtimes: [] };
|
|
149
|
+
}
|
|
150
|
+
const targets = theaters.flatMap((theater) => movies.map((movie) => ({ theater, movie })));
|
|
151
|
+
const results = await Promise.all(targets.map(({ theater, movie }) => fetchPlaySequenceByPair(playDate, theater, movie, timeout)));
|
|
152
|
+
const uniqueShowtimes = Array.from(new Map(results.flat().map((item) => [item.scheduleId, item])).values());
|
|
153
|
+
const theaterIds = new Set(uniqueShowtimes.map((item) => item.theaterId));
|
|
154
|
+
const movieIds = new Set(uniqueShowtimes.map((item) => item.movieId));
|
|
155
|
+
return {
|
|
156
|
+
theaters: ticketingPage.theaters.filter((theater) => params.theaterId ? theater.theaterId === params.theaterId : theaterIds.has(theater.theaterId)),
|
|
157
|
+
movies: ticketingPage.movies.filter((movie) => params.movieId ? movie.movieId === params.movieId : movieIds.has(movie.movieId)),
|
|
158
|
+
showtimes: uniqueShowtimes,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
export { toYyyymmdd };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 롯데시네마 서비스 프로바이더
|
|
3
|
+
*/
|
|
4
|
+
import { createFindNearbyTheatersTool } from './tools/findNearbyTheaters.js';
|
|
5
|
+
import { createGetRemainingSeatsTool } from './tools/getRemainingSeats.js';
|
|
6
|
+
import { createListNowShowingTool } from './tools/listNowShowing.js';
|
|
7
|
+
const LOTTECINEMA_METADATA = {
|
|
8
|
+
id: 'lottecinema',
|
|
9
|
+
name: '롯데시네마',
|
|
10
|
+
version: '1.0.0',
|
|
11
|
+
description: '롯데시네마 주변 지점 탐색, 영화 목록 조회, 잔여 좌석 조회 서비스',
|
|
12
|
+
};
|
|
13
|
+
class LotteCinemaService {
|
|
14
|
+
options;
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.options = options;
|
|
17
|
+
}
|
|
18
|
+
metadata = LOTTECINEMA_METADATA;
|
|
19
|
+
getTools() {
|
|
20
|
+
return [
|
|
21
|
+
createFindNearbyTheatersTool(this.options.googleMapsApiKey),
|
|
22
|
+
createListNowShowingTool(this.options.googleMapsApiKey),
|
|
23
|
+
createGetRemainingSeatsTool(this.options.googleMapsApiKey),
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function createLotteCinemaService(options = {}) {
|
|
28
|
+
return new LotteCinemaService(options);
|
|
29
|
+
}
|
|
30
|
+
export * from './types.js';
|