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.
- package/README.md +45 -7
- package/dist/api/configStatus.js +19 -0
- package/dist/api/gs25Handlers.js +25 -14
- package/dist/api/handlers.js +32 -3
- package/dist/api/healthChecks.js +185 -13
- package/dist/api/lottemartHandlers.js +17 -1
- package/dist/api/response.js +2 -0
- package/dist/api/routes/healthRoutes.js +20 -3
- package/dist/bin.js +0 -0
- package/dist/cli/args.js +21 -0
- package/dist/cli/commandOptions.js +38 -0
- package/dist/cli/commands/convenience.js +53 -1
- package/dist/cli/commands/daiso.js +18 -2
- package/dist/cli/http.js +58 -0
- package/dist/cli/interactiveItemSearch.js +4 -2
- package/dist/cli/interactivePrompt.js +3 -2
- package/dist/cli.js +22 -0
- package/dist/cliHelp.js +6 -0
- package/dist/cliInteractive.js +75 -6
- package/dist/cliPicker.js +1 -0
- package/dist/cliRenderer.js +86 -3
- package/dist/core/errors.js +66 -0
- package/dist/core/registry.js +227 -3
- package/dist/index.js +2 -1
- package/dist/pages/openapiSpecActions.js +3 -3
- package/dist/pages/openapiSpecPathsCu.js +7 -0
- package/dist/pages/openapiSpecPathsLotteMart.js +14 -0
- package/dist/pages/openapiSpecPathsOliveyoung.js +35 -0
- package/dist/pages/promptReferenceText.js +13 -7
- package/dist/pages/promptRetailCinemaText.js +2 -1
- package/dist/services/daiso/client.js +8 -0
- package/dist/services/daiso/index.js +2 -0
- package/dist/services/daiso/tools/checkInventory.js +4 -2
- package/dist/services/daiso/tools/findInventoryByName.js +222 -0
- package/dist/services/daiso/tools/getDisplayLocation.js +2 -2
- package/dist/services/emart24/client.js +9 -0
- package/dist/services/emart24/tools/searchProducts.js +27 -17
- package/dist/services/gs25/client.js +10 -0
- package/dist/services/gs25/tools/searchProducts.js +28 -21
- package/dist/services/lottemart/client.js +10 -1
- package/dist/services/lottemart/tools/searchProducts.js +8 -1
- package/dist/services/lottemart/zetta.js +1 -1
- package/dist/services/oliveyoung/client.js +124 -53
- package/dist/services/oliveyoung/tools/checkInventory.js +7 -2
- package/dist/services/seveneleven/client.js +7 -0
- package/dist/services/seveneleven/tools/searchProducts.js +29 -18
- package/dist/utils/http.js +82 -10
- package/dist/utils/zyte.js +47 -26
- package/openapi.json +3 -3
- package/openapi.yaml +3 -3
- 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:
|
|
95
|
+
timeout: timeoutMs,
|
|
80
96
|
zyteApiKey: c.env?.ZYTE_API_KEY,
|
|
81
97
|
});
|
|
82
98
|
return successResponse(c, {
|
package/dist/api/response.js
CHANGED
|
@@ -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')
|
|
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
|
|
29
|
-
const
|
|
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 =
|
|
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
|
}
|