daiso 1.0.0
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/LICENSE +21 -0
- package/README.md +741 -0
- package/dist/api/cgvHandlers.js +97 -0
- package/dist/api/handlers.js +208 -0
- package/dist/api/megaboxHandlers.js +145 -0
- package/dist/api/response.js +22 -0
- package/dist/api/routes/cgvRoutes.js +22 -0
- package/dist/api/routes/daisoRoutes.js +27 -0
- package/dist/api/routes/megaboxRoutes.js +22 -0
- package/dist/api/routes/oliveyoungRoutes.js +17 -0
- package/dist/cli.js +140 -0
- package/dist/core/interfaces.js +6 -0
- package/dist/core/registry.js +121 -0
- package/dist/core/types.js +6 -0
- package/dist/index.js +123 -0
- package/dist/pages/openapi.js +6 -0
- package/dist/pages/openapiResponse.js +32 -0
- package/dist/pages/openapiSpec.js +43 -0
- package/dist/pages/openapiSpecComponents.js +419 -0
- package/dist/pages/openapiSpecComponentsCgv.js +123 -0
- package/dist/pages/openapiSpecPathsCgv.js +149 -0
- package/dist/pages/openapiSpecPathsDaisoOliveyoung.js +381 -0
- package/dist/pages/openapiSpecPathsMegabox.js +184 -0
- package/dist/pages/openapiYaml.js +58 -0
- package/dist/pages/privacy.js +262 -0
- package/dist/pages/prompt.js +425 -0
- package/dist/services/cgv/api.js +15 -0
- package/dist/services/cgv/client.js +146 -0
- package/dist/services/cgv/index.js +30 -0
- package/dist/services/cgv/timetable.js +16 -0
- package/dist/services/cgv/tools/findTheaters.js +43 -0
- package/dist/services/cgv/tools/getTimetable.js +47 -0
- package/dist/services/cgv/tools/searchMovies.js +40 -0
- package/dist/services/cgv/transport.js +91 -0
- package/dist/services/cgv/types.js +4 -0
- package/dist/services/daiso/api.js +53 -0
- package/dist/services/daiso/client.js +35 -0
- package/dist/services/daiso/index.js +42 -0
- package/dist/services/daiso/tools/checkInventory.js +122 -0
- package/dist/services/daiso/tools/findStores.js +145 -0
- package/dist/services/daiso/tools/getPriceInfo.js +90 -0
- package/dist/services/daiso/tools/searchProducts.js +74 -0
- package/dist/services/daiso/types.js +4 -0
- package/dist/services/megabox/api.js +8 -0
- package/dist/services/megabox/client.js +140 -0
- package/dist/services/megabox/index.js +26 -0
- package/dist/services/megabox/tools/findNearbyTheaters.js +74 -0
- package/dist/services/megabox/tools/getRemainingSeats.js +57 -0
- package/dist/services/megabox/tools/listNowShowing.js +51 -0
- package/dist/services/megabox/types.js +4 -0
- package/dist/services/oliveyoung/api.js +8 -0
- package/dist/services/oliveyoung/client.js +90 -0
- package/dist/services/oliveyoung/index.js +28 -0
- package/dist/services/oliveyoung/tools/checkInventory.js +87 -0
- package/dist/services/oliveyoung/tools/findNearbyStores.js +53 -0
- package/dist/services/oliveyoung/types.js +4 -0
- package/dist/tools/checkInventory.js +98 -0
- package/dist/tools/findStores.js +125 -0
- package/dist/tools/getPriceInfo.js +77 -0
- package/dist/tools/searchProducts.js +61 -0
- package/dist/types/index.js +4 -0
- package/dist/utils/cache.js +54 -0
- package/dist/utils/fetch.js +14 -0
- package/dist/utils/format.js +31 -0
- package/dist/utils/http.js +35 -0
- package/dist/utils/zyte.js +76 -0
- package/openapi.json +1938 -0
- package/openapi.yaml +1338 -0
- package/package.json +79 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 매장 찾기 도구
|
|
3
|
+
*
|
|
4
|
+
* 다이소 API를 사용하여 매장을 검색합니다.
|
|
5
|
+
*/
|
|
6
|
+
import * as z from 'zod';
|
|
7
|
+
import { DAISO_WEB_API, formatTime } from '../api.js';
|
|
8
|
+
import { fetchDaisoHtml, fetchDaisoJson } from '../client.js';
|
|
9
|
+
/**
|
|
10
|
+
* HTML에서 매장 정보 파싱
|
|
11
|
+
*/
|
|
12
|
+
function parseStoresFromHtml(html) {
|
|
13
|
+
const stores = [];
|
|
14
|
+
// bx-store div 태그의 시작 위치 찾기
|
|
15
|
+
const divStartRegex = /<div[^>]*class="bx-store"[^>]*>/gi;
|
|
16
|
+
let divMatch;
|
|
17
|
+
while ((divMatch = divStartRegex.exec(html)) !== null) {
|
|
18
|
+
const startIdx = divMatch.index;
|
|
19
|
+
const divTag = divMatch[0];
|
|
20
|
+
// 해당 div의 끝 찾기
|
|
21
|
+
const endIdx = html.indexOf('<div class="bx-store"', startIdx + 1);
|
|
22
|
+
const blockEnd = endIdx > 0 ? endIdx : startIdx + 2000;
|
|
23
|
+
const block = html.slice(startIdx, blockEnd);
|
|
24
|
+
// data 속성 추출
|
|
25
|
+
const startMatch = /data-start="(\d+)"/.exec(divTag);
|
|
26
|
+
const endMatch = /data-end="(\d+)"/.exec(divTag);
|
|
27
|
+
const latMatch = /data-lat="([^"]+)"/.exec(divTag);
|
|
28
|
+
const lngMatch = /data-lng="([^"]+)"/.exec(divTag);
|
|
29
|
+
const infoMatch = /data-info='([^']*)'/.exec(divTag);
|
|
30
|
+
if (!startMatch || !endMatch || !latMatch || !lngMatch)
|
|
31
|
+
continue;
|
|
32
|
+
// 매장명, 전화번호, 주소 추출
|
|
33
|
+
const nameMatch = /<h4[^>]*class="place"[^>]*>([^<]+)<\/h4>/i.exec(block);
|
|
34
|
+
const phoneMatch = /<em[^>]*class="phone"[^>]*>([^<]*)<\/em>/i.exec(block);
|
|
35
|
+
const addrMatch = /<p[^>]*class="addr"[^>]*>([^<]+)<\/p>/i.exec(block);
|
|
36
|
+
if (!nameMatch)
|
|
37
|
+
continue;
|
|
38
|
+
// data-info JSON 파싱
|
|
39
|
+
let info = {};
|
|
40
|
+
if (infoMatch) {
|
|
41
|
+
try {
|
|
42
|
+
const infoStr = infoMatch[1].replace(/"/g, '"').replace(/&/g, '&');
|
|
43
|
+
info = JSON.parse(infoStr);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// JSON 파싱 실패 시 빈 객체 사용
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const options = {
|
|
50
|
+
parking: info.shp_pak === 'Y',
|
|
51
|
+
ramp: info.entrramp === 'Y',
|
|
52
|
+
elevator: info.elvtor === 'Y',
|
|
53
|
+
cashless: info.ptcard === 'Y',
|
|
54
|
+
photoSticker: info.ptstk === 'Y',
|
|
55
|
+
nameSticker: info.nmstk === 'Y',
|
|
56
|
+
simCard: info.usim_yn === 'Y',
|
|
57
|
+
taxFree: info.tax_free === 'Y',
|
|
58
|
+
groupOrder: info['ext.group_yn'] === 'Y' || info.group_yn === 'Y',
|
|
59
|
+
pickup: info.online_yn === 'Y',
|
|
60
|
+
};
|
|
61
|
+
stores.push({
|
|
62
|
+
name: nameMatch[1].trim(),
|
|
63
|
+
phone: phoneMatch ? phoneMatch[1].replace('T.', '').trim() : '',
|
|
64
|
+
address: addrMatch ? addrMatch[1].trim() : '',
|
|
65
|
+
lat: parseFloat(latMatch[1]),
|
|
66
|
+
lng: parseFloat(lngMatch[1]),
|
|
67
|
+
openTime: formatTime(startMatch[1]),
|
|
68
|
+
closeTime: formatTime(endMatch[1]),
|
|
69
|
+
options,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return stores;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 다이소 매장 검색 API 호출
|
|
76
|
+
*/
|
|
77
|
+
export async function fetchStores(keyword, sido, gugun, dong) {
|
|
78
|
+
const url = new URL(DAISO_WEB_API.SHOP_SEARCH);
|
|
79
|
+
url.searchParams.set('name_address', keyword || '');
|
|
80
|
+
url.searchParams.set('sido', sido || '');
|
|
81
|
+
url.searchParams.set('gugun', gugun || '');
|
|
82
|
+
url.searchParams.set('dong', dong || '');
|
|
83
|
+
const html = await fetchDaisoHtml(url.toString());
|
|
84
|
+
return parseStoresFromHtml(html);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 시/도별 구/군 목록 조회
|
|
88
|
+
*/
|
|
89
|
+
export async function getDistricts(sido) {
|
|
90
|
+
const url = new URL(DAISO_WEB_API.SIDO_SEARCH);
|
|
91
|
+
url.searchParams.set('sido', sido);
|
|
92
|
+
const data = await fetchDaisoJson(url.toString());
|
|
93
|
+
return data.map((item) => item.value);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 구/군별 동 목록 조회
|
|
97
|
+
*/
|
|
98
|
+
export async function getNeighborhoods(sido, gugun) {
|
|
99
|
+
const url = new URL(DAISO_WEB_API.GUGUN_SEARCH);
|
|
100
|
+
url.searchParams.set('sido', sido);
|
|
101
|
+
url.searchParams.set('gugun', gugun);
|
|
102
|
+
const data = await fetchDaisoJson(url.toString());
|
|
103
|
+
return data.map((item) => item.value);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 매장 검색 핸들러
|
|
107
|
+
*/
|
|
108
|
+
async function findStores(args) {
|
|
109
|
+
const { keyword, sido, gugun, dong, limit = 50 } = args;
|
|
110
|
+
// 최소한 하나의 검색 조건이 필요
|
|
111
|
+
if (!keyword && !sido) {
|
|
112
|
+
throw new Error('검색어(keyword) 또는 지역(sido)을 입력해주세요.');
|
|
113
|
+
}
|
|
114
|
+
const stores = await fetchStores(keyword, sido, gugun, dong);
|
|
115
|
+
const limitedStores = stores.slice(0, limit);
|
|
116
|
+
const result = {
|
|
117
|
+
searchParams: { keyword, sido, gugun, dong },
|
|
118
|
+
totalCount: stores.length,
|
|
119
|
+
count: limitedStores.length,
|
|
120
|
+
stores: limitedStores,
|
|
121
|
+
};
|
|
122
|
+
return {
|
|
123
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 도구 등록 정보 생성
|
|
128
|
+
*/
|
|
129
|
+
export function createFindStoresTool() {
|
|
130
|
+
return {
|
|
131
|
+
name: 'daiso_find_stores',
|
|
132
|
+
metadata: {
|
|
133
|
+
title: '매장 검색',
|
|
134
|
+
description: '다이소 매장을 검색합니다. 키워드(매장명, 주소) 또는 지역(시도/구군/동)으로 검색할 수 있습니다.',
|
|
135
|
+
inputSchema: {
|
|
136
|
+
keyword: z.string().optional().describe('검색할 매장명 또는 주소 키워드 (예: 강남, 홍대)'),
|
|
137
|
+
sido: z.string().optional().describe('시/도 (예: 서울, 경기, 부산)'),
|
|
138
|
+
gugun: z.string().optional().describe('구/군 (예: 강남구, 마포구)'),
|
|
139
|
+
dong: z.string().optional().describe('동 (예: 역삼동, 합정동)'),
|
|
140
|
+
limit: z.number().optional().default(50).describe('반환할 최대 매장 수 (기본값: 50)'),
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
handler: findStores,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 가격 정보 조회 도구
|
|
3
|
+
*
|
|
4
|
+
* 다이소몰 API를 사용하여 상품의 가격 정보를 조회합니다.
|
|
5
|
+
*/
|
|
6
|
+
import * as z from 'zod';
|
|
7
|
+
import { DAISOMALL_API, getImageUrl } from '../api.js';
|
|
8
|
+
import { fetchDaisoJson } from '../client.js';
|
|
9
|
+
/**
|
|
10
|
+
* 상품 ID로 검색
|
|
11
|
+
*/
|
|
12
|
+
export async function fetchProductById(productId) {
|
|
13
|
+
const url = new URL(DAISOMALL_API.SEARCH_PRODUCTS);
|
|
14
|
+
url.searchParams.set('searchTerm', productId);
|
|
15
|
+
url.searchParams.set('cntPerPage', '10');
|
|
16
|
+
url.searchParams.set('pageNum', '1');
|
|
17
|
+
const data = await fetchDaisoJson(url.toString());
|
|
18
|
+
if (!data.resultSet?.result?.[0]?.resultDocuments) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const docs = data.resultSet.result[0].resultDocuments;
|
|
22
|
+
// 정확한 ID 매칭 시도
|
|
23
|
+
const exactMatch = docs.find((doc) => doc.PD_NO === productId);
|
|
24
|
+
if (exactMatch) {
|
|
25
|
+
return exactMatch;
|
|
26
|
+
}
|
|
27
|
+
return docs.length > 0 ? docs[0] : null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 상품명으로 검색
|
|
31
|
+
*/
|
|
32
|
+
async function fetchProductByName(productName) {
|
|
33
|
+
const url = new URL(DAISOMALL_API.SEARCH_PRODUCTS);
|
|
34
|
+
url.searchParams.set('searchTerm', productName);
|
|
35
|
+
url.searchParams.set('cntPerPage', '1');
|
|
36
|
+
url.searchParams.set('pageNum', '1');
|
|
37
|
+
const data = await fetchDaisoJson(url.toString());
|
|
38
|
+
if (!data.resultSet?.result?.[0]?.resultDocuments?.length) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return data.resultSet.result[0].resultDocuments[0];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 가격 정보 조회 핸들러
|
|
45
|
+
*/
|
|
46
|
+
async function getPriceInfo(args) {
|
|
47
|
+
const { productId, productName } = args;
|
|
48
|
+
if (!productId && !productName) {
|
|
49
|
+
throw new Error('상품 ID(productId) 또는 상품명(productName)을 입력해주세요.');
|
|
50
|
+
}
|
|
51
|
+
let product = null;
|
|
52
|
+
// productId가 있으면 ID로 검색, 없으면 productName으로 검색
|
|
53
|
+
// 위에서 둘 다 없는 경우는 이미 에러 처리됨
|
|
54
|
+
product = productId
|
|
55
|
+
? await fetchProductById(productId)
|
|
56
|
+
: await fetchProductByName(productName);
|
|
57
|
+
if (!product) {
|
|
58
|
+
throw new Error(`상품을 찾을 수 없습니다: ${productId || productName}`);
|
|
59
|
+
}
|
|
60
|
+
const price = parseInt(product.PD_PRC) || 0;
|
|
61
|
+
const result = {
|
|
62
|
+
productId: product.PD_NO,
|
|
63
|
+
productName: product.PDNM || product.EXH_PD_NM,
|
|
64
|
+
currentPrice: price,
|
|
65
|
+
currency: 'KRW',
|
|
66
|
+
imageUrl: getImageUrl(product.ATCH_FILE_URL),
|
|
67
|
+
brand: product.BRND_NM || undefined,
|
|
68
|
+
soldOut: product.SOLD_OUT_YN === 'Y',
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 도구 등록 정보 생성
|
|
76
|
+
*/
|
|
77
|
+
export function createGetPriceInfoTool() {
|
|
78
|
+
return {
|
|
79
|
+
name: 'daiso_get_price_info',
|
|
80
|
+
metadata: {
|
|
81
|
+
title: '가격 정보',
|
|
82
|
+
description: '제품의 가격 정보를 조회합니다. 제품 ID 또는 제품명으로 조회할 수 있습니다.',
|
|
83
|
+
inputSchema: {
|
|
84
|
+
productId: z.string().optional().describe('제품 ID'),
|
|
85
|
+
productName: z.string().optional().describe('제품명 (productId가 없을 경우 사용)'),
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
handler: getPriceInfo,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 제품 검색 도구
|
|
3
|
+
*
|
|
4
|
+
* 다이소몰 API를 사용하여 제품을 검색합니다.
|
|
5
|
+
*/
|
|
6
|
+
import * as z from 'zod';
|
|
7
|
+
import { DAISOMALL_API, getImageUrl } from '../api.js';
|
|
8
|
+
import { fetchDaisoJson } from '../client.js';
|
|
9
|
+
/**
|
|
10
|
+
* 다이소몰 API에서 상품 검색
|
|
11
|
+
*/
|
|
12
|
+
export async function fetchProducts(query, page = 1, pageSize = 30) {
|
|
13
|
+
const url = new URL(DAISOMALL_API.SEARCH_PRODUCTS);
|
|
14
|
+
url.searchParams.set('searchTerm', query);
|
|
15
|
+
url.searchParams.set('cntPerPage', pageSize.toString());
|
|
16
|
+
url.searchParams.set('pageNum', page.toString());
|
|
17
|
+
const data = await fetchDaisoJson(url.toString());
|
|
18
|
+
// 결과가 없는 경우
|
|
19
|
+
if (!data.resultSet?.result?.[0]?.resultDocuments) {
|
|
20
|
+
return { products: [], totalCount: 0 };
|
|
21
|
+
}
|
|
22
|
+
const result = data.resultSet.result[0];
|
|
23
|
+
const totalCount = result.totalSize || 0;
|
|
24
|
+
const products = result.resultDocuments.map((doc) => ({
|
|
25
|
+
id: doc.PD_NO,
|
|
26
|
+
name: doc.PDNM || doc.EXH_PD_NM || '',
|
|
27
|
+
price: parseInt(doc.PD_PRC) || 0,
|
|
28
|
+
imageUrl: getImageUrl(doc.ATCH_FILE_URL),
|
|
29
|
+
brand: doc.BRND_NM || undefined,
|
|
30
|
+
soldOut: doc.SOLD_OUT_YN === 'Y',
|
|
31
|
+
isNew: doc.NEW_PD_YN === 'Y',
|
|
32
|
+
pickupAvailable: doc.PKUP_OR_PSBL_YN === 'Y',
|
|
33
|
+
}));
|
|
34
|
+
return { products, totalCount };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 제품 검색 핸들러
|
|
38
|
+
*/
|
|
39
|
+
async function searchProducts(args) {
|
|
40
|
+
const { query, page = 1, pageSize = 30 } = args;
|
|
41
|
+
if (!query || query.trim().length === 0) {
|
|
42
|
+
throw new Error('검색어를 입력해주세요.');
|
|
43
|
+
}
|
|
44
|
+
const { products, totalCount } = await fetchProducts(query, page, pageSize);
|
|
45
|
+
const result = {
|
|
46
|
+
query,
|
|
47
|
+
page,
|
|
48
|
+
pageSize,
|
|
49
|
+
totalCount,
|
|
50
|
+
count: products.length,
|
|
51
|
+
products,
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 도구 등록 정보 생성
|
|
59
|
+
*/
|
|
60
|
+
export function createSearchProductsTool() {
|
|
61
|
+
return {
|
|
62
|
+
name: 'daiso_search_products',
|
|
63
|
+
metadata: {
|
|
64
|
+
title: '제품 검색',
|
|
65
|
+
description: '다이소 제품을 검색합니다. 키워드로 제품을 검색할 수 있습니다.',
|
|
66
|
+
inputSchema: {
|
|
67
|
+
query: z.string().describe('검색할 제품명 또는 키워드'),
|
|
68
|
+
page: z.number().optional().default(1).describe('페이지 번호 (기본값: 1)'),
|
|
69
|
+
pageSize: z.number().optional().default(30).describe('페이지당 결과 수 (기본값: 30)'),
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
handler: searchProducts,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 메가박스 API 클라이언트
|
|
3
|
+
*/
|
|
4
|
+
import { MEGABOX_API } from './api.js';
|
|
5
|
+
import { formatTime, toNumber, toYyyymmdd } from '../../utils/format.js';
|
|
6
|
+
import { createTimeoutController } from '../../utils/http.js';
|
|
7
|
+
export async function fetchMegaboxBookingList(params) {
|
|
8
|
+
const { timeout = 15000 } = params;
|
|
9
|
+
const form = new URLSearchParams({
|
|
10
|
+
playDe: params.playDate,
|
|
11
|
+
sellChnlCd: 'ONLINE',
|
|
12
|
+
brchNoListCnt: '1',
|
|
13
|
+
areaCd1: params.areaCode || '11',
|
|
14
|
+
spclbYn1: 'N',
|
|
15
|
+
theabKindCd1: '',
|
|
16
|
+
});
|
|
17
|
+
if (params.movieId) {
|
|
18
|
+
form.set('arrMovieNo', params.movieId);
|
|
19
|
+
}
|
|
20
|
+
if (params.theaterId) {
|
|
21
|
+
form.set('brchNo1', params.theaterId);
|
|
22
|
+
}
|
|
23
|
+
const { controller, timeoutId } = createTimeoutController(timeout);
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(`${MEGABOX_API.BASE_URL}${MEGABOX_API.SELECT_BOOKING_LIST_PATH}`, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: {
|
|
28
|
+
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
29
|
+
Accept: 'application/json, text/javascript, */*; q=0.01',
|
|
30
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
31
|
+
},
|
|
32
|
+
body: form.toString(),
|
|
33
|
+
signal: controller.signal,
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
throw new Error(`메가박스 상영 목록 조회 실패: ${response.status}`);
|
|
37
|
+
}
|
|
38
|
+
const body = (await response.json());
|
|
39
|
+
const theaters = (body.areaBrchList || [])
|
|
40
|
+
.filter((item) => item.brchNo && item.brchNm)
|
|
41
|
+
.map((item) => ({
|
|
42
|
+
theaterId: item.brchNo,
|
|
43
|
+
theaterName: item.brchNm,
|
|
44
|
+
}));
|
|
45
|
+
const movies = (body.movieList || [])
|
|
46
|
+
.filter((item) => item.movieNo && item.movieNm)
|
|
47
|
+
.map((item) => ({
|
|
48
|
+
movieId: item.movieNo,
|
|
49
|
+
movieName: item.movieNm,
|
|
50
|
+
movieStatus: item.movieStatCdNm || undefined,
|
|
51
|
+
}));
|
|
52
|
+
const showtimes = (body.movieFormList || [])
|
|
53
|
+
.filter((item) => item.playSchdlNo && item.movieNo && item.brchNo)
|
|
54
|
+
.map((item) => ({
|
|
55
|
+
scheduleId: item.playSchdlNo,
|
|
56
|
+
movieId: item.movieNo,
|
|
57
|
+
movieName: item.movieNm || '',
|
|
58
|
+
theaterId: item.brchNo,
|
|
59
|
+
theaterName: item.brchNm || '',
|
|
60
|
+
playDate: item.playDe || params.playDate,
|
|
61
|
+
startTime: formatTime(item.playStartTime),
|
|
62
|
+
endTime: formatTime(item.playEndTime),
|
|
63
|
+
totalSeats: toNumber(item.totSeatCnt),
|
|
64
|
+
remainingSeats: toNumber(item.restSeatCnt),
|
|
65
|
+
}));
|
|
66
|
+
return {
|
|
67
|
+
theaters,
|
|
68
|
+
movies,
|
|
69
|
+
showtimes,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
74
|
+
throw new Error('메가박스 상영 목록 조회 시간 초과');
|
|
75
|
+
}
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
clearTimeout(timeoutId);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function parseCoordinates(html) {
|
|
83
|
+
const latMatch = html.match(/(?:lat=|mapLat\s*[:=]\s*["']?)(-?\d+\.\d+)/i);
|
|
84
|
+
const lngMatch = html.match(/(?:lng=|mapLng\s*[:=]\s*["']?)(-?\d+\.\d+)/i);
|
|
85
|
+
return {
|
|
86
|
+
latitude: latMatch ? parseFloat(latMatch[1]) : null,
|
|
87
|
+
longitude: lngMatch ? parseFloat(lngMatch[1]) : null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function parseAddress(html) {
|
|
91
|
+
const roadAddressMatch = html.match(/도로명주소<\/dt>\s*<dd>([^<]+)<\/dd>/i);
|
|
92
|
+
if (roadAddressMatch) {
|
|
93
|
+
return roadAddressMatch[1].trim();
|
|
94
|
+
}
|
|
95
|
+
const addressMatch = html.match(/주소<\/dt>\s*<dd>([^<]+)<\/dd>/i);
|
|
96
|
+
if (addressMatch) {
|
|
97
|
+
return addressMatch[1].trim();
|
|
98
|
+
}
|
|
99
|
+
return '';
|
|
100
|
+
}
|
|
101
|
+
export async function fetchMegaboxTheaterInfo(theaterId, timeout = 15000) {
|
|
102
|
+
const form = new URLSearchParams({
|
|
103
|
+
brchNo: theaterId,
|
|
104
|
+
});
|
|
105
|
+
const { controller, timeoutId } = createTimeoutController(timeout);
|
|
106
|
+
try {
|
|
107
|
+
const response = await fetch(`${MEGABOX_API.BASE_URL}${MEGABOX_API.THEATER_INFO_PATH}`, {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: {
|
|
110
|
+
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
111
|
+
Accept: 'text/html, */*; q=0.01',
|
|
112
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
113
|
+
},
|
|
114
|
+
body: form.toString(),
|
|
115
|
+
signal: controller.signal,
|
|
116
|
+
});
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
throw new Error(`메가박스 지점 정보 조회 실패: ${response.status}`);
|
|
119
|
+
}
|
|
120
|
+
const html = await response.text();
|
|
121
|
+
const coordinates = parseCoordinates(html);
|
|
122
|
+
const address = parseAddress(html);
|
|
123
|
+
return {
|
|
124
|
+
theaterId,
|
|
125
|
+
address,
|
|
126
|
+
latitude: coordinates.latitude,
|
|
127
|
+
longitude: coordinates.longitude,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
132
|
+
throw new Error('메가박스 지점 정보 조회 시간 초과');
|
|
133
|
+
}
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
clearTimeout(timeoutId);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export { toYyyymmdd };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 메가박스 서비스 프로바이더
|
|
3
|
+
*/
|
|
4
|
+
import { createFindNearbyTheatersTool } from './tools/findNearbyTheaters.js';
|
|
5
|
+
import { createListNowShowingTool } from './tools/listNowShowing.js';
|
|
6
|
+
import { createGetRemainingSeatsTool } from './tools/getRemainingSeats.js';
|
|
7
|
+
const MEGABOX_METADATA = {
|
|
8
|
+
id: 'megabox',
|
|
9
|
+
name: '메가박스',
|
|
10
|
+
version: '1.0.0',
|
|
11
|
+
description: '메가박스 주변 지점 탐색, 영화 목록 조회, 잔여 좌석 조회 서비스',
|
|
12
|
+
};
|
|
13
|
+
class MegaboxService {
|
|
14
|
+
metadata = MEGABOX_METADATA;
|
|
15
|
+
getTools() {
|
|
16
|
+
return [
|
|
17
|
+
createFindNearbyTheatersTool(),
|
|
18
|
+
createListNowShowingTool(),
|
|
19
|
+
createGetRemainingSeatsTool(),
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function createMegaboxService() {
|
|
24
|
+
return new MegaboxService();
|
|
25
|
+
}
|
|
26
|
+
export * from './types.js';
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 메가박스 주변 지점 탐색 도구
|
|
3
|
+
*/
|
|
4
|
+
import * as z from 'zod';
|
|
5
|
+
import { fetchMegaboxBookingList, fetchMegaboxTheaterInfo, toYyyymmdd } from '../client.js';
|
|
6
|
+
function calculateDistanceKm(lat1, lng1, lat2, lng2) {
|
|
7
|
+
const toRad = (deg) => (deg * Math.PI) / 180;
|
|
8
|
+
const dLat = toRad(lat2 - lat1);
|
|
9
|
+
const dLng = toRad(lng2 - lng1);
|
|
10
|
+
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
11
|
+
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
|
12
|
+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
13
|
+
return 6371 * c;
|
|
14
|
+
}
|
|
15
|
+
async function findNearbyTheaters(args) {
|
|
16
|
+
const { latitude = 37.5665, longitude = 126.978, playDate = toYyyymmdd(), areaCode = '11', limit = 10, timeoutMs = 15000, } = args;
|
|
17
|
+
const { theaters } = await fetchMegaboxBookingList({
|
|
18
|
+
playDate,
|
|
19
|
+
areaCode,
|
|
20
|
+
timeout: timeoutMs,
|
|
21
|
+
});
|
|
22
|
+
const infoResults = await Promise.allSettled(theaters.map((theater) => fetchMegaboxTheaterInfo(theater.theaterId, timeoutMs)));
|
|
23
|
+
const merged = theaters
|
|
24
|
+
.map((theater, index) => {
|
|
25
|
+
const infoResult = infoResults[index];
|
|
26
|
+
if (infoResult.status !== 'fulfilled') {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const info = infoResult.value;
|
|
30
|
+
if (info.latitude === null || info.longitude === null) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const distanceKm = calculateDistanceKm(latitude, longitude, info.latitude, info.longitude);
|
|
34
|
+
return {
|
|
35
|
+
theaterId: theater.theaterId,
|
|
36
|
+
theaterName: theater.theaterName,
|
|
37
|
+
address: info.address,
|
|
38
|
+
latitude: info.latitude,
|
|
39
|
+
longitude: info.longitude,
|
|
40
|
+
distanceKm: Number(distanceKm.toFixed(2)),
|
|
41
|
+
};
|
|
42
|
+
})
|
|
43
|
+
.filter((theater) => theater !== null)
|
|
44
|
+
.sort((a, b) => a.distanceKm - b.distanceKm)
|
|
45
|
+
.slice(0, limit);
|
|
46
|
+
const result = {
|
|
47
|
+
location: { latitude, longitude },
|
|
48
|
+
playDate,
|
|
49
|
+
areaCode,
|
|
50
|
+
count: merged.length,
|
|
51
|
+
theaters: merged,
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function createFindNearbyTheatersTool() {
|
|
58
|
+
return {
|
|
59
|
+
name: 'megabox_find_nearby_theaters',
|
|
60
|
+
metadata: {
|
|
61
|
+
title: '메가박스 주변 지점 탐색',
|
|
62
|
+
description: '사용자 좌표 기준으로 메가박스 지점을 거리순으로 조회합니다.',
|
|
63
|
+
inputSchema: {
|
|
64
|
+
latitude: z.number().optional().default(37.5665).describe('위도 (기본값: 서울 시청 37.5665)'),
|
|
65
|
+
longitude: z.number().optional().default(126.978).describe('경도 (기본값: 서울 시청 126.978)'),
|
|
66
|
+
playDate: z.string().optional().describe('조회 날짜(YYYYMMDD, 기본값: 오늘)'),
|
|
67
|
+
areaCode: z.string().optional().default('11').describe('지역 코드 (기본값: 11, 서울)'),
|
|
68
|
+
limit: z.number().optional().default(10).describe('반환할 최대 지점 수 (기본값: 10)'),
|
|
69
|
+
timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
handler: findNearbyTheaters,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 메가박스 잔여 좌석 조회 도구
|
|
3
|
+
*/
|
|
4
|
+
import * as z from 'zod';
|
|
5
|
+
import { fetchMegaboxBookingList, toYyyymmdd } from '../client.js';
|
|
6
|
+
async function getRemainingSeats(args) {
|
|
7
|
+
const { playDate = toYyyymmdd(), theaterId, movieId, areaCode = '11', limit = 50, timeoutMs = 15000, } = args;
|
|
8
|
+
const { showtimes } = await fetchMegaboxBookingList({
|
|
9
|
+
playDate,
|
|
10
|
+
theaterId,
|
|
11
|
+
movieId,
|
|
12
|
+
areaCode,
|
|
13
|
+
timeout: timeoutMs,
|
|
14
|
+
});
|
|
15
|
+
const filteredShowtimes = showtimes
|
|
16
|
+
.filter((item) => (theaterId ? item.theaterId === theaterId : true))
|
|
17
|
+
.filter((item) => (movieId ? item.movieId === movieId : true))
|
|
18
|
+
.sort((a, b) => {
|
|
19
|
+
if (a.startTime === b.startTime) {
|
|
20
|
+
return a.theaterName.localeCompare(b.theaterName);
|
|
21
|
+
}
|
|
22
|
+
return a.startTime.localeCompare(b.startTime);
|
|
23
|
+
})
|
|
24
|
+
.slice(0, limit);
|
|
25
|
+
const result = {
|
|
26
|
+
playDate,
|
|
27
|
+
filters: {
|
|
28
|
+
theaterId: theaterId || null,
|
|
29
|
+
movieId: movieId || null,
|
|
30
|
+
areaCode,
|
|
31
|
+
limit,
|
|
32
|
+
},
|
|
33
|
+
count: filteredShowtimes.length,
|
|
34
|
+
seats: filteredShowtimes,
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function createGetRemainingSeatsTool() {
|
|
41
|
+
return {
|
|
42
|
+
name: 'megabox_get_remaining_seats',
|
|
43
|
+
metadata: {
|
|
44
|
+
title: '메가박스 잔여 좌석 조회',
|
|
45
|
+
description: '영화/지점/날짜 조건으로 상영 회차별 남은 좌석 수를 조회합니다.',
|
|
46
|
+
inputSchema: {
|
|
47
|
+
playDate: z.string().optional().describe('조회 날짜(YYYYMMDD, 기본값: 오늘)'),
|
|
48
|
+
theaterId: z.string().optional().describe('메가박스 지점 번호 (예: 1372)'),
|
|
49
|
+
movieId: z.string().optional().describe('메가박스 영화 번호 (예: 25104500)'),
|
|
50
|
+
areaCode: z.string().optional().default('11').describe('지역 코드 (기본값: 11, 서울)'),
|
|
51
|
+
limit: z.number().optional().default(50).describe('반환할 최대 회차 수 (기본값: 50)'),
|
|
52
|
+
timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
handler: getRemainingSeats,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 메가박스 영화 목록 조회 도구
|
|
3
|
+
*/
|
|
4
|
+
import * as z from 'zod';
|
|
5
|
+
import { fetchMegaboxBookingList, toYyyymmdd } from '../client.js';
|
|
6
|
+
async function listNowShowing(args) {
|
|
7
|
+
const { playDate = toYyyymmdd(), theaterId, movieId, areaCode = '11', timeoutMs = 15000, } = args;
|
|
8
|
+
const { theaters, movies, showtimes } = await fetchMegaboxBookingList({
|
|
9
|
+
playDate,
|
|
10
|
+
theaterId,
|
|
11
|
+
movieId,
|
|
12
|
+
areaCode,
|
|
13
|
+
timeout: timeoutMs,
|
|
14
|
+
});
|
|
15
|
+
const result = {
|
|
16
|
+
playDate,
|
|
17
|
+
filters: {
|
|
18
|
+
theaterId: theaterId || null,
|
|
19
|
+
movieId: movieId || null,
|
|
20
|
+
areaCode,
|
|
21
|
+
},
|
|
22
|
+
counts: {
|
|
23
|
+
theaters: theaters.length,
|
|
24
|
+
movies: movies.length,
|
|
25
|
+
showtimes: showtimes.length,
|
|
26
|
+
},
|
|
27
|
+
theaters,
|
|
28
|
+
movies,
|
|
29
|
+
showtimes,
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function createListNowShowingTool() {
|
|
36
|
+
return {
|
|
37
|
+
name: 'megabox_list_now_showing',
|
|
38
|
+
metadata: {
|
|
39
|
+
title: '메가박스 영화 목록 조회',
|
|
40
|
+
description: '날짜/지점 조건으로 메가박스 영화 및 상영 회차 목록을 조회합니다.',
|
|
41
|
+
inputSchema: {
|
|
42
|
+
playDate: z.string().optional().describe('조회 날짜(YYYYMMDD, 기본값: 오늘)'),
|
|
43
|
+
theaterId: z.string().optional().describe('메가박스 지점 번호 (예: 1372)'),
|
|
44
|
+
movieId: z.string().optional().describe('메가박스 영화 번호 (예: 25104500)'),
|
|
45
|
+
areaCode: z.string().optional().default('11').describe('지역 코드 (기본값: 11, 서울)'),
|
|
46
|
+
timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
handler: listNowShowing,
|
|
50
|
+
};
|
|
51
|
+
}
|