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,243 @@
1
+ /**
2
+ * CLI 인터랙티브 모드
3
+ *
4
+ * 좌표 입력 없이 서비스/매장/상품을 순차 선택해 재고를 확인합니다.
5
+ */
6
+ import { createInterface } from 'node:readline/promises';
7
+ import { stdin as input, stdout as output } from 'node:process';
8
+ import { pickFromList } from './cliPicker.js';
9
+ import { fetchEnvelope, fetchStoresWithKeywordFallback } from './cli/interactiveFetch.js';
10
+ import { askMenu, askNextAction, askNonEmpty, askYesNo, printStoreDetail } from './cli/interactivePrompt.js';
11
+ import { parseLotteCinemaTheaters, printTheaterDetail, runCuItemSearch, runDaisoItemSearch, runLotteCinemaSearch, runOliveyoungItemSearch, } from './cli/interactiveItemSearch.js';
12
+ import { buildDaisoStoreKeywordVariants, isRecord, parseDaisoProducts, parseStores, toText } from './utils/cliInteractiveHelpers.js';
13
+ function isLocationToken(token) {
14
+ return /(?:역|점|동|구|시|군|읍|면|로|길)$/.test(token);
15
+ }
16
+ export function inferDaisoFreeTextIntent(input) {
17
+ const tokens = input.trim().split(/\s+/).filter(Boolean);
18
+ if (tokens.length <= 1) {
19
+ const value = tokens[0] || input.trim();
20
+ return { productQuery: value, storeKeyword: value };
21
+ }
22
+ const locationIndex = tokens.findIndex(isLocationToken);
23
+ if (locationIndex >= 0) {
24
+ const storeKeyword = tokens[locationIndex];
25
+ const productQuery = tokens.filter((_, index) => index !== locationIndex).join(' ').trim() || input.trim();
26
+ return { productQuery, storeKeyword };
27
+ }
28
+ const storeKeyword = tokens[tokens.length - 1];
29
+ const productQuery = tokens.slice(0, -1).join(' ').trim() || input.trim();
30
+ return { productQuery, storeKeyword };
31
+ }
32
+ async function askServiceChoice(prompt, writeOut) {
33
+ while (true) {
34
+ const answer = await prompt.ask('서비스 번호를 선택하세요 (0: 종료): ');
35
+ const picked = Number.parseInt(answer, 10);
36
+ if (!Number.isNaN(picked) && String(picked) === answer.trim()) {
37
+ if (picked >= 0 && picked <= 4) {
38
+ return picked;
39
+ }
40
+ writeOut('번호로 입력하세요. 0부터 4 사이에서 선택할 수 있습니다.');
41
+ continue;
42
+ }
43
+ if (answer.trim().length > 0) {
44
+ return answer.trim();
45
+ }
46
+ writeOut('번호로 입력하세요. 0부터 4 사이에서 선택할 수 있습니다.');
47
+ }
48
+ }
49
+ function createPrompt() {
50
+ const rl = createInterface({ input, output });
51
+ return {
52
+ ask: async (question) => {
53
+ const answer = await rl.question(question);
54
+ return answer.trim();
55
+ },
56
+ close: () => {
57
+ rl.close();
58
+ },
59
+ };
60
+ }
61
+ /**
62
+ * 테스트 전용 내부 헬퍼 노출
63
+ */
64
+ export const cliInteractiveTestables = {
65
+ isRecord,
66
+ toText,
67
+ buildDaisoStoreKeywordVariants,
68
+ fetchEnvelope,
69
+ fetchStoresWithKeywordFallback,
70
+ parseStores,
71
+ parseDaisoProducts,
72
+ askMenu,
73
+ askYesNo,
74
+ askNonEmpty,
75
+ askNextAction,
76
+ printStoreDetail,
77
+ inferDaisoFreeTextIntent,
78
+ runDaisoItemSearch,
79
+ runOliveyoungItemSearch,
80
+ runCuItemSearch,
81
+ runLotteCinemaSearch,
82
+ };
83
+ export async function runInteractiveCli(deps) {
84
+ const prompt = deps.createPrompt ? deps.createPrompt() : createPrompt();
85
+ try {
86
+ deps.writeOut('daiso 인터랙티브 모드');
87
+ let keepRunning = true;
88
+ while (keepRunning) {
89
+ deps.writeOut('');
90
+ deps.writeOut('[서비스 선택]');
91
+ deps.writeOut('1. 다이소');
92
+ deps.writeOut('2. 올리브영');
93
+ deps.writeOut('3. CU');
94
+ deps.writeOut('4. 롯데시네마');
95
+ const serviceChoice = await askServiceChoice(prompt, deps.writeOut);
96
+ if (typeof serviceChoice === 'string') {
97
+ const intent = inferDaisoFreeTextIntent(serviceChoice);
98
+ deps.writeOut('자유 입력을 다이소 조회로 처리합니다.');
99
+ deps.writeOut(`- 상품 키워드: ${intent.productQuery}`);
100
+ deps.writeOut(`- 위치 키워드: ${intent.storeKeyword}`);
101
+ const storeResult = await fetchStoresWithKeywordFallback(deps.fetchImpl, 'daiso', intent.storeKeyword);
102
+ const stores = storeResult.stores;
103
+ if (stores.length === 0) {
104
+ deps.writeOut('검색된 매장이 없습니다.');
105
+ deps.writeOut('힌트: "안산 중앙역" 대신 "안산중앙" 또는 "고잔"으로 검색해보세요.');
106
+ keepRunning = await askYesNo(prompt, '다시 시도할까요? (y/n): ');
107
+ continue;
108
+ }
109
+ const selectedStore = stores.length === 1
110
+ ? stores[0]
111
+ : await pickFromList({
112
+ prompt,
113
+ writeOut: deps.writeOut,
114
+ title: '[매장 선택]',
115
+ emptyText: '검색된 매장이 없습니다.',
116
+ cancelText: '매장 검색으로 돌아갑니다.',
117
+ items: stores,
118
+ renderItem: (store, index) => `${index + 1}. ${store.name} | ${store.address || '주소 정보 없음'}`,
119
+ filterText: (store) => `${store.name} ${store.address} ${store.phone}`,
120
+ indexText: '입력: 번호 선택 | /키워드 필터 | all 전체보기 | 0 다시 검색',
121
+ });
122
+ if (!selectedStore) {
123
+ continue;
124
+ }
125
+ printStoreDetail(deps.writeOut, selectedStore);
126
+ await runDaisoItemSearch(deps, prompt, selectedStore, intent.productQuery);
127
+ const nextAction = await askNextAction(prompt, deps.writeOut);
128
+ if (nextAction === 'exit') {
129
+ keepRunning = false;
130
+ }
131
+ continue;
132
+ }
133
+ if (serviceChoice === 0) {
134
+ break;
135
+ }
136
+ if (serviceChoice === 4) {
137
+ const theaterKeyword = await askNonEmpty(prompt, '극장 검색 키워드를 입력하세요: ');
138
+ const payload = await fetchEnvelope(deps.fetchImpl, '/api/lottecinema/theaters', {
139
+ keyword: theaterKeyword,
140
+ limit: '10',
141
+ });
142
+ const theaters = parseLotteCinemaTheaters(payload).filter((entry) => `${entry.name} ${entry.address}`.includes(theaterKeyword));
143
+ if (theaters.length === 0) {
144
+ deps.writeOut('검색된 극장이 없습니다.');
145
+ keepRunning = await askYesNo(prompt, '다시 시도할까요? (y/n): ');
146
+ continue;
147
+ }
148
+ const selectedTheater = await pickFromList({
149
+ prompt,
150
+ writeOut: deps.writeOut,
151
+ title: '[극장 선택]',
152
+ emptyText: '검색된 극장이 없습니다.',
153
+ cancelText: '극장 검색으로 돌아갑니다.',
154
+ items: theaters,
155
+ renderItem: (theater, index) => `${index + 1}. ${theater.name} | ${theater.address || '주소 정보 없음'} | ${theater.distanceKm}km`,
156
+ filterText: (theater) => `${theater.name} ${theater.address} ${theater.theaterId}`,
157
+ indexText: '입력: 번호 선택 | /키워드 필터 | all 전체보기 | 0 다시 검색',
158
+ });
159
+ if (!selectedTheater) {
160
+ continue;
161
+ }
162
+ printTheaterDetail(deps.writeOut, selectedTheater);
163
+ let keepTheaterSearch = true;
164
+ while (keepTheaterSearch) {
165
+ await runLotteCinemaSearch(deps, prompt, selectedTheater);
166
+ const nextAction = await askNextAction(prompt, deps.writeOut);
167
+ if (nextAction === 'same-store') {
168
+ continue;
169
+ }
170
+ if (nextAction === 'change-store') {
171
+ keepTheaterSearch = false;
172
+ continue;
173
+ }
174
+ keepRunning = false;
175
+ keepTheaterSearch = false;
176
+ }
177
+ continue;
178
+ }
179
+ const service = serviceChoice === 1 ? 'daiso' : serviceChoice === 2 ? 'oliveyoung' : 'cu';
180
+ const storeKeyword = await askNonEmpty(prompt, '매장 검색 키워드를 입력하세요: ');
181
+ const storeResult = await fetchStoresWithKeywordFallback(deps.fetchImpl, service, storeKeyword);
182
+ const stores = storeResult.stores;
183
+ if (service === 'daiso' && stores.length > 0 && storeResult.matchedKeyword !== storeKeyword) {
184
+ deps.writeOut(`입력 키워드 "${storeKeyword}" 대신 "${storeResult.matchedKeyword}"로 매장을 찾았습니다.`);
185
+ }
186
+ if (stores.length === 0) {
187
+ deps.writeOut('검색된 매장이 없습니다.');
188
+ if (service === 'daiso') {
189
+ deps.writeOut('힌트: "안산 중앙역" 대신 "안산중앙" 또는 "고잔"으로 검색해보세요.');
190
+ }
191
+ keepRunning = await askYesNo(prompt, '다시 시도할까요? (y/n): ');
192
+ continue;
193
+ }
194
+ const selectedStore = await pickFromList({
195
+ prompt,
196
+ writeOut: deps.writeOut,
197
+ title: '[매장 선택]',
198
+ emptyText: '검색된 매장이 없습니다.',
199
+ cancelText: '매장 검색으로 돌아갑니다.',
200
+ items: stores,
201
+ renderItem: (store, index) => `${index + 1}. ${store.name} | ${store.address || '주소 정보 없음'}`,
202
+ filterText: (store) => `${store.name} ${store.address} ${store.phone}`,
203
+ indexText: '입력: 번호 선택 | /키워드 필터 | all 전체보기 | 0 다시 검색',
204
+ });
205
+ if (!selectedStore) {
206
+ continue;
207
+ }
208
+ printStoreDetail(deps.writeOut, selectedStore);
209
+ let keepItemSearch = true;
210
+ while (keepItemSearch) {
211
+ if (service === 'daiso') {
212
+ await runDaisoItemSearch(deps, prompt, selectedStore);
213
+ }
214
+ else if (service === 'oliveyoung') {
215
+ await runOliveyoungItemSearch(deps, prompt, selectedStore);
216
+ }
217
+ else {
218
+ await runCuItemSearch(deps, prompt, selectedStore);
219
+ }
220
+ const nextAction = await askNextAction(prompt, deps.writeOut);
221
+ if (nextAction === 'same-store') {
222
+ continue;
223
+ }
224
+ if (nextAction === 'change-store') {
225
+ keepItemSearch = false;
226
+ continue;
227
+ }
228
+ keepRunning = false;
229
+ keepItemSearch = false;
230
+ }
231
+ }
232
+ deps.writeOut('인터랙티브 모드를 종료합니다.');
233
+ return 0;
234
+ }
235
+ catch (error) {
236
+ const message = error instanceof Error ? error.message : String(error);
237
+ deps.writeErr(`인터랙티브 실행 중 오류 발생: ${message}`);
238
+ return 1;
239
+ }
240
+ finally {
241
+ prompt.close();
242
+ }
243
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * 인터랙티브 목록 선택 유틸리티
3
+ *
4
+ * 번호 선택, /키워드 필터, all(전체 복원), 0(취소)을 지원합니다.
5
+ */
6
+ function normalize(value) {
7
+ return value.toLowerCase();
8
+ }
9
+ function defaultFilterText(item) {
10
+ return typeof item === 'string' ? item : JSON.stringify(item);
11
+ }
12
+ export async function pickFromList(options) {
13
+ const { prompt, writeOut, title, emptyText, cancelText, items, renderItem, indexText = '입력: 번호 선택 | /키워드 필터 | all 전체보기 | 0 취소', filterText = defaultFilterText, } = options;
14
+ if (items.length === 0) {
15
+ writeOut(emptyText);
16
+ return null;
17
+ }
18
+ let visibleItems = items;
19
+ while (true) {
20
+ writeOut('');
21
+ writeOut(title);
22
+ for (let i = 0; i < visibleItems.length; i += 1) {
23
+ writeOut(renderItem(visibleItems[i], i));
24
+ }
25
+ writeOut(indexText);
26
+ const answer = await prompt.ask('선택: ');
27
+ if (answer === '0') {
28
+ writeOut(cancelText);
29
+ return null;
30
+ }
31
+ if (answer === 'all') {
32
+ visibleItems = items;
33
+ continue;
34
+ }
35
+ if (answer.startsWith('/')) {
36
+ const query = answer.slice(1).trim();
37
+ if (!query) {
38
+ visibleItems = items;
39
+ continue;
40
+ }
41
+ const needle = normalize(query);
42
+ const filtered = items.filter((item) => normalize(filterText(item)).includes(needle));
43
+ if (filtered.length === 0) {
44
+ writeOut(`"${query}" 검색 결과가 없습니다.`);
45
+ continue;
46
+ }
47
+ visibleItems = filtered;
48
+ continue;
49
+ }
50
+ const picked = Number.parseInt(answer, 10);
51
+ if (Number.isNaN(picked) || picked < 1 || picked > visibleItems.length) {
52
+ writeOut(`번호로 입력하세요. 1부터 ${visibleItems.length} 사이에서 선택하거나 0으로 취소하세요.`);
53
+ continue;
54
+ }
55
+ return visibleItems[picked - 1];
56
+ }
57
+ }
@@ -29,7 +29,17 @@ function formatMeta(meta) {
29
29
  return [`메타: ${pairs.join(', ')}`];
30
30
  }
31
31
  function detectName(item) {
32
- const keys = ['name', 'productName', 'storeName', 'theaterName', 'movieName'];
32
+ const keys = [
33
+ 'name',
34
+ 'productName',
35
+ 'goodsNm',
36
+ 'goodsName',
37
+ 'itemName',
38
+ 'searchItemName',
39
+ 'storeName',
40
+ 'theaterName',
41
+ 'movieName',
42
+ ];
33
43
  for (const key of keys) {
34
44
  if (typeof item[key] === 'string' && item[key].trim().length > 0) {
35
45
  return item[key];
@@ -49,8 +59,18 @@ function formatCollection(title, items) {
49
59
  continue;
50
60
  }
51
61
  const name = detectName(entry);
52
- const id = entry.id ?? entry.productId ?? entry.PD_NO ?? entry.storeCode ?? entry.theaterId ?? entry.movieId;
53
- const price = entry.price ?? entry.prc ?? entry.PD_PRC;
62
+ const id = entry.id ??
63
+ entry.productId ??
64
+ entry.PD_NO ??
65
+ entry.itemCode ??
66
+ entry.pluCd ??
67
+ entry.goodsNumber ??
68
+ entry.productNo ??
69
+ entry.storeCode ??
70
+ entry.bizNo ??
71
+ entry.theaterId ??
72
+ entry.movieId;
73
+ const price = entry.price ?? entry.prc ?? entry.PD_PRC ?? entry.viewPrice ?? entry.priceToPay ?? entry.searchItemSellPrice;
54
74
  let detail = `- ${name}`;
55
75
  if (id !== undefined) {
56
76
  detail += ` [${toText(id)}]`;
@@ -58,6 +78,22 @@ function formatCollection(title, items) {
58
78
  if (price !== undefined) {
59
79
  detail += ` / ${toText(price)}원`;
60
80
  }
81
+ const detailParts = [
82
+ entry.address ?? entry.storeAddress,
83
+ entry.distance !== undefined ? `${toText(entry.distance)}km` : undefined,
84
+ entry.distanceKm !== undefined ? `${toText(entry.distanceKm)}km` : undefined,
85
+ entry.distanceM !== undefined ? `${toText(entry.distanceM)}m` : undefined,
86
+ entry.quantity !== undefined ? `수량 ${toText(entry.quantity)}` : undefined,
87
+ entry.stock !== undefined ? `수량 ${toText(entry.stock)}` : undefined,
88
+ entry.realStockQuantity !== undefined ? `수량 ${toText(entry.realStockQuantity)}` : undefined,
89
+ entry.bizQty !== undefined ? `수량 ${toText(entry.bizQty)}` : undefined,
90
+ entry.remainQuantity !== undefined ? `수량 ${toText(entry.remainQuantity)}` : undefined,
91
+ entry.o2oRemainQuantity !== undefined ? `수량 ${toText(entry.o2oRemainQuantity)}` : undefined,
92
+ entry.stockLabel,
93
+ ].filter((value) => value !== undefined);
94
+ if (detailParts.length > 0) {
95
+ detail += ` / ${detailParts.map(toText).join(' / ')}`;
96
+ }
61
97
  lines.push(detail);
62
98
  }
63
99
  if (items.length > preview.length) {
@@ -65,6 +101,47 @@ function formatCollection(title, items) {
65
101
  }
66
102
  return lines;
67
103
  }
104
+ function formatNestedInventory(data) {
105
+ const lines = [];
106
+ if (isRecord(data.nearbyStores)) {
107
+ lines.push(...formatCollection('주변 매장', data.nearbyStores.stores));
108
+ }
109
+ if (isRecord(data.inventory)) {
110
+ lines.push(...formatCollection('재고 매장', data.inventory.stores));
111
+ lines.push(...formatCollection('재고 항목', data.inventory.items));
112
+ lines.push(...formatCollection('재고 상품', data.inventory.products));
113
+ if (Array.isArray(data.inventory.products)) {
114
+ for (const product of data.inventory.products.slice(0, 3)) {
115
+ if (!isRecord(product) || !isRecord(product.storeInventory)) {
116
+ continue;
117
+ }
118
+ lines.push(...formatCollection('상품별 매장 재고', product.storeInventory.stores));
119
+ }
120
+ }
121
+ }
122
+ return lines;
123
+ }
124
+ function formatDisplayLocation(data) {
125
+ const lines = [`진열 위치: ${data.hasLocation === true ? '있음' : '없음'}`];
126
+ if (!Array.isArray(data.locations) || data.locations.length === 0) {
127
+ return lines;
128
+ }
129
+ lines.push(`위치 목록: ${data.locations.length}건`);
130
+ for (const location of data.locations.slice(0, 5)) {
131
+ if (!isRecord(location)) {
132
+ lines.push(`- ${toText(location)}`);
133
+ continue;
134
+ }
135
+ const zoneNo = toText(location.zoneNo);
136
+ const stairNo = toText(location.stairNo);
137
+ const storeErp = location.storeErp !== undefined ? ` / storeErp ${toText(location.storeErp)}` : '';
138
+ lines.push(`- 구역 ${zoneNo} / 층 ${stairNo}${storeErp}`);
139
+ }
140
+ if (data.locations.length > 5) {
141
+ lines.push(`...외 ${data.locations.length - 5}건`);
142
+ }
143
+ return lines;
144
+ }
68
145
  function formatProductDetail(data) {
69
146
  const id = data.id ?? data.productId ?? '-';
70
147
  const name = data.name ?? data.productName ?? '-';
@@ -113,6 +190,16 @@ export function renderApiEnvelope(command, url, payload) {
113
190
  else if (command === 'inventory') {
114
191
  lines.push(...formatInventory(data));
115
192
  }
193
+ else if (command === 'display-location') {
194
+ lines.push(...formatDisplayLocation(data));
195
+ }
196
+ else if (command === 'cu-inventory') {
197
+ lines.push(...formatCollection('매장 목록', isRecord(data.nearbyStores) ? data.nearbyStores.stores : undefined));
198
+ lines.push(...formatCollection('재고 항목', isRecord(data.inventory) ? data.inventory.items : undefined));
199
+ }
200
+ else if (command.endsWith('-inventory')) {
201
+ lines.push(...formatNestedInventory(data));
202
+ }
116
203
  else {
117
204
  lines.push(...formatCollection('제품 목록', data.products));
118
205
  lines.push(...formatCollection('매장 목록', data.stores));
@@ -0,0 +1,66 @@
1
+ /**
2
+ * MCP/API 공통 에러 진단 구조
3
+ */
4
+ const SERVICE_PREFIXES = [
5
+ 'daiso',
6
+ 'gs25',
7
+ 'seveneleven',
8
+ 'cu',
9
+ 'emart24',
10
+ 'lottemart',
11
+ 'megabox',
12
+ 'lottecinema',
13
+ 'cgv',
14
+ 'oliveyoung',
15
+ ];
16
+ function normalizeService(value) {
17
+ return value.toLowerCase().replace(/_/g, '');
18
+ }
19
+ function toOperation(parts) {
20
+ const filtered = parts.filter((part) => part.length > 0 && part !== 'FAILED');
21
+ if (filtered.length === 0) {
22
+ return undefined;
23
+ }
24
+ return filtered.map((part) => part.toLowerCase()).join('_');
25
+ }
26
+ function inferServiceAndOperation(code) {
27
+ const parts = code.split('_').filter((part) => part.length > 0);
28
+ const normalizedFirst = normalizeService(parts[0] || '');
29
+ const service = SERVICE_PREFIXES.find((item) => normalizeService(item) === normalizedFirst);
30
+ if (!service) {
31
+ return { operation: toOperation(parts) };
32
+ }
33
+ return {
34
+ service,
35
+ operation: toOperation(parts.slice(1)),
36
+ };
37
+ }
38
+ function isRetryable(code, status) {
39
+ if (typeof status === 'number') {
40
+ return status >= 500 || status === 408 || status === 429;
41
+ }
42
+ const normalized = code.toUpperCase();
43
+ return normalized.includes('TIMEOUT') || normalized.includes('FAILED');
44
+ }
45
+ function buildHint(retryable) {
46
+ return retryable
47
+ ? '일시적인 외부 서비스 오류일 수 있습니다. 잠시 후 다시 시도하세요.'
48
+ : '입력값 또는 요청 조건을 확인하세요.';
49
+ }
50
+ export function toStandardErrorDiagnostics(code, message, options = {}) {
51
+ const inferred = inferServiceAndOperation(code);
52
+ const retryable = isRetryable(code, options.status || options.upstreamStatus);
53
+ return {
54
+ code,
55
+ message,
56
+ status: options.status,
57
+ retryable,
58
+ service: options.service || inferred.service,
59
+ operation: options.operation || inferred.operation,
60
+ upstreamStatus: options.upstreamStatus,
61
+ hint: buildHint(retryable),
62
+ };
63
+ }
64
+ export function getErrorMessage(error) {
65
+ return error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
66
+ }