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,146 @@
1
+ /**
2
+ * CGV API 클라이언트
3
+ */
4
+ import { formatTime, toNumber, toYyyymmdd } from '../../utils/format.js';
5
+ import { CGV_API } from './api.js';
6
+ import { requestCgv } from './transport.js';
7
+ const DEFAULT_THEATER_CODE = '0056';
8
+ const MAX_FALLBACK_THEATERS = 5;
9
+ async function resolveTheaterCode(playDate, theaterCode, params) {
10
+ if (theaterCode) {
11
+ return theaterCode;
12
+ }
13
+ const theaters = await fetchCgvTheaters({
14
+ playDate,
15
+ regionCode: params.regionCode,
16
+ timeout: params.timeout,
17
+ zyteApiKey: params.zyteApiKey,
18
+ });
19
+ return theaters[0]?.theaterCode || DEFAULT_THEATER_CODE;
20
+ }
21
+ async function fetchMoviesByTheaterCode(playDate, theaterCode, params) {
22
+ const searchParams = new URLSearchParams({
23
+ coCd: CGV_API.COMPANY_CODE,
24
+ siteNo: theaterCode,
25
+ scnYmd: playDate,
26
+ });
27
+ const response = await requestCgv(CGV_API.MOVIE_LIST_PATH, searchParams, params.timeout, params.zyteApiKey);
28
+ return (response.data || [])
29
+ .filter((item) => item.movNo && item.movNm)
30
+ .map((item) => ({
31
+ movieCode: item.movNo,
32
+ movieName: item.movNm,
33
+ rating: item.cratgClsNm || undefined,
34
+ }));
35
+ }
36
+ async function fetchTimetableByMovieCode(playDate, theaterCode, movieCode, params) {
37
+ const searchParams = new URLSearchParams({
38
+ coCd: CGV_API.COMPANY_CODE,
39
+ siteNo: theaterCode,
40
+ scnYmd: playDate,
41
+ movNo: movieCode,
42
+ rtctlScopCd: CGV_API.TIMETABLE_SCOPE_CODE,
43
+ });
44
+ const response = await requestCgv(CGV_API.TIMETABLE_PATH, searchParams, params.timeout, params.zyteApiKey);
45
+ return (response.data || [])
46
+ .filter((item) => item.siteNo && item.movNo && item.scnYmd)
47
+ .map((item) => ({
48
+ scheduleId: `${item.scnYmd}${item.siteNo}${item.scnSseq || ''}`,
49
+ movieCode: item.movNo,
50
+ movieName: item.movNm || '',
51
+ theaterCode: item.siteNo,
52
+ theaterName: item.siteNm || '',
53
+ playDate: item.scnYmd,
54
+ startTime: formatTime(item.scnsrtTm),
55
+ endTime: formatTime(item.scnendTm),
56
+ totalSeats: toNumber(item.stcnt),
57
+ remainingSeats: toNumber(item.frSeatCnt || item.frtmpSeatCnt),
58
+ }));
59
+ }
60
+ async function fetchTimetableBySite(playDate, theaterCode, params) {
61
+ const searchParams = new URLSearchParams({
62
+ coCd: CGV_API.COMPANY_CODE,
63
+ siteNo: theaterCode,
64
+ scnYmd: playDate,
65
+ rtctlScopCd: CGV_API.TIMETABLE_SITE_SCOPE_CODE,
66
+ });
67
+ const response = await requestCgv(CGV_API.TIMETABLE_BY_SITE_PATH, searchParams, params.timeout, params.zyteApiKey);
68
+ return (response.data || [])
69
+ .filter((item) => item.siteNo && item.scnYmd)
70
+ .map((item) => ({
71
+ scheduleId: `${item.scnYmd}${item.siteNo}${item.scnSseq || ''}`,
72
+ movieCode: (item.movNo || ''),
73
+ movieName: item.movNm || item.prodNm || '',
74
+ theaterCode: item.siteNo,
75
+ theaterName: item.siteNm || '',
76
+ playDate: item.scnYmd,
77
+ startTime: formatTime(item.scnsrtTm),
78
+ endTime: formatTime(item.scnendTm),
79
+ totalSeats: toNumber(item.stcnt),
80
+ remainingSeats: toNumber(item.frSeatCnt || item.frtmpSeatCnt),
81
+ }));
82
+ }
83
+ export async function fetchCgvTheaters(params) {
84
+ const searchParams = new URLSearchParams({
85
+ coCd: CGV_API.COMPANY_CODE,
86
+ });
87
+ const response = await requestCgv(CGV_API.THEATER_LIST_PATH, searchParams, params.timeout, params.zyteApiKey);
88
+ const list = (response.data || []).flatMap((region) => (region.siteList || []).map((site) => ({
89
+ theaterCode: site.siteNo || '',
90
+ theaterName: site.siteNm || '',
91
+ regionCode: region.regnGrpCd || undefined,
92
+ regionName: region.regnGrpNm || '',
93
+ })));
94
+ return list
95
+ .filter((item) => item.theaterCode && item.theaterName)
96
+ .filter((item) => (params.regionCode ? item.regionCode === params.regionCode : true))
97
+ .map(({ theaterCode, theaterName, regionCode }) => ({ theaterCode, theaterName, regionCode }));
98
+ }
99
+ export async function fetchCgvMovies(params) {
100
+ const playDate = params.playDate || toYyyymmdd();
101
+ const theaterCode = await resolveTheaterCode(playDate, params.theaterCode, params);
102
+ return fetchMoviesByTheaterCode(playDate, theaterCode, params);
103
+ }
104
+ function pickFallbackTheaterCodes(theaters) {
105
+ const uniqueCodes = theaters
106
+ .map((theater) => theater.theaterCode)
107
+ .filter((code, index, array) => code && array.indexOf(code) === index);
108
+ return [DEFAULT_THEATER_CODE, ...uniqueCodes.filter((code) => code !== DEFAULT_THEATER_CODE)].slice(0, MAX_FALLBACK_THEATERS);
109
+ }
110
+ export async function fetchCgvTimetable(params) {
111
+ const playDate = params.playDate || toYyyymmdd();
112
+ const theaterCode = await resolveTheaterCode(playDate, params.theaterCode, params);
113
+ const timetableBySite = await fetchTimetableBySite(playDate, theaterCode, params);
114
+ if (timetableBySite.length > 0) {
115
+ if (params.movieCode) {
116
+ return timetableBySite.filter((item) => item.movieCode === params.movieCode);
117
+ }
118
+ return timetableBySite;
119
+ }
120
+ if (params.movieCode) {
121
+ return fetchTimetableByMovieCode(playDate, theaterCode, params.movieCode, params);
122
+ }
123
+ const theaterCodes = params.theaterCode
124
+ ? [params.theaterCode]
125
+ : pickFallbackTheaterCodes(await fetchCgvTheaters({
126
+ playDate,
127
+ regionCode: params.regionCode,
128
+ timeout: params.timeout,
129
+ zyteApiKey: params.zyteApiKey,
130
+ }));
131
+ for (const theaterCode of theaterCodes) {
132
+ const movies = await fetchMoviesByTheaterCode(playDate, theaterCode, params);
133
+ const timetableByTheater = [];
134
+ for (const movie of movies) {
135
+ const timetable = await fetchTimetableByMovieCode(playDate, theaterCode, movie.movieCode, params);
136
+ if (timetable.length > 0) {
137
+ timetableByTheater.push(...timetable);
138
+ }
139
+ }
140
+ if (timetableByTheater.length > 0) {
141
+ return timetableByTheater;
142
+ }
143
+ }
144
+ return [];
145
+ }
146
+ export { toYyyymmdd };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * CGV 서비스 프로바이더
3
+ */
4
+ import { createFindTheatersTool } from './tools/findTheaters.js';
5
+ import { createSearchMoviesTool } from './tools/searchMovies.js';
6
+ import { createGetTimetableTool } from './tools/getTimetable.js';
7
+ const CGV_METADATA = {
8
+ id: 'cgv',
9
+ name: 'CGV',
10
+ version: '1.0.0',
11
+ description: 'CGV 극장 검색, 영화 검색, 시간표 조회 서비스',
12
+ };
13
+ class CgvService {
14
+ options;
15
+ constructor(options = {}) {
16
+ this.options = options;
17
+ }
18
+ metadata = CGV_METADATA;
19
+ getTools() {
20
+ return [
21
+ createFindTheatersTool(this.options.zyteApiKey),
22
+ createSearchMoviesTool(this.options.zyteApiKey),
23
+ createGetTimetableTool(this.options.zyteApiKey),
24
+ ];
25
+ }
26
+ }
27
+ export function createCgvService(options = {}) {
28
+ return new CgvService(options);
29
+ }
30
+ export * from './types.js';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * CGV 시간표 후처리 유틸리티
3
+ */
4
+ export function filterAndSortTimetable(timetable, options) {
5
+ const { theaterCode, movieCode, limit } = options;
6
+ return timetable
7
+ .filter((item) => (theaterCode ? item.theaterCode === theaterCode : true))
8
+ .filter((item) => (movieCode ? item.movieCode === movieCode : true))
9
+ .sort((a, b) => {
10
+ if (a.startTime === b.startTime) {
11
+ return a.theaterName.localeCompare(b.theaterName);
12
+ }
13
+ return a.startTime.localeCompare(b.startTime);
14
+ })
15
+ .slice(0, limit);
16
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * CGV 극장 검색 도구
3
+ */
4
+ import * as z from 'zod';
5
+ import { fetchCgvTheaters, toYyyymmdd } from '../client.js';
6
+ async function findTheaters(args, apiKey) {
7
+ const { playDate = toYyyymmdd(), regionCode, limit = 30, timeoutMs = 15000 } = args;
8
+ const theaters = await fetchCgvTheaters({
9
+ playDate,
10
+ regionCode,
11
+ timeout: timeoutMs,
12
+ zyteApiKey: apiKey,
13
+ });
14
+ const sliced = theaters.slice(0, limit);
15
+ const result = {
16
+ playDate,
17
+ filters: {
18
+ regionCode: regionCode || null,
19
+ limit,
20
+ },
21
+ count: sliced.length,
22
+ theaters: sliced,
23
+ };
24
+ return {
25
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
26
+ };
27
+ }
28
+ export function createFindTheatersTool(apiKey) {
29
+ return {
30
+ name: 'cgv_find_theaters',
31
+ metadata: {
32
+ title: 'CGV 극장 검색',
33
+ description: '지역 코드 기준으로 CGV 극장 목록을 조회합니다.',
34
+ inputSchema: {
35
+ playDate: z.string().optional().describe('조회 날짜(YYYYMMDD, 기본값: 오늘)'),
36
+ regionCode: z.string().optional().describe('지역 코드 (예: 01 서울)'),
37
+ limit: z.number().optional().default(30).describe('최대 결과 수 (기본값: 30)'),
38
+ timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
39
+ },
40
+ },
41
+ handler: ((args) => findTheaters(args, apiKey)),
42
+ };
43
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * CGV 시간표 조회 도구
3
+ */
4
+ import * as z from 'zod';
5
+ import { fetchCgvTimetable, toYyyymmdd } from '../client.js';
6
+ import { filterAndSortTimetable } from '../timetable.js';
7
+ async function getTimetable(args, apiKey) {
8
+ const { playDate = toYyyymmdd(), theaterCode, movieCode, limit = 50, timeoutMs = 15000, } = args;
9
+ const timetable = await fetchCgvTimetable({
10
+ playDate,
11
+ theaterCode,
12
+ movieCode,
13
+ timeout: timeoutMs,
14
+ zyteApiKey: apiKey,
15
+ });
16
+ const filtered = filterAndSortTimetable(timetable, { theaterCode, movieCode, limit });
17
+ const result = {
18
+ playDate,
19
+ filters: {
20
+ theaterCode: theaterCode || null,
21
+ movieCode: movieCode || null,
22
+ limit,
23
+ },
24
+ count: filtered.length,
25
+ timetable: filtered,
26
+ };
27
+ return {
28
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
29
+ };
30
+ }
31
+ export function createGetTimetableTool(apiKey) {
32
+ return {
33
+ name: 'cgv_get_timetable',
34
+ metadata: {
35
+ title: 'CGV 시간표 조회',
36
+ description: '날짜/극장/영화 조건으로 CGV 상영 시간표를 조회합니다.',
37
+ inputSchema: {
38
+ playDate: z.string().optional().describe('조회 날짜(YYYYMMDD, 기본값: 오늘)'),
39
+ theaterCode: z.string().optional().describe('CGV 극장 코드 (예: 0056)'),
40
+ movieCode: z.string().optional().describe('CGV 영화 코드'),
41
+ limit: z.number().optional().default(50).describe('최대 결과 수 (기본값: 50)'),
42
+ timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
43
+ },
44
+ },
45
+ handler: ((args) => getTimetable(args, apiKey)),
46
+ };
47
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * CGV 영화 검색 도구
3
+ */
4
+ import * as z from 'zod';
5
+ import { fetchCgvMovies, toYyyymmdd } from '../client.js';
6
+ async function searchMovies(args, apiKey) {
7
+ const { playDate = toYyyymmdd(), theaterCode, timeoutMs = 15000 } = args;
8
+ const movies = await fetchCgvMovies({
9
+ playDate,
10
+ theaterCode,
11
+ timeout: timeoutMs,
12
+ zyteApiKey: apiKey,
13
+ });
14
+ const result = {
15
+ playDate,
16
+ filters: {
17
+ theaterCode: theaterCode || null,
18
+ },
19
+ count: movies.length,
20
+ movies,
21
+ };
22
+ return {
23
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
24
+ };
25
+ }
26
+ export function createSearchMoviesTool(apiKey) {
27
+ return {
28
+ name: 'cgv_search_movies',
29
+ metadata: {
30
+ title: 'CGV 영화 검색',
31
+ description: 'CGV 상영 영화 목록을 조회합니다.',
32
+ inputSchema: {
33
+ playDate: z.string().optional().describe('조회 날짜(YYYYMMDD, 기본값: 오늘)'),
34
+ theaterCode: z.string().optional().describe('CGV 극장 코드 (예: 0056)'),
35
+ timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
36
+ },
37
+ },
38
+ handler: ((args) => searchMovies(args, apiKey)),
39
+ };
40
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * CGV 전송 계층
3
+ *
4
+ * 서명 헤더 생성, 직접 호출, Zyte fallback을 담당합니다.
5
+ */
6
+ import { createTimeoutController } from '../../utils/http.js';
7
+ import { decodeZyteHttpBody, requestByZyte } from '../../utils/zyte.js';
8
+ import { CGV_API } from './api.js';
9
+ function toBase64(bytes) {
10
+ /* c8 ignore start */
11
+ if (typeof Buffer !== 'undefined') {
12
+ return Buffer.from(bytes).toString('base64');
13
+ }
14
+ /* c8 ignore end */
15
+ let binary = '';
16
+ for (const byte of bytes) {
17
+ binary += String.fromCharCode(byte);
18
+ }
19
+ if (typeof btoa === 'function') {
20
+ return btoa(binary);
21
+ }
22
+ /* c8 ignore next */
23
+ throw new Error('Base64 인코딩을 지원하지 않는 런타임입니다.');
24
+ }
25
+ async function createSignature(pathname, bodyText, timestamp) {
26
+ const payload = `${timestamp}|${pathname}|${bodyText}`;
27
+ const encoder = new TextEncoder();
28
+ const key = await crypto.subtle.importKey('raw', encoder.encode(CGV_API.SIGNING_SECRET), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
29
+ const signed = await crypto.subtle.sign('HMAC', key, encoder.encode(payload));
30
+ return toBase64(new Uint8Array(signed));
31
+ }
32
+ async function parseJsonResponse(response) {
33
+ const text = await response.text();
34
+ try {
35
+ return JSON.parse(text);
36
+ }
37
+ catch {
38
+ throw new Error(text.slice(0, 120) || 'CGV API 응답 파싱 실패');
39
+ }
40
+ }
41
+ async function requestByZyteCgv(path, searchParams, timeout, apiKey) {
42
+ const url = `${CGV_API.BASE_URL}${path}?${searchParams.toString()}`;
43
+ const timestamp = Math.floor(Date.now() / 1000).toString();
44
+ const signature = await createSignature(path, '', timestamp);
45
+ const result = await requestByZyte({
46
+ apiKey,
47
+ timeout,
48
+ url,
49
+ headers: [
50
+ { name: 'Accept', value: 'application/json' },
51
+ { name: 'Accept-Language', value: 'ko-KR' },
52
+ { name: 'X-TIMESTAMP', value: timestamp },
53
+ { name: 'X-SIGNATURE', value: signature },
54
+ ],
55
+ });
56
+ return decodeZyteHttpBody(result);
57
+ }
58
+ export async function requestCgv(path, searchParams, timeout = 15000, zyteApiKey) {
59
+ const url = `${CGV_API.BASE_URL}${path}?${searchParams.toString()}`;
60
+ const timestamp = Math.floor(Date.now() / 1000).toString();
61
+ const signature = await createSignature(path, '', timestamp);
62
+ const { controller, timeoutId } = createTimeoutController(timeout);
63
+ try {
64
+ const response = await fetch(url, {
65
+ method: 'GET',
66
+ headers: {
67
+ Accept: 'application/json',
68
+ 'Accept-Language': 'ko-KR',
69
+ 'X-TIMESTAMP': timestamp,
70
+ 'X-SIGNATURE': signature,
71
+ },
72
+ signal: controller.signal,
73
+ });
74
+ if (response.ok) {
75
+ return await parseJsonResponse(response);
76
+ }
77
+ if (response.status === 403 && zyteApiKey) {
78
+ return await requestByZyteCgv(path, searchParams, timeout, zyteApiKey);
79
+ }
80
+ throw new Error(`CGV API 호출 실패: ${response.status}`);
81
+ }
82
+ catch (error) {
83
+ if (error instanceof Error && error.name === 'AbortError') {
84
+ throw new Error('CGV API 요청 시간 초과');
85
+ }
86
+ throw error;
87
+ }
88
+ finally {
89
+ clearTimeout(timeoutId);
90
+ }
91
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * CGV 서비스 전용 타입 정의
3
+ */
4
+ export {};
@@ -0,0 +1,53 @@
1
+ /**
2
+ * 다이소 API 엔드포인트 중앙 관리
3
+ *
4
+ * 모든 API URL을 한 곳에서 관리하여 유지보수성을 높입니다.
5
+ */
6
+ /**
7
+ * 다이소몰 API (daisomall.co.kr)
8
+ * 상품 검색, 재고 조회 등
9
+ */
10
+ export const DAISOMALL_API = {
11
+ /** 상품 검색 API */
12
+ SEARCH_PRODUCTS: 'https://prdm.daisomall.co.kr/ssn/search/FindStoreGoods',
13
+ /** 온라인 재고 조회 API */
14
+ ONLINE_STOCK: 'https://mapi.daisomall.co.kr/ms/msg/selOnlStck',
15
+ /** 매장별 재고 조회 API */
16
+ STORE_INVENTORY: 'https://mapi.daisomall.co.kr/ms/msg/newIntSelStr',
17
+ /** 이미지 CDN 베이스 URL */
18
+ IMAGE_BASE_URL: 'https://img.daisomall.co.kr',
19
+ };
20
+ /**
21
+ * 다이소 공식 사이트 API (daiso.co.kr)
22
+ * 매장 검색 등
23
+ */
24
+ export const DAISO_WEB_API = {
25
+ /** 매장 검색 API */
26
+ SHOP_SEARCH: 'https://www.daiso.co.kr/cs/ajax/shop_search',
27
+ /** 시도별 구군 목록 조회 */
28
+ SIDO_SEARCH: 'https://www.daiso.co.kr/cs/ajax/sido_search',
29
+ /** 구군별 동 목록 조회 */
30
+ GUGUN_SEARCH: 'https://www.daiso.co.kr/cs/ajax/gugun_search',
31
+ };
32
+ /**
33
+ * 이미지 URL 생성 헬퍼
34
+ * @param path 이미지 경로 (API 응답에서 받은 상대 경로)
35
+ * @returns 전체 이미지 URL 또는 undefined
36
+ */
37
+ export function getImageUrl(path) {
38
+ if (!path)
39
+ return undefined;
40
+ return `${DAISOMALL_API.IMAGE_BASE_URL}${path}`;
41
+ }
42
+ /**
43
+ * 영업 시간 포맷팅 헬퍼
44
+ * 4자리 숫자(예: 0900)를 시간 형식(예: 09:00)으로 변환
45
+ * @param time 4자리 시간 문자열
46
+ * @returns 포맷팅된 시간 문자열
47
+ */
48
+ export function formatTime(time) {
49
+ if (time.length === 4) {
50
+ return `${time.slice(0, 2)}:${time.slice(2)}`;
51
+ }
52
+ return time;
53
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 다이소 HTTP 클라이언트
3
+ *
4
+ * 다이소 API 호출 시 필요한 기본 헤더를 적용합니다.
5
+ */
6
+ import { fetchJson, fetchText, fetchWithTimeout } from '../../utils/http.js';
7
+ const DAISO_DEFAULT_HEADERS = {
8
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
9
+ Accept: 'application/json, text/html, */*',
10
+ 'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
11
+ };
12
+ function withDaisoHeaders(headers = {}) {
13
+ return {
14
+ ...DAISO_DEFAULT_HEADERS,
15
+ ...headers,
16
+ };
17
+ }
18
+ export async function daisoFetch(url, options = {}) {
19
+ return fetchWithTimeout(url, {
20
+ ...options,
21
+ headers: withDaisoHeaders(options.headers),
22
+ });
23
+ }
24
+ export async function fetchDaisoJson(url, options = {}) {
25
+ return fetchJson(url, {
26
+ ...options,
27
+ headers: withDaisoHeaders(options.headers),
28
+ });
29
+ }
30
+ export async function fetchDaisoHtml(url, options = {}) {
31
+ return fetchText(url, {
32
+ ...options,
33
+ headers: withDaisoHeaders(options.headers),
34
+ });
35
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * 다이소 서비스 프로바이더
3
+ *
4
+ * 다이소 관련 모든 MCP 도구를 제공하는 서비스입니다.
5
+ */
6
+ import { createSearchProductsTool } from './tools/searchProducts.js';
7
+ import { createFindStoresTool } from './tools/findStores.js';
8
+ import { createCheckInventoryTool } from './tools/checkInventory.js';
9
+ import { createGetPriceInfoTool } from './tools/getPriceInfo.js';
10
+ /**
11
+ * 다이소 서비스 메타데이터
12
+ */
13
+ const DAISO_METADATA = {
14
+ id: 'daiso',
15
+ name: '다이소',
16
+ version: '1.0.0',
17
+ description: '다이소 제품 검색, 매장 검색, 재고 확인, 가격 조회 서비스',
18
+ };
19
+ /**
20
+ * 다이소 서비스 프로바이더 클래스
21
+ */
22
+ class DaisoService {
23
+ metadata = DAISO_METADATA;
24
+ getTools() {
25
+ return [
26
+ createSearchProductsTool(),
27
+ createFindStoresTool(),
28
+ createCheckInventoryTool(),
29
+ createGetPriceInfoTool(),
30
+ ];
31
+ }
32
+ }
33
+ /**
34
+ * 다이소 서비스 팩토리 함수
35
+ * ServiceRegistry에 등록할 때 사용합니다.
36
+ */
37
+ export function createDaisoService() {
38
+ return new DaisoService();
39
+ }
40
+ // 타입 및 유틸리티 재내보내기 (필요 시 사용)
41
+ export * from './types.js';
42
+ export { getImageUrl, formatTime } from './api.js';
@@ -0,0 +1,122 @@
1
+ /**
2
+ * 재고 확인 도구
3
+ *
4
+ * 다이소몰 API를 사용하여 매장별 재고를 확인합니다.
5
+ */
6
+ import * as z from 'zod';
7
+ import { DAISOMALL_API } from '../api.js';
8
+ import { fetchDaisoJson } from '../client.js';
9
+ /**
10
+ * 온라인 재고 조회
11
+ */
12
+ export async function fetchOnlineStock(productNo) {
13
+ const data = await fetchDaisoJson(DAISOMALL_API.ONLINE_STOCK, {
14
+ method: 'POST',
15
+ headers: { 'Content-Type': 'application/json' },
16
+ body: JSON.stringify({ pdNo: productNo }),
17
+ });
18
+ if (!data.success) {
19
+ return 0;
20
+ }
21
+ return data.data?.stck || 0;
22
+ }
23
+ /**
24
+ * 매장별 재고 조회
25
+ */
26
+ export async function fetchStoreInventory(productNo, lat, lng, page = 1, pageSize = 30, keyword = '') {
27
+ const data = await fetchDaisoJson(DAISOMALL_API.STORE_INVENTORY, {
28
+ method: 'POST',
29
+ headers: { 'Content-Type': 'application/json' },
30
+ body: JSON.stringify({
31
+ keyword,
32
+ pdNo: productNo,
33
+ curLttd: lat,
34
+ curLitd: lng,
35
+ geolocationAgrYn: 'Y',
36
+ pkupYn: '',
37
+ intCd: '',
38
+ pageSize,
39
+ currentPage: page,
40
+ }),
41
+ });
42
+ if (!data.success || !data.data?.msStrVOList) {
43
+ return { stores: [], totalCount: 0 };
44
+ }
45
+ const stores = data.data.msStrVOList.map((store) => ({
46
+ storeCode: store.strCd,
47
+ storeName: store.strNm,
48
+ address: store.strAddr,
49
+ phone: store.strTno,
50
+ openTime: store.opngTime,
51
+ closeTime: store.clsngTime,
52
+ lat: store.strLttd,
53
+ lng: store.strLitd,
54
+ distance: store.km,
55
+ quantity: parseInt(store.qty) || 0,
56
+ options: {
57
+ parking: store.parkYn === 'Y',
58
+ simCard: store.usimYn === 'Y',
59
+ pickup: store.pkupYn === 'Y',
60
+ taxFree: store.taxfYn === 'Y',
61
+ elevator: store.elvtYn === 'Y',
62
+ ramp: store.entrRampYn === 'Y',
63
+ cashless: store.nocashYn === 'Y',
64
+ },
65
+ }));
66
+ return { stores, totalCount: data.data.intStrCont || stores.length };
67
+ }
68
+ /**
69
+ * 재고 확인 핸들러
70
+ */
71
+ async function checkInventory(args) {
72
+ const { productId, storeQuery = '', latitude = 37.5665, // 기본값: 서울 시청
73
+ longitude = 126.978, page = 1, pageSize = 30, } = args;
74
+ if (!productId || productId.trim().length === 0) {
75
+ throw new Error('상품 ID(productId)를 입력해주세요.');
76
+ }
77
+ // 온라인 재고와 매장 재고 동시 조회
78
+ const [onlineStock, storeResult] = await Promise.all([
79
+ fetchOnlineStock(productId),
80
+ fetchStoreInventory(productId, latitude, longitude, page, pageSize, storeQuery),
81
+ ]);
82
+ // 재고 있는 매장과 없는 매장 분류
83
+ const inStockStores = storeResult.stores.filter((s) => s.quantity > 0);
84
+ const outOfStockStores = storeResult.stores.filter((s) => s.quantity === 0);
85
+ const result = {
86
+ productId,
87
+ location: { latitude, longitude },
88
+ onlineStock,
89
+ storeInventory: {
90
+ totalStores: storeResult.totalCount,
91
+ inStockCount: inStockStores.length,
92
+ outOfStockCount: outOfStockStores.length,
93
+ page,
94
+ pageSize,
95
+ stores: storeResult.stores,
96
+ },
97
+ };
98
+ return {
99
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
100
+ };
101
+ }
102
+ /**
103
+ * 도구 등록 정보 생성
104
+ */
105
+ export function createCheckInventoryTool() {
106
+ return {
107
+ name: 'daiso_check_inventory',
108
+ metadata: {
109
+ title: '재고 확인',
110
+ description: '특정 제품의 매장별 재고를 확인합니다. 매장명/주소 검색 또는 위치 기반으로 조회합니다.',
111
+ inputSchema: {
112
+ productId: z.string().describe('제품 ID (daiso_search_products로 조회한 상품의 id)'),
113
+ storeQuery: z.string().optional().describe('매장 검색어 (매장명 또는 주소, 예: 안산 중앙역)'),
114
+ latitude: z.number().optional().default(37.5665).describe('위도 (기본값: 서울 시청 37.5665)'),
115
+ longitude: z.number().optional().default(126.978).describe('경도 (기본값: 서울 시청 126.978)'),
116
+ page: z.number().optional().default(1).describe('페이지 번호 (기본값: 1)'),
117
+ pageSize: z.number().optional().default(30).describe('페이지당 결과 수 (기본값: 30)'),
118
+ },
119
+ },
120
+ handler: checkInventory,
121
+ };
122
+ }