daiso 1.0.4 → 1.0.6

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 (166) hide show
  1. package/README.md +249 -533
  2. package/dist/api/actionsProxy.js +82 -0
  3. package/dist/api/cgvHandlers.js +103 -16
  4. package/dist/api/configStatus.js +19 -0
  5. package/dist/api/daisoHandlers.js +127 -0
  6. package/dist/api/emart24Handlers.js +176 -0
  7. package/dist/api/gs25Handlers.js +267 -0
  8. package/dist/api/handlers.js +209 -104
  9. package/dist/api/healthChecks.js +504 -0
  10. package/dist/api/lottecinemaHandlers.js +155 -0
  11. package/dist/api/lottemartHandlers.js +144 -0
  12. package/dist/api/megaboxHandlers.js +81 -50
  13. package/dist/api/response.js +2 -0
  14. package/dist/api/routes/cuRoutes.js +17 -0
  15. package/dist/api/routes/daisoRoutes.js +6 -1
  16. package/dist/api/routes/emart24Routes.js +22 -0
  17. package/dist/api/routes/gs25Routes.js +22 -0
  18. package/dist/api/routes/healthRoutes.js +61 -0
  19. package/dist/api/routes/lottecinemaRoutes.js +22 -0
  20. package/dist/api/routes/lottemartRoutes.js +18 -0
  21. package/dist/api/routes/oliveyoungRoutes.js +6 -1
  22. package/dist/api/routes/sevenelevenRoutes.js +32 -0
  23. package/dist/api/sevenelevenHandlers.js +200 -0
  24. package/dist/bin.js +0 -0
  25. package/dist/cli/args.js +75 -0
  26. package/dist/cli/commandOptions.js +38 -0
  27. package/dist/cli/commands/convenience.js +293 -0
  28. package/dist/cli/commands/daiso.js +112 -0
  29. package/dist/cli/constants.js +5 -0
  30. package/dist/cli/deps.js +48 -0
  31. package/dist/cli/http.js +156 -0
  32. package/dist/cli/interactiveFetch.js +31 -0
  33. package/dist/cli/interactiveItemSearch.js +261 -0
  34. package/dist/cli/interactivePrompt.js +50 -0
  35. package/dist/cli/interactiveTypes.js +4 -0
  36. package/dist/cli/types.js +4 -0
  37. package/dist/cli.js +87 -196
  38. package/dist/cliHelp.js +212 -1
  39. package/dist/cliInteractive.js +243 -0
  40. package/dist/cliPicker.js +57 -0
  41. package/dist/cliRenderer.js +90 -3
  42. package/dist/core/errors.js +66 -0
  43. package/dist/core/registry.js +227 -3
  44. package/dist/index.js +149 -9
  45. package/dist/pages/openapi.js +2 -1
  46. package/dist/pages/openapiFullSpec.js +58 -0
  47. package/dist/pages/openapiResponse.js +28 -0
  48. package/dist/pages/openapiSpec.js +2 -41
  49. package/dist/pages/openapiSpecActions.js +109 -0
  50. package/dist/pages/openapiSpecComponents.js +30 -391
  51. package/dist/pages/openapiSpecComponentsCgv.js +24 -0
  52. package/dist/pages/openapiSpecComponentsCu.js +112 -0
  53. package/dist/pages/openapiSpecComponentsDaisoOliveyoung.js +342 -0
  54. package/dist/pages/openapiSpecComponentsEmart24.js +141 -0
  55. package/dist/pages/openapiSpecComponentsGs25.js +109 -0
  56. package/dist/pages/openapiSpecComponentsLotteMart.js +85 -0
  57. package/dist/pages/openapiSpecComponentsLottecinema.js +163 -0
  58. package/dist/pages/openapiSpecComponentsMegabox.js +144 -0
  59. package/dist/pages/openapiSpecComponentsSeveneleven.js +82 -0
  60. package/dist/pages/openapiSpecPathsCgv.js +66 -3
  61. package/dist/pages/openapiSpecPathsCu.js +145 -0
  62. package/dist/pages/openapiSpecPathsDaiso.js +302 -0
  63. package/dist/pages/openapiSpecPathsDaisoOliveyoung.js +4 -376
  64. package/dist/pages/openapiSpecPathsEmart24.js +223 -0
  65. package/dist/pages/openapiSpecPathsGs25.js +166 -0
  66. package/dist/pages/openapiSpecPathsLotteMart.js +149 -0
  67. package/dist/pages/openapiSpecPathsLottecinema.js +212 -0
  68. package/dist/pages/openapiSpecPathsMegabox.js +52 -3
  69. package/dist/pages/openapiSpecPathsOliveyoung.js +266 -0
  70. package/dist/pages/openapiSpecPathsSeveneleven.js +234 -0
  71. package/dist/pages/prompt.js +8 -402
  72. package/dist/pages/promptCoreText.js +278 -0
  73. package/dist/pages/promptReferenceText.js +137 -0
  74. package/dist/pages/promptRetailCinemaText.js +274 -0
  75. package/dist/services/cgv/client.js +6 -1
  76. package/dist/services/cgv/index.js +3 -3
  77. package/dist/services/cgv/location.js +235 -0
  78. package/dist/services/cgv/tools/findTheaters.js +30 -5
  79. package/dist/services/cgv/tools/getTimetable.js +42 -14
  80. package/dist/services/cgv/tools/searchMovies.js +40 -12
  81. package/dist/services/cgv/transport.js +0 -3
  82. package/dist/services/cu/api.js +11 -0
  83. package/dist/services/cu/client.js +297 -0
  84. package/dist/services/cu/index.js +21 -0
  85. package/dist/services/cu/tools/checkInventory.js +123 -0
  86. package/dist/services/cu/tools/findNearbyStores.js +49 -0
  87. package/dist/services/cu/types.js +4 -0
  88. package/dist/services/daiso/api.js +22 -2
  89. package/dist/services/daiso/client.js +67 -0
  90. package/dist/services/daiso/index.js +4 -0
  91. package/dist/services/daiso/product.js +17 -0
  92. package/dist/services/daiso/tools/checkInventory.js +80 -43
  93. package/dist/services/daiso/tools/findInventoryByName.js +222 -0
  94. package/dist/services/daiso/tools/findStores.js +17 -7
  95. package/dist/services/daiso/tools/getDisplayLocation.js +75 -0
  96. package/dist/services/emart24/api.js +11 -0
  97. package/dist/services/emart24/client.js +182 -0
  98. package/dist/services/emart24/index.js +22 -0
  99. package/dist/services/emart24/inventoryLookup.js +134 -0
  100. package/dist/services/emart24/tools/checkInventory.js +54 -0
  101. package/dist/services/emart24/tools/findNearbyStores.js +72 -0
  102. package/dist/services/emart24/tools/searchProducts.js +73 -0
  103. package/dist/services/emart24/types.js +4 -0
  104. package/dist/services/gs25/api.js +9 -0
  105. package/dist/services/gs25/client.js +208 -0
  106. package/dist/services/gs25/index.js +22 -0
  107. package/dist/services/gs25/storeUtils.js +168 -0
  108. package/dist/services/gs25/tools/checkInventory.js +163 -0
  109. package/dist/services/gs25/tools/findNearbyStores.js +103 -0
  110. package/dist/services/gs25/tools/searchProducts.js +57 -0
  111. package/dist/services/gs25/types.js +4 -0
  112. package/dist/services/lottecinema/api.js +12 -0
  113. package/dist/services/lottecinema/client.js +161 -0
  114. package/dist/services/lottecinema/index.js +30 -0
  115. package/dist/services/lottecinema/location.js +191 -0
  116. package/dist/services/lottecinema/tools/findNearbyTheaters.js +47 -0
  117. package/dist/services/lottecinema/tools/getRemainingSeats.js +85 -0
  118. package/dist/services/lottecinema/tools/listNowShowing.js +79 -0
  119. package/dist/services/lottecinema/types.js +4 -0
  120. package/dist/services/lottemart/api.js +31 -0
  121. package/dist/services/lottemart/client.js +254 -0
  122. package/dist/services/lottemart/clientTypes.js +4 -0
  123. package/dist/services/lottemart/config.js +4 -0
  124. package/dist/services/lottemart/debug.js +92 -0
  125. package/dist/services/lottemart/geocode.js +49 -0
  126. package/dist/services/lottemart/index.js +30 -0
  127. package/dist/services/lottemart/keyword.js +29 -0
  128. package/dist/services/lottemart/parser.js +185 -0
  129. package/dist/services/lottemart/session.js +331 -0
  130. package/dist/services/lottemart/storeSearch.js +52 -0
  131. package/dist/services/lottemart/tools/findNearbyStores.js +67 -0
  132. package/dist/services/lottemart/tools/searchProducts.js +69 -0
  133. package/dist/services/lottemart/types.js +4 -0
  134. package/dist/services/lottemart/zetta.js +105 -0
  135. package/dist/services/megabox/location.js +219 -0
  136. package/dist/services/megabox/tools/findNearbyTheaters.js +13 -44
  137. package/dist/services/megabox/tools/getRemainingSeats.js +31 -3
  138. package/dist/services/megabox/tools/listNowShowing.js +31 -3
  139. package/dist/services/oliveyoung/api.js +2 -0
  140. package/dist/services/oliveyoung/client.js +225 -13
  141. package/dist/services/oliveyoung/index.js +3 -1
  142. package/dist/services/oliveyoung/tools/checkInventory.js +21 -5
  143. package/dist/services/oliveyoung/tools/searchProducts.js +57 -0
  144. package/dist/services/seveneleven/api.js +15 -0
  145. package/dist/services/seveneleven/client.js +286 -0
  146. package/dist/services/seveneleven/index.js +30 -0
  147. package/dist/services/seveneleven/inventory.js +302 -0
  148. package/dist/services/seveneleven/productKeyword.js +141 -0
  149. package/dist/services/seveneleven/tools/checkInventory.js +82 -0
  150. package/dist/services/seveneleven/tools/getCatalogSnapshot.js +55 -0
  151. package/dist/services/seveneleven/tools/getSearchPopwords.js +41 -0
  152. package/dist/services/seveneleven/tools/searchProducts.js +67 -0
  153. package/dist/services/seveneleven/tools/searchStores.js +45 -0
  154. package/dist/services/seveneleven/types.js +4 -0
  155. package/dist/utils/cliInteractiveHelpers.js +63 -0
  156. package/dist/utils/daisoKeyword.js +30 -0
  157. package/dist/utils/http.js +98 -12
  158. package/dist/utils/zyte.js +47 -26
  159. package/openapi.json +1979 -736
  160. package/openapi.yaml +1428 -541
  161. package/package.json +49 -18
  162. package/dist/tools/checkInventory.js +0 -98
  163. package/dist/tools/findStores.js +0 -125
  164. package/dist/tools/getPriceInfo.js +0 -77
  165. package/dist/tools/searchProducts.js +0 -61
  166. package/dist/types/index.js +0 -4
@@ -0,0 +1,123 @@
1
+ /**
2
+ * CU 재고 확인 도구
3
+ */
4
+ import * as z from 'zod';
5
+ import { fetchCuStock, fetchCuStores, geocodeCuAddress } from '../client.js';
6
+ async function checkInventory(args) {
7
+ const { keyword, latitude, longitude, storeKeyword = '', size = 20, offset = 0, searchSort = 'recom', storeLimit = 10, timeoutMs = 15000, } = args;
8
+ if (!keyword || keyword.trim().length === 0) {
9
+ throw new Error('상품 검색어(keyword)를 입력해주세요.');
10
+ }
11
+ const stockResult = await fetchCuStock({
12
+ keyword,
13
+ limit: size,
14
+ offset,
15
+ searchSort,
16
+ }, {
17
+ timeout: timeoutMs,
18
+ });
19
+ const firstStockItem = stockResult.items.find((item) => item.itemCode.trim().length > 0) || null;
20
+ const hasInputLocation = typeof latitude === 'number' && typeof longitude === 'number';
21
+ const resolvedLatitude = hasInputLocation ? latitude : undefined;
22
+ const resolvedLongitude = hasInputLocation ? longitude : undefined;
23
+ let storeResult = null;
24
+ // 좌표 미입력 + 매장 키워드 입력 시, 키워드 기반 매장 검색 결과를 우선 사용합니다.
25
+ if (!hasInputLocation && storeKeyword.trim().length > 0) {
26
+ const keywordStoreResult = await fetchCuStores({
27
+ searchWord: storeKeyword,
28
+ }, {
29
+ timeout: timeoutMs,
30
+ });
31
+ const firstAddress = keywordStoreResult.stores.find((store) => store.address.trim().length > 0)?.address || '';
32
+ if (firstAddress.length > 0) {
33
+ const googleMapsApiKey = process.env.GOOGLE_MAPS_API_KEY;
34
+ const geocoded = await geocodeCuAddress(firstAddress, {
35
+ timeout: timeoutMs,
36
+ googleMapsApiKey,
37
+ });
38
+ if (geocoded) {
39
+ const hasStockSeed = !!firstStockItem?.itemCode;
40
+ storeResult = await fetchCuStores({
41
+ latitude: geocoded.latitude,
42
+ longitude: geocoded.longitude,
43
+ searchWord: storeKeyword,
44
+ itemCd: firstStockItem?.itemCode || '',
45
+ onItemNo: firstStockItem?.onItemNo || '',
46
+ jipCd: firstStockItem?.itemCode || '',
47
+ isRecommend: hasStockSeed ? 'Y' : '',
48
+ recommendId: hasStockSeed ? 'stock' : '',
49
+ pageType: hasStockSeed ? 'search_improve stock_sch_improve' : 'search_improve',
50
+ }, {
51
+ timeout: timeoutMs,
52
+ });
53
+ }
54
+ }
55
+ storeResult ||= keywordStoreResult;
56
+ }
57
+ if (!storeResult) {
58
+ const hasStockSeed = !!firstStockItem?.itemCode;
59
+ storeResult = await fetchCuStores({
60
+ latitude: resolvedLatitude,
61
+ longitude: resolvedLongitude,
62
+ searchWord: storeKeyword,
63
+ itemCd: firstStockItem?.itemCode || '',
64
+ onItemNo: firstStockItem?.onItemNo || '',
65
+ jipCd: firstStockItem?.itemCode || '',
66
+ isRecommend: hasStockSeed ? 'Y' : '',
67
+ recommendId: hasStockSeed ? 'stock' : '',
68
+ pageType: hasStockSeed ? 'search_improve stock_sch_improve' : 'search_improve',
69
+ }, {
70
+ timeout: timeoutMs,
71
+ });
72
+ }
73
+ const limitedStores = storeResult.stores.slice(0, storeLimit);
74
+ const result = {
75
+ keyword,
76
+ searchOptions: {
77
+ size,
78
+ offset,
79
+ searchSort,
80
+ storeKeyword,
81
+ },
82
+ location: typeof resolvedLatitude === 'number' && typeof resolvedLongitude === 'number'
83
+ ? { latitude: resolvedLatitude, longitude: resolvedLongitude }
84
+ : null,
85
+ nearbyStores: {
86
+ totalCount: storeResult.totalCount,
87
+ count: limitedStores.length,
88
+ stockItemCode: firstStockItem?.itemCode || null,
89
+ stockItemName: firstStockItem?.itemName || null,
90
+ stores: limitedStores,
91
+ },
92
+ inventory: {
93
+ totalCount: stockResult.totalCount,
94
+ count: stockResult.items.length,
95
+ spellModifyYn: stockResult.spellModifyYn,
96
+ items: stockResult.items,
97
+ },
98
+ };
99
+ return {
100
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
101
+ };
102
+ }
103
+ export function createCheckInventoryTool() {
104
+ return {
105
+ name: 'cu_check_inventory',
106
+ metadata: {
107
+ title: 'CU 재고 확인',
108
+ description: '상품 키워드로 CU 재고를 조회하고 주변 매장 정보를 함께 반환합니다.',
109
+ inputSchema: {
110
+ keyword: z.string().describe('재고를 확인할 상품 검색어 (예: 과자, 커피, 도시락)'),
111
+ latitude: z.number().optional().describe('위도 (없으면 storeKeyword 기반으로 좌표 추정 시도)'),
112
+ longitude: z.number().optional().describe('경도 (없으면 storeKeyword 기반으로 좌표 추정 시도)'),
113
+ storeKeyword: z.string().optional().describe('주변 매장 필터 키워드 (예: 강남, 안산)'),
114
+ size: z.number().optional().default(20).describe('재고 검색 결과 수 (기본값: 20)'),
115
+ offset: z.number().optional().default(0).describe('재고 검색 시작 오프셋 (기본값: 0)'),
116
+ searchSort: z.string().optional().default('recom').describe('재고 정렬 방식 (기본값: recom)'),
117
+ storeLimit: z.number().optional().default(10).describe('반환할 주변 매장 최대 수 (기본값: 10)'),
118
+ timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
119
+ },
120
+ },
121
+ handler: checkInventory,
122
+ };
123
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * CU 주변 매장 탐색 도구
3
+ */
4
+ import * as z from 'zod';
5
+ import { fetchCuStores } from '../client.js';
6
+ async function findNearbyStores(args) {
7
+ const { latitude, longitude, keyword = '', limit = 20, timeoutMs = 15000, } = args;
8
+ const { totalCount, stores } = await fetchCuStores({
9
+ latitude,
10
+ longitude,
11
+ searchWord: keyword,
12
+ }, {
13
+ timeout: timeoutMs,
14
+ });
15
+ const limitedStores = stores.slice(0, limit);
16
+ return {
17
+ content: [
18
+ {
19
+ type: 'text',
20
+ text: JSON.stringify({
21
+ location: typeof latitude === 'number' && typeof longitude === 'number'
22
+ ? { latitude, longitude }
23
+ : null,
24
+ keyword,
25
+ totalCount,
26
+ count: limitedStores.length,
27
+ stores: limitedStores,
28
+ }, null, 2),
29
+ },
30
+ ],
31
+ };
32
+ }
33
+ export function createFindNearbyStoresTool() {
34
+ return {
35
+ name: 'cu_find_nearby_stores',
36
+ metadata: {
37
+ title: 'CU 주변 매장 탐색',
38
+ description: '위치(위도/경도)와 키워드로 CU 매장을 검색합니다.',
39
+ inputSchema: {
40
+ latitude: z.number().optional().describe('위도 (없으면 키워드 기반 검색)'),
41
+ longitude: z.number().optional().describe('경도 (없으면 키워드 기반 검색)'),
42
+ keyword: z.string().optional().describe('매장 검색 키워드 (예: 강남, 명동, 안산)'),
43
+ limit: z.number().optional().default(20).describe('반환할 최대 매장 수 (기본값: 20)'),
44
+ timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
45
+ },
46
+ },
47
+ handler: findNearbyStores,
48
+ };
49
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * CU 서비스 전용 타입 정의
3
+ */
4
+ export {};
@@ -12,10 +12,18 @@ export const DAISOMALL_API = {
12
12
  SEARCH_PRODUCTS: 'https://prdm.daisomall.co.kr/ssn/search/FindStoreGoods',
13
13
  /** 온라인 재고 조회 API */
14
14
  ONLINE_STOCK: 'https://mapi.daisomall.co.kr/ms/msg/selOnlStck',
15
- /** 매장별 재고 조회 API */
15
+ /** 구 재고 조회 API (현재 403 Unauthorized 발생) */
16
16
  STORE_INVENTORY: 'https://mapi.daisomall.co.kr/ms/msg/newIntSelStr',
17
+ /** 현재 사용 중인 인증 토큰 요청 API */
18
+ AUTH_REQUEST: 'https://fapi.daisomall.co.kr/auth/request',
19
+ /** 현재 사용 중인 위치 기반 매장 조회 API */
20
+ STORE_SEARCH_V2: 'https://fapi.daisomall.co.kr/ms/msg/selStr',
21
+ /** 현재 사용 중인 매장별 재고 조회 API */
22
+ STORE_INVENTORY_V2: 'https://fapi.daisomall.co.kr/pd/pdh/selStrPkupStck',
23
+ /** 매장 내 상품 진열 위치 조회 API */
24
+ DISPLAY_LOCATION: 'https://fapi.daisomall.co.kr/pdo/selIntPdStDispInfo',
17
25
  /** 이미지 CDN 베이스 URL */
18
- IMAGE_BASE_URL: 'https://img.daisomall.co.kr',
26
+ IMAGE_BASE_URL: 'https://cdn.daisomall.co.kr',
19
27
  };
20
28
  /**
21
29
  * 다이소 공식 사이트 API (daiso.co.kr)
@@ -37,6 +45,18 @@ export const DAISO_WEB_API = {
37
45
  export function getImageUrl(path) {
38
46
  if (!path)
39
47
  return undefined;
48
+ if (path.startsWith('http://') || path.startsWith('https://')) {
49
+ try {
50
+ const url = new URL(path);
51
+ if (url.hostname === 'img.daisomall.co.kr') {
52
+ url.hostname = 'cdn.daisomall.co.kr';
53
+ }
54
+ return url.toString();
55
+ }
56
+ catch {
57
+ return path;
58
+ }
59
+ }
40
60
  return `${DAISOMALL_API.IMAGE_BASE_URL}${path}`;
41
61
  }
42
62
  /**
@@ -4,11 +4,17 @@
4
4
  * 다이소 API 호출 시 필요한 기본 헤더를 적용합니다.
5
5
  */
6
6
  import { fetchJson, fetchText, fetchWithTimeout } from '../../utils/http.js';
7
+ import { DAISOMALL_API } from './api.js';
8
+ const DAISO_AUTH_KEY = 'PRE_AUTH_ENC_KEY';
7
9
  const DAISO_DEFAULT_HEADERS = {
8
10
  '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
11
  Accept: 'application/json, text/html, */*',
10
12
  'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
11
13
  };
14
+ const DAISO_DEFAULT_FETCH_OPTIONS = {
15
+ retries: 1,
16
+ retryDelayMs: 250,
17
+ };
12
18
  function withDaisoHeaders(headers = {}) {
13
19
  return {
14
20
  ...DAISO_DEFAULT_HEADERS,
@@ -17,19 +23,80 @@ function withDaisoHeaders(headers = {}) {
17
23
  }
18
24
  export async function daisoFetch(url, options = {}) {
19
25
  return fetchWithTimeout(url, {
26
+ ...DAISO_DEFAULT_FETCH_OPTIONS,
20
27
  ...options,
21
28
  headers: withDaisoHeaders(options.headers),
22
29
  });
23
30
  }
24
31
  export async function fetchDaisoJson(url, options = {}) {
25
32
  return fetchJson(url, {
33
+ ...DAISO_DEFAULT_FETCH_OPTIONS,
26
34
  ...options,
27
35
  headers: withDaisoHeaders(options.headers),
28
36
  });
29
37
  }
30
38
  export async function fetchDaisoHtml(url, options = {}) {
31
39
  return fetchText(url, {
40
+ ...DAISO_DEFAULT_FETCH_OPTIONS,
32
41
  ...options,
33
42
  headers: withDaisoHeaders(options.headers),
34
43
  });
35
44
  }
45
+ function base64FromBytes(bytes) {
46
+ /* c8 ignore next -- Node 20 CI always provides Buffer; the fallback below is for browser-like runtimes. */
47
+ if (typeof Buffer !== 'undefined') {
48
+ return Buffer.from(bytes).toString('base64');
49
+ }
50
+ /* c8 ignore start -- Node 20 CI always provides Buffer; this fallback is for browser-like runtimes. */
51
+ let binary = '';
52
+ for (const byte of bytes) {
53
+ binary += String.fromCharCode(byte);
54
+ }
55
+ if (typeof btoa === 'function') {
56
+ return btoa(binary);
57
+ }
58
+ throw new Error('Base64 인코딩을 지원하지 않는 환경입니다.');
59
+ /* c8 ignore stop */
60
+ }
61
+ async function createDaisoAuthHeader(token) {
62
+ const encoder = new TextEncoder();
63
+ const iv = crypto.getRandomValues(new Uint8Array(16));
64
+ const key = await crypto.subtle.importKey('raw', encoder.encode(DAISO_AUTH_KEY), { name: 'AES-CBC' }, false, ['encrypt']);
65
+ const encrypted = await crypto.subtle.encrypt({ name: 'AES-CBC', iv }, key, encoder.encode(token));
66
+ return `${base64FromBytes(iv)}${base64FromBytes(new Uint8Array(encrypted))}`;
67
+ }
68
+ export async function createDaisoAuthContext() {
69
+ const response = await daisoFetch(DAISOMALL_API.AUTH_REQUEST, {
70
+ method: 'GET',
71
+ });
72
+ if (!response.ok) {
73
+ throw new Error(`다이소 인증 토큰 요청 실패: ${response.status} ${response.statusText}`);
74
+ }
75
+ const token = (await response.text()).trim();
76
+ const dmUid = response.headers.get('x-dm-uid')?.trim() || '';
77
+ if (!token) {
78
+ throw new Error('다이소 인증 토큰이 비어 있습니다.');
79
+ }
80
+ if (!dmUid) {
81
+ throw new Error('다이소 인증 응답에 X-DM-UID 헤더가 없습니다.');
82
+ }
83
+ const authorization = await createDaisoAuthHeader(token);
84
+ return {
85
+ authorization: `Bearer ${authorization}`,
86
+ dmUid,
87
+ cookie: `DM_UID=${dmUid}`,
88
+ };
89
+ }
90
+ export async function fetchDaisoJsonWithAuth(url, options = {}) {
91
+ const auth = await createDaisoAuthContext();
92
+ return fetchJson(url, {
93
+ ...DAISO_DEFAULT_FETCH_OPTIONS,
94
+ ...options,
95
+ headers: withDaisoHeaders({
96
+ ...options.headers,
97
+ Authorization: auth.authorization,
98
+ 'X-DM-UID': auth.dmUid,
99
+ Cookie: auth.cookie,
100
+ }),
101
+ });
102
+ }
@@ -6,7 +6,9 @@
6
6
  import { createSearchProductsTool } from './tools/searchProducts.js';
7
7
  import { createFindStoresTool } from './tools/findStores.js';
8
8
  import { createCheckInventoryTool } from './tools/checkInventory.js';
9
+ import { createFindInventoryByNameTool } from './tools/findInventoryByName.js';
9
10
  import { createGetPriceInfoTool } from './tools/getPriceInfo.js';
11
+ import { createGetDisplayLocationTool } from './tools/getDisplayLocation.js';
10
12
  /**
11
13
  * 다이소 서비스 메타데이터
12
14
  */
@@ -26,7 +28,9 @@ class DaisoService {
26
28
  createSearchProductsTool(),
27
29
  createFindStoresTool(),
28
30
  createCheckInventoryTool(),
31
+ createFindInventoryByNameTool(),
29
32
  createGetPriceInfoTool(),
33
+ createGetDisplayLocationTool(),
30
34
  ];
31
35
  }
32
36
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 다이소 상품 매핑 유틸리티
3
+ */
4
+ import { getImageUrl } from './api.js';
5
+ /**
6
+ * 검색/상세 응답의 상품 문서를 공통 요약 형태로 변환합니다.
7
+ */
8
+ export function toProductSummary(product) {
9
+ return {
10
+ id: product.PD_NO,
11
+ name: product.PDNM || product.EXH_PD_NM || '',
12
+ imageUrl: getImageUrl(product.ATCH_FILE_URL),
13
+ brand: product.BRND_NM || undefined,
14
+ soldOut: product.SOLD_OUT_YN === 'Y',
15
+ isNew: product.NEW_PD_YN === 'Y',
16
+ };
17
+ }
@@ -5,7 +5,23 @@
5
5
  */
6
6
  import * as z from 'zod';
7
7
  import { DAISOMALL_API } from '../api.js';
8
- import { fetchDaisoJson } from '../client.js';
8
+ import { fetchDaisoJson, fetchDaisoJsonWithAuth } from '../client.js';
9
+ import { buildDaisoStoreKeywordVariants } from '../../../utils/daisoKeyword.js';
10
+ import { fetchProductById } from './getPriceInfo.js';
11
+ import { toProductSummary } from '../product.js';
12
+ /**
13
+ * 재고 응답에 함께 노출할 상품 요약 정보를 조회합니다.
14
+ * 부가 정보이므로 실패해도 전체 재고 조회는 계속 진행합니다.
15
+ */
16
+ async function fetchInventoryProduct(productId) {
17
+ try {
18
+ const product = await fetchProductById(productId);
19
+ return product ? toProductSummary(product) : undefined;
20
+ }
21
+ catch {
22
+ return undefined;
23
+ }
24
+ }
9
25
  /**
10
26
  * 온라인 재고 조회
11
27
  */
@@ -24,46 +40,63 @@ export async function fetchOnlineStock(productNo) {
24
40
  * 매장별 재고 조회
25
41
  */
26
42
  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 };
43
+ const searchKeywords = keyword ? buildDaisoStoreKeywordVariants(keyword) : [''];
44
+ for (const searchKeyword of searchKeywords) {
45
+ const storeSearch = await fetchDaisoJson(DAISOMALL_API.STORE_SEARCH_V2, {
46
+ method: 'POST',
47
+ headers: { 'Content-Type': 'application/json' },
48
+ body: JSON.stringify({
49
+ inclusiveStrCd: '',
50
+ keyword: searchKeyword,
51
+ curLttd: lat,
52
+ curLitd: lng,
53
+ }),
54
+ });
55
+ const allStores = storeSearch.data || [];
56
+ const searchedStores = allStores.slice((page - 1) * pageSize, page * pageSize);
57
+ if (searchedStores.length === 0) {
58
+ if (searchKeyword === searchKeywords[searchKeywords.length - 1]) {
59
+ return { stores: [], totalCount: 0 };
60
+ }
61
+ continue;
62
+ }
63
+ const inventoryResponse = await fetchDaisoJsonWithAuth(DAISOMALL_API.STORE_INVENTORY_V2, {
64
+ method: 'POST',
65
+ headers: { 'Content-Type': 'application/json' },
66
+ body: JSON.stringify(searchedStores.map((store) => ({
67
+ pdNo: productNo,
68
+ strCd: store.strCd,
69
+ }))),
70
+ });
71
+ const quantities = new Map((inventoryResponse.data || []).map((item) => [item.strCd, parseInt(item.stck) || 0]));
72
+ const stores = searchedStores.map((store) => ({
73
+ storeCode: store.strCd,
74
+ storeName: store.strNm,
75
+ address: store.strAddr,
76
+ phone: store.strTno,
77
+ openTime: store.opngTime,
78
+ closeTime: store.clsngTime,
79
+ lat: store.strLttd,
80
+ lng: store.strLitd,
81
+ distance: store.km,
82
+ quantity: quantities.get(store.strCd) ?? 0,
83
+ options: {
84
+ parking: store.parkYn === 'Y',
85
+ simCard: store.usimYn === 'Y',
86
+ pickup: store.pkupYn === 'Y',
87
+ taxFree: store.taxfYn === 'Y',
88
+ elevator: store.elvtYn === 'Y',
89
+ ramp: store.entrRampYn === 'Y',
90
+ cashless: store.nocashYn === 'Y',
91
+ },
92
+ }));
93
+ return {
94
+ stores,
95
+ totalCount: allStores.length,
96
+ };
44
97
  }
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 };
98
+ /* c8 ignore next -- searchKeywords is always non-empty; this is a defensive fallback. */
99
+ return { stores: [], totalCount: 0 };
67
100
  }
68
101
  /**
69
102
  * 재고 확인 핸들러
@@ -75,15 +108,17 @@ async function checkInventory(args) {
75
108
  throw new Error('상품 ID(productId)를 입력해주세요.');
76
109
  }
77
110
  // 온라인 재고와 매장 재고 동시 조회
78
- const [onlineStock, storeResult] = await Promise.all([
111
+ const [onlineStock, storeResult, product] = await Promise.all([
79
112
  fetchOnlineStock(productId),
80
113
  fetchStoreInventory(productId, latitude, longitude, page, pageSize, storeQuery),
114
+ fetchInventoryProduct(productId),
81
115
  ]);
82
116
  // 재고 있는 매장과 없는 매장 분류
83
117
  const inStockStores = storeResult.stores.filter((s) => s.quantity > 0);
84
118
  const outOfStockStores = storeResult.stores.filter((s) => s.quantity === 0);
85
119
  const result = {
86
120
  productId,
121
+ product,
87
122
  location: { latitude, longitude },
88
123
  onlineStock,
89
124
  storeInventory: {
@@ -107,9 +142,11 @@ export function createCheckInventoryTool() {
107
142
  name: 'daiso_check_inventory',
108
143
  metadata: {
109
144
  title: '재고 확인',
110
- description: '특정 제품의 매장별 재고를 확인합니다. 매장명/주소 검색 또는 위치 기반으로 조회합니다.',
145
+ description: '특정 제품의 매장별 재고를 확인합니다. 상품명만 아는 경우 daiso_search_products로 후보를 먼저 찾은 뒤 productId를 전달하세요. 매장명/주소 검색 또는 위치 기반으로 조회합니다. "근처/주변"처럼 위치가 모호하면 storeQuery, latitude/longitude 또는 사용자 위치를 먼저 확인하세요.',
111
146
  inputSchema: {
112
- productId: z.string().describe('제품 ID (daiso_search_products로 조회한 상품의 id)'),
147
+ productId: z
148
+ .string()
149
+ .describe('제품 ID. 상품명만 알면 먼저 daiso_search_products로 상품의 id를 확인하세요.'),
113
150
  storeQuery: z.string().optional().describe('매장 검색어 (매장명 또는 주소, 예: 안산 중앙역)'),
114
151
  latitude: z.number().optional().default(37.5665).describe('위도 (기본값: 서울 시청 37.5665)'),
115
152
  longitude: z.number().optional().default(126.978).describe('경도 (기본값: 서울 시청 126.978)'),