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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +741 -0
  3. package/dist/api/cgvHandlers.js +97 -0
  4. package/dist/api/handlers.js +208 -0
  5. package/dist/api/megaboxHandlers.js +145 -0
  6. package/dist/api/response.js +22 -0
  7. package/dist/api/routes/cgvRoutes.js +22 -0
  8. package/dist/api/routes/daisoRoutes.js +27 -0
  9. package/dist/api/routes/megaboxRoutes.js +22 -0
  10. package/dist/api/routes/oliveyoungRoutes.js +17 -0
  11. package/dist/cli.js +140 -0
  12. package/dist/core/interfaces.js +6 -0
  13. package/dist/core/registry.js +121 -0
  14. package/dist/core/types.js +6 -0
  15. package/dist/index.js +123 -0
  16. package/dist/pages/openapi.js +6 -0
  17. package/dist/pages/openapiResponse.js +32 -0
  18. package/dist/pages/openapiSpec.js +43 -0
  19. package/dist/pages/openapiSpecComponents.js +419 -0
  20. package/dist/pages/openapiSpecComponentsCgv.js +123 -0
  21. package/dist/pages/openapiSpecPathsCgv.js +149 -0
  22. package/dist/pages/openapiSpecPathsDaisoOliveyoung.js +381 -0
  23. package/dist/pages/openapiSpecPathsMegabox.js +184 -0
  24. package/dist/pages/openapiYaml.js +58 -0
  25. package/dist/pages/privacy.js +262 -0
  26. package/dist/pages/prompt.js +425 -0
  27. package/dist/services/cgv/api.js +15 -0
  28. package/dist/services/cgv/client.js +146 -0
  29. package/dist/services/cgv/index.js +30 -0
  30. package/dist/services/cgv/timetable.js +16 -0
  31. package/dist/services/cgv/tools/findTheaters.js +43 -0
  32. package/dist/services/cgv/tools/getTimetable.js +47 -0
  33. package/dist/services/cgv/tools/searchMovies.js +40 -0
  34. package/dist/services/cgv/transport.js +91 -0
  35. package/dist/services/cgv/types.js +4 -0
  36. package/dist/services/daiso/api.js +53 -0
  37. package/dist/services/daiso/client.js +35 -0
  38. package/dist/services/daiso/index.js +42 -0
  39. package/dist/services/daiso/tools/checkInventory.js +122 -0
  40. package/dist/services/daiso/tools/findStores.js +145 -0
  41. package/dist/services/daiso/tools/getPriceInfo.js +90 -0
  42. package/dist/services/daiso/tools/searchProducts.js +74 -0
  43. package/dist/services/daiso/types.js +4 -0
  44. package/dist/services/megabox/api.js +8 -0
  45. package/dist/services/megabox/client.js +140 -0
  46. package/dist/services/megabox/index.js +26 -0
  47. package/dist/services/megabox/tools/findNearbyTheaters.js +74 -0
  48. package/dist/services/megabox/tools/getRemainingSeats.js +57 -0
  49. package/dist/services/megabox/tools/listNowShowing.js +51 -0
  50. package/dist/services/megabox/types.js +4 -0
  51. package/dist/services/oliveyoung/api.js +8 -0
  52. package/dist/services/oliveyoung/client.js +90 -0
  53. package/dist/services/oliveyoung/index.js +28 -0
  54. package/dist/services/oliveyoung/tools/checkInventory.js +87 -0
  55. package/dist/services/oliveyoung/tools/findNearbyStores.js +53 -0
  56. package/dist/services/oliveyoung/types.js +4 -0
  57. package/dist/tools/checkInventory.js +98 -0
  58. package/dist/tools/findStores.js +125 -0
  59. package/dist/tools/getPriceInfo.js +77 -0
  60. package/dist/tools/searchProducts.js +61 -0
  61. package/dist/types/index.js +4 -0
  62. package/dist/utils/cache.js +54 -0
  63. package/dist/utils/fetch.js +14 -0
  64. package/dist/utils/format.js +31 -0
  65. package/dist/utils/http.js +35 -0
  66. package/dist/utils/zyte.js +76 -0
  67. package/openapi.json +1938 -0
  68. package/openapi.yaml +1338 -0
  69. package/package.json +79 -0
@@ -0,0 +1,8 @@
1
+ /**
2
+ * 올리브영 API 엔드포인트 중앙 관리
3
+ */
4
+ export const OLIVEYOUNG_API = {
5
+ BASE_URL: 'https://www.oliveyoung.co.kr',
6
+ STORE_FINDER_PATH: '/oystore/api/storeFinder/find-store',
7
+ PRODUCT_SEARCH_PATH: '/oystore/api/stock/product-search-v3',
8
+ };
@@ -0,0 +1,90 @@
1
+ /**
2
+ * 올리브영 Zyte 클라이언트
3
+ *
4
+ * Zyte extract API를 통해 올리브영 내부 API를 우회 호출합니다.
5
+ */
6
+ import { OLIVEYOUNG_API } from './api.js';
7
+ import { decodeBase64, requestByZyte } from '../../utils/zyte.js';
8
+ async function zyteExtract(targetPath, requestBody, options = {}) {
9
+ const { timeout = 15000, apiKey } = options;
10
+ try {
11
+ const result = await requestByZyte({
12
+ apiKey,
13
+ timeout,
14
+ url: `${OLIVEYOUNG_API.BASE_URL}${targetPath}`,
15
+ method: 'POST',
16
+ headers: [
17
+ { name: 'Content-Type', value: 'application/json' },
18
+ { name: 'Accept', value: 'application/json' },
19
+ { name: 'X-Requested-With', value: 'XMLHttpRequest' },
20
+ ],
21
+ bodyText: JSON.stringify(requestBody),
22
+ });
23
+ if (result.statusCode !== 200 || !result.httpResponseBody) {
24
+ throw new Error(`올리브영 API 응답 실패: ${result.statusCode || 'unknown'}`);
25
+ }
26
+ const decodedBody = decodeBase64(result.httpResponseBody);
27
+ const parsedBody = JSON.parse(decodedBody);
28
+ if (parsedBody.status !== 'SUCCESS') {
29
+ throw new Error(`올리브영 API 상태 오류: ${parsedBody.status || 'UNKNOWN'}`);
30
+ }
31
+ return parsedBody;
32
+ }
33
+ catch (error) {
34
+ if (error instanceof Error && error.name === 'AbortError') {
35
+ throw new Error('올리브영 API 요청 시간 초과');
36
+ }
37
+ throw error;
38
+ }
39
+ }
40
+ export async function fetchOliveyoungStores(params, options = {}) {
41
+ const payload = {
42
+ lat: params.latitude,
43
+ lon: params.longitude,
44
+ pageIdx: params.pageIdx,
45
+ searchWords: params.searchWords,
46
+ pogKeys: '',
47
+ serviceKeys: '',
48
+ mapLat: params.latitude,
49
+ mapLon: params.longitude,
50
+ };
51
+ const body = await zyteExtract(OLIVEYOUNG_API.STORE_FINDER_PATH, payload, options);
52
+ const stores = (body.data?.storeList || []).map((store) => ({
53
+ storeCode: store.storeCode || '',
54
+ storeName: store.storeName || '',
55
+ address: store.address || '',
56
+ latitude: store.latitude || 0,
57
+ longitude: store.longitude || 0,
58
+ pickupYn: Boolean(store.pickupYn),
59
+ o2oRemainQuantity: store.o2oRemainQuantity || 0,
60
+ }));
61
+ return {
62
+ totalCount: body.data?.totalCount || 0,
63
+ stores,
64
+ };
65
+ }
66
+ export async function fetchOliveyoungProducts(params, options = {}) {
67
+ const payload = {
68
+ includeSoldOut: params.includeSoldOut,
69
+ keyword: params.keyword,
70
+ page: params.page,
71
+ sort: params.sort,
72
+ size: params.size,
73
+ };
74
+ const body = await zyteExtract(OLIVEYOUNG_API.PRODUCT_SEARCH_PATH, payload, options);
75
+ const list = body.data?.serachList || body.data?.searchList || [];
76
+ const products = list.map((product) => ({
77
+ goodsNumber: product.goodsNumber || '',
78
+ goodsName: product.goodsName || '',
79
+ priceToPay: product.priceToPay || 0,
80
+ originalPrice: product.originalPrice || 0,
81
+ discountRate: product.discountRate || 0,
82
+ o2oStockFlag: Boolean(product.o2oStockFlag),
83
+ o2oRemainQuantity: product.o2oRemainQuantity || 0,
84
+ }));
85
+ return {
86
+ totalCount: body.data?.totalCount || 0,
87
+ nextPage: Boolean(body.data?.nextPage),
88
+ products,
89
+ };
90
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * 올리브영 서비스 프로바이더
3
+ */
4
+ import { createFindNearbyStoresTool } from './tools/findNearbyStores.js';
5
+ import { createCheckInventoryTool } from './tools/checkInventory.js';
6
+ const OLIVEYOUNG_METADATA = {
7
+ id: 'oliveyoung',
8
+ name: '올리브영',
9
+ version: '1.0.0',
10
+ description: '올리브영 주변 매장 탐색 및 재고 파악 서비스 (Zyte API 기반)',
11
+ };
12
+ class OliveyoungService {
13
+ options;
14
+ metadata = OLIVEYOUNG_METADATA;
15
+ constructor(options = {}) {
16
+ this.options = options;
17
+ }
18
+ getTools() {
19
+ return [
20
+ createFindNearbyStoresTool(this.options.zyteApiKey),
21
+ createCheckInventoryTool(this.options.zyteApiKey),
22
+ ];
23
+ }
24
+ }
25
+ export function createOliveyoungService(options = {}) {
26
+ return new OliveyoungService(options);
27
+ }
28
+ export * from './types.js';
@@ -0,0 +1,87 @@
1
+ /**
2
+ * 올리브영 재고 파악 도구
3
+ */
4
+ import * as z from 'zod';
5
+ import { fetchOliveyoungProducts, fetchOliveyoungStores } from '../client.js';
6
+ async function checkInventory(args) {
7
+ const { keyword, latitude = 37.5665, longitude = 126.978, storeKeyword = '', page = 1, size = 20, sort = '01', includeSoldOut = false, storeLimit = 10, timeoutMs = 15000, zyteApiKey, } = args;
8
+ if (!keyword || keyword.trim().length === 0) {
9
+ throw new Error('상품 검색어(keyword)를 입력해주세요.');
10
+ }
11
+ const [storeResult, productResult] = await Promise.all([
12
+ fetchOliveyoungStores({
13
+ latitude,
14
+ longitude,
15
+ pageIdx: 1,
16
+ searchWords: storeKeyword,
17
+ }, {
18
+ timeout: timeoutMs,
19
+ apiKey: zyteApiKey,
20
+ }),
21
+ fetchOliveyoungProducts({
22
+ keyword,
23
+ page,
24
+ size,
25
+ sort,
26
+ includeSoldOut,
27
+ }, {
28
+ timeout: timeoutMs,
29
+ apiKey: zyteApiKey,
30
+ }),
31
+ ]);
32
+ const inStockProducts = productResult.products.filter((product) => product.o2oStockFlag || product.o2oRemainQuantity > 0);
33
+ const result = {
34
+ keyword,
35
+ searchOptions: {
36
+ page,
37
+ size,
38
+ sort,
39
+ includeSoldOut,
40
+ storeKeyword,
41
+ },
42
+ location: {
43
+ latitude,
44
+ longitude,
45
+ },
46
+ nearbyStores: {
47
+ totalCount: storeResult.totalCount,
48
+ count: Math.min(storeLimit, storeResult.stores.length),
49
+ stores: storeResult.stores.slice(0, storeLimit),
50
+ },
51
+ inventory: {
52
+ totalCount: productResult.totalCount,
53
+ nextPage: productResult.nextPage,
54
+ inStockCount: inStockProducts.length,
55
+ outOfStockCount: productResult.products.length - inStockProducts.length,
56
+ products: productResult.products,
57
+ },
58
+ };
59
+ return {
60
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
61
+ };
62
+ }
63
+ export function createCheckInventoryTool(apiKey) {
64
+ return {
65
+ name: 'oliveyoung_check_inventory',
66
+ metadata: {
67
+ title: '올리브영 재고 파악',
68
+ description: '상품 키워드로 올리브영 재고(품절 포함 여부 선택 가능)를 조회하고, 주변 매장 정보와 함께 반환합니다.',
69
+ inputSchema: {
70
+ keyword: z.string().describe('재고를 확인할 상품 키워드 (예: 선크림, 립밤, 샴푸)'),
71
+ latitude: z.number().optional().default(37.5665).describe('위도 (기본값: 서울 시청 37.5665)'),
72
+ longitude: z.number().optional().default(126.978).describe('경도 (기본값: 서울 시청 126.978)'),
73
+ storeKeyword: z
74
+ .string()
75
+ .optional()
76
+ .describe('주변 매장 필터 키워드 (예: 명동, 강남). 비우면 주변 매장 전체'),
77
+ page: z.number().optional().default(1).describe('상품 검색 페이지 번호 (기본값: 1)'),
78
+ size: z.number().optional().default(20).describe('페이지당 상품 수 (기본값: 20)'),
79
+ sort: z.string().optional().default('01').describe('정렬 코드 (기본값: 01)'),
80
+ includeSoldOut: z.boolean().optional().default(false).describe('품절 상품 포함 여부 (기본값: false)'),
81
+ storeLimit: z.number().optional().default(10).describe('반환할 주변 매장 최대 수 (기본값: 10)'),
82
+ timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
83
+ },
84
+ },
85
+ handler: ((args) => checkInventory({ ...args, zyteApiKey: apiKey })),
86
+ };
87
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * 올리브영 주변 매장 탐색 도구
3
+ */
4
+ import * as z from 'zod';
5
+ import { fetchOliveyoungStores } from '../client.js';
6
+ async function findNearbyStores(args) {
7
+ const { latitude = 37.5665, longitude = 126.978, keyword = '', pageIdx = 1, limit = 20, timeoutMs = 15000, zyteApiKey, } = args;
8
+ const { totalCount, stores } = await fetchOliveyoungStores({
9
+ latitude,
10
+ longitude,
11
+ pageIdx,
12
+ searchWords: keyword,
13
+ }, {
14
+ timeout: timeoutMs,
15
+ apiKey: zyteApiKey,
16
+ });
17
+ const limitedStores = stores.slice(0, limit);
18
+ const result = {
19
+ location: {
20
+ latitude,
21
+ longitude,
22
+ },
23
+ keyword,
24
+ pageIdx,
25
+ totalCount,
26
+ count: limitedStores.length,
27
+ stores: limitedStores,
28
+ };
29
+ return {
30
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
31
+ };
32
+ }
33
+ export function createFindNearbyStoresTool(apiKey) {
34
+ return {
35
+ name: 'oliveyoung_find_nearby_stores',
36
+ metadata: {
37
+ title: '올리브영 주변 매장 탐색',
38
+ description: '위치(위도/경도)와 키워드로 내 주변 올리브영 매장을 조회합니다.',
39
+ inputSchema: {
40
+ latitude: z.number().optional().default(37.5665).describe('위도 (기본값: 서울 시청 37.5665)'),
41
+ longitude: z.number().optional().default(126.978).describe('경도 (기본값: 서울 시청 126.978)'),
42
+ keyword: z
43
+ .string()
44
+ .optional()
45
+ .describe('매장 검색어 (예: 강남, 명동, 신촌). 비우면 주변 매장 전체 조회'),
46
+ pageIdx: z.number().optional().default(1).describe('매장 결과 페이지 번호 (기본값: 1)'),
47
+ limit: z.number().optional().default(20).describe('반환할 최대 매장 수 (기본값: 20)'),
48
+ timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
49
+ },
50
+ },
51
+ handler: ((args) => findNearbyStores({ ...args, zyteApiKey: apiKey })),
52
+ };
53
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * 올리브영 서비스 전용 타입 정의
3
+ */
4
+ export {};
@@ -0,0 +1,98 @@
1
+ /**
2
+ * 재고 확인 도구
3
+ *
4
+ * 다이소몰 API를 사용하여 매장별 재고를 확인합니다.
5
+ * API: https://mapi.daisomall.co.kr/ms/msg/newIntSelStr
6
+ */
7
+ import { fetchJson } from '../utils/fetch.js';
8
+ // 온라인 재고 조회
9
+ async function fetchOnlineStock(productNo) {
10
+ const data = await fetchJson('https://mapi.daisomall.co.kr/ms/msg/selOnlStck', {
11
+ method: 'POST',
12
+ headers: { 'Content-Type': 'application/json' },
13
+ body: JSON.stringify({ pdNo: productNo }),
14
+ });
15
+ if (!data.success) {
16
+ return 0;
17
+ }
18
+ return data.data?.stck || 0;
19
+ }
20
+ // 매장별 재고 조회
21
+ async function fetchStoreInventory(productNo, lat, lng, page = 1, pageSize = 30, keyword = '') {
22
+ const data = await fetchJson('https://mapi.daisomall.co.kr/ms/msg/newIntSelStr', {
23
+ method: 'POST',
24
+ headers: { 'Content-Type': 'application/json' },
25
+ body: JSON.stringify({
26
+ keyword,
27
+ pdNo: productNo,
28
+ curLttd: lat,
29
+ curLitd: lng,
30
+ geolocationAgrYn: 'Y',
31
+ pkupYn: '',
32
+ intCd: '',
33
+ pageSize,
34
+ currentPage: page,
35
+ }),
36
+ });
37
+ if (!data.success || !data.data?.msStrVOList) {
38
+ return { stores: [], totalCount: 0 };
39
+ }
40
+ const stores = data.data.msStrVOList.map((store) => ({
41
+ storeCode: store.strCd,
42
+ storeName: store.strNm,
43
+ address: store.strAddr,
44
+ phone: store.strTno,
45
+ openTime: store.opngTime,
46
+ closeTime: store.clsngTime,
47
+ lat: store.strLttd,
48
+ lng: store.strLitd,
49
+ distance: store.km,
50
+ quantity: parseInt(store.qty) || 0,
51
+ options: {
52
+ parking: store.parkYn === 'Y',
53
+ simCard: store.usimYn === 'Y',
54
+ pickup: store.pkupYn === 'Y',
55
+ taxFree: store.taxfYn === 'Y',
56
+ elevator: store.elvtYn === 'Y',
57
+ ramp: store.entrRampYn === 'Y',
58
+ cashless: store.nocashYn === 'Y',
59
+ },
60
+ }));
61
+ return { stores, totalCount: data.data.intStrCont || stores.length };
62
+ }
63
+ export async function checkInventory(args) {
64
+ const { productId, storeQuery = '', latitude = 37.5665, // 기본값: 서울 시청
65
+ longitude = 126.978, page = 1, pageSize = 30, } = args;
66
+ if (!productId || productId.trim().length === 0) {
67
+ throw new Error('상품 ID(productId)를 입력해주세요.');
68
+ }
69
+ // 온라인 재고와 매장 재고 동시 조회
70
+ const [onlineStock, storeResult] = await Promise.all([
71
+ fetchOnlineStock(productId),
72
+ fetchStoreInventory(productId, latitude, longitude, page, pageSize, storeQuery),
73
+ ]);
74
+ // 재고 있는 매장과 없는 매장 분류
75
+ const inStockStores = storeResult.stores.filter((s) => s.quantity > 0);
76
+ const outOfStockStores = storeResult.stores.filter((s) => s.quantity === 0);
77
+ const result = {
78
+ productId,
79
+ location: { latitude, longitude },
80
+ onlineStock,
81
+ storeInventory: {
82
+ totalStores: storeResult.totalCount,
83
+ inStockCount: inStockStores.length,
84
+ outOfStockCount: outOfStockStores.length,
85
+ page,
86
+ pageSize,
87
+ stores: storeResult.stores,
88
+ },
89
+ };
90
+ return {
91
+ content: [
92
+ {
93
+ type: 'text',
94
+ text: JSON.stringify(result, null, 2),
95
+ },
96
+ ],
97
+ };
98
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * 매장 찾기 도구
3
+ *
4
+ * 다이소 API를 사용하여 매장을 검색합니다.
5
+ * API: https://www.daiso.co.kr/cs/ajax/shop_search
6
+ */
7
+ import { fetchHtml, fetchJson } from '../utils/fetch.js';
8
+ // HTML에서 매장 정보 파싱
9
+ function parseStoresFromHtml(html) {
10
+ const stores = [];
11
+ // bx-store div 태그의 시작 위치 찾기
12
+ const divStartRegex = /<div[^>]*class="bx-store"[^>]*>/gi;
13
+ let divMatch;
14
+ while ((divMatch = divStartRegex.exec(html)) !== null) {
15
+ const startIdx = divMatch.index;
16
+ const divTag = divMatch[0];
17
+ // 해당 div의 끝 찾기 (다음 bx-store div나 특정 마커까지)
18
+ const endIdx = html.indexOf('<div class="bx-store"', startIdx + 1);
19
+ const blockEnd = endIdx > 0 ? endIdx : startIdx + 2000;
20
+ const block = html.slice(startIdx, blockEnd);
21
+ // data 속성 추출
22
+ const startMatch = /data-start="(\d+)"/.exec(divTag);
23
+ const endMatch = /data-end="(\d+)"/.exec(divTag);
24
+ const latMatch = /data-lat="([^"]+)"/.exec(divTag);
25
+ const lngMatch = /data-lng="([^"]+)"/.exec(divTag);
26
+ const infoMatch = /data-info='([^']*)'/.exec(divTag);
27
+ if (!startMatch || !endMatch || !latMatch || !lngMatch)
28
+ continue;
29
+ // 매장명, 전화번호, 주소 추출
30
+ const nameMatch = /<h4[^>]*class="place"[^>]*>([^<]+)<\/h4>/i.exec(block);
31
+ const phoneMatch = /<em[^>]*class="phone"[^>]*>([^<]*)<\/em>/i.exec(block);
32
+ const addrMatch = /<p[^>]*class="addr"[^>]*>([^<]+)<\/p>/i.exec(block);
33
+ if (!nameMatch)
34
+ continue;
35
+ // data-info JSON 파싱
36
+ let info = {};
37
+ if (infoMatch) {
38
+ try {
39
+ const infoStr = infoMatch[1].replace(/&quot;/g, '"').replace(/&amp;/g, '&');
40
+ info = JSON.parse(infoStr);
41
+ }
42
+ catch {
43
+ // JSON 파싱 실패 시 빈 객체 사용
44
+ }
45
+ }
46
+ const options = {
47
+ parking: info.shp_pak === 'Y',
48
+ ramp: info.entrramp === 'Y',
49
+ elevator: info.elvtor === 'Y',
50
+ cashless: info.ptcard === 'Y',
51
+ photoSticker: info.ptstk === 'Y',
52
+ nameSticker: info.nmstk === 'Y',
53
+ simCard: info.usim_yn === 'Y',
54
+ taxFree: info.tax_free === 'Y',
55
+ groupOrder: info['ext.group_yn'] === 'Y' || info.group_yn === 'Y',
56
+ pickup: info.online_yn === 'Y',
57
+ };
58
+ // 영업 시간 포맷팅
59
+ const formatTime = (time) => {
60
+ if (time.length === 4) {
61
+ return `${time.slice(0, 2)}:${time.slice(2)}`;
62
+ }
63
+ return time;
64
+ };
65
+ stores.push({
66
+ name: nameMatch[1].trim(),
67
+ phone: phoneMatch ? phoneMatch[1].replace('T.', '').trim() : '',
68
+ address: addrMatch ? addrMatch[1].trim() : '',
69
+ lat: parseFloat(latMatch[1]),
70
+ lng: parseFloat(lngMatch[1]),
71
+ openTime: formatTime(startMatch[1]),
72
+ closeTime: formatTime(endMatch[1]),
73
+ options,
74
+ });
75
+ }
76
+ return stores;
77
+ }
78
+ // 다이소 매장 검색 API 호출
79
+ async function fetchStores(keyword, sido, gugun, dong) {
80
+ const url = new URL('https://www.daiso.co.kr/cs/ajax/shop_search');
81
+ url.searchParams.set('name_address', keyword || '');
82
+ url.searchParams.set('sido', sido || '');
83
+ url.searchParams.set('gugun', gugun || '');
84
+ url.searchParams.set('dong', dong || '');
85
+ const html = await fetchHtml(url.toString());
86
+ return parseStoresFromHtml(html);
87
+ }
88
+ // 시/도별 구/군 목록 조회
89
+ export async function getDistricts(sido) {
90
+ const url = new URL('https://www.daiso.co.kr/cs/ajax/sido_search');
91
+ url.searchParams.set('sido', sido);
92
+ const data = await fetchJson(url.toString());
93
+ return data.map((item) => item.value);
94
+ }
95
+ // 구/군별 동 목록 조회
96
+ export async function getNeighborhoods(sido, gugun) {
97
+ const url = new URL('https://www.daiso.co.kr/cs/ajax/gugun_search');
98
+ url.searchParams.set('sido', sido);
99
+ url.searchParams.set('gugun', gugun);
100
+ const data = await fetchJson(url.toString());
101
+ return data.map((item) => item.value);
102
+ }
103
+ export async function findStores(args) {
104
+ const { keyword, sido, gugun, dong, limit = 50 } = args;
105
+ // 최소한 하나의 검색 조건이 필요
106
+ if (!keyword && !sido) {
107
+ throw new Error('검색어(keyword) 또는 지역(sido)을 입력해주세요.');
108
+ }
109
+ const stores = await fetchStores(keyword, sido, gugun, dong);
110
+ const limitedStores = stores.slice(0, limit);
111
+ const result = {
112
+ searchParams: { keyword, sido, gugun, dong },
113
+ totalCount: stores.length,
114
+ count: limitedStores.length,
115
+ stores: limitedStores,
116
+ };
117
+ return {
118
+ content: [
119
+ {
120
+ type: 'text',
121
+ text: JSON.stringify(result, null, 2),
122
+ },
123
+ ],
124
+ };
125
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * 가격 정보 조회 도구
3
+ *
4
+ * 다이소몰 API를 사용하여 상품의 가격 정보를 조회합니다.
5
+ * 상품 검색 API를 통해 가격 정보를 가져옵니다.
6
+ */
7
+ import { fetchJson } from '../utils/fetch.js';
8
+ // 이미지 URL 생성
9
+ function getImageUrl(path) {
10
+ if (!path)
11
+ return undefined;
12
+ return `https://img.daisomall.co.kr${path}`;
13
+ }
14
+ // 상품 ID로 검색
15
+ async function fetchProductById(productId) {
16
+ const url = new URL('https://prdm.daisomall.co.kr/ssn/search/FindStoreGoods');
17
+ url.searchParams.set('searchTerm', productId);
18
+ url.searchParams.set('cntPerPage', '10');
19
+ url.searchParams.set('pageNum', '1');
20
+ const data = await fetchJson(url.toString());
21
+ if (!data.resultSet?.result?.[0]?.resultDocuments) {
22
+ return null;
23
+ }
24
+ const docs = data.resultSet.result[0].resultDocuments;
25
+ // 정확한 ID 매칭 시도
26
+ const exactMatch = docs.find((doc) => doc.PD_NO === productId);
27
+ if (exactMatch) {
28
+ return exactMatch;
29
+ }
30
+ return docs.length > 0 ? docs[0] : null;
31
+ }
32
+ // 상품명으로 검색
33
+ async function fetchProductByName(productName) {
34
+ const url = new URL('https://prdm.daisomall.co.kr/ssn/search/FindStoreGoods');
35
+ url.searchParams.set('searchTerm', productName);
36
+ url.searchParams.set('cntPerPage', '1');
37
+ url.searchParams.set('pageNum', '1');
38
+ const data = await fetchJson(url.toString());
39
+ if (!data.resultSet?.result?.[0]?.resultDocuments?.length) {
40
+ return null;
41
+ }
42
+ return data.resultSet.result[0].resultDocuments[0];
43
+ }
44
+ export async function getPriceInfo(args) {
45
+ const { productId, productName } = args;
46
+ if (!productId && !productName) {
47
+ throw new Error('상품 ID(productId) 또는 상품명(productName)을 입력해주세요.');
48
+ }
49
+ let product = null;
50
+ if (productId) {
51
+ product = await fetchProductById(productId);
52
+ }
53
+ else if (productName) {
54
+ product = await fetchProductByName(productName);
55
+ }
56
+ if (!product) {
57
+ throw new Error(`상품을 찾을 수 없습니다: ${productId || productName}`);
58
+ }
59
+ const price = parseInt(product.PD_PRC) || 0;
60
+ const result = {
61
+ productId: product.PD_NO,
62
+ productName: product.PDNM || product.EXH_PD_NM,
63
+ currentPrice: price,
64
+ currency: 'KRW',
65
+ imageUrl: getImageUrl(product.ATCH_FILE_URL),
66
+ brand: product.BRND_NM || undefined,
67
+ soldOut: product.SOLD_OUT_YN === 'Y',
68
+ };
69
+ return {
70
+ content: [
71
+ {
72
+ type: 'text',
73
+ text: JSON.stringify(result, null, 2),
74
+ },
75
+ ],
76
+ };
77
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * 제품 검색 도구
3
+ *
4
+ * 다이소몰 API를 사용하여 제품을 검색합니다.
5
+ * API: https://prdm.daisomall.co.kr/ssn/search/FindStoreGoods
6
+ */
7
+ import { fetchJson } from '../utils/fetch.js';
8
+ // 이미지 URL 생성
9
+ function getImageUrl(path) {
10
+ if (!path)
11
+ return undefined;
12
+ return `https://img.daisomall.co.kr${path}`;
13
+ }
14
+ // 다이소몰 API에서 상품 검색
15
+ async function fetchProducts(query, page = 1, pageSize = 30) {
16
+ const url = new URL('https://prdm.daisomall.co.kr/ssn/search/FindStoreGoods');
17
+ url.searchParams.set('searchTerm', query);
18
+ url.searchParams.set('cntPerPage', pageSize.toString());
19
+ url.searchParams.set('pageNum', page.toString());
20
+ const data = await fetchJson(url.toString());
21
+ // 결과가 없는 경우
22
+ if (!data.resultSet?.result?.[0]?.resultDocuments) {
23
+ return { products: [], totalCount: 0 };
24
+ }
25
+ const result = data.resultSet.result[0];
26
+ const totalCount = result.totalSize || 0;
27
+ const products = result.resultDocuments.map((doc) => ({
28
+ id: doc.PD_NO,
29
+ name: doc.PDNM || doc.EXH_PD_NM || '',
30
+ price: parseInt(doc.PD_PRC) || 0,
31
+ imageUrl: getImageUrl(doc.ATCH_FILE_URL),
32
+ brand: doc.BRND_NM || undefined,
33
+ soldOut: doc.SOLD_OUT_YN === 'Y',
34
+ isNew: doc.NEW_PD_YN === 'Y',
35
+ pickupAvailable: doc.PKUP_OR_PSBL_YN === 'Y',
36
+ }));
37
+ return { products, totalCount };
38
+ }
39
+ export 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: [
55
+ {
56
+ type: 'text',
57
+ text: JSON.stringify(result, null, 2),
58
+ },
59
+ ],
60
+ };
61
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * 다이소 MCP 서버 타입 정의
3
+ */
4
+ export {};