daiso 1.0.4 → 1.0.5

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 (158) hide show
  1. package/README.md +218 -540
  2. package/dist/api/actionsProxy.js +82 -0
  3. package/dist/api/cgvHandlers.js +103 -16
  4. package/dist/api/daisoHandlers.js +127 -0
  5. package/dist/api/emart24Handlers.js +176 -0
  6. package/dist/api/gs25Handlers.js +256 -0
  7. package/dist/api/handlers.js +184 -108
  8. package/dist/api/healthChecks.js +332 -0
  9. package/dist/api/lottecinemaHandlers.js +155 -0
  10. package/dist/api/lottemartHandlers.js +128 -0
  11. package/dist/api/megaboxHandlers.js +81 -50
  12. package/dist/api/routes/cuRoutes.js +17 -0
  13. package/dist/api/routes/daisoRoutes.js +6 -1
  14. package/dist/api/routes/emart24Routes.js +22 -0
  15. package/dist/api/routes/gs25Routes.js +22 -0
  16. package/dist/api/routes/healthRoutes.js +44 -0
  17. package/dist/api/routes/lottecinemaRoutes.js +22 -0
  18. package/dist/api/routes/lottemartRoutes.js +18 -0
  19. package/dist/api/routes/oliveyoungRoutes.js +6 -1
  20. package/dist/api/routes/sevenelevenRoutes.js +32 -0
  21. package/dist/api/sevenelevenHandlers.js +200 -0
  22. package/dist/cli/args.js +54 -0
  23. package/dist/cli/commands/convenience.js +241 -0
  24. package/dist/cli/commands/daiso.js +96 -0
  25. package/dist/cli/constants.js +5 -0
  26. package/dist/cli/deps.js +48 -0
  27. package/dist/cli/http.js +98 -0
  28. package/dist/cli/interactiveFetch.js +31 -0
  29. package/dist/cli/interactiveItemSearch.js +259 -0
  30. package/dist/cli/interactivePrompt.js +49 -0
  31. package/dist/cli/interactiveTypes.js +4 -0
  32. package/dist/cli/types.js +4 -0
  33. package/dist/cli.js +66 -197
  34. package/dist/cliHelp.js +206 -1
  35. package/dist/cliInteractive.js +174 -0
  36. package/dist/cliPicker.js +56 -0
  37. package/dist/cliRenderer.js +4 -0
  38. package/dist/index.js +147 -8
  39. package/dist/pages/openapi.js +2 -1
  40. package/dist/pages/openapiFullSpec.js +58 -0
  41. package/dist/pages/openapiResponse.js +28 -0
  42. package/dist/pages/openapiSpec.js +2 -41
  43. package/dist/pages/openapiSpecActions.js +109 -0
  44. package/dist/pages/openapiSpecComponents.js +30 -391
  45. package/dist/pages/openapiSpecComponentsCgv.js +24 -0
  46. package/dist/pages/openapiSpecComponentsCu.js +112 -0
  47. package/dist/pages/openapiSpecComponentsDaisoOliveyoung.js +342 -0
  48. package/dist/pages/openapiSpecComponentsEmart24.js +141 -0
  49. package/dist/pages/openapiSpecComponentsGs25.js +109 -0
  50. package/dist/pages/openapiSpecComponentsLotteMart.js +85 -0
  51. package/dist/pages/openapiSpecComponentsLottecinema.js +163 -0
  52. package/dist/pages/openapiSpecComponentsMegabox.js +144 -0
  53. package/dist/pages/openapiSpecComponentsSeveneleven.js +82 -0
  54. package/dist/pages/openapiSpecPathsCgv.js +66 -3
  55. package/dist/pages/openapiSpecPathsCu.js +138 -0
  56. package/dist/pages/openapiSpecPathsDaiso.js +302 -0
  57. package/dist/pages/openapiSpecPathsDaisoOliveyoung.js +4 -376
  58. package/dist/pages/openapiSpecPathsEmart24.js +223 -0
  59. package/dist/pages/openapiSpecPathsGs25.js +166 -0
  60. package/dist/pages/openapiSpecPathsLotteMart.js +135 -0
  61. package/dist/pages/openapiSpecPathsLottecinema.js +212 -0
  62. package/dist/pages/openapiSpecPathsMegabox.js +52 -3
  63. package/dist/pages/openapiSpecPathsOliveyoung.js +231 -0
  64. package/dist/pages/openapiSpecPathsSeveneleven.js +234 -0
  65. package/dist/pages/prompt.js +8 -402
  66. package/dist/pages/promptCoreText.js +278 -0
  67. package/dist/pages/promptReferenceText.js +131 -0
  68. package/dist/pages/promptRetailCinemaText.js +273 -0
  69. package/dist/services/cgv/client.js +6 -1
  70. package/dist/services/cgv/index.js +3 -3
  71. package/dist/services/cgv/location.js +235 -0
  72. package/dist/services/cgv/tools/findTheaters.js +30 -5
  73. package/dist/services/cgv/tools/getTimetable.js +42 -14
  74. package/dist/services/cgv/tools/searchMovies.js +40 -12
  75. package/dist/services/cgv/transport.js +0 -3
  76. package/dist/services/cu/api.js +11 -0
  77. package/dist/services/cu/client.js +297 -0
  78. package/dist/services/cu/index.js +21 -0
  79. package/dist/services/cu/tools/checkInventory.js +123 -0
  80. package/dist/services/cu/tools/findNearbyStores.js +49 -0
  81. package/dist/services/cu/types.js +4 -0
  82. package/dist/services/daiso/api.js +22 -2
  83. package/dist/services/daiso/client.js +59 -0
  84. package/dist/services/daiso/index.js +2 -0
  85. package/dist/services/daiso/product.js +17 -0
  86. package/dist/services/daiso/tools/checkInventory.js +76 -41
  87. package/dist/services/daiso/tools/findStores.js +17 -7
  88. package/dist/services/daiso/tools/getDisplayLocation.js +75 -0
  89. package/dist/services/emart24/api.js +11 -0
  90. package/dist/services/emart24/client.js +173 -0
  91. package/dist/services/emart24/index.js +22 -0
  92. package/dist/services/emart24/inventoryLookup.js +134 -0
  93. package/dist/services/emart24/tools/checkInventory.js +54 -0
  94. package/dist/services/emart24/tools/findNearbyStores.js +72 -0
  95. package/dist/services/emart24/tools/searchProducts.js +63 -0
  96. package/dist/services/emart24/types.js +4 -0
  97. package/dist/services/gs25/api.js +9 -0
  98. package/dist/services/gs25/client.js +198 -0
  99. package/dist/services/gs25/index.js +22 -0
  100. package/dist/services/gs25/storeUtils.js +168 -0
  101. package/dist/services/gs25/tools/checkInventory.js +163 -0
  102. package/dist/services/gs25/tools/findNearbyStores.js +103 -0
  103. package/dist/services/gs25/tools/searchProducts.js +50 -0
  104. package/dist/services/gs25/types.js +4 -0
  105. package/dist/services/lottecinema/api.js +12 -0
  106. package/dist/services/lottecinema/client.js +161 -0
  107. package/dist/services/lottecinema/index.js +30 -0
  108. package/dist/services/lottecinema/location.js +191 -0
  109. package/dist/services/lottecinema/tools/findNearbyTheaters.js +47 -0
  110. package/dist/services/lottecinema/tools/getRemainingSeats.js +85 -0
  111. package/dist/services/lottecinema/tools/listNowShowing.js +79 -0
  112. package/dist/services/lottecinema/types.js +4 -0
  113. package/dist/services/lottemart/api.js +31 -0
  114. package/dist/services/lottemart/client.js +245 -0
  115. package/dist/services/lottemart/clientTypes.js +4 -0
  116. package/dist/services/lottemart/config.js +4 -0
  117. package/dist/services/lottemart/debug.js +92 -0
  118. package/dist/services/lottemart/geocode.js +49 -0
  119. package/dist/services/lottemart/index.js +30 -0
  120. package/dist/services/lottemart/keyword.js +29 -0
  121. package/dist/services/lottemart/parser.js +185 -0
  122. package/dist/services/lottemart/session.js +331 -0
  123. package/dist/services/lottemart/storeSearch.js +52 -0
  124. package/dist/services/lottemart/tools/findNearbyStores.js +67 -0
  125. package/dist/services/lottemart/tools/searchProducts.js +62 -0
  126. package/dist/services/lottemart/types.js +4 -0
  127. package/dist/services/lottemart/zetta.js +105 -0
  128. package/dist/services/megabox/location.js +219 -0
  129. package/dist/services/megabox/tools/findNearbyTheaters.js +13 -44
  130. package/dist/services/megabox/tools/getRemainingSeats.js +31 -3
  131. package/dist/services/megabox/tools/listNowShowing.js +31 -3
  132. package/dist/services/oliveyoung/api.js +2 -0
  133. package/dist/services/oliveyoung/client.js +150 -9
  134. package/dist/services/oliveyoung/index.js +3 -1
  135. package/dist/services/oliveyoung/tools/checkInventory.js +15 -4
  136. package/dist/services/oliveyoung/tools/searchProducts.js +57 -0
  137. package/dist/services/seveneleven/api.js +15 -0
  138. package/dist/services/seveneleven/client.js +279 -0
  139. package/dist/services/seveneleven/index.js +30 -0
  140. package/dist/services/seveneleven/inventory.js +302 -0
  141. package/dist/services/seveneleven/productKeyword.js +141 -0
  142. package/dist/services/seveneleven/tools/checkInventory.js +82 -0
  143. package/dist/services/seveneleven/tools/getCatalogSnapshot.js +55 -0
  144. package/dist/services/seveneleven/tools/getSearchPopwords.js +41 -0
  145. package/dist/services/seveneleven/tools/searchProducts.js +56 -0
  146. package/dist/services/seveneleven/tools/searchStores.js +45 -0
  147. package/dist/services/seveneleven/types.js +4 -0
  148. package/dist/utils/cliInteractiveHelpers.js +63 -0
  149. package/dist/utils/daisoKeyword.js +30 -0
  150. package/dist/utils/http.js +16 -2
  151. package/openapi.json +1977 -734
  152. package/openapi.yaml +1430 -543
  153. package/package.json +28 -15
  154. package/dist/tools/checkInventory.js +0 -98
  155. package/dist/tools/findStores.js +0 -125
  156. package/dist/tools/getPriceInfo.js +0 -77
  157. package/dist/tools/searchProducts.js +0 -61
  158. package/dist/types/index.js +0 -4
@@ -0,0 +1,141 @@
1
+ /**
2
+ * 세븐일레븐 상품 검색어 보정
3
+ */
4
+ import { searchSevenElevenProducts } from './client.js';
5
+ const SANDWICH_SUFFIX_FAMILY = ['샌드위치', '샌드', '산도'];
6
+ function normalizeProductText(value) {
7
+ return value.replace(/[^0-9A-Za-z가-힣]+/g, '').toLowerCase();
8
+ }
9
+ function removeSuffixFamily(value) {
10
+ for (const suffix of SANDWICH_SUFFIX_FAMILY) {
11
+ if (value.endsWith(suffix)) {
12
+ return value.slice(0, -suffix.length);
13
+ }
14
+ }
15
+ return value;
16
+ }
17
+ /**
18
+ * 세븐일레븐 상품 검색용 보정 질의를 생성합니다.
19
+ */
20
+ export function buildSevenElevenProductKeywordVariants(rawKeyword) {
21
+ const base = rawKeyword.trim();
22
+ if (!base) {
23
+ return [];
24
+ }
25
+ const compact = base.replace(/\s+/g, '');
26
+ const stem = removeSuffixFamily(compact);
27
+ const candidates = [base, compact];
28
+ if (stem && stem !== compact) {
29
+ candidates.push(stem);
30
+ for (const suffix of SANDWICH_SUFFIX_FAMILY) {
31
+ candidates.push(`${stem}${suffix}`);
32
+ }
33
+ candidates.push('샌드');
34
+ }
35
+ return [...new Set(candidates.map((value) => value.trim()).filter((value) => value.length > 0))];
36
+ }
37
+ /**
38
+ * 원본 검색어 기준으로 가장 그럴듯한 상품을 고릅니다.
39
+ */
40
+ export function pickBestSevenElevenProduct(products, rawKeyword) {
41
+ let bestProduct = null;
42
+ let bestScore = -1;
43
+ for (const product of products) {
44
+ const score = scoreSevenElevenProduct(product, rawKeyword);
45
+ if (score > bestScore) {
46
+ bestScore = score;
47
+ bestProduct = product;
48
+ }
49
+ }
50
+ return bestScore > 0 ? bestProduct : null;
51
+ }
52
+ function scoreSevenElevenProduct(product, rawKeyword) {
53
+ const original = normalizeProductText(rawKeyword);
54
+ const stem = normalizeProductText(removeSuffixFamily(rawKeyword.replace(/\s+/g, '')));
55
+ const variants = buildSevenElevenProductKeywordVariants(rawKeyword)
56
+ .map(normalizeProductText)
57
+ .filter((value) => value.length > 0);
58
+ const hasSandwichSuffix = stem.length > 0 && stem !== original;
59
+ const normalizedName = normalizeProductText(product.itemName);
60
+ if (normalizedName.length === 0) {
61
+ return -1;
62
+ }
63
+ let score = 0;
64
+ if (original.length > 0 && normalizedName === original) {
65
+ score += 200;
66
+ }
67
+ if (original.length > 0 && normalizedName.includes(original)) {
68
+ score += 120;
69
+ }
70
+ if (stem.length > 0 && stem !== original && normalizedName.includes(stem)) {
71
+ score += 45;
72
+ }
73
+ for (const variant of variants) {
74
+ if (variant.length < 2 || variant === original || variant === stem) {
75
+ continue;
76
+ }
77
+ if (normalizedName.includes(variant)) {
78
+ score += 15;
79
+ }
80
+ }
81
+ if (hasSandwichSuffix && SANDWICH_SUFFIX_FAMILY.some((suffix) => normalizedName.includes(suffix))) {
82
+ score += 35;
83
+ }
84
+ if (hasSandwichSuffix &&
85
+ stem.length > 0 &&
86
+ SANDWICH_SUFFIX_FAMILY.some((suffix) => normalizedName.includes(`${stem}${suffix}`))) {
87
+ score += 50;
88
+ }
89
+ if (product.itemCode.length > 0) {
90
+ score += 1;
91
+ }
92
+ return score;
93
+ }
94
+ /**
95
+ * 보정 질의를 순차 적용해 세븐일레븐 상품을 찾습니다.
96
+ */
97
+ export async function searchSevenElevenProductsWithVariants(query, options = {}) {
98
+ const { page = 1, size = 20, sort = 'recommend', timeout } = options;
99
+ const appliedQueries = buildSevenElevenProductKeywordVariants(query);
100
+ const seenKeys = new Set();
101
+ const collectionIds = new Set();
102
+ const mergedProducts = [];
103
+ for (const candidate of appliedQueries) {
104
+ const result = await searchSevenElevenProducts({
105
+ query: candidate,
106
+ page,
107
+ size,
108
+ sort,
109
+ }, {
110
+ timeout,
111
+ });
112
+ for (const collectionId of result.collectionIds) {
113
+ collectionIds.add(collectionId);
114
+ }
115
+ for (const product of result.products) {
116
+ const key = product.itemCode || product.productNo || product.itemName;
117
+ if (!key || seenKeys.has(key)) {
118
+ continue;
119
+ }
120
+ seenKeys.add(key);
121
+ mergedProducts.push(product);
122
+ }
123
+ }
124
+ const rankedProducts = [...mergedProducts].sort((left, right) => {
125
+ const leftScore = scoreSevenElevenProduct(left, query);
126
+ const rightScore = scoreSevenElevenProduct(right, query);
127
+ if (leftScore !== rightScore) {
128
+ return rightScore - leftScore;
129
+ }
130
+ return 0;
131
+ });
132
+ const start = Math.max(page - 1, 0) * size;
133
+ const pagedProducts = rankedProducts.slice(start, start + size);
134
+ return {
135
+ query,
136
+ totalCount: rankedProducts.length,
137
+ products: pagedProducts,
138
+ collectionIds: [...collectionIds],
139
+ appliedQueries,
140
+ };
141
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * 세븐일레븐 재고 확인 도구
3
+ *
4
+ * 상품 검색 + 매장 검색을 결합하여 특정 지역 매장의 재고를 조회합니다.
5
+ * 재고 API가 암호화로 차단된 경우에도 상품/매장 정보를 함께 제공합니다.
6
+ */
7
+ /* c8 ignore start */
8
+ import * as z from 'zod';
9
+ import { checkSevenElevenInventory } from '../inventory.js';
10
+ function isEncryptedStockFailure(stockError) {
11
+ if (!stockError) {
12
+ return false;
13
+ }
14
+ const codeMatched = stockError.code === 501 || stockError.code === 503;
15
+ const messageSource = `${stockError.message} ${stockError.raw ?? ''}`;
16
+ const messageMatched = /RSA|복호화|암호화|서비스를 사용할 수 없습니다/i.test(messageSource);
17
+ return codeMatched || messageMatched;
18
+ }
19
+ async function checkInventory(args) {
20
+ const { keyword, storeKeyword = '', storeLimit = 20, timeoutMs = 20000, } = args;
21
+ if (!keyword || keyword.trim().length === 0) {
22
+ throw new Error('상품 검색어(keyword)를 입력해주세요.');
23
+ }
24
+ const result = await checkSevenElevenInventory({
25
+ productKeyword: keyword,
26
+ storeKeyword,
27
+ storeLimit,
28
+ }, {
29
+ timeout: timeoutMs,
30
+ });
31
+ const note = !result.product
32
+ ? '상품 검색 결과가 없어 재고를 조회하지 못했습니다. 다른 상품명이나 더 짧은 키워드로 다시 시도해주세요.'
33
+ : result.stockAvailable
34
+ ? '실시간 재고 데이터가 포함되어 있습니다.'
35
+ : isEncryptedStockFailure(result.stockError)
36
+ ? '실시간 재고 API가 암호화 검증에서 거절되었습니다. 현재 구현의 평문 요청만으로는 재고 수량을 조회할 수 없습니다.'
37
+ : result.stockError
38
+ ? `실시간 재고 API 호출에 실패했습니다: ${result.stockError.message}`
39
+ : '실시간 재고 API가 현재 제한되어 있어 매장 목록만 제공됩니다. stockQuantity가 -1인 경우 재고 수량 미확인 상태입니다.';
40
+ return {
41
+ content: [
42
+ {
43
+ type: 'text',
44
+ text: JSON.stringify({
45
+ keyword,
46
+ storeKeyword,
47
+ product: result.product,
48
+ stockAvailable: result.stockAvailable,
49
+ stockError: result.stockError,
50
+ note,
51
+ inventory: {
52
+ totalStoreCount: result.totalStoreCount,
53
+ inStockStoreCount: result.inStockStoreCount,
54
+ count: result.stores.length,
55
+ stores: result.stores,
56
+ },
57
+ }, null, 2),
58
+ },
59
+ ],
60
+ };
61
+ }
62
+ export function createCheckInventoryTool() {
63
+ return {
64
+ name: 'seveneleven_check_inventory',
65
+ metadata: {
66
+ title: '세븐일레븐 재고 확인',
67
+ description: '상품 키워드와 매장/지역 키워드로 세븐일레븐 매장별 재고를 조회합니다. 실시간 재고 API가 제한된 경우에도 상품 정보와 근처 매장 목록을 함께 제공합니다.',
68
+ inputSchema: {
69
+ keyword: z.string().describe('상품 검색어 (예: 핫식스, 삼각김밥, 도시락)'),
70
+ storeKeyword: z
71
+ .string()
72
+ .optional()
73
+ .default('')
74
+ .describe('매장/지역 검색 키워드 (예: 안산 중앙역, 강남역, 홍대)'),
75
+ storeLimit: z.number().optional().default(20).describe('반환할 최대 매장 수 (기본값: 20)'),
76
+ timeoutMs: z.number().optional().default(20000).describe('요청 제한 시간(ms, 기본값: 20000)'),
77
+ },
78
+ },
79
+ handler: checkInventory,
80
+ };
81
+ }
82
+ /* c8 ignore stop */
@@ -0,0 +1,55 @@
1
+ /**
2
+ * 세븐일레븐 카탈로그 스냅샷 도구
3
+ */
4
+ import * as z from 'zod';
5
+ import { fetchSevenElevenCatalogSnapshot } from '../client.js';
6
+ async function getCatalogSnapshot(args) {
7
+ const { includeIssues = true, includeExhibition = true, limit = 20, timeoutMs = 15000 } = args;
8
+ const result = await fetchSevenElevenCatalogSnapshot({
9
+ includeIssues,
10
+ includeExhibition,
11
+ timeout: timeoutMs,
12
+ });
13
+ return {
14
+ content: [
15
+ {
16
+ type: 'text',
17
+ text: JSON.stringify({
18
+ options: {
19
+ includeIssues,
20
+ includeExhibition,
21
+ limit,
22
+ },
23
+ pages: {
24
+ totalCount: result.pages.length,
25
+ items: result.pages.slice(0, limit),
26
+ },
27
+ issues: {
28
+ totalCount: result.issues.length,
29
+ items: result.issues.slice(0, limit),
30
+ },
31
+ exhibitions: {
32
+ totalCount: result.exhibitions.length,
33
+ items: result.exhibitions.slice(0, limit),
34
+ },
35
+ }, null, 2),
36
+ },
37
+ ],
38
+ };
39
+ }
40
+ export function createGetCatalogSnapshotTool() {
41
+ return {
42
+ name: 'seveneleven_get_catalog_snapshot',
43
+ metadata: {
44
+ title: '세븐일레븐 카탈로그 스냅샷',
45
+ description: '세븐일레븐 공개 카탈로그(페이지/이슈/기획전) 데이터를 조회합니다.',
46
+ inputSchema: {
47
+ includeIssues: z.boolean().optional().default(true).describe('이슈 상품 포함 여부 (기본값: true)'),
48
+ includeExhibition: z.boolean().optional().default(true).describe('기획전 포함 여부 (기본값: true)'),
49
+ limit: z.number().optional().default(20).describe('각 목록 최대 반환 수 (기본값: 20)'),
50
+ timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
51
+ },
52
+ },
53
+ handler: getCatalogSnapshot,
54
+ };
55
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * 세븐일레븐 인기 검색어 조회 도구
3
+ */
4
+ import * as z from 'zod';
5
+ import { fetchSevenElevenSearchPopwords } from '../client.js';
6
+ async function getSearchPopwords(args) {
7
+ const { label = 'home', timeoutMs = 15000 } = args;
8
+ const keywords = await fetchSevenElevenSearchPopwords(label, {
9
+ timeout: timeoutMs,
10
+ });
11
+ return {
12
+ content: [
13
+ {
14
+ type: 'text',
15
+ text: JSON.stringify({
16
+ label,
17
+ available: keywords.length > 0,
18
+ count: keywords.length,
19
+ keywords,
20
+ note: keywords.length === 0
21
+ ? '현재 응답에서 인기 검색어 목록을 찾지 못했습니다.'
22
+ : '홈 인기 검색어를 조회했습니다.',
23
+ }, null, 2),
24
+ },
25
+ ],
26
+ };
27
+ }
28
+ export function createGetSearchPopwordsTool() {
29
+ return {
30
+ name: 'seveneleven_get_search_popwords',
31
+ metadata: {
32
+ title: '세븐일레븐 인기 검색어 조회',
33
+ description: '세븐일레븐 인기 검색어 목록을 조회합니다.',
34
+ inputSchema: {
35
+ label: z.string().optional().default('home').describe('조회 라벨 (기본값: home)'),
36
+ timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
37
+ },
38
+ },
39
+ handler: getSearchPopwords,
40
+ };
41
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * 세븐일레븐 상품 검색 도구
3
+ */
4
+ import * as z from 'zod';
5
+ import { searchSevenElevenProductsWithVariants } from '../productKeyword.js';
6
+ async function searchProducts(args) {
7
+ const { query, page = 1, size = 20, sort = 'recommend', timeoutMs = 15000 } = args;
8
+ if (!query || query.trim().length === 0) {
9
+ throw new Error('상품 검색어(query)를 입력해주세요.');
10
+ }
11
+ const result = await searchSevenElevenProductsWithVariants(query, {
12
+ page,
13
+ size,
14
+ sort,
15
+ timeout: timeoutMs,
16
+ });
17
+ return {
18
+ content: [
19
+ {
20
+ type: 'text',
21
+ text: JSON.stringify({
22
+ query: result.query,
23
+ page,
24
+ size,
25
+ sort,
26
+ totalCount: result.totalCount,
27
+ count: result.products.length,
28
+ collectionIds: result.collectionIds,
29
+ appliedQueries: result.appliedQueries,
30
+ products: result.products,
31
+ }, null, 2),
32
+ },
33
+ ],
34
+ };
35
+ }
36
+ export function createSearchProductsTool() {
37
+ return {
38
+ name: 'seveneleven_search_products',
39
+ metadata: {
40
+ title: '세븐일레븐 상품 검색',
41
+ description: '키워드로 세븐일레븐 상품을 검색합니다.',
42
+ inputSchema: {
43
+ query: z.string().describe('검색어 (예: 삼각김밥, 콜라, 도시락)'),
44
+ page: z.number().optional().default(1).describe('페이지 번호 (기본값: 1)'),
45
+ size: z.number().optional().default(20).describe('페이지당 결과 수 (기본값: 20)'),
46
+ sort: z
47
+ .enum(['recommend', 'recent', 'popular'])
48
+ .optional()
49
+ .default('recommend')
50
+ .describe('정렬 기준 (기본값: recommend)'),
51
+ timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
52
+ },
53
+ },
54
+ handler: searchProducts,
55
+ };
56
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * 세븐일레븐 매장 검색 도구
3
+ */
4
+ import * as z from 'zod';
5
+ import { fetchSevenElevenStoresByKeyword } from '../client.js';
6
+ async function searchStores(args) {
7
+ const { keyword, limit = 20, timeoutMs = 15000 } = args;
8
+ if (!keyword || keyword.trim().length === 0) {
9
+ throw new Error('매장 검색어(keyword)를 입력해주세요.');
10
+ }
11
+ const result = await fetchSevenElevenStoresByKeyword({
12
+ keyword,
13
+ limit,
14
+ }, {
15
+ timeout: timeoutMs,
16
+ });
17
+ return {
18
+ content: [
19
+ {
20
+ type: 'text',
21
+ text: JSON.stringify({
22
+ keyword: result.query,
23
+ totalCount: result.totalCount,
24
+ count: result.stores.length,
25
+ stores: result.stores,
26
+ }, null, 2),
27
+ },
28
+ ],
29
+ };
30
+ }
31
+ export function createSearchStoresTool() {
32
+ return {
33
+ name: 'seveneleven_search_stores',
34
+ metadata: {
35
+ title: '세븐일레븐 매장 검색',
36
+ description: '키워드로 세븐일레븐 매장을 검색합니다.',
37
+ inputSchema: {
38
+ keyword: z.string().describe('매장명/지역 키워드 (예: 안산 중앙역, 강남)'),
39
+ limit: z.number().optional().default(20).describe('반환할 최대 매장 수 (기본값: 20)'),
40
+ timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
41
+ },
42
+ },
43
+ handler: searchStores,
44
+ };
45
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * 세븐일레븐 타입 정의
3
+ */
4
+ export {};
@@ -0,0 +1,63 @@
1
+ /**
2
+ * 인터랙티브 CLI 보조 유틸
3
+ */
4
+ import { buildDaisoStoreKeywordVariants } from './daisoKeyword.js';
5
+ export function isRecord(value) {
6
+ return typeof value === 'object' && value !== null;
7
+ }
8
+ export function toText(value) {
9
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
10
+ return String(value);
11
+ }
12
+ return '';
13
+ }
14
+ export function parseStores(payload) {
15
+ if (!isRecord(payload) || payload.success !== true || !isRecord(payload.data)) {
16
+ return [];
17
+ }
18
+ const stores = payload.data.stores;
19
+ if (!Array.isArray(stores)) {
20
+ return [];
21
+ }
22
+ const result = [];
23
+ for (const store of stores) {
24
+ if (!isRecord(store)) {
25
+ continue;
26
+ }
27
+ const name = toText(store.name) || toText(store.storeName);
28
+ if (!name) {
29
+ continue;
30
+ }
31
+ result.push({
32
+ name,
33
+ address: toText(store.address),
34
+ phone: toText(store.phone),
35
+ });
36
+ }
37
+ return result;
38
+ }
39
+ export function parseDaisoProducts(payload) {
40
+ if (!isRecord(payload) || payload.success !== true || !isRecord(payload.data)) {
41
+ return [];
42
+ }
43
+ const products = payload.data.products;
44
+ if (!Array.isArray(products)) {
45
+ return [];
46
+ }
47
+ const result = [];
48
+ for (const product of products) {
49
+ if (!isRecord(product)) {
50
+ continue;
51
+ }
52
+ const id = toText(product.id);
53
+ const name = toText(product.name);
54
+ if (!id || !name) {
55
+ continue;
56
+ }
57
+ const priceRaw = product.price;
58
+ const price = typeof priceRaw === 'number' ? priceRaw : Number.parseInt(toText(priceRaw), 10) || 0;
59
+ result.push({ id, name, price });
60
+ }
61
+ return result;
62
+ }
63
+ export { buildDaisoStoreKeywordVariants };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * 다이소 매장 검색 키워드 보정
3
+ */
4
+ /**
5
+ * 역/공백/서비스명이 섞인 검색어를 다이소 매장 검색용으로 보정합니다.
6
+ */
7
+ export function buildDaisoStoreKeywordVariants(rawKeyword) {
8
+ const base = rawKeyword.trim();
9
+ if (!base) {
10
+ return [];
11
+ }
12
+ const compact = base.replace(/\s+/g, '');
13
+ const withoutNoise = compact.replace(/(다이소|근처|주변)/g, '');
14
+ const withoutStation = withoutNoise.replace(/역$/g, '');
15
+ const candidates = [
16
+ base,
17
+ compact,
18
+ withoutNoise,
19
+ withoutStation,
20
+ withoutStation.replace(/(중앙)(역)?$/g, '$1'),
21
+ ].map((item) => item.trim());
22
+ const unique = [];
23
+ for (const item of candidates) {
24
+ if (!item || unique.includes(item)) {
25
+ continue;
26
+ }
27
+ unique.push(item);
28
+ }
29
+ return unique;
30
+ }
@@ -1,6 +1,20 @@
1
1
  /**
2
2
  * 공용 HTTP 유틸리티
3
3
  */
4
+ export class HttpError extends Error {
5
+ status;
6
+ statusText;
7
+ bodyText;
8
+ constructor(status, statusText, bodyText) {
9
+ const normalizedBody = bodyText.trim().replace(/\s+/g, ' ').slice(0, 300);
10
+ const detail = normalizedBody.length > 0 ? ` - ${normalizedBody}` : '';
11
+ super(`API 요청 실패: ${status} ${statusText}${detail}`);
12
+ this.name = 'HttpError';
13
+ this.status = status;
14
+ this.statusText = statusText;
15
+ this.bodyText = bodyText;
16
+ }
17
+ }
4
18
  export function createTimeoutController(timeout) {
5
19
  const controller = new AbortController();
6
20
  const timeoutId = setTimeout(() => controller.abort(), timeout);
@@ -22,14 +36,14 @@ export async function fetchWithTimeout(url, options = {}) {
22
36
  export async function fetchJson(url, options = {}) {
23
37
  const response = await fetchWithTimeout(url, options);
24
38
  if (!response.ok) {
25
- throw new Error(`API 요청 실패: ${response.status} ${response.statusText}`);
39
+ throw new HttpError(response.status, response.statusText, await response.text());
26
40
  }
27
41
  return response.json();
28
42
  }
29
43
  export async function fetchText(url, options = {}) {
30
44
  const response = await fetchWithTimeout(url, options);
31
45
  if (!response.ok) {
32
- throw new Error(`API 요청 실패: ${response.status} ${response.statusText}`);
46
+ throw new HttpError(response.status, response.statusText, await response.text());
33
47
  }
34
48
  return response.text();
35
49
  }