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,259 @@
1
+ /**
2
+ * 인터랙티브 CLI 상품/극장 조회 절차
3
+ */
4
+ import { pickFromList } from '../cliPicker.js';
5
+ import { isRecord, parseDaisoProducts, toText } from '../utils/cliInteractiveHelpers.js';
6
+ import { askNonEmpty } from './interactivePrompt.js';
7
+ import { fetchEnvelope } from './interactiveFetch.js';
8
+ export function parseLotteCinemaTheaters(payload) {
9
+ if (!isRecord(payload) || payload.success !== true || !isRecord(payload.data)) {
10
+ return [];
11
+ }
12
+ const theaters = payload.data.theaters;
13
+ if (!Array.isArray(theaters)) {
14
+ return [];
15
+ }
16
+ return theaters
17
+ .filter((entry) => isRecord(entry))
18
+ .map((entry) => ({
19
+ theaterId: toText(entry.theaterId),
20
+ name: toText(entry.theaterName),
21
+ address: toText(entry.address),
22
+ distanceKm: toText(entry.distanceKm),
23
+ }))
24
+ .filter((entry) => entry.theaterId.length > 0 && entry.name.length > 0);
25
+ }
26
+ export function printTheaterDetail(writeOut, theater) {
27
+ writeOut('');
28
+ writeOut('[선택한 극장 정보]');
29
+ writeOut(`- 극장명: ${theater.name}`);
30
+ writeOut(`- 극장 ID: ${theater.theaterId}`);
31
+ writeOut(`- 주소: ${theater.address || '정보 없음'}`);
32
+ writeOut(`- 거리: ${theater.distanceKm ? `${theater.distanceKm}km` : '정보 없음'}`);
33
+ writeOut('');
34
+ }
35
+ export async function runLotteCinemaSearch(deps, prompt, theater) {
36
+ const playDate = await askNonEmpty(prompt, '조회 날짜를 입력하세요 (YYYYMMDD): ');
37
+ const payload = await fetchEnvelope(deps.fetchImpl, '/api/lottecinema/movies', {
38
+ playDate,
39
+ theaterId: theater.theaterId,
40
+ });
41
+ if (!isRecord(payload) || payload.success !== true || !isRecord(payload.data)) {
42
+ deps.writeOut('롯데시네마 응답을 해석하지 못했습니다.');
43
+ return;
44
+ }
45
+ const moviesRaw = payload.data.movies;
46
+ const showtimesRaw = payload.data.showtimes;
47
+ const movies = Array.isArray(moviesRaw)
48
+ ? moviesRaw
49
+ .filter((entry) => isRecord(entry))
50
+ .map((entry) => ({
51
+ movieId: toText(entry.movieId),
52
+ movieName: toText(entry.movieName),
53
+ }))
54
+ .filter((entry) => entry.movieId.length > 0 && entry.movieName.length > 0)
55
+ : [];
56
+ const seats = Array.isArray(showtimesRaw)
57
+ ? showtimesRaw
58
+ .filter((entry) => isRecord(entry))
59
+ .map((entry) => ({
60
+ movieName: toText(entry.movieName),
61
+ screenName: toText(entry.screenName),
62
+ startTime: toText(entry.startTime),
63
+ remainingSeats: toText(entry.remainingSeats),
64
+ totalSeats: toText(entry.totalSeats),
65
+ }))
66
+ .filter((entry) => entry.movieName.length > 0)
67
+ : [];
68
+ deps.writeOut('');
69
+ deps.writeOut('[롯데시네마 상영 결과]');
70
+ deps.writeOut(`- 극장: ${theater.name}`);
71
+ deps.writeOut(`- 날짜: ${playDate}`);
72
+ deps.writeOut(`- 상영 영화 수: ${movies.length}`);
73
+ if (movies.length === 0) {
74
+ deps.writeOut('- 상영 중인 영화가 없습니다.');
75
+ }
76
+ else {
77
+ for (const movie of movies.slice(0, 10)) {
78
+ deps.writeOut(`- 영화: ${movie.movieName} (${movie.movieId})`);
79
+ }
80
+ if (movies.length > 10) {
81
+ deps.writeOut(`- 영화: ...외 ${movies.length - 10}편`);
82
+ }
83
+ }
84
+ if (seats.length === 0) {
85
+ deps.writeOut('- 회차/좌석 정보가 없습니다.');
86
+ return;
87
+ }
88
+ deps.writeOut('- 회차/좌석:');
89
+ for (const seat of seats.slice(0, 15)) {
90
+ deps.writeOut(` ${seat.startTime} | ${seat.movieName} | ${seat.screenName || '상영관 정보 없음'} | ${seat.remainingSeats}/${seat.totalSeats}`);
91
+ }
92
+ if (seats.length > 15) {
93
+ deps.writeOut(` ...외 ${seats.length - 15}건`);
94
+ }
95
+ }
96
+ export async function runDaisoItemSearch(deps, prompt, store) {
97
+ const keyword = await askNonEmpty(prompt, '찾을 상품 키워드를 입력하세요: ');
98
+ const productsPayload = await fetchEnvelope(deps.fetchImpl, '/api/daiso/products', {
99
+ q: keyword,
100
+ pageSize: '10',
101
+ });
102
+ const products = parseDaisoProducts(productsPayload);
103
+ if (products.length === 0) {
104
+ deps.writeOut('검색된 다이소 상품이 없습니다.');
105
+ return;
106
+ }
107
+ const selectedProduct = await pickFromList({
108
+ prompt,
109
+ writeOut: deps.writeOut,
110
+ title: '[상품 선택]',
111
+ emptyText: '검색된 다이소 상품이 없습니다.',
112
+ cancelText: '상품 선택을 취소했습니다.',
113
+ items: products,
114
+ renderItem: (product, index) => `${index + 1}. ${product.name} (${product.price}원, ID: ${product.id})`,
115
+ filterText: (product) => `${product.name} ${product.id}`,
116
+ indexText: '입력: 번호 선택 | /키워드 필터 | all 전체보기 | 0 취소',
117
+ });
118
+ if (!selectedProduct) {
119
+ return;
120
+ }
121
+ const inventoryPayload = await fetchEnvelope(deps.fetchImpl, '/api/daiso/inventory', {
122
+ productId: selectedProduct.id,
123
+ keyword: store.name,
124
+ pageSize: '50',
125
+ });
126
+ if (!isRecord(inventoryPayload) || inventoryPayload.success !== true || !isRecord(inventoryPayload.data)) {
127
+ deps.writeOut('재고 응답을 해석하지 못했습니다.');
128
+ return;
129
+ }
130
+ const storeInventory = inventoryPayload.data.storeInventory;
131
+ if (!isRecord(storeInventory) || !Array.isArray(storeInventory.stores)) {
132
+ deps.writeOut('매장 재고 데이터가 없습니다.');
133
+ return;
134
+ }
135
+ const match = storeInventory.stores.find((entry) => {
136
+ if (!isRecord(entry)) {
137
+ return false;
138
+ }
139
+ const storeName = toText(entry.storeName);
140
+ return storeName === store.name || storeName.includes(store.name);
141
+ });
142
+ deps.writeOut('');
143
+ deps.writeOut('[재고 결과]');
144
+ deps.writeOut(`- 상품: ${selectedProduct.name}`);
145
+ deps.writeOut(`- 매장: ${store.name}`);
146
+ if (isRecord(match)) {
147
+ const quantity = toText(match.quantity) || '0';
148
+ deps.writeOut(`- 재고 수량: ${quantity}`);
149
+ return;
150
+ }
151
+ deps.writeOut('- 선택 매장에 대한 재고 정보를 찾지 못했습니다.');
152
+ }
153
+ export async function runOliveyoungItemSearch(deps, prompt, store) {
154
+ const keyword = await askNonEmpty(prompt, '찾을 상품 키워드를 입력하세요: ');
155
+ const payload = await fetchEnvelope(deps.fetchImpl, '/api/oliveyoung/inventory', {
156
+ keyword,
157
+ storeKeyword: store.name,
158
+ size: '10',
159
+ });
160
+ if (!isRecord(payload) || payload.success !== true || !isRecord(payload.data)) {
161
+ deps.writeOut('재고 응답을 해석하지 못했습니다.');
162
+ return;
163
+ }
164
+ const inventory = payload.data.inventory;
165
+ if (!isRecord(inventory) || !Array.isArray(inventory.products)) {
166
+ deps.writeOut('올리브영 상품 데이터가 없습니다.');
167
+ return;
168
+ }
169
+ const products = inventory.products
170
+ .filter((entry) => isRecord(entry))
171
+ .map((entry) => ({
172
+ goodsNumber: toText(entry.goodsNumber),
173
+ goodsName: toText(entry.goodsName),
174
+ priceToPay: Number.parseInt(toText(entry.priceToPay), 10) || 0,
175
+ o2oRemainQuantity: Number.parseInt(toText(entry.o2oRemainQuantity), 10) || 0,
176
+ }))
177
+ .filter((entry) => entry.goodsName.length > 0);
178
+ deps.writeOut('');
179
+ deps.writeOut('[재고 결과]');
180
+ deps.writeOut(`- 매장: ${store.name}`);
181
+ deps.writeOut(`- 검색어: ${keyword}`);
182
+ if (products.length === 0) {
183
+ deps.writeOut('- 검색된 상품이 없습니다.');
184
+ return;
185
+ }
186
+ const selected = await pickFromList({
187
+ prompt,
188
+ writeOut: deps.writeOut,
189
+ title: '[상품 선택]',
190
+ emptyText: '검색된 상품이 없습니다.',
191
+ cancelText: '상품 선택을 취소했습니다.',
192
+ items: products,
193
+ renderItem: (product, index) => `${index + 1}. ${product.goodsName} (${product.priceToPay}원, 남은수량 ${product.o2oRemainQuantity})`,
194
+ filterText: (product) => `${product.goodsName} ${product.goodsNumber}`,
195
+ indexText: '입력: 번호 선택 | /키워드 필터 | all 전체보기 | 0 취소',
196
+ });
197
+ if (!selected) {
198
+ return;
199
+ }
200
+ deps.writeOut(`- 상품: ${selected.goodsName}`);
201
+ deps.writeOut(`- 가격: ${selected.priceToPay}원`);
202
+ deps.writeOut(`- 남은수량: ${selected.o2oRemainQuantity}`);
203
+ }
204
+ export async function runCuItemSearch(deps, prompt, store) {
205
+ const keyword = await askNonEmpty(prompt, '찾을 상품 키워드를 입력하세요: ');
206
+ const payload = await fetchEnvelope(deps.fetchImpl, '/api/cu/inventory', {
207
+ keyword,
208
+ storeKeyword: store.name,
209
+ size: '10',
210
+ storeLimit: '10',
211
+ });
212
+ if (!isRecord(payload) || payload.success !== true || !isRecord(payload.data)) {
213
+ deps.writeOut('재고 응답을 해석하지 못했습니다.');
214
+ return;
215
+ }
216
+ const inventory = payload.data.inventory;
217
+ if (!isRecord(inventory) || !Array.isArray(inventory.items)) {
218
+ deps.writeOut('CU 상품 데이터가 없습니다.');
219
+ return;
220
+ }
221
+ const items = inventory.items
222
+ .filter((entry) => isRecord(entry))
223
+ .map((entry) => ({
224
+ itemCode: toText(entry.itemCode),
225
+ itemName: toText(entry.itemName),
226
+ price: Number.parseInt(toText(entry.price), 10) || 0,
227
+ pickupYn: toText(entry.pickupYn).toLowerCase() === 'true',
228
+ deliveryYn: toText(entry.deliveryYn).toLowerCase() === 'true',
229
+ reserveYn: toText(entry.reserveYn).toLowerCase() === 'true',
230
+ }))
231
+ .filter((entry) => entry.itemName.length > 0);
232
+ deps.writeOut('');
233
+ deps.writeOut('[재고 결과]');
234
+ deps.writeOut(`- 매장: ${store.name}`);
235
+ deps.writeOut(`- 검색어: ${keyword}`);
236
+ if (items.length === 0) {
237
+ deps.writeOut('- 검색된 상품이 없습니다.');
238
+ return;
239
+ }
240
+ const selected = await pickFromList({
241
+ prompt,
242
+ writeOut: deps.writeOut,
243
+ title: '[상품 선택]',
244
+ emptyText: '검색된 상품이 없습니다.',
245
+ cancelText: '상품 선택을 취소했습니다.',
246
+ items,
247
+ renderItem: (item, index) => `${index + 1}. ${item.itemName} (${item.price}원)`,
248
+ filterText: (item) => `${item.itemName} ${item.itemCode}`,
249
+ indexText: '입력: 번호 선택 | /키워드 필터 | all 전체보기 | 0 취소',
250
+ });
251
+ if (!selected) {
252
+ return;
253
+ }
254
+ deps.writeOut(`- 상품: ${selected.itemName}`);
255
+ deps.writeOut(`- 가격: ${selected.price}원`);
256
+ deps.writeOut(`- 픽업 가능: ${selected.pickupYn ? '예' : '아니오'}`);
257
+ deps.writeOut(`- 배달 가능: ${selected.deliveryYn ? '예' : '아니오'}`);
258
+ deps.writeOut(`- 예약 가능: ${selected.reserveYn ? '예' : '아니오'}`);
259
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * 인터랙티브 CLI 입력/출력 헬퍼
3
+ */
4
+ export async function askMenu(prompt, title, options) {
5
+ while (true) {
6
+ const answer = await prompt.ask(`${title} `);
7
+ const picked = Number.parseInt(answer, 10);
8
+ if (Number.isNaN(picked) || picked < 0 || picked > options.length) {
9
+ continue;
10
+ }
11
+ return picked;
12
+ }
13
+ }
14
+ export async function askYesNo(prompt, question) {
15
+ while (true) {
16
+ const answer = (await prompt.ask(question)).toLowerCase();
17
+ if (answer === 'y' || answer === 'yes') {
18
+ return true;
19
+ }
20
+ if (answer === 'n' || answer === 'no') {
21
+ return false;
22
+ }
23
+ }
24
+ }
25
+ export async function askNonEmpty(prompt, question) {
26
+ while (true) {
27
+ const answer = await prompt.ask(question);
28
+ if (answer.length > 0) {
29
+ return answer;
30
+ }
31
+ }
32
+ }
33
+ export async function askNextAction(prompt, writeOut) {
34
+ writeOut('');
35
+ writeOut('[다음 동작]');
36
+ writeOut('1. 같은 매장에서 다른 상품 찾기');
37
+ writeOut('2. 다른 매장/서비스 다시 선택하기');
38
+ writeOut('3. 종료하기');
39
+ const choice = await askMenu(prompt, '번호를 선택하세요:', ['same-store', 'change-store', 'exit']);
40
+ return choice === 1 ? 'same-store' : choice === 2 ? 'change-store' : 'exit';
41
+ }
42
+ export function printStoreDetail(writeOut, store) {
43
+ writeOut('');
44
+ writeOut('[선택한 매장 정보]');
45
+ writeOut(`- 매장명: ${store.name}`);
46
+ writeOut(`- 주소: ${store.address || '정보 없음'}`);
47
+ writeOut(`- 전화: ${store.phone || '정보 없음'}`);
48
+ writeOut('');
49
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * 인터랙티브 CLI 공용 타입
3
+ */
4
+ export {};
@@ -0,0 +1,4 @@
1
+ /**
2
+ * CLI 공통 타입 정의
3
+ */
4
+ export {};
package/dist/cli.js CHANGED
@@ -4,135 +4,33 @@
4
4
  *
5
5
  * npx daiso 명령으로 원격 MCP 서버 정보를 확인하고 상태를 점검합니다.
6
6
  */
7
- import { existsSync, readFileSync } from 'node:fs';
8
7
  import path from 'node:path';
9
8
  import { fileURLToPath } from 'node:url';
10
- import { spawn } from 'node:child_process';
11
- import { printCommandHelp, printHelp } from './cliHelp.js';
12
- import { renderApiEnvelope } from './cliRenderer.js';
13
- const DEFAULT_BASE_URL = 'https://mcp.aka.page';
14
- const DEFAULT_MCP_URL = `${DEFAULT_BASE_URL}/mcp`;
15
- function loadVersion() {
16
- const cliPath = fileURLToPath(import.meta.url);
17
- const packagePath = path.resolve(path.dirname(cliPath), '../package.json');
18
- if (!existsSync(packagePath)) {
19
- return '0.0.0';
20
- }
21
- const raw = readFileSync(packagePath, 'utf8');
22
- const parsed = JSON.parse(raw);
23
- return parsed.version ?? '0.0.0';
24
- }
25
- async function execCommand(command, args) {
26
- return await new Promise((resolve, reject) => {
27
- const child = spawn(command, args, {
28
- stdio: 'inherit',
29
- shell: false,
30
- });
31
- child.on('error', (error) => {
32
- reject(error);
33
- });
34
- child.on('close', (code) => {
35
- resolve(code ?? 1);
36
- });
37
- });
38
- }
39
- function createDefaultDeps() {
40
- return {
41
- fetchImpl: fetch,
42
- writeOut: (message) => {
43
- process.stdout.write(`${message}\n`);
44
- },
45
- writeErr: (message) => {
46
- process.stderr.write(`${message}\n`);
47
- },
48
- getVersion: loadVersion,
49
- nowIso: () => new Date().toISOString(),
50
- runCommand: execCommand,
51
- };
52
- }
53
- function parseCliArgs(args) {
54
- const positionals = [];
55
- const options = {};
56
- for (let index = 0; index < args.length; index += 1) {
57
- const token = args[index];
58
- if (!token.startsWith('--')) {
59
- positionals.push(token);
60
- continue;
61
- }
62
- const withoutPrefix = token.slice(2);
63
- const equalIndex = withoutPrefix.indexOf('=');
64
- if (equalIndex >= 0) {
65
- const key = withoutPrefix.slice(0, equalIndex);
66
- const value = withoutPrefix.slice(equalIndex + 1);
67
- options[key] = value;
68
- continue;
69
- }
70
- const key = withoutPrefix;
71
- const nextValue = args[index + 1];
72
- if (!nextValue || nextValue.startsWith('--')) {
73
- options[key] = 'true';
74
- continue;
75
- }
76
- options[key] = nextValue;
77
- index += 1;
78
- }
79
- return { positionals, options };
80
- }
81
- function toUrl(pathOrUrl) {
82
- if (pathOrUrl.startsWith('http://') || pathOrUrl.startsWith('https://')) {
83
- return new URL(pathOrUrl);
84
- }
85
- const normalizedPath = pathOrUrl.startsWith('/') ? pathOrUrl : `/${pathOrUrl}`;
86
- return new URL(normalizedPath, DEFAULT_BASE_URL);
87
- }
88
- function applyOptionsToQuery(url, options) {
89
- for (const [key, value] of Object.entries(options)) {
90
- url.searchParams.set(key, value);
91
- }
92
- }
93
- function toQueryOptions(options) {
94
- const queryOptions = {};
95
- for (const [key, value] of Object.entries(options)) {
96
- if (key === 'help' || key === 'json') {
97
- continue;
98
- }
99
- queryOptions[key] = value;
100
- }
101
- return queryOptions;
102
- }
103
- async function requestAndPrintResponse(fetchImpl, writeOut, writeErr, url, command, asJson) {
104
- try {
105
- const response = await fetchImpl(url.toString());
106
- if (!response.ok) {
107
- const bodyText = await response.text();
108
- writeErr(`요청 실패: HTTP ${response.status}`);
109
- if (bodyText) {
110
- writeErr(bodyText);
111
- }
112
- return 1;
113
- }
114
- const payload = (await response.json());
115
- if (asJson) {
116
- writeOut(JSON.stringify(payload, null, 2));
117
- }
118
- else {
119
- writeOut(renderApiEnvelope(command, url, payload));
120
- }
121
- return 0;
122
- }
123
- catch (error) {
124
- const message = error instanceof Error ? error.message : String(error);
125
- writeErr(`요청 중 오류 발생: ${message}`);
126
- return 1;
127
- }
128
- }
9
+ import { printHelp, printCommandHelp } from './cliHelp.js';
10
+ import { createDefaultDeps } from './cli/deps.js';
11
+ import { DEFAULT_BASE_URL, DEFAULT_MCP_URL } from './cli/constants.js';
12
+ import { handleGet, handleProducts, handleProduct, handleStores, handleInventory, handleDisplayLocation, } from './cli/commands/daiso.js';
13
+ import { handleCuStores, handleCuInventory, handleEmart24Stores, handleEmart24Products, handleEmart24Inventory, handleLotteMartStores, handleLotteMartProducts, handleGs25Stores, handleGs25Products, handleGs25Inventory, handleSevenElevenProducts, handleSevenElevenStores, handleSevenElevenPopwords, handleSevenElevenCatalog, handleLottecinemaTheaters, handleLottecinemaMovies, handleLottecinemaSeats, } from './cli/commands/convenience.js';
129
14
  export async function runCli(argv, deps) {
130
15
  const resolvedDeps = {
131
16
  ...createDefaultDeps(),
132
17
  ...deps,
133
18
  };
134
- const [command, ...options] = argv;
135
- if (!command || command === 'help' || command === '--help' || command === '-h') {
19
+ const nonInteractive = argv.includes('--non-interactive');
20
+ const normalizedArgv = argv.filter((arg) => arg !== '--non-interactive');
21
+ const [command, ...options] = normalizedArgv;
22
+ if (!command) {
23
+ if (!nonInteractive && resolvedDeps.isInteractiveTerminal()) {
24
+ return await resolvedDeps.runInteractive({
25
+ fetchImpl: resolvedDeps.fetchImpl,
26
+ writeOut: resolvedDeps.writeOut,
27
+ writeErr: resolvedDeps.writeErr,
28
+ });
29
+ }
30
+ printHelp(resolvedDeps.writeOut);
31
+ return 0;
32
+ }
33
+ if (command === 'help' || command === '--help' || command === '-h') {
136
34
  const maybeCommand = options[0];
137
35
  if (maybeCommand) {
138
36
  return printCommandHelp(maybeCommand, resolvedDeps.writeOut, resolvedDeps.writeErr);
@@ -184,81 +82,52 @@ export async function runCli(argv, deps) {
184
82
  resolvedDeps.writeOut(`claude ${cliArgs.join(' ')}`);
185
83
  return 0;
186
84
  }
187
- if (command === 'get') {
188
- const parsed = parseCliArgs(options);
189
- if (parsed.options.help === 'true') {
190
- return printCommandHelp('get', resolvedDeps.writeOut, resolvedDeps.writeErr);
191
- }
192
- const targetPath = parsed.positionals[0];
193
- if (!targetPath) {
194
- resolvedDeps.writeErr('get 명령은 경로가 필요합니다. 예: daiso get /api/daiso/products --q 수납박스');
195
- return 1;
196
- }
197
- const targetUrl = toUrl(targetPath);
198
- applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
199
- return await requestAndPrintResponse(resolvedDeps.fetchImpl, resolvedDeps.writeOut, resolvedDeps.writeErr, targetUrl, command, parsed.options.json === 'true');
200
- }
201
- if (command === 'products') {
202
- const parsed = parseCliArgs(options);
203
- if (parsed.options.help === 'true') {
204
- return printCommandHelp('products', resolvedDeps.writeOut, resolvedDeps.writeErr);
205
- }
206
- const query = parsed.positionals[0];
207
- if (!query) {
208
- resolvedDeps.writeErr('products 명령은 검색어가 필요합니다. 예: daiso products 수납박스');
209
- return 1;
210
- }
211
- const targetUrl = toUrl('/api/daiso/products');
212
- targetUrl.searchParams.set('q', query);
213
- applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
214
- return await requestAndPrintResponse(resolvedDeps.fetchImpl, resolvedDeps.writeOut, resolvedDeps.writeErr, targetUrl, command, parsed.options.json === 'true');
215
- }
216
- if (command === 'product') {
217
- const parsed = parseCliArgs(options);
218
- if (parsed.options.help === 'true') {
219
- return printCommandHelp('product', resolvedDeps.writeOut, resolvedDeps.writeErr);
220
- }
221
- const productId = parsed.positionals[0];
222
- if (!productId) {
223
- resolvedDeps.writeErr('product 명령은 제품 ID가 필요합니다. 예: daiso product 1034604');
224
- return 1;
225
- }
226
- const targetUrl = toUrl(`/api/daiso/products/${productId}`);
227
- applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
228
- return await requestAndPrintResponse(resolvedDeps.fetchImpl, resolvedDeps.writeOut, resolvedDeps.writeErr, targetUrl, command, parsed.options.json === 'true');
229
- }
230
- if (command === 'stores') {
231
- const parsed = parseCliArgs(options);
232
- if (parsed.options.help === 'true') {
233
- return printCommandHelp('stores', resolvedDeps.writeOut, resolvedDeps.writeErr);
234
- }
235
- const keyword = parsed.positionals[0];
236
- if (keyword) {
237
- parsed.options.keyword = keyword;
238
- }
239
- if (!parsed.options.keyword && !parsed.options.sido) {
240
- resolvedDeps.writeErr('stores 명령은 keyword 또는 --sido가 필요합니다. 예: daiso stores 강남역 / daiso stores --sido 서울');
241
- return 1;
242
- }
243
- const targetUrl = toUrl('/api/daiso/stores');
244
- applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
245
- return await requestAndPrintResponse(resolvedDeps.fetchImpl, resolvedDeps.writeOut, resolvedDeps.writeErr, targetUrl, command, parsed.options.json === 'true');
246
- }
247
- if (command === 'inventory') {
248
- const parsed = parseCliArgs(options);
249
- if (parsed.options.help === 'true') {
250
- return printCommandHelp('inventory', resolvedDeps.writeOut, resolvedDeps.writeErr);
251
- }
252
- const productId = parsed.positionals[0];
253
- if (!productId) {
254
- resolvedDeps.writeErr('inventory 명령은 제품 ID가 필요합니다. 예: daiso inventory 1034604 --keyword 강남역');
255
- return 1;
256
- }
257
- const targetUrl = toUrl('/api/daiso/inventory');
258
- targetUrl.searchParams.set('productId', productId);
259
- applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
260
- return await requestAndPrintResponse(resolvedDeps.fetchImpl, resolvedDeps.writeOut, resolvedDeps.writeErr, targetUrl, command, parsed.options.json === 'true');
261
- }
85
+ if (command === 'get')
86
+ return await handleGet(options, resolvedDeps);
87
+ if (command === 'products')
88
+ return await handleProducts(options, resolvedDeps);
89
+ if (command === 'product')
90
+ return await handleProduct(options, resolvedDeps);
91
+ if (command === 'stores')
92
+ return await handleStores(options, resolvedDeps);
93
+ if (command === 'inventory')
94
+ return await handleInventory(options, resolvedDeps);
95
+ if (command === 'display-location')
96
+ return await handleDisplayLocation(options, resolvedDeps);
97
+ if (command === 'cu-stores')
98
+ return await handleCuStores(options, resolvedDeps);
99
+ if (command === 'cu-inventory')
100
+ return await handleCuInventory(options, resolvedDeps);
101
+ if (command === 'lottecinema-theaters')
102
+ return await handleLottecinemaTheaters(options, resolvedDeps);
103
+ if (command === 'lottecinema-movies')
104
+ return await handleLottecinemaMovies(options, resolvedDeps);
105
+ if (command === 'lottecinema-seats')
106
+ return await handleLottecinemaSeats(options, resolvedDeps);
107
+ if (command === 'emart24-stores')
108
+ return await handleEmart24Stores(options, resolvedDeps);
109
+ if (command === 'emart24-products')
110
+ return await handleEmart24Products(options, resolvedDeps);
111
+ if (command === 'emart24-inventory')
112
+ return await handleEmart24Inventory(options, resolvedDeps);
113
+ if (command === 'lottemart-stores')
114
+ return await handleLotteMartStores(options, resolvedDeps);
115
+ if (command === 'lottemart-products')
116
+ return await handleLotteMartProducts(options, resolvedDeps);
117
+ if (command === 'gs25-stores')
118
+ return await handleGs25Stores(options, resolvedDeps);
119
+ if (command === 'gs25-products')
120
+ return await handleGs25Products(options, resolvedDeps);
121
+ if (command === 'gs25-inventory')
122
+ return await handleGs25Inventory(options, resolvedDeps);
123
+ if (command === 'seveneleven-products')
124
+ return await handleSevenElevenProducts(options, resolvedDeps);
125
+ if (command === 'seveneleven-stores')
126
+ return await handleSevenElevenStores(options, resolvedDeps);
127
+ if (command === 'seveneleven-popwords')
128
+ return await handleSevenElevenPopwords(options, resolvedDeps);
129
+ if (command === 'seveneleven-catalog')
130
+ return await handleSevenElevenCatalog(options, resolvedDeps);
262
131
  resolvedDeps.writeErr(`알 수 없는 명령어: ${command}`);
263
132
  resolvedDeps.writeErr('도움말: daiso help');
264
133
  return 1;