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,222 @@
1
+ /**
2
+ * 상품명 기반 재고 통합 조회 도구
3
+ *
4
+ * 상품명과 위치만 아는 사용자를 위해 상품 검색과 재고 조회를 한 번에 수행합니다.
5
+ */
6
+ import * as z from 'zod';
7
+ import { fetchProducts } from './searchProducts.js';
8
+ import { fetchOnlineStock, fetchStoreInventory } from './checkInventory.js';
9
+ import { fetchStores } from './findStores.js';
10
+ function buildTextResponse(payload) {
11
+ return {
12
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
13
+ structuredContent: payload,
14
+ };
15
+ }
16
+ function buildEmptySummary(query, storeQuery) {
17
+ return {
18
+ headline: `"${query}" 상품 후보를 찾지 못했습니다.`,
19
+ selectedProduct: '',
20
+ storeQuery: storeQuery || '미지정',
21
+ inventorySummary: '상품 후보가 없어 온라인/매장 재고 조회를 건너뛰었습니다.',
22
+ displayLocationHint: '상품 후보를 먼저 확인한 뒤 daiso_get_display_location을 사용할 수 있습니다.',
23
+ };
24
+ }
25
+ async function resolveInventoryLocation(args) {
26
+ if (typeof args.latitude === 'number' && typeof args.longitude === 'number') {
27
+ return {
28
+ latitude: args.latitude,
29
+ longitude: args.longitude,
30
+ source: 'input',
31
+ };
32
+ }
33
+ if (args.storeQuery.trim().length > 0) {
34
+ const stores = await fetchStores(args.storeQuery);
35
+ const firstStore = stores[0];
36
+ if (firstStore) {
37
+ return {
38
+ latitude: firstStore.lat,
39
+ longitude: firstStore.lng,
40
+ source: 'storeQuery',
41
+ storeName: firstStore.name,
42
+ storeAddress: firstStore.address,
43
+ };
44
+ }
45
+ }
46
+ return {
47
+ latitude: args.latitude ?? 37.5665,
48
+ longitude: args.longitude ?? 126.978,
49
+ source: 'default',
50
+ };
51
+ }
52
+ function buildInventorySummary(args) {
53
+ const storeScope = args.storeQuery || '기본 위치 주변';
54
+ return {
55
+ headline: `"${args.query}" 조회 결과, "${args.selectedProductName}" 상품을 기준으로 재고를 확인했습니다.`,
56
+ selectedProduct: args.selectedProductName,
57
+ storeQuery: args.storeQuery || '미지정',
58
+ inventorySummary: `${storeScope} 매장 ${args.totalStores}곳 중 ${args.inStockCount}곳에서 재고가 확인되었습니다. 온라인 재고는 ${args.onlineStock}개입니다.`,
59
+ displayLocationHint: '진열 위치가 필요하면 storeInventory.stores[].storeCode로 daiso_get_display_location을 호출하세요.',
60
+ };
61
+ }
62
+ function toSummary(product) {
63
+ return {
64
+ id: product.id,
65
+ name: product.name,
66
+ imageUrl: product.imageUrl,
67
+ brand: product.brand,
68
+ soldOut: product.soldOut,
69
+ isNew: product.isNew,
70
+ };
71
+ }
72
+ function normalizeProductName(value) {
73
+ return value.toLowerCase().replace(/\s+/g, '');
74
+ }
75
+ function scoreProductCandidate(query, candidate, index) {
76
+ const normalizedQuery = normalizeProductName(query);
77
+ const normalizedName = normalizeProductName(candidate.name);
78
+ const exactMatchScore = normalizedName === normalizedQuery ? 100 : 0;
79
+ const availabilityScore = candidate.soldOut ? 0 : 20;
80
+ const startsWithScore = normalizedName.startsWith(normalizedQuery) ? 10 : 0;
81
+ const containsScore = normalizedName.includes(normalizedQuery) ? 5 : 0;
82
+ return exactMatchScore + availabilityScore + startsWithScore + containsScore - index / 1000;
83
+ }
84
+ function selectProductCandidate(query, candidates) {
85
+ return (candidates
86
+ .map((candidate, index) => ({
87
+ candidate,
88
+ score: scoreProductCandidate(query, candidate, index),
89
+ }))
90
+ .sort((a, b) => b.score - a.score)[0]?.candidate ?? null);
91
+ }
92
+ async function findInventoryByName(args) {
93
+ const { query, storeQuery = '', latitude, longitude, page = 1, pageSize = 30, productLimit = 5, } = args;
94
+ if (!query || query.trim().length === 0) {
95
+ throw new Error('상품명(query)을 입력해주세요.');
96
+ }
97
+ const productResult = await fetchProducts(query, 1, productLimit);
98
+ const productCandidates = productResult.products.map(toSummary);
99
+ const selectedProduct = selectProductCandidate(query, productCandidates);
100
+ if (!selectedProduct) {
101
+ return buildTextResponse({
102
+ query,
103
+ storeQuery,
104
+ summary: buildEmptySummary(query, storeQuery),
105
+ productCandidates: [],
106
+ selectedProduct: null,
107
+ onlineStock: 0,
108
+ storeInventory: {
109
+ totalStores: 0,
110
+ inStockCount: 0,
111
+ outOfStockCount: 0,
112
+ page,
113
+ pageSize,
114
+ stores: [],
115
+ },
116
+ nextSteps: {
117
+ productSearchTool: 'daiso_search_products',
118
+ inventoryTool: 'daiso_check_inventory',
119
+ note: '상품 후보가 없어서 재고 조회를 진행하지 않았습니다.',
120
+ },
121
+ });
122
+ }
123
+ const resolvedLocation = await resolveInventoryLocation({
124
+ storeQuery,
125
+ latitude,
126
+ longitude,
127
+ });
128
+ const [onlineStock, storeResult] = await Promise.all([
129
+ fetchOnlineStock(selectedProduct.id),
130
+ fetchStoreInventory(selectedProduct.id, resolvedLocation.latitude, resolvedLocation.longitude, page, pageSize, storeQuery),
131
+ ]);
132
+ const stores = storeResult.stores;
133
+ const inStockCount = stores.filter((store) => store.quantity > 0).length;
134
+ const result = {
135
+ query,
136
+ storeQuery,
137
+ location: resolvedLocation,
138
+ summary: buildInventorySummary({
139
+ query,
140
+ storeQuery,
141
+ selectedProductName: selectedProduct.name,
142
+ onlineStock,
143
+ inStockCount,
144
+ totalStores: storeResult.totalCount,
145
+ }),
146
+ productCandidates,
147
+ selectedProduct,
148
+ onlineStock,
149
+ storeInventory: {
150
+ totalStores: storeResult.totalCount,
151
+ inStockCount,
152
+ outOfStockCount: stores.filter((store) => store.quantity === 0).length,
153
+ page,
154
+ pageSize,
155
+ stores,
156
+ },
157
+ nextSteps: {
158
+ productSearchTool: 'daiso_search_products',
159
+ inventoryTool: 'daiso_check_inventory',
160
+ displayLocationTool: 'daiso_get_display_location',
161
+ storeCodeSource: '진열 위치가 필요하면 storeInventory.stores[].storeCode를 사용하세요.',
162
+ },
163
+ };
164
+ return buildTextResponse(result);
165
+ }
166
+ const inventoryByNameOutputSchema = {
167
+ query: z.string().describe('사용자가 입력한 상품 검색어'),
168
+ storeQuery: z.string().describe('사용자가 입력한 위치 키워드'),
169
+ location: z
170
+ .object({
171
+ latitude: z.number(),
172
+ longitude: z.number(),
173
+ source: z.enum(['input', 'storeQuery', 'default']),
174
+ storeName: z.string().optional(),
175
+ storeAddress: z.string().optional(),
176
+ })
177
+ .optional()
178
+ .describe('재고 조회에 사용한 위치와 위치 해석 출처'),
179
+ summary: z
180
+ .object({
181
+ headline: z.string(),
182
+ selectedProduct: z.string(),
183
+ storeQuery: z.string(),
184
+ inventorySummary: z.string(),
185
+ displayLocationHint: z.string(),
186
+ })
187
+ .describe('사용자에게 바로 보여줄 요약'),
188
+ productCandidates: z.array(z.unknown()).describe('검색된 상품 후보 목록'),
189
+ selectedProduct: z.unknown().nullable().describe('재고 조회 기준으로 선택한 상품'),
190
+ onlineStock: z.number().describe('온라인 재고 수량'),
191
+ storeInventory: z
192
+ .object({
193
+ totalStores: z.number(),
194
+ inStockCount: z.number(),
195
+ outOfStockCount: z.number(),
196
+ page: z.number(),
197
+ pageSize: z.number(),
198
+ stores: z.array(z.unknown()),
199
+ })
200
+ .describe('매장별 재고 조회 결과'),
201
+ nextSteps: z.record(z.string(), z.unknown()).describe('후속 도구 호출 안내'),
202
+ };
203
+ export function createFindInventoryByNameTool() {
204
+ return {
205
+ name: 'daiso_find_inventory_by_name',
206
+ metadata: {
207
+ title: '상품명 기반 재고 통합 조회',
208
+ description: '상품명과 대강의 위치만 아는 사용자를 위해 제품 검색부터 재고 조회까지 한 번에 수행합니다. 결과의 storeInventory.stores[].storeCode는 진열 위치 조회에 사용할 수 있습니다.',
209
+ inputSchema: {
210
+ query: z.string().describe('검색할 상품명 또는 키워드'),
211
+ storeQuery: z.string().optional().describe('역명, 동네, 매장명 같은 대강의 위치'),
212
+ latitude: z.number().optional().describe('위도 (생략 시 서울 시청 37.5665)'),
213
+ longitude: z.number().optional().describe('경도 (생략 시 서울 시청 126.978)'),
214
+ page: z.number().optional().default(1).describe('재고 매장 페이지 번호 (기본값: 1)'),
215
+ pageSize: z.number().optional().default(30).describe('재고 매장 페이지 크기 (기본값: 30)'),
216
+ productLimit: z.number().optional().default(5).describe('상품 후보 수 (기본값: 5)'),
217
+ },
218
+ outputSchema: inventoryByNameOutputSchema,
219
+ },
220
+ handler: findInventoryByName,
221
+ };
222
+ }
@@ -6,6 +6,7 @@
6
6
  import * as z from 'zod';
7
7
  import { DAISO_WEB_API, formatTime } from '../api.js';
8
8
  import { fetchDaisoHtml, fetchDaisoJson } from '../client.js';
9
+ import { buildDaisoStoreKeywordVariants } from '../../../utils/daisoKeyword.js';
9
10
  /**
10
11
  * HTML에서 매장 정보 파싱
11
12
  */
@@ -75,13 +76,22 @@ function parseStoresFromHtml(html) {
75
76
  * 다이소 매장 검색 API 호출
76
77
  */
77
78
  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);
79
+ const searchKeywords = keyword && !sido && !gugun && !dong
80
+ ? buildDaisoStoreKeywordVariants(keyword)
81
+ : [keyword || ''];
82
+ for (const searchKeyword of searchKeywords) {
83
+ const url = new URL(DAISO_WEB_API.SHOP_SEARCH);
84
+ url.searchParams.set('name_address', searchKeyword);
85
+ url.searchParams.set('sido', sido || '');
86
+ url.searchParams.set('gugun', gugun || '');
87
+ url.searchParams.set('dong', dong || '');
88
+ const html = await fetchDaisoHtml(url.toString());
89
+ const stores = parseStoresFromHtml(html);
90
+ if (stores.length > 0) {
91
+ return stores;
92
+ }
93
+ }
94
+ return [];
85
95
  }
86
96
  /**
87
97
  * 시/도별 구/군 목록 조회
@@ -0,0 +1,75 @@
1
+ /**
2
+ * 진열 위치 조회 도구
3
+ *
4
+ * 다이소몰 API를 사용하여 매장 내 상품 진열 위치(구역/층)를 조회합니다.
5
+ */
6
+ import * as z from 'zod';
7
+ import { DAISOMALL_API } from '../api.js';
8
+ import { fetchDaisoJsonWithAuth } from '../client.js';
9
+ /**
10
+ * 매장 내 상품 진열 위치 조회
11
+ */
12
+ export async function fetchDisplayLocation(productId, storeCode) {
13
+ const data = await fetchDaisoJsonWithAuth(DAISOMALL_API.DISPLAY_LOCATION, {
14
+ method: 'POST',
15
+ headers: { 'Content-Type': 'application/json' },
16
+ body: JSON.stringify({ pdNo: productId, strCd: storeCode }),
17
+ });
18
+ if (!data.success) {
19
+ return {
20
+ productId,
21
+ storeCode,
22
+ hasLocation: false,
23
+ locations: [],
24
+ message: data.message ?? null,
25
+ };
26
+ }
27
+ const locations = Array.isArray(data.data)
28
+ ? data.data.map((item) => ({
29
+ zoneNo: String(item.zoneNo ?? ''),
30
+ stairNo: String(item.stairNo ?? ''),
31
+ storeErp: String(item.storeErp ?? storeCode),
32
+ }))
33
+ : [];
34
+ return {
35
+ productId,
36
+ storeCode,
37
+ hasLocation: locations.length > 0,
38
+ locations,
39
+ };
40
+ }
41
+ /**
42
+ * 진열 위치 조회 핸들러
43
+ */
44
+ async function getDisplayLocation(args) {
45
+ const { productId, storeCode } = args;
46
+ if (!productId || productId.trim().length === 0) {
47
+ throw new Error('상품 ID(productId)를 입력해주세요.');
48
+ }
49
+ if (!storeCode || storeCode.trim().length === 0) {
50
+ throw new Error('매장 코드(storeCode)를 입력해주세요.');
51
+ }
52
+ const result = await fetchDisplayLocation(productId, storeCode);
53
+ return {
54
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
55
+ };
56
+ }
57
+ /**
58
+ * 도구 등록 정보 생성
59
+ */
60
+ export function createGetDisplayLocationTool() {
61
+ return {
62
+ name: 'daiso_get_display_location',
63
+ metadata: {
64
+ title: '진열 위치 조회',
65
+ description: '다이소 매장 내 상품의 진열 위치(구역/층)를 조회합니다. 상품 ID와 매장 코드가 필요합니다. storeCode를 모르면 먼저 daiso_check_inventory로 재고를 조회하고 storeInventory.stores[].storeCode를 사용하세요.',
66
+ inputSchema: {
67
+ productId: z.string().describe('상품 ID (daiso_search_products로 조회한 상품의 id)'),
68
+ storeCode: z
69
+ .string()
70
+ .describe('매장 코드. storeCode를 모르면 먼저 daiso_check_inventory 결과의 storeInventory.stores[].storeCode를 확인하세요.'),
71
+ },
72
+ },
73
+ handler: getDisplayLocation,
74
+ };
75
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 이마트24 API 엔드포인트 중앙 관리
3
+ */
4
+ export const EMART24_API = {
5
+ WEB_BASE_URL: 'https://emart24.co.kr',
6
+ EVERSE_BASE_URL: 'https://everse.emart24.co.kr',
7
+ STORE_PATH: '/api1/store',
8
+ PRODUCT_SEARCH_PATH: '/stock/stock/search',
9
+ STOCK_SEARCH_STORE_PATH: '/api/stock/v2/stock-search/store',
10
+ STORE_DETAIL_PATH_PREFIX: '/api/stock/stock/store/',
11
+ };
@@ -0,0 +1,182 @@
1
+ /**
2
+ * 이마트24 API 클라이언트
3
+ */
4
+ import { fetchJson } from '../../utils/http.js';
5
+ import { EMART24_API } from './api.js';
6
+ const EMART24_JSON_HEADERS = {
7
+ Accept: 'application/json, text/javascript, */*; q=0.01',
8
+ 'X-Requested-With': 'XMLHttpRequest',
9
+ };
10
+ const EMART24_FORM_HEADERS = {
11
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
12
+ Accept: 'application/json, text/javascript, */*; q=0.01',
13
+ 'X-Requested-With': 'XMLHttpRequest',
14
+ };
15
+ const EMART24_DEFAULT_FETCH_OPTIONS = {
16
+ retries: 1,
17
+ retryDelayMs: 250,
18
+ };
19
+ function buildKeywordVariants(keyword) {
20
+ const trimmed = keyword.trim();
21
+ if (trimmed.length === 0) {
22
+ return [''];
23
+ }
24
+ const tokens = trimmed.split(/\s+/).filter((token) => token.length > 0);
25
+ const variants = [
26
+ trimmed,
27
+ trimmed.replace(/\s+/g, ''),
28
+ ...tokens,
29
+ ...tokens.map((token) => token.replace(/역$/, '')),
30
+ ].filter((value) => value.length > 0);
31
+ return [...new Set(variants)];
32
+ }
33
+ function toNumber(value) {
34
+ if (typeof value === 'number') {
35
+ return value;
36
+ }
37
+ if (typeof value === 'string') {
38
+ const parsed = Number.parseFloat(value);
39
+ return Number.isFinite(parsed) ? parsed : 0;
40
+ }
41
+ return 0;
42
+ }
43
+ function toBooleanFlag(value) {
44
+ if (typeof value === 'boolean') {
45
+ return value;
46
+ }
47
+ const normalized = String(value || '').trim().toUpperCase();
48
+ return normalized === '1' || normalized === 'Y' || normalized === 'TRUE';
49
+ }
50
+ function toStore(item) {
51
+ return {
52
+ storeCode: item.CODE || '',
53
+ storeName: item.TITLE || '',
54
+ address: item.ADDRESS || '',
55
+ addressDetail: item.ADDRESS_DE || '',
56
+ phone: item.PHONE || '',
57
+ latitude: toNumber(item.LATITUDE),
58
+ longitude: toNumber(item.LONGITUDE),
59
+ openTime: item.START_HHMM || '',
60
+ closeTime: item.END_HHMM || '',
61
+ openDate: item.OPEN_DATE || '',
62
+ endDate: item.END_DATE || '',
63
+ service24h: toBooleanFlag(item.SVR_24),
64
+ distanceM: null,
65
+ };
66
+ }
67
+ function toProduct(item) {
68
+ return {
69
+ pluCd: item.pluCd || '',
70
+ goodsName: item.goodsNm || '',
71
+ originPrice: toNumber(item.originPrice),
72
+ viewPrice: toNumber(item.viewPrice),
73
+ category: item.categoryNm || '',
74
+ kind: item.kindNm || '',
75
+ };
76
+ }
77
+ export function calculateDistanceM(lat1, lng1, lat2, lng2) {
78
+ const toRad = (deg) => (deg * Math.PI) / 180;
79
+ const dLat = toRad(lat2 - lat1);
80
+ const dLng = toRad(lng2 - lng1);
81
+ const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
82
+ Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
83
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
84
+ return Math.round(6371000 * c);
85
+ }
86
+ export async function fetchEmart24Stores(params = {}, options = {}) {
87
+ const { timeout = 15000 } = options;
88
+ const { keyword = '', area1 = '', area2 = '', page = 1, service24h = false } = params;
89
+ const keywordVariants = buildKeywordVariants(keyword);
90
+ let lastError;
91
+ for (let index = 0; index < keywordVariants.length; index += 1) {
92
+ const searchKeyword = keywordVariants[index];
93
+ const endpoint = new URL(EMART24_API.STORE_PATH, EMART24_API.WEB_BASE_URL);
94
+ endpoint.searchParams.set('page', String(page));
95
+ if (searchKeyword.length > 0) {
96
+ endpoint.searchParams.set('search', searchKeyword);
97
+ }
98
+ if (area1.trim().length > 0) {
99
+ endpoint.searchParams.set('AREA1', area1.trim());
100
+ }
101
+ if (area2.trim().length > 0) {
102
+ endpoint.searchParams.set('AREA2', area2.trim());
103
+ }
104
+ if (service24h) {
105
+ endpoint.searchParams.set('SVR_24', '1');
106
+ }
107
+ const body = await fetchJson(endpoint.toString(), {
108
+ ...EMART24_DEFAULT_FETCH_OPTIONS,
109
+ method: 'GET',
110
+ timeout,
111
+ headers: EMART24_JSON_HEADERS,
112
+ });
113
+ if (body.error === 0 || body.error === undefined) {
114
+ const stores = (body.data || []).map(toStore).filter((store) => store.storeCode.length > 0);
115
+ return {
116
+ totalCount: toNumber(body.count),
117
+ stores,
118
+ appliedKeyword: searchKeyword,
119
+ };
120
+ }
121
+ lastError = body.error;
122
+ const hasMoreVariants = index < keywordVariants.length - 1;
123
+ if (body.error !== 1 || !hasMoreVariants) {
124
+ break;
125
+ }
126
+ }
127
+ throw new Error(`이마트24 매장 조회 실패: error=${lastError}`);
128
+ }
129
+ export async function searchEmart24Products(params, options = {}) {
130
+ const { keyword, page = 1, pageSize = 10, sortType = 'SALE', saleProductYn = 'N', } = params;
131
+ const { timeout = 15000 } = options;
132
+ const form = new URLSearchParams();
133
+ form.set('currentPage', String(page));
134
+ form.set('pageCnt', String(pageSize));
135
+ form.set('sortType', sortType);
136
+ form.set('saleProductYn', saleProductYn);
137
+ form.set('searchWord', keyword.trim());
138
+ const body = await fetchJson(`${EMART24_API.EVERSE_BASE_URL}${EMART24_API.PRODUCT_SEARCH_PATH}`, {
139
+ ...EMART24_DEFAULT_FETCH_OPTIONS,
140
+ method: 'POST',
141
+ retryUnsafeMethods: true,
142
+ timeout,
143
+ headers: EMART24_FORM_HEADERS,
144
+ body: form.toString(),
145
+ });
146
+ const products = (body.productList || []).map(toProduct).filter((item) => item.pluCd.length > 0);
147
+ return {
148
+ totalCount: toNumber(body.totalCnt),
149
+ products,
150
+ };
151
+ }
152
+ export async function searchEmart24StockByStores(params, options = {}) {
153
+ const { timeout = 15000 } = options;
154
+ const { pluCd, bizNos } = params;
155
+ const filteredBizNos = bizNos.map((value) => value.trim()).filter((value) => value.length > 0);
156
+ if (filteredBizNos.length === 0) {
157
+ return {
158
+ storeGoodsInfo: {
159
+ pluCd,
160
+ },
161
+ storeGoodsQty: [],
162
+ };
163
+ }
164
+ const endpoint = new URL(EMART24_API.STOCK_SEARCH_STORE_PATH, EMART24_API.EVERSE_BASE_URL);
165
+ endpoint.searchParams.set('searchPluCode', pluCd);
166
+ endpoint.searchParams.set('bizNoArr', filteredBizNos.join(','));
167
+ return fetchJson(endpoint.toString(), {
168
+ ...EMART24_DEFAULT_FETCH_OPTIONS,
169
+ method: 'GET',
170
+ timeout,
171
+ headers: EMART24_JSON_HEADERS,
172
+ });
173
+ }
174
+ export async function fetchEmart24StoreDetail(bizNo, options = {}) {
175
+ const { timeout = 15000 } = options;
176
+ return fetchJson(`${EMART24_API.EVERSE_BASE_URL}${EMART24_API.STORE_DETAIL_PATH_PREFIX}${encodeURIComponent(bizNo)}`, {
177
+ ...EMART24_DEFAULT_FETCH_OPTIONS,
178
+ method: 'GET',
179
+ timeout,
180
+ headers: EMART24_JSON_HEADERS,
181
+ });
182
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * 이마트24 서비스 프로바이더
3
+ */
4
+ import { createFindNearbyStoresTool } from './tools/findNearbyStores.js';
5
+ import { createSearchProductsTool } from './tools/searchProducts.js';
6
+ import { createCheckInventoryTool } from './tools/checkInventory.js';
7
+ const EMART24_METADATA = {
8
+ id: 'emart24',
9
+ name: '이마트24',
10
+ version: '1.0.0',
11
+ description: '이마트24 매장 탐색, 상품 검색, 매장별 재고 조회 서비스',
12
+ };
13
+ class Emart24Service {
14
+ metadata = EMART24_METADATA;
15
+ getTools() {
16
+ return [createFindNearbyStoresTool(), createSearchProductsTool(), createCheckInventoryTool()];
17
+ }
18
+ }
19
+ export function createEmart24Service() {
20
+ return new Emart24Service();
21
+ }
22
+ export * from './types.js';
@@ -0,0 +1,134 @@
1
+ /**
2
+ * 이마트24 재고 조회 공통 로직
3
+ */
4
+ import { calculateDistanceM, fetchEmart24StoreDetail, fetchEmart24Stores, searchEmart24Products, searchEmart24StockByStores, } from './client.js';
5
+ async function resolvePluCd(pluCd, keyword, productPage, productPageSize, timeoutMs) {
6
+ const normalizedPluCd = (pluCd || '').trim();
7
+ if (normalizedPluCd.length > 0) {
8
+ return {
9
+ pluCd: normalizedPluCd,
10
+ products: [],
11
+ };
12
+ }
13
+ if (keyword.trim().length === 0) {
14
+ throw new Error('pluCd 또는 keyword 중 하나는 반드시 입력해주세요.');
15
+ }
16
+ const productResult = await searchEmart24Products({
17
+ keyword,
18
+ page: productPage,
19
+ pageSize: productPageSize,
20
+ }, {
21
+ timeout: timeoutMs,
22
+ });
23
+ const firstProduct = productResult.products.find((item) => item.pluCd.trim().length > 0);
24
+ if (!firstProduct) {
25
+ throw new Error('상품 검색 결과에서 PLU 코드를 찾을 수 없습니다.');
26
+ }
27
+ return {
28
+ pluCd: firstProduct.pluCd,
29
+ products: productResult.products,
30
+ };
31
+ }
32
+ function sortStoresByDistance(stores, latitude, longitude) {
33
+ return [...stores]
34
+ .map((store) => {
35
+ if (typeof latitude !== 'number' ||
36
+ typeof longitude !== 'number' ||
37
+ store.latitude === 0 ||
38
+ store.longitude === 0) {
39
+ return {
40
+ store,
41
+ sortDistance: Number.MAX_SAFE_INTEGER,
42
+ };
43
+ }
44
+ const distanceM = calculateDistanceM(latitude, longitude, store.latitude, store.longitude);
45
+ return {
46
+ store: {
47
+ ...store,
48
+ distanceM,
49
+ },
50
+ sortDistance: distanceM,
51
+ };
52
+ })
53
+ .sort((a, b) => a.sortDistance - b.sortDistance)
54
+ .map((entry) => entry.store);
55
+ }
56
+ export async function lookupEmart24Inventory(args) {
57
+ const { pluCd, keyword = '', latitude, longitude, storeKeyword = '', area1 = '', area2 = '', service24h = false, productPage = 1, productPageSize = 10, storeLimit = 10, timeoutMs = 15000, bizNos = [], includeRequestedStoresWithoutQty = false, } = args;
58
+ const resolved = await resolvePluCd(pluCd, keyword, productPage, productPageSize, timeoutMs);
59
+ const normalizedBizNos = [...new Set(bizNos.map((value) => value.trim()).filter((value) => value.length > 0))];
60
+ let nearbyStores = [];
61
+ let nearbyTotalCount = normalizedBizNos.length;
62
+ let appliedStoreKeyword = storeKeyword;
63
+ if (normalizedBizNos.length === 0) {
64
+ const nearbyStoreResult = await fetchEmart24Stores({
65
+ keyword: storeKeyword,
66
+ area1,
67
+ area2,
68
+ service24h,
69
+ page: 1,
70
+ }, {
71
+ timeout: timeoutMs,
72
+ });
73
+ appliedStoreKeyword = nearbyStoreResult.appliedKeyword;
74
+ nearbyTotalCount = nearbyStoreResult.totalCount;
75
+ nearbyStores = sortStoresByDistance(nearbyStoreResult.stores, latitude, longitude).slice(0, storeLimit);
76
+ }
77
+ const requestedBizNos = normalizedBizNos.length > 0 ? normalizedBizNos : nearbyStores.map((store) => store.storeCode);
78
+ const stockResult = await searchEmart24StockByStores({
79
+ pluCd: resolved.pluCd,
80
+ bizNos: requestedBizNos,
81
+ }, {
82
+ timeout: timeoutMs,
83
+ });
84
+ const qtyByBizNo = new Map((stockResult.storeGoodsQty || []).map((item) => [
85
+ String(item.BIZNO || '').trim(),
86
+ Number.parseInt(String(item.BIZQTY || 0), 10) || 0,
87
+ ]));
88
+ const inventoryBizNos = includeRequestedStoresWithoutQty && requestedBizNos.length > 0
89
+ ? requestedBizNos
90
+ : (stockResult.storeGoodsQty || [])
91
+ .map((item) => String(item.BIZNO || '').trim())
92
+ .filter((value) => value.length > 0);
93
+ const detailResults = await Promise.allSettled(inventoryBizNos.map((bizNo) => fetchEmart24StoreDetail(bizNo, { timeout: timeoutMs })));
94
+ const nearbyStoreByCode = new Map(nearbyStores.map((store) => [store.storeCode, store]));
95
+ const stores = inventoryBizNos.map((bizNo, index) => {
96
+ const detail = detailResults[index];
97
+ const detailInfo = detail.status === 'fulfilled' ? detail.value.storeInfo : undefined;
98
+ const nearby = nearbyStoreByCode.get(bizNo);
99
+ return {
100
+ bizNo,
101
+ bizQty: qtyByBizNo.get(bizNo) ?? 0,
102
+ storeName: detailInfo?.storeNm || nearby?.storeName || '',
103
+ address: detailInfo?.storeAddr || nearby?.address || '',
104
+ phone: detailInfo?.tel || nearby?.phone || '',
105
+ distanceM: nearby?.distanceM ?? null,
106
+ };
107
+ });
108
+ return {
109
+ keyword,
110
+ pluCd: resolved.pluCd,
111
+ productCandidates: resolved.products,
112
+ location: typeof latitude === 'number' && typeof longitude === 'number'
113
+ ? { latitude, longitude }
114
+ : null,
115
+ storeFilters: {
116
+ storeKeyword,
117
+ appliedStoreKeyword,
118
+ area1,
119
+ area2,
120
+ service24h,
121
+ storeLimit,
122
+ directBizNos: normalizedBizNos,
123
+ },
124
+ nearbyStores: {
125
+ totalCount: nearbyTotalCount,
126
+ stores: nearbyStores,
127
+ },
128
+ inventory: {
129
+ goodsInfo: stockResult.storeGoodsInfo || null,
130
+ count: stores.length,
131
+ stores,
132
+ },
133
+ };
134
+ }