daiso 1.0.5 → 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 (51) hide show
  1. package/README.md +45 -7
  2. package/dist/api/configStatus.js +19 -0
  3. package/dist/api/gs25Handlers.js +25 -14
  4. package/dist/api/handlers.js +32 -3
  5. package/dist/api/healthChecks.js +185 -13
  6. package/dist/api/lottemartHandlers.js +17 -1
  7. package/dist/api/response.js +2 -0
  8. package/dist/api/routes/healthRoutes.js +20 -3
  9. package/dist/bin.js +0 -0
  10. package/dist/cli/args.js +21 -0
  11. package/dist/cli/commandOptions.js +38 -0
  12. package/dist/cli/commands/convenience.js +53 -1
  13. package/dist/cli/commands/daiso.js +18 -2
  14. package/dist/cli/http.js +58 -0
  15. package/dist/cli/interactiveItemSearch.js +4 -2
  16. package/dist/cli/interactivePrompt.js +3 -2
  17. package/dist/cli.js +22 -0
  18. package/dist/cliHelp.js +6 -0
  19. package/dist/cliInteractive.js +75 -6
  20. package/dist/cliPicker.js +1 -0
  21. package/dist/cliRenderer.js +86 -3
  22. package/dist/core/errors.js +66 -0
  23. package/dist/core/registry.js +227 -3
  24. package/dist/index.js +2 -1
  25. package/dist/pages/openapiSpecActions.js +3 -3
  26. package/dist/pages/openapiSpecPathsCu.js +7 -0
  27. package/dist/pages/openapiSpecPathsLotteMart.js +14 -0
  28. package/dist/pages/openapiSpecPathsOliveyoung.js +35 -0
  29. package/dist/pages/promptReferenceText.js +13 -7
  30. package/dist/pages/promptRetailCinemaText.js +2 -1
  31. package/dist/services/daiso/client.js +8 -0
  32. package/dist/services/daiso/index.js +2 -0
  33. package/dist/services/daiso/tools/checkInventory.js +4 -2
  34. package/dist/services/daiso/tools/findInventoryByName.js +222 -0
  35. package/dist/services/daiso/tools/getDisplayLocation.js +2 -2
  36. package/dist/services/emart24/client.js +9 -0
  37. package/dist/services/emart24/tools/searchProducts.js +27 -17
  38. package/dist/services/gs25/client.js +10 -0
  39. package/dist/services/gs25/tools/searchProducts.js +28 -21
  40. package/dist/services/lottemart/client.js +10 -1
  41. package/dist/services/lottemart/tools/searchProducts.js +8 -1
  42. package/dist/services/lottemart/zetta.js +1 -1
  43. package/dist/services/oliveyoung/client.js +124 -53
  44. package/dist/services/oliveyoung/tools/checkInventory.js +7 -2
  45. package/dist/services/seveneleven/client.js +7 -0
  46. package/dist/services/seveneleven/tools/searchProducts.js +29 -18
  47. package/dist/utils/http.js +82 -10
  48. package/dist/utils/zyte.js +47 -26
  49. package/openapi.json +3 -3
  50. package/openapi.yaml +3 -3
  51. package/package.json +26 -8
@@ -5,6 +5,15 @@ import { errorResponse, successResponse } from './response.js';
5
5
  import { fetchLotteMartStores, searchLotteMartProducts } from '../services/lottemart/client.js';
6
6
  import { DEFAULT_LOTTEMART_TIMEOUT_MS } from '../services/lottemart/config.js';
7
7
  import { probeLotteMartUpstream } from '../services/lottemart/debug.js';
8
+ function parseLotteMartProductSource(value) {
9
+ if (!value || value === 'auto') {
10
+ return 'auto';
11
+ }
12
+ if (value === 'legacy' || value === 'zetta') {
13
+ return value;
14
+ }
15
+ return null;
16
+ }
8
17
  /**
9
18
  * 롯데마트 매장 검색 API 핸들러
10
19
  * GET /api/lottemart/stores?keyword={키워드}&lat={위도}&lng={경도}
@@ -62,9 +71,15 @@ export async function handleLotteMartSearchProducts(c) {
62
71
  const storeCode = c.req.query('storeCode') || '';
63
72
  const storeName = c.req.query('storeName') || '';
64
73
  const pageLimit = parseInt(c.req.query('pageLimit') || '3', 10);
74
+ const parsedTimeoutMs = parseInt(c.req.query('timeoutMs') || String(DEFAULT_LOTTEMART_TIMEOUT_MS), 10);
75
+ const timeoutMs = Number.isFinite(parsedTimeoutMs) && parsedTimeoutMs > 0 ? parsedTimeoutMs : DEFAULT_LOTTEMART_TIMEOUT_MS;
76
+ const source = parseLotteMartProductSource(c.req.query('source'));
65
77
  if (keyword.trim().length === 0) {
66
78
  return errorResponse(c, 'MISSING_QUERY', '검색어(keyword)를 입력해주세요.');
67
79
  }
80
+ if (!source) {
81
+ return errorResponse(c, 'INVALID_LOTTEMART_SOURCE', 'source는 auto, legacy, zetta 중 하나여야 합니다.');
82
+ }
68
83
  if (storeCode.trim().length === 0 && storeName.trim().length === 0) {
69
84
  return errorResponse(c, 'MISSING_STORE', 'storeCode 또는 storeName 중 하나를 입력해주세요.');
70
85
  }
@@ -75,8 +90,9 @@ export async function handleLotteMartSearchProducts(c) {
75
90
  storeName,
76
91
  keyword,
77
92
  pageLimit,
93
+ source,
78
94
  }, {
79
- timeout: DEFAULT_LOTTEMART_TIMEOUT_MS,
95
+ timeout: timeoutMs,
80
96
  zyteApiKey: c.env?.ZYTE_API_KEY,
81
97
  });
82
98
  return successResponse(c, {
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * API 공통 응답 유틸리티
3
3
  */
4
+ import { toStandardErrorDiagnostics } from '../core/errors.js';
4
5
  /**
5
6
  * 성공 응답 생성
6
7
  */
@@ -18,5 +19,6 @@ export function errorResponse(c, code, message, status = 400) {
18
19
  return c.json({
19
20
  success: false,
20
21
  error: { code, message },
22
+ diagnostics: toStandardErrorDiagnostics(code, message, { status }),
21
23
  }, status);
22
24
  }
@@ -14,6 +14,15 @@ function readHealthCheckToken(headers) {
14
14
  }
15
15
  return (headers.get('x-health-check-key') || '').trim();
16
16
  }
17
+ function parseMode(value) {
18
+ if (!value || value === 'quick') {
19
+ return 'quick';
20
+ }
21
+ if (value === 'deep' || value === 'full') {
22
+ return value;
23
+ }
24
+ return null;
25
+ }
17
26
  export function registerHealthRoutes(app) {
18
27
  app.get('/api/health/checks', async (c) => {
19
28
  const secret = c.env?.HEALTH_CHECK_SECRET?.trim();
@@ -23,10 +32,16 @@ export function registerHealthRoutes(app) {
23
32
  if (readHealthCheckToken(c.req.raw.headers) !== secret) {
24
33
  return errorResponse(c, 'UNAUTHORIZED_HEALTH_CHECK', '유효한 헬스 체크 시크릿 키가 필요합니다.', 401);
25
34
  }
26
- const mode = c.req.query('mode') === 'deep' ? 'deep' : 'quick';
35
+ const mode = parseMode(c.req.query('mode'));
36
+ if (!mode) {
37
+ return errorResponse(c, 'INVALID_HEALTH_CHECK_MODE', 'mode는 quick, deep, full 중 하나여야 합니다.', 400);
38
+ }
27
39
  const timeoutMs = Number.parseInt(c.req.query('timeoutMs') || '3000', 10);
28
- const baseUrl = c.env?.HEALTH_CHECK_BASE_URL?.trim() || new URL(c.req.url).origin;
29
- const fetchImpl = c.env?.HEALTH_CHECK_BASE_URL && c.env?.HEALTH_CHECK_TRANSPORT !== 'network'
40
+ const slowThresholdMs = Number.parseInt(c.req.query('slowThresholdMs') || c.env?.HEALTH_CHECK_SLOW_THRESHOLD_MS || '0', 10);
41
+ const baseUrl = c.req.query('baseUrl')?.trim() || c.env?.HEALTH_CHECK_BASE_URL?.trim() || new URL(c.req.url).origin;
42
+ const transport = c.req.query('transport') || c.env?.HEALTH_CHECK_TRANSPORT || 'internal';
43
+ const cacheBust = parseBoolean(c.req.query('cacheBust')) || transport === 'network';
44
+ const fetchImpl = c.env?.HEALTH_CHECK_BASE_URL && transport !== 'network'
30
45
  ? async (input, init) => app.fetch(new Request(input, init), c.env)
31
46
  : undefined;
32
47
  const result = await runHealthChecks({
@@ -35,8 +50,10 @@ export function registerHealthRoutes(app) {
35
50
  check: c.req.query('check') || undefined,
36
51
  mode,
37
52
  timeoutMs,
53
+ slowThresholdMs,
38
54
  includeSamples: parseBoolean(c.req.query('includeSamples')),
39
55
  fresh: parseBoolean(c.req.query('fresh')),
56
+ cacheBust,
40
57
  fetchImpl,
41
58
  });
42
59
  return c.json(result);
package/dist/bin.js CHANGED
File without changes
package/dist/cli/args.js CHANGED
@@ -52,3 +52,24 @@ export function toQueryOptions(options) {
52
52
  }
53
53
  return queryOptions;
54
54
  }
55
+ export function findUnknownOption(options, allowedOptions) {
56
+ const allowed = new Set(allowedOptions);
57
+ for (const key of Object.keys(options)) {
58
+ if (!allowed.has(key)) {
59
+ return key;
60
+ }
61
+ }
62
+ return null;
63
+ }
64
+ export function writeUnknownOptionError(options, allowedOptions, writeErr, hint) {
65
+ const unknownOption = findUnknownOption(options, allowedOptions);
66
+ if (!unknownOption) {
67
+ return false;
68
+ }
69
+ writeErr(`알 수 없는 옵션: --${unknownOption}`);
70
+ if (hint) {
71
+ writeErr(hint);
72
+ }
73
+ writeErr('도움말: daiso help');
74
+ return true;
75
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * CLI 명령별 허용 옵션 정의
3
+ */
4
+ import { writeUnknownOptionError } from './args.js';
5
+ export const COMMAND_OPTION_ALLOWLISTS = {
6
+ version: [],
7
+ url: [],
8
+ health: [],
9
+ claude: ['exec'],
10
+ products: ['help', 'json', 'page', 'pageSize'],
11
+ product: ['help', 'json'],
12
+ stores: ['help', 'json', 'keyword', 'sido', 'gugun', 'dong', 'limit'],
13
+ inventory: ['help', 'json', 'keyword', 'lat', 'lng', 'page', 'pageSize'],
14
+ 'display-location': ['help', 'json'],
15
+ 'cu-stores': ['help', 'json', 'keyword', 'lat', 'lng', 'limit'],
16
+ 'cu-inventory': ['help', 'json', 'storeKeyword', 'lat', 'lng', 'size', 'offset', 'searchSort', 'storeLimit'],
17
+ 'lottecinema-theaters': ['help', 'json', 'keyword', 'lat', 'lng', 'playDate', 'limit'],
18
+ 'lottecinema-movies': ['help', 'json', 'keyword', 'playDate', 'theaterId', 'movieId', 'lat', 'lng'],
19
+ 'lottecinema-seats': ['help', 'json', 'keyword', 'playDate', 'theaterId', 'movieId', 'lat', 'lng', 'limit'],
20
+ 'emart24-stores': ['help', 'json', 'keyword', 'area1', 'area2', 'lat', 'lng', 'service24h', 'limit'],
21
+ 'emart24-products': ['help', 'json', 'page', 'pageSize'],
22
+ 'emart24-inventory': ['help', 'json', 'bizNoArr', 'storeKeyword', 'area1', 'area2', 'lat', 'lng', 'storeLimit'],
23
+ 'lottemart-stores': ['help', 'json', 'keyword', 'area', 'brandVariant', 'lat', 'lng', 'limit'],
24
+ 'lottemart-products': ['help', 'json', 'storeCode', 'storeName', 'area', 'brandVariant', 'pageLimit'],
25
+ 'gs25-stores': ['help', 'json', 'keyword', 'lat', 'lng', 'serviceCode', 'limit'],
26
+ 'gs25-products': ['help', 'json', 'serviceCode', 'limit'],
27
+ 'gs25-inventory': ['help', 'json', 'storeKeyword', 'lat', 'lng', 'serviceCode', 'storeLimit'],
28
+ 'seveneleven-products': ['help', 'json', 'page', 'size', 'sort'],
29
+ 'seveneleven-stores': ['help', 'json', 'limit'],
30
+ 'seveneleven-popwords': ['help', 'json', 'label'],
31
+ 'seveneleven-catalog': ['help', 'json', 'includeIssues', 'includeExhibition', 'limit'],
32
+ };
33
+ const COMMAND_OPTION_HINTS = {
34
+ inventory: '매장명은 --keyword로 전달하세요. 예: daiso inventory 1034604 --keyword 강남역',
35
+ };
36
+ export function validateCommandOptions(command, options, writeErr) {
37
+ return writeUnknownOptionError(options, COMMAND_OPTION_ALLOWLISTS[command], writeErr, COMMAND_OPTION_HINTS[command]);
38
+ }
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { printCommandHelp } from '../../cliHelp.js';
6
6
  import { parseCliArgs, toUrl, applyOptionsToQuery, toQueryOptions } from '../args.js';
7
+ import { validateCommandOptions } from '../commandOptions.js';
7
8
  import { requestAndPrintResponse } from '../http.js';
8
9
  export async function handleCuStores(options, deps) {
9
10
  const parsed = parseCliArgs(options);
@@ -14,6 +15,9 @@ export async function handleCuStores(options, deps) {
14
15
  if (keyword) {
15
16
  parsed.options.keyword = keyword;
16
17
  }
18
+ if (validateCommandOptions('cu-stores', parsed.options, deps.writeErr)) {
19
+ return 1;
20
+ }
17
21
  const targetUrl = toUrl('/api/cu/stores');
18
22
  applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
19
23
  return await requestAndPrintResponse(deps.fetchImpl, deps.writeOut, deps.writeErr, targetUrl, 'cu-stores', parsed.options.json === 'true');
@@ -23,6 +27,9 @@ export async function handleCuInventory(options, deps) {
23
27
  if (parsed.options.help === 'true') {
24
28
  return printCommandHelp('cu-inventory', deps.writeOut, deps.writeErr);
25
29
  }
30
+ if (validateCommandOptions('cu-inventory', parsed.options, deps.writeErr)) {
31
+ return 1;
32
+ }
26
33
  const keyword = parsed.positionals[0];
27
34
  if (!keyword) {
28
35
  deps.writeErr('cu-inventory 명령은 검색어가 필요합니다. 예: daiso cu-inventory 과자');
@@ -42,6 +49,9 @@ export async function handleEmart24Stores(options, deps) {
42
49
  if (keyword) {
43
50
  parsed.options.keyword = keyword;
44
51
  }
52
+ if (validateCommandOptions('emart24-stores', parsed.options, deps.writeErr)) {
53
+ return 1;
54
+ }
45
55
  const targetUrl = toUrl('/api/emart24/stores');
46
56
  applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
47
57
  return await requestAndPrintResponse(deps.fetchImpl, deps.writeOut, deps.writeErr, targetUrl, 'emart24-stores', parsed.options.json === 'true');
@@ -51,6 +61,9 @@ export async function handleEmart24Products(options, deps) {
51
61
  if (parsed.options.help === 'true') {
52
62
  return printCommandHelp('emart24-products', deps.writeOut, deps.writeErr);
53
63
  }
64
+ if (validateCommandOptions('emart24-products', parsed.options, deps.writeErr)) {
65
+ return 1;
66
+ }
54
67
  const keyword = parsed.positionals[0];
55
68
  if (!keyword) {
56
69
  deps.writeErr('emart24-products 명령은 검색어가 필요합니다. 예: daiso emart24-products 두바이');
@@ -66,6 +79,9 @@ export async function handleEmart24Inventory(options, deps) {
66
79
  if (parsed.options.help === 'true') {
67
80
  return printCommandHelp('emart24-inventory', deps.writeOut, deps.writeErr);
68
81
  }
82
+ if (validateCommandOptions('emart24-inventory', parsed.options, deps.writeErr)) {
83
+ return 1;
84
+ }
69
85
  const pluCd = parsed.positionals[0];
70
86
  const bizNoArr = parsed.options.bizNoArr;
71
87
  if (!pluCd || !bizNoArr) {
@@ -86,6 +102,9 @@ export async function handleLotteMartStores(options, deps) {
86
102
  if (keyword) {
87
103
  parsed.options.keyword = keyword;
88
104
  }
105
+ if (validateCommandOptions('lottemart-stores', parsed.options, deps.writeErr)) {
106
+ return 1;
107
+ }
89
108
  const targetUrl = toUrl('/api/lottemart/stores');
90
109
  applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
91
110
  return await requestAndPrintResponse(deps.fetchImpl, deps.writeOut, deps.writeErr, targetUrl, 'lottemart-stores', parsed.options.json === 'true');
@@ -95,13 +114,16 @@ export async function handleLotteMartProducts(options, deps) {
95
114
  if (parsed.options.help === 'true') {
96
115
  return printCommandHelp('lottemart-products', deps.writeOut, deps.writeErr);
97
116
  }
117
+ if (validateCommandOptions('lottemart-products', parsed.options, deps.writeErr)) {
118
+ return 1;
119
+ }
98
120
  const keyword = parsed.positionals[0];
99
121
  if (!keyword) {
100
122
  deps.writeErr('lottemart-products 명령은 검색어가 필요합니다. 예: daiso lottemart-products 콜라 --storeName 강변점');
101
123
  return 1;
102
124
  }
103
125
  if (!parsed.options.storeCode && !parsed.options.storeName) {
104
- deps.writeErr('lottemart-products 명령은 --storeCode 또는 --storeName이 필요합니다. 예: daiso lottemart-products 콜라 --storeName 강변점');
126
+ deps.writeErr('lottemart-products 명령은 --storeCode 또는 --storeName이 필요합니다. 매장을 모르면 먼저 daiso lottemart-stores 잠실 --area 서울 명령으로 storeName/storeCode를 확인하세요. 예: daiso lottemart-products 콜라 --storeName 강변점');
105
127
  return 1;
106
128
  }
107
129
  const targetUrl = toUrl('/api/lottemart/products');
@@ -118,6 +140,9 @@ export async function handleGs25Stores(options, deps) {
118
140
  if (keyword) {
119
141
  parsed.options.keyword = keyword;
120
142
  }
143
+ if (validateCommandOptions('gs25-stores', parsed.options, deps.writeErr)) {
144
+ return 1;
145
+ }
121
146
  const targetUrl = toUrl('/api/gs25/stores');
122
147
  applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
123
148
  return await requestAndPrintResponse(deps.fetchImpl, deps.writeOut, deps.writeErr, targetUrl, 'gs25-stores', parsed.options.json === 'true');
@@ -127,6 +152,9 @@ export async function handleGs25Products(options, deps) {
127
152
  if (parsed.options.help === 'true') {
128
153
  return printCommandHelp('gs25-products', deps.writeOut, deps.writeErr);
129
154
  }
155
+ if (validateCommandOptions('gs25-products', parsed.options, deps.writeErr)) {
156
+ return 1;
157
+ }
130
158
  const keyword = parsed.positionals[0];
131
159
  if (!keyword) {
132
160
  deps.writeErr('gs25-products 명령은 검색어가 필요합니다. 예: daiso gs25-products 오감자');
@@ -142,6 +170,9 @@ export async function handleGs25Inventory(options, deps) {
142
170
  if (parsed.options.help === 'true') {
143
171
  return printCommandHelp('gs25-inventory', deps.writeOut, deps.writeErr);
144
172
  }
173
+ if (validateCommandOptions('gs25-inventory', parsed.options, deps.writeErr)) {
174
+ return 1;
175
+ }
145
176
  const keyword = parsed.positionals[0];
146
177
  if (!keyword) {
147
178
  deps.writeErr('gs25-inventory 명령은 검색어가 필요합니다. 예: daiso gs25-inventory 오감자');
@@ -157,6 +188,9 @@ export async function handleSevenElevenProducts(options, deps) {
157
188
  if (parsed.options.help === 'true') {
158
189
  return printCommandHelp('seveneleven-products', deps.writeOut, deps.writeErr);
159
190
  }
191
+ if (validateCommandOptions('seveneleven-products', parsed.options, deps.writeErr)) {
192
+ return 1;
193
+ }
160
194
  const query = parsed.positionals[0];
161
195
  if (!query) {
162
196
  deps.writeErr('seveneleven-products 명령은 검색어가 필요합니다. 예: daiso seveneleven-products 삼각김밥');
@@ -172,6 +206,9 @@ export async function handleSevenElevenStores(options, deps) {
172
206
  if (parsed.options.help === 'true') {
173
207
  return printCommandHelp('seveneleven-stores', deps.writeOut, deps.writeErr);
174
208
  }
209
+ if (validateCommandOptions('seveneleven-stores', parsed.options, deps.writeErr)) {
210
+ return 1;
211
+ }
175
212
  const keyword = parsed.positionals[0];
176
213
  if (!keyword) {
177
214
  deps.writeErr('seveneleven-stores 명령은 검색어가 필요합니다. 예: daiso seveneleven-stores 안산 중앙역');
@@ -187,6 +224,9 @@ export async function handleSevenElevenPopwords(options, deps) {
187
224
  if (parsed.options.help === 'true') {
188
225
  return printCommandHelp('seveneleven-popwords', deps.writeOut, deps.writeErr);
189
226
  }
227
+ if (validateCommandOptions('seveneleven-popwords', parsed.options, deps.writeErr)) {
228
+ return 1;
229
+ }
190
230
  const targetUrl = toUrl('/api/seveneleven/popwords');
191
231
  applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
192
232
  return await requestAndPrintResponse(deps.fetchImpl, deps.writeOut, deps.writeErr, targetUrl, 'seveneleven-popwords', parsed.options.json === 'true');
@@ -196,6 +236,9 @@ export async function handleSevenElevenCatalog(options, deps) {
196
236
  if (parsed.options.help === 'true') {
197
237
  return printCommandHelp('seveneleven-catalog', deps.writeOut, deps.writeErr);
198
238
  }
239
+ if (validateCommandOptions('seveneleven-catalog', parsed.options, deps.writeErr)) {
240
+ return 1;
241
+ }
199
242
  const targetUrl = toUrl('/api/seveneleven/catalog');
200
243
  applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
201
244
  return await requestAndPrintResponse(deps.fetchImpl, deps.writeOut, deps.writeErr, targetUrl, 'seveneleven-catalog', parsed.options.json === 'true');
@@ -209,6 +252,9 @@ export async function handleLottecinemaTheaters(options, deps) {
209
252
  if (keyword) {
210
253
  parsed.options.keyword = keyword;
211
254
  }
255
+ if (validateCommandOptions('lottecinema-theaters', parsed.options, deps.writeErr)) {
256
+ return 1;
257
+ }
212
258
  const targetUrl = toUrl('/api/lottecinema/theaters');
213
259
  applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
214
260
  return await requestAndPrintResponse(deps.fetchImpl, deps.writeOut, deps.writeErr, targetUrl, 'lottecinema-theaters', parsed.options.json === 'true');
@@ -222,6 +268,9 @@ export async function handleLottecinemaMovies(options, deps) {
222
268
  if (keyword) {
223
269
  parsed.options.keyword = keyword;
224
270
  }
271
+ if (validateCommandOptions('lottecinema-movies', parsed.options, deps.writeErr)) {
272
+ return 1;
273
+ }
225
274
  const targetUrl = toUrl('/api/lottecinema/movies');
226
275
  applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
227
276
  return await requestAndPrintResponse(deps.fetchImpl, deps.writeOut, deps.writeErr, targetUrl, 'lottecinema-movies', parsed.options.json === 'true');
@@ -235,6 +284,9 @@ export async function handleLottecinemaSeats(options, deps) {
235
284
  if (keyword) {
236
285
  parsed.options.keyword = keyword;
237
286
  }
287
+ if (validateCommandOptions('lottecinema-seats', parsed.options, deps.writeErr)) {
288
+ return 1;
289
+ }
238
290
  const targetUrl = toUrl('/api/lottecinema/seats');
239
291
  applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
240
292
  return await requestAndPrintResponse(deps.fetchImpl, deps.writeOut, deps.writeErr, targetUrl, 'lottecinema-seats', parsed.options.json === 'true');
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { printCommandHelp } from '../../cliHelp.js';
5
5
  import { parseCliArgs, toUrl, applyOptionsToQuery, toQueryOptions } from '../args.js';
6
+ import { validateCommandOptions } from '../commandOptions.js';
6
7
  import { requestAndPrintResponse, requestAndPrintStoresWithKeywordFallback } from '../http.js';
7
8
  export async function handleGet(options, deps) {
8
9
  const parsed = parseCliArgs(options);
@@ -23,6 +24,9 @@ export async function handleProducts(options, deps) {
23
24
  if (parsed.options.help === 'true') {
24
25
  return printCommandHelp('products', deps.writeOut, deps.writeErr);
25
26
  }
27
+ if (validateCommandOptions('products', parsed.options, deps.writeErr)) {
28
+ return 1;
29
+ }
26
30
  const query = parsed.positionals[0];
27
31
  if (!query) {
28
32
  deps.writeErr('products 명령은 검색어가 필요합니다. 예: daiso products 수납박스');
@@ -38,6 +42,9 @@ export async function handleProduct(options, deps) {
38
42
  if (parsed.options.help === 'true') {
39
43
  return printCommandHelp('product', deps.writeOut, deps.writeErr);
40
44
  }
45
+ if (validateCommandOptions('product', parsed.options, deps.writeErr)) {
46
+ return 1;
47
+ }
41
48
  const productId = parsed.positionals[0];
42
49
  if (!productId) {
43
50
  deps.writeErr('product 명령은 제품 ID가 필요합니다. 예: daiso product 1034604');
@@ -56,6 +63,9 @@ export async function handleStores(options, deps) {
56
63
  if (keyword) {
57
64
  parsed.options.keyword = keyword;
58
65
  }
66
+ if (validateCommandOptions('stores', parsed.options, deps.writeErr)) {
67
+ return 1;
68
+ }
59
69
  if (!parsed.options.keyword && !parsed.options.sido) {
60
70
  deps.writeErr('stores 명령은 keyword 또는 --sido가 필요합니다. 예: daiso stores 강남역 / daiso stores --sido 서울');
61
71
  return 1;
@@ -67,9 +77,12 @@ export async function handleInventory(options, deps) {
67
77
  if (parsed.options.help === 'true') {
68
78
  return printCommandHelp('inventory', deps.writeOut, deps.writeErr);
69
79
  }
80
+ if (validateCommandOptions('inventory', parsed.options, deps.writeErr)) {
81
+ return 1;
82
+ }
70
83
  const productId = parsed.positionals[0];
71
84
  if (!productId) {
72
- deps.writeErr('inventory 명령은 제품 ID가 필요합니다. 예: daiso inventory 1034604 --keyword 강남역');
85
+ deps.writeErr('inventory 명령은 제품 ID가 필요합니다. 제품명만 알면 먼저 daiso products 수납박스 명령으로 productId를 확인하세요. 예: daiso inventory 1034604 --keyword 강남역');
73
86
  return 1;
74
87
  }
75
88
  const targetUrl = toUrl('/api/daiso/inventory');
@@ -82,10 +95,13 @@ export async function handleDisplayLocation(options, deps) {
82
95
  if (parsed.options.help === 'true') {
83
96
  return printCommandHelp('display-location', deps.writeOut, deps.writeErr);
84
97
  }
98
+ if (validateCommandOptions('display-location', parsed.options, deps.writeErr)) {
99
+ return 1;
100
+ }
85
101
  const productId = parsed.positionals[0];
86
102
  const storeCode = parsed.positionals[1];
87
103
  if (!productId || !storeCode) {
88
- deps.writeErr('display-location 명령은 productId와 storeCode가 필요합니다. 예: daiso display-location 1034604 04515');
104
+ deps.writeErr('display-location 명령은 productId와 storeCode가 필요합니다. storeCode를 모르면 먼저 daiso inventory 1034604 --keyword 매장명 결과의 storeInventory.stores[].storeCode를 확인하세요. 예: daiso display-location 1034604 04515');
89
105
  return 1;
90
106
  }
91
107
  const targetUrl = toUrl('/api/daiso/display-location');
package/dist/cli/http.js CHANGED
@@ -4,6 +4,61 @@
4
4
  import { renderApiEnvelope } from '../cliRenderer.js';
5
5
  import { buildDaisoStoreKeywordVariants } from '../utils/daisoKeyword.js';
6
6
  import { applyOptionsToQuery, toUrl } from './args.js';
7
+ function parseErrorCode(bodyText) {
8
+ try {
9
+ const parsed = JSON.parse(bodyText);
10
+ return typeof parsed.error?.code === 'string' ? parsed.error.code : '';
11
+ }
12
+ catch {
13
+ return '';
14
+ }
15
+ }
16
+ function buildCliHint(command, url, bodyText) {
17
+ if (command !== 'get') {
18
+ return [];
19
+ }
20
+ const code = parseErrorCode(bodyText);
21
+ if (url.pathname === '/api/daiso/inventory' || code === 'MISSING_PRODUCT_ID') {
22
+ return [
23
+ '힌트: 제품명만 알면 먼저 daiso products <상품명> 명령으로 productId를 확인하세요.',
24
+ '다음 명령 예시: daiso inventory <productId> --keyword 강남역',
25
+ ];
26
+ }
27
+ if (url.pathname === '/api/daiso/products') {
28
+ return ['힌트: 제품 검색은 daiso products <상품명> 명령을 사용할 수 있습니다.'];
29
+ }
30
+ if (url.pathname === '/api/daiso/display-location' || code === 'MISSING_STORE_CODE') {
31
+ return [
32
+ '힌트: storeCode는 daiso inventory <productId> --keyword <매장명> 결과에서 확인하세요.',
33
+ '다음 명령 예시: daiso display-location <productId> <storeCode>',
34
+ ];
35
+ }
36
+ if (url.pathname === '/api/lottemart/products') {
37
+ return [
38
+ '힌트: 롯데마트 상품 조회는 매장 정보가 필요합니다.',
39
+ '다음 명령 예시: daiso lottemart-products <상품명> --storeName <매장명>',
40
+ ];
41
+ }
42
+ if (url.pathname === '/api/gs25/inventory') {
43
+ return [
44
+ '힌트: GS25 상품명만 알면 daiso gs25-inventory <상품명> --storeKeyword <매장명> 명령을 사용하세요.',
45
+ 'itemCode만 알면 daiso gs25-inventory <itemCode> 형식 대신 daiso get /api/gs25/inventory --itemCode <itemCode> --storeKeyword <매장명> 을 사용하세요.',
46
+ ];
47
+ }
48
+ if (url.pathname === '/api/emart24/inventory') {
49
+ return [
50
+ '힌트: 이마트24 상품명만 알면 daiso emart24-products <상품명> 명령으로 pluCd를 확인하세요.',
51
+ '다음 명령 예시: daiso emart24-inventory <pluCd> --storeKeyword 강남',
52
+ ];
53
+ }
54
+ if (url.pathname === '/api/seveneleven/inventory') {
55
+ return [
56
+ '힌트: 세븐일레븐 재고는 매장 키워드가 필요합니다.',
57
+ '다음 명령 예시: daiso get /api/seveneleven/inventory --keyword <상품명> --storeKeyword 강남',
58
+ ];
59
+ }
60
+ return [];
61
+ }
7
62
  export async function requestAndPrintResponse(fetchImpl, writeOut, writeErr, url, command, asJson) {
8
63
  try {
9
64
  const response = await fetchImpl(url.toString());
@@ -12,6 +67,9 @@ export async function requestAndPrintResponse(fetchImpl, writeOut, writeErr, url
12
67
  writeErr(`요청 실패: HTTP ${response.status}`);
13
68
  if (bodyText) {
14
69
  writeErr(bodyText);
70
+ for (const hint of buildCliHint(command, url, bodyText)) {
71
+ writeErr(hint);
72
+ }
15
73
  }
16
74
  return 1;
17
75
  }
@@ -93,8 +93,10 @@ export async function runLotteCinemaSearch(deps, prompt, theater) {
93
93
  deps.writeOut(` ...외 ${seats.length - 15}건`);
94
94
  }
95
95
  }
96
- export async function runDaisoItemSearch(deps, prompt, store) {
97
- const keyword = await askNonEmpty(prompt, '찾을 상품 키워드를 입력하세요: ');
96
+ export async function runDaisoItemSearch(deps, prompt, store, initialKeyword) {
97
+ const keyword = initialKeyword && initialKeyword.trim().length > 0
98
+ ? initialKeyword.trim()
99
+ : await askNonEmpty(prompt, '찾을 상품 키워드를 입력하세요: ');
98
100
  const productsPayload = await fetchEnvelope(deps.fetchImpl, '/api/daiso/products', {
99
101
  q: keyword,
100
102
  pageSize: '10',
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * 인터랙티브 CLI 입력/출력 헬퍼
3
3
  */
4
- export async function askMenu(prompt, title, options) {
4
+ export async function askMenu(prompt, title, options, writeOut) {
5
5
  while (true) {
6
6
  const answer = await prompt.ask(`${title} `);
7
7
  const picked = Number.parseInt(answer, 10);
8
8
  if (Number.isNaN(picked) || picked < 0 || picked > options.length) {
9
+ writeOut?.(`번호로 입력하세요. 0부터 ${options.length} 사이에서 선택할 수 있습니다.`);
9
10
  continue;
10
11
  }
11
12
  return picked;
@@ -36,7 +37,7 @@ export async function askNextAction(prompt, writeOut) {
36
37
  writeOut('1. 같은 매장에서 다른 상품 찾기');
37
38
  writeOut('2. 다른 매장/서비스 다시 선택하기');
38
39
  writeOut('3. 종료하기');
39
- const choice = await askMenu(prompt, '번호를 선택하세요:', ['same-store', 'change-store', 'exit']);
40
+ const choice = await askMenu(prompt, '번호를 선택하세요:', ['same-store', 'change-store', 'exit'], writeOut);
40
41
  return choice === 1 ? 'same-store' : choice === 2 ? 'change-store' : 'exit';
41
42
  }
42
43
  export function printStoreDetail(writeOut, store) {
package/dist/cli.js CHANGED
@@ -9,8 +9,14 @@ import { fileURLToPath } from 'node:url';
9
9
  import { printHelp, printCommandHelp } from './cliHelp.js';
10
10
  import { createDefaultDeps } from './cli/deps.js';
11
11
  import { DEFAULT_BASE_URL, DEFAULT_MCP_URL } from './cli/constants.js';
12
+ import { parseCliArgs } from './cli/args.js';
13
+ import { validateCommandOptions } from './cli/commandOptions.js';
12
14
  import { handleGet, handleProducts, handleProduct, handleStores, handleInventory, handleDisplayLocation, } from './cli/commands/daiso.js';
13
15
  import { handleCuStores, handleCuInventory, handleEmart24Stores, handleEmart24Products, handleEmart24Inventory, handleLotteMartStores, handleLotteMartProducts, handleGs25Stores, handleGs25Products, handleGs25Inventory, handleSevenElevenProducts, handleSevenElevenStores, handleSevenElevenPopwords, handleSevenElevenCatalog, handleLottecinemaTheaters, handleLottecinemaMovies, handleLottecinemaSeats, } from './cli/commands/convenience.js';
16
+ function rejectTopLevelUnknownOptions(options, command, deps) {
17
+ const parsed = parseCliArgs(options);
18
+ return validateCommandOptions(command, parsed.options, deps.writeErr);
19
+ }
14
20
  export async function runCli(argv, deps) {
15
21
  const resolvedDeps = {
16
22
  ...createDefaultDeps(),
@@ -39,14 +45,23 @@ export async function runCli(argv, deps) {
39
45
  return 0;
40
46
  }
41
47
  if (command === 'version' || command === '--version' || command === '-v') {
48
+ if (rejectTopLevelUnknownOptions(options, 'version', resolvedDeps)) {
49
+ return 1;
50
+ }
42
51
  resolvedDeps.writeOut(resolvedDeps.getVersion());
43
52
  return 0;
44
53
  }
45
54
  if (command === 'url') {
55
+ if (rejectTopLevelUnknownOptions(options, 'url', resolvedDeps)) {
56
+ return 1;
57
+ }
46
58
  resolvedDeps.writeOut(DEFAULT_MCP_URL);
47
59
  return 0;
48
60
  }
49
61
  if (command === 'health') {
62
+ if (rejectTopLevelUnknownOptions(options, 'health', resolvedDeps)) {
63
+ return 1;
64
+ }
50
65
  try {
51
66
  const response = await resolvedDeps.fetchImpl(`${DEFAULT_BASE_URL}/health`);
52
67
  if (!response.ok) {
@@ -68,6 +83,9 @@ export async function runCli(argv, deps) {
68
83
  }
69
84
  }
70
85
  if (command === 'claude') {
86
+ if (rejectTopLevelUnknownOptions(options, 'claude', resolvedDeps)) {
87
+ return 1;
88
+ }
71
89
  const cliArgs = ['mcp', 'add', 'daiso', DEFAULT_BASE_URL, '--transport', 'sse'];
72
90
  if (options.includes('--exec')) {
73
91
  try {
@@ -129,6 +147,10 @@ export async function runCli(argv, deps) {
129
147
  if (command === 'seveneleven-catalog')
130
148
  return await handleSevenElevenCatalog(options, resolvedDeps);
131
149
  resolvedDeps.writeErr(`알 수 없는 명령어: ${command}`);
150
+ if (command === 'search') {
151
+ const query = options[0] ?? '<검색어>';
152
+ resolvedDeps.writeErr(`혹시 찾으신 명령: daiso products ${query}`);
153
+ }
132
154
  resolvedDeps.writeErr('도움말: daiso help');
133
155
  return 1;
134
156
  }
package/dist/cliHelp.js CHANGED
@@ -323,6 +323,12 @@ export function printHelp(writeOut) {
323
323
  writeOut(' npx daiso seveneleven-catalog --limit 10');
324
324
  writeOut(' npx daiso get /api/cgv/movies --playDate 20260307 --theaterCode 0056');
325
325
  writeOut('');
326
+ writeOut('정보가 부족할 때:');
327
+ writeOut(' 제품명을 먼저 검색해 productId를 확인: npx daiso products 수납박스');
328
+ writeOut(' productId로 재고 확인: npx daiso inventory <productId> --keyword 강남역');
329
+ writeOut(' 매장을 먼저 검색해 storeName 또는 storeCode를 확인: npx daiso stores 강남역');
330
+ writeOut(' 롯데마트 매장 확인: npx daiso lottemart-stores 잠실 --area 서울');
331
+ writeOut('');
326
332
  writeOut('상세 도움말:');
327
333
  writeOut(' npx daiso help <command>');
328
334
  }