daiso 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +249 -533
- package/dist/api/actionsProxy.js +82 -0
- package/dist/api/cgvHandlers.js +103 -16
- package/dist/api/configStatus.js +19 -0
- package/dist/api/daisoHandlers.js +127 -0
- package/dist/api/emart24Handlers.js +176 -0
- package/dist/api/gs25Handlers.js +267 -0
- package/dist/api/handlers.js +209 -104
- package/dist/api/healthChecks.js +504 -0
- package/dist/api/lottecinemaHandlers.js +155 -0
- package/dist/api/lottemartHandlers.js +144 -0
- package/dist/api/megaboxHandlers.js +81 -50
- package/dist/api/response.js +2 -0
- package/dist/api/routes/cuRoutes.js +17 -0
- package/dist/api/routes/daisoRoutes.js +6 -1
- package/dist/api/routes/emart24Routes.js +22 -0
- package/dist/api/routes/gs25Routes.js +22 -0
- package/dist/api/routes/healthRoutes.js +61 -0
- package/dist/api/routes/lottecinemaRoutes.js +22 -0
- package/dist/api/routes/lottemartRoutes.js +18 -0
- package/dist/api/routes/oliveyoungRoutes.js +6 -1
- package/dist/api/routes/sevenelevenRoutes.js +32 -0
- package/dist/api/sevenelevenHandlers.js +200 -0
- package/dist/bin.js +0 -0
- package/dist/cli/args.js +75 -0
- package/dist/cli/commandOptions.js +38 -0
- package/dist/cli/commands/convenience.js +293 -0
- package/dist/cli/commands/daiso.js +112 -0
- package/dist/cli/constants.js +5 -0
- package/dist/cli/deps.js +48 -0
- package/dist/cli/http.js +156 -0
- package/dist/cli/interactiveFetch.js +31 -0
- package/dist/cli/interactiveItemSearch.js +261 -0
- package/dist/cli/interactivePrompt.js +50 -0
- package/dist/cli/interactiveTypes.js +4 -0
- package/dist/cli/types.js +4 -0
- package/dist/cli.js +87 -196
- package/dist/cliHelp.js +212 -1
- package/dist/cliInteractive.js +243 -0
- package/dist/cliPicker.js +57 -0
- package/dist/cliRenderer.js +90 -3
- package/dist/core/errors.js +66 -0
- package/dist/core/registry.js +227 -3
- package/dist/index.js +149 -9
- package/dist/pages/openapi.js +2 -1
- package/dist/pages/openapiFullSpec.js +58 -0
- package/dist/pages/openapiResponse.js +28 -0
- package/dist/pages/openapiSpec.js +2 -41
- package/dist/pages/openapiSpecActions.js +109 -0
- package/dist/pages/openapiSpecComponents.js +30 -391
- package/dist/pages/openapiSpecComponentsCgv.js +24 -0
- package/dist/pages/openapiSpecComponentsCu.js +112 -0
- package/dist/pages/openapiSpecComponentsDaisoOliveyoung.js +342 -0
- package/dist/pages/openapiSpecComponentsEmart24.js +141 -0
- package/dist/pages/openapiSpecComponentsGs25.js +109 -0
- package/dist/pages/openapiSpecComponentsLotteMart.js +85 -0
- package/dist/pages/openapiSpecComponentsLottecinema.js +163 -0
- package/dist/pages/openapiSpecComponentsMegabox.js +144 -0
- package/dist/pages/openapiSpecComponentsSeveneleven.js +82 -0
- package/dist/pages/openapiSpecPathsCgv.js +66 -3
- package/dist/pages/openapiSpecPathsCu.js +145 -0
- package/dist/pages/openapiSpecPathsDaiso.js +302 -0
- package/dist/pages/openapiSpecPathsDaisoOliveyoung.js +4 -376
- package/dist/pages/openapiSpecPathsEmart24.js +223 -0
- package/dist/pages/openapiSpecPathsGs25.js +166 -0
- package/dist/pages/openapiSpecPathsLotteMart.js +149 -0
- package/dist/pages/openapiSpecPathsLottecinema.js +212 -0
- package/dist/pages/openapiSpecPathsMegabox.js +52 -3
- package/dist/pages/openapiSpecPathsOliveyoung.js +266 -0
- package/dist/pages/openapiSpecPathsSeveneleven.js +234 -0
- package/dist/pages/prompt.js +8 -402
- package/dist/pages/promptCoreText.js +278 -0
- package/dist/pages/promptReferenceText.js +137 -0
- package/dist/pages/promptRetailCinemaText.js +274 -0
- package/dist/services/cgv/client.js +6 -1
- package/dist/services/cgv/index.js +3 -3
- package/dist/services/cgv/location.js +235 -0
- package/dist/services/cgv/tools/findTheaters.js +30 -5
- package/dist/services/cgv/tools/getTimetable.js +42 -14
- package/dist/services/cgv/tools/searchMovies.js +40 -12
- package/dist/services/cgv/transport.js +0 -3
- package/dist/services/cu/api.js +11 -0
- package/dist/services/cu/client.js +297 -0
- package/dist/services/cu/index.js +21 -0
- package/dist/services/cu/tools/checkInventory.js +123 -0
- package/dist/services/cu/tools/findNearbyStores.js +49 -0
- package/dist/services/cu/types.js +4 -0
- package/dist/services/daiso/api.js +22 -2
- package/dist/services/daiso/client.js +67 -0
- package/dist/services/daiso/index.js +4 -0
- package/dist/services/daiso/product.js +17 -0
- package/dist/services/daiso/tools/checkInventory.js +80 -43
- package/dist/services/daiso/tools/findInventoryByName.js +222 -0
- package/dist/services/daiso/tools/findStores.js +17 -7
- package/dist/services/daiso/tools/getDisplayLocation.js +75 -0
- package/dist/services/emart24/api.js +11 -0
- package/dist/services/emart24/client.js +182 -0
- package/dist/services/emart24/index.js +22 -0
- package/dist/services/emart24/inventoryLookup.js +134 -0
- package/dist/services/emart24/tools/checkInventory.js +54 -0
- package/dist/services/emart24/tools/findNearbyStores.js +72 -0
- package/dist/services/emart24/tools/searchProducts.js +73 -0
- package/dist/services/emart24/types.js +4 -0
- package/dist/services/gs25/api.js +9 -0
- package/dist/services/gs25/client.js +208 -0
- package/dist/services/gs25/index.js +22 -0
- package/dist/services/gs25/storeUtils.js +168 -0
- package/dist/services/gs25/tools/checkInventory.js +163 -0
- package/dist/services/gs25/tools/findNearbyStores.js +103 -0
- package/dist/services/gs25/tools/searchProducts.js +57 -0
- package/dist/services/gs25/types.js +4 -0
- package/dist/services/lottecinema/api.js +12 -0
- package/dist/services/lottecinema/client.js +161 -0
- package/dist/services/lottecinema/index.js +30 -0
- package/dist/services/lottecinema/location.js +191 -0
- package/dist/services/lottecinema/tools/findNearbyTheaters.js +47 -0
- package/dist/services/lottecinema/tools/getRemainingSeats.js +85 -0
- package/dist/services/lottecinema/tools/listNowShowing.js +79 -0
- package/dist/services/lottecinema/types.js +4 -0
- package/dist/services/lottemart/api.js +31 -0
- package/dist/services/lottemart/client.js +254 -0
- package/dist/services/lottemart/clientTypes.js +4 -0
- package/dist/services/lottemart/config.js +4 -0
- package/dist/services/lottemart/debug.js +92 -0
- package/dist/services/lottemart/geocode.js +49 -0
- package/dist/services/lottemart/index.js +30 -0
- package/dist/services/lottemart/keyword.js +29 -0
- package/dist/services/lottemart/parser.js +185 -0
- package/dist/services/lottemart/session.js +331 -0
- package/dist/services/lottemart/storeSearch.js +52 -0
- package/dist/services/lottemart/tools/findNearbyStores.js +67 -0
- package/dist/services/lottemart/tools/searchProducts.js +69 -0
- package/dist/services/lottemart/types.js +4 -0
- package/dist/services/lottemart/zetta.js +105 -0
- package/dist/services/megabox/location.js +219 -0
- package/dist/services/megabox/tools/findNearbyTheaters.js +13 -44
- package/dist/services/megabox/tools/getRemainingSeats.js +31 -3
- package/dist/services/megabox/tools/listNowShowing.js +31 -3
- package/dist/services/oliveyoung/api.js +2 -0
- package/dist/services/oliveyoung/client.js +225 -13
- package/dist/services/oliveyoung/index.js +3 -1
- package/dist/services/oliveyoung/tools/checkInventory.js +21 -5
- package/dist/services/oliveyoung/tools/searchProducts.js +57 -0
- package/dist/services/seveneleven/api.js +15 -0
- package/dist/services/seveneleven/client.js +286 -0
- package/dist/services/seveneleven/index.js +30 -0
- package/dist/services/seveneleven/inventory.js +302 -0
- package/dist/services/seveneleven/productKeyword.js +141 -0
- package/dist/services/seveneleven/tools/checkInventory.js +82 -0
- package/dist/services/seveneleven/tools/getCatalogSnapshot.js +55 -0
- package/dist/services/seveneleven/tools/getSearchPopwords.js +41 -0
- package/dist/services/seveneleven/tools/searchProducts.js +67 -0
- package/dist/services/seveneleven/tools/searchStores.js +45 -0
- package/dist/services/seveneleven/types.js +4 -0
- package/dist/utils/cliInteractiveHelpers.js +63 -0
- package/dist/utils/daisoKeyword.js +30 -0
- package/dist/utils/http.js +98 -12
- package/dist/utils/zyte.js +47 -26
- package/openapi.json +1979 -736
- package/openapi.yaml +1428 -541
- package/package.json +49 -18
- package/dist/tools/checkInventory.js +0 -98
- package/dist/tools/findStores.js +0 -125
- package/dist/tools/getPriceInfo.js +0 -77
- package/dist/tools/searchProducts.js +0 -61
- package/dist/types/index.js +0 -4
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 다이소 관련 CLI 명령 핸들러
|
|
3
|
+
*/
|
|
4
|
+
import { printCommandHelp } from '../../cliHelp.js';
|
|
5
|
+
import { parseCliArgs, toUrl, applyOptionsToQuery, toQueryOptions } from '../args.js';
|
|
6
|
+
import { validateCommandOptions } from '../commandOptions.js';
|
|
7
|
+
import { requestAndPrintResponse, requestAndPrintStoresWithKeywordFallback } from '../http.js';
|
|
8
|
+
export async function handleGet(options, deps) {
|
|
9
|
+
const parsed = parseCliArgs(options);
|
|
10
|
+
if (parsed.options.help === 'true') {
|
|
11
|
+
return printCommandHelp('get', deps.writeOut, deps.writeErr);
|
|
12
|
+
}
|
|
13
|
+
const targetPath = parsed.positionals[0];
|
|
14
|
+
if (!targetPath) {
|
|
15
|
+
deps.writeErr('get 명령은 경로가 필요합니다. 예: daiso get /api/daiso/products --q 수납박스');
|
|
16
|
+
return 1;
|
|
17
|
+
}
|
|
18
|
+
const targetUrl = toUrl(targetPath);
|
|
19
|
+
applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
|
|
20
|
+
return await requestAndPrintResponse(deps.fetchImpl, deps.writeOut, deps.writeErr, targetUrl, 'get', parsed.options.json === 'true');
|
|
21
|
+
}
|
|
22
|
+
export async function handleProducts(options, deps) {
|
|
23
|
+
const parsed = parseCliArgs(options);
|
|
24
|
+
if (parsed.options.help === 'true') {
|
|
25
|
+
return printCommandHelp('products', deps.writeOut, deps.writeErr);
|
|
26
|
+
}
|
|
27
|
+
if (validateCommandOptions('products', parsed.options, deps.writeErr)) {
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
const query = parsed.positionals[0];
|
|
31
|
+
if (!query) {
|
|
32
|
+
deps.writeErr('products 명령은 검색어가 필요합니다. 예: daiso products 수납박스');
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
const targetUrl = toUrl('/api/daiso/products');
|
|
36
|
+
targetUrl.searchParams.set('q', query);
|
|
37
|
+
applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
|
|
38
|
+
return await requestAndPrintResponse(deps.fetchImpl, deps.writeOut, deps.writeErr, targetUrl, 'products', parsed.options.json === 'true');
|
|
39
|
+
}
|
|
40
|
+
export async function handleProduct(options, deps) {
|
|
41
|
+
const parsed = parseCliArgs(options);
|
|
42
|
+
if (parsed.options.help === 'true') {
|
|
43
|
+
return printCommandHelp('product', deps.writeOut, deps.writeErr);
|
|
44
|
+
}
|
|
45
|
+
if (validateCommandOptions('product', parsed.options, deps.writeErr)) {
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
const productId = parsed.positionals[0];
|
|
49
|
+
if (!productId) {
|
|
50
|
+
deps.writeErr('product 명령은 제품 ID가 필요합니다. 예: daiso product 1034604');
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
const targetUrl = toUrl(`/api/daiso/products/${productId}`);
|
|
54
|
+
applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
|
|
55
|
+
return await requestAndPrintResponse(deps.fetchImpl, deps.writeOut, deps.writeErr, targetUrl, 'product', parsed.options.json === 'true');
|
|
56
|
+
}
|
|
57
|
+
export async function handleStores(options, deps) {
|
|
58
|
+
const parsed = parseCliArgs(options);
|
|
59
|
+
if (parsed.options.help === 'true') {
|
|
60
|
+
return printCommandHelp('stores', deps.writeOut, deps.writeErr);
|
|
61
|
+
}
|
|
62
|
+
const keyword = parsed.positionals[0];
|
|
63
|
+
if (keyword) {
|
|
64
|
+
parsed.options.keyword = keyword;
|
|
65
|
+
}
|
|
66
|
+
if (validateCommandOptions('stores', parsed.options, deps.writeErr)) {
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
if (!parsed.options.keyword && !parsed.options.sido) {
|
|
70
|
+
deps.writeErr('stores 명령은 keyword 또는 --sido가 필요합니다. 예: daiso stores 강남역 / daiso stores --sido 서울');
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
return await requestAndPrintStoresWithKeywordFallback(deps.fetchImpl, deps.writeOut, deps.writeErr, toQueryOptions(parsed.options), parsed.options.json === 'true');
|
|
74
|
+
}
|
|
75
|
+
export async function handleInventory(options, deps) {
|
|
76
|
+
const parsed = parseCliArgs(options);
|
|
77
|
+
if (parsed.options.help === 'true') {
|
|
78
|
+
return printCommandHelp('inventory', deps.writeOut, deps.writeErr);
|
|
79
|
+
}
|
|
80
|
+
if (validateCommandOptions('inventory', parsed.options, deps.writeErr)) {
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
const productId = parsed.positionals[0];
|
|
84
|
+
if (!productId) {
|
|
85
|
+
deps.writeErr('inventory 명령은 제품 ID가 필요합니다. 제품명만 알면 먼저 daiso products 수납박스 명령으로 productId를 확인하세요. 예: daiso inventory 1034604 --keyword 강남역');
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
const targetUrl = toUrl('/api/daiso/inventory');
|
|
89
|
+
targetUrl.searchParams.set('productId', productId);
|
|
90
|
+
applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
|
|
91
|
+
return await requestAndPrintResponse(deps.fetchImpl, deps.writeOut, deps.writeErr, targetUrl, 'inventory', parsed.options.json === 'true');
|
|
92
|
+
}
|
|
93
|
+
export async function handleDisplayLocation(options, deps) {
|
|
94
|
+
const parsed = parseCliArgs(options);
|
|
95
|
+
if (parsed.options.help === 'true') {
|
|
96
|
+
return printCommandHelp('display-location', deps.writeOut, deps.writeErr);
|
|
97
|
+
}
|
|
98
|
+
if (validateCommandOptions('display-location', parsed.options, deps.writeErr)) {
|
|
99
|
+
return 1;
|
|
100
|
+
}
|
|
101
|
+
const productId = parsed.positionals[0];
|
|
102
|
+
const storeCode = parsed.positionals[1];
|
|
103
|
+
if (!productId || !storeCode) {
|
|
104
|
+
deps.writeErr('display-location 명령은 productId와 storeCode가 필요합니다. storeCode를 모르면 먼저 daiso inventory 1034604 --keyword 매장명 결과의 storeInventory.stores[].storeCode를 확인하세요. 예: daiso display-location 1034604 04515');
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
const targetUrl = toUrl('/api/daiso/display-location');
|
|
108
|
+
targetUrl.searchParams.set('productId', productId);
|
|
109
|
+
targetUrl.searchParams.set('storeCode', storeCode);
|
|
110
|
+
applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
|
|
111
|
+
return await requestAndPrintResponse(deps.fetchImpl, deps.writeOut, deps.writeErr, targetUrl, 'display-location', parsed.options.json === 'true');
|
|
112
|
+
}
|
package/dist/cli/deps.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI 기본 의존성 생성
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { runInteractiveCli } from '../cliInteractive.js';
|
|
9
|
+
function loadVersion() {
|
|
10
|
+
const cliPath = fileURLToPath(import.meta.url);
|
|
11
|
+
const packagePath = path.resolve(path.dirname(cliPath), '../../package.json');
|
|
12
|
+
if (!existsSync(packagePath)) {
|
|
13
|
+
return '0.0.0';
|
|
14
|
+
}
|
|
15
|
+
const raw = readFileSync(packagePath, 'utf8');
|
|
16
|
+
const parsed = JSON.parse(raw);
|
|
17
|
+
return parsed.version ?? '0.0.0';
|
|
18
|
+
}
|
|
19
|
+
async function execCommand(command, args) {
|
|
20
|
+
return await new Promise((resolve, reject) => {
|
|
21
|
+
const child = spawn(command, args, {
|
|
22
|
+
stdio: 'inherit',
|
|
23
|
+
shell: false,
|
|
24
|
+
});
|
|
25
|
+
child.on('error', (error) => {
|
|
26
|
+
reject(error);
|
|
27
|
+
});
|
|
28
|
+
child.on('close', (code) => {
|
|
29
|
+
resolve(code ?? 1);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export function createDefaultDeps() {
|
|
34
|
+
return {
|
|
35
|
+
fetchImpl: fetch,
|
|
36
|
+
writeOut: (message) => {
|
|
37
|
+
process.stdout.write(`${message}\n`);
|
|
38
|
+
},
|
|
39
|
+
writeErr: (message) => {
|
|
40
|
+
process.stderr.write(`${message}\n`);
|
|
41
|
+
},
|
|
42
|
+
getVersion: loadVersion,
|
|
43
|
+
nowIso: () => new Date().toISOString(),
|
|
44
|
+
runCommand: execCommand,
|
|
45
|
+
isInteractiveTerminal: () => Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
46
|
+
runInteractive: runInteractiveCli,
|
|
47
|
+
};
|
|
48
|
+
}
|
package/dist/cli/http.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP 요청 및 응답 처리 유틸리티
|
|
3
|
+
*/
|
|
4
|
+
import { renderApiEnvelope } from '../cliRenderer.js';
|
|
5
|
+
import { buildDaisoStoreKeywordVariants } from '../utils/daisoKeyword.js';
|
|
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
|
+
}
|
|
62
|
+
export async function requestAndPrintResponse(fetchImpl, writeOut, writeErr, url, command, asJson) {
|
|
63
|
+
try {
|
|
64
|
+
const response = await fetchImpl(url.toString());
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
const bodyText = await response.text();
|
|
67
|
+
writeErr(`요청 실패: HTTP ${response.status}`);
|
|
68
|
+
if (bodyText) {
|
|
69
|
+
writeErr(bodyText);
|
|
70
|
+
for (const hint of buildCliHint(command, url, bodyText)) {
|
|
71
|
+
writeErr(hint);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
const payload = (await response.json());
|
|
77
|
+
if (asJson) {
|
|
78
|
+
writeOut(JSON.stringify(payload, null, 2));
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
writeOut(renderApiEnvelope(command, url, payload));
|
|
82
|
+
}
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
87
|
+
writeErr(`요청 중 오류 발생: ${message}`);
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function toStoreCount(payload) {
|
|
92
|
+
if (typeof payload !== 'object' || payload === null) {
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
const record = payload;
|
|
96
|
+
if (record.success !== true || typeof record.data !== 'object' || record.data === null) {
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
const stores = record.data.stores;
|
|
100
|
+
return Array.isArray(stores) ? stores.length : 0;
|
|
101
|
+
}
|
|
102
|
+
export async function requestAndPrintStoresWithKeywordFallback(fetchImpl, writeOut, writeErr, options, asJson) {
|
|
103
|
+
const originalKeyword = options.keyword;
|
|
104
|
+
if (!originalKeyword) {
|
|
105
|
+
const url = toUrl('/api/daiso/stores');
|
|
106
|
+
applyOptionsToQuery(url, options);
|
|
107
|
+
return await requestAndPrintResponse(fetchImpl, writeOut, writeErr, url, 'stores', asJson);
|
|
108
|
+
}
|
|
109
|
+
const keywords = buildDaisoStoreKeywordVariants(originalKeyword);
|
|
110
|
+
const candidates = keywords.length > 0 ? keywords : [originalKeyword];
|
|
111
|
+
try {
|
|
112
|
+
let lastUrl = toUrl('/api/daiso/stores');
|
|
113
|
+
let lastPayload = null;
|
|
114
|
+
for (const keyword of candidates) {
|
|
115
|
+
const targetUrl = toUrl('/api/daiso/stores');
|
|
116
|
+
applyOptionsToQuery(targetUrl, { ...options, keyword });
|
|
117
|
+
lastUrl = targetUrl;
|
|
118
|
+
const response = await fetchImpl(targetUrl.toString());
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
const bodyText = await response.text();
|
|
121
|
+
writeErr(`요청 실패: HTTP ${response.status}`);
|
|
122
|
+
if (bodyText) {
|
|
123
|
+
writeErr(bodyText);
|
|
124
|
+
}
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
const payload = (await response.json());
|
|
128
|
+
lastPayload = payload;
|
|
129
|
+
if (toStoreCount(payload) > 0) {
|
|
130
|
+
if (keyword !== originalKeyword) {
|
|
131
|
+
writeOut(`입력 키워드 "${originalKeyword}" 대신 "${keyword}"로 매장을 찾았습니다.`);
|
|
132
|
+
}
|
|
133
|
+
if (asJson) {
|
|
134
|
+
writeOut(JSON.stringify(payload, null, 2));
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
writeOut(renderApiEnvelope('stores', targetUrl, payload));
|
|
138
|
+
}
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (asJson) {
|
|
143
|
+
writeOut(JSON.stringify(lastPayload, null, 2));
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
writeOut(renderApiEnvelope('stores', lastUrl, lastPayload));
|
|
147
|
+
}
|
|
148
|
+
writeOut('힌트: "안산 중앙역" 대신 "안산중앙" 또는 "고잔"으로 검색해보세요.');
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
153
|
+
writeErr(`요청 중 오류 발생: ${message}`);
|
|
154
|
+
return 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 인터랙티브 CLI API 호출 헬퍼
|
|
3
|
+
*/
|
|
4
|
+
import { buildDaisoStoreKeywordVariants, parseStores } from '../utils/cliInteractiveHelpers.js';
|
|
5
|
+
const BASE_URL = 'https://mcp.aka.page';
|
|
6
|
+
export async function fetchEnvelope(fetchImpl, path, query = {}) {
|
|
7
|
+
const url = new URL(path, BASE_URL);
|
|
8
|
+
for (const [key, value] of Object.entries(query)) {
|
|
9
|
+
url.searchParams.set(key, value);
|
|
10
|
+
}
|
|
11
|
+
const response = await fetchImpl(url.toString());
|
|
12
|
+
if (!response.ok) {
|
|
13
|
+
throw new Error(`HTTP ${response.status} (${url.pathname})`);
|
|
14
|
+
}
|
|
15
|
+
return (await response.json());
|
|
16
|
+
}
|
|
17
|
+
export async function fetchStoresWithKeywordFallback(fetchImpl, service, keyword) {
|
|
18
|
+
const candidates = service === 'daiso' ? buildDaisoStoreKeywordVariants(keyword) : [keyword];
|
|
19
|
+
const keywords = candidates.length > 0 ? candidates : [keyword];
|
|
20
|
+
for (const currentKeyword of keywords) {
|
|
21
|
+
const payload = await fetchEnvelope(fetchImpl, `/api/${service}/stores`, {
|
|
22
|
+
keyword: currentKeyword,
|
|
23
|
+
limit: '10',
|
|
24
|
+
});
|
|
25
|
+
const stores = parseStores(payload);
|
|
26
|
+
if (stores.length > 0) {
|
|
27
|
+
return { stores, matchedKeyword: currentKeyword };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { stores: [], matchedKeyword: keyword };
|
|
31
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
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, initialKeyword) {
|
|
97
|
+
const keyword = initialKeyword && initialKeyword.trim().length > 0
|
|
98
|
+
? initialKeyword.trim()
|
|
99
|
+
: await askNonEmpty(prompt, '찾을 상품 키워드를 입력하세요: ');
|
|
100
|
+
const productsPayload = await fetchEnvelope(deps.fetchImpl, '/api/daiso/products', {
|
|
101
|
+
q: keyword,
|
|
102
|
+
pageSize: '10',
|
|
103
|
+
});
|
|
104
|
+
const products = parseDaisoProducts(productsPayload);
|
|
105
|
+
if (products.length === 0) {
|
|
106
|
+
deps.writeOut('검색된 다이소 상품이 없습니다.');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const selectedProduct = await pickFromList({
|
|
110
|
+
prompt,
|
|
111
|
+
writeOut: deps.writeOut,
|
|
112
|
+
title: '[상품 선택]',
|
|
113
|
+
emptyText: '검색된 다이소 상품이 없습니다.',
|
|
114
|
+
cancelText: '상품 선택을 취소했습니다.',
|
|
115
|
+
items: products,
|
|
116
|
+
renderItem: (product, index) => `${index + 1}. ${product.name} (${product.price}원, ID: ${product.id})`,
|
|
117
|
+
filterText: (product) => `${product.name} ${product.id}`,
|
|
118
|
+
indexText: '입력: 번호 선택 | /키워드 필터 | all 전체보기 | 0 취소',
|
|
119
|
+
});
|
|
120
|
+
if (!selectedProduct) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const inventoryPayload = await fetchEnvelope(deps.fetchImpl, '/api/daiso/inventory', {
|
|
124
|
+
productId: selectedProduct.id,
|
|
125
|
+
keyword: store.name,
|
|
126
|
+
pageSize: '50',
|
|
127
|
+
});
|
|
128
|
+
if (!isRecord(inventoryPayload) || inventoryPayload.success !== true || !isRecord(inventoryPayload.data)) {
|
|
129
|
+
deps.writeOut('재고 응답을 해석하지 못했습니다.');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const storeInventory = inventoryPayload.data.storeInventory;
|
|
133
|
+
if (!isRecord(storeInventory) || !Array.isArray(storeInventory.stores)) {
|
|
134
|
+
deps.writeOut('매장 재고 데이터가 없습니다.');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const match = storeInventory.stores.find((entry) => {
|
|
138
|
+
if (!isRecord(entry)) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
const storeName = toText(entry.storeName);
|
|
142
|
+
return storeName === store.name || storeName.includes(store.name);
|
|
143
|
+
});
|
|
144
|
+
deps.writeOut('');
|
|
145
|
+
deps.writeOut('[재고 결과]');
|
|
146
|
+
deps.writeOut(`- 상품: ${selectedProduct.name}`);
|
|
147
|
+
deps.writeOut(`- 매장: ${store.name}`);
|
|
148
|
+
if (isRecord(match)) {
|
|
149
|
+
const quantity = toText(match.quantity) || '0';
|
|
150
|
+
deps.writeOut(`- 재고 수량: ${quantity}`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
deps.writeOut('- 선택 매장에 대한 재고 정보를 찾지 못했습니다.');
|
|
154
|
+
}
|
|
155
|
+
export async function runOliveyoungItemSearch(deps, prompt, store) {
|
|
156
|
+
const keyword = await askNonEmpty(prompt, '찾을 상품 키워드를 입력하세요: ');
|
|
157
|
+
const payload = await fetchEnvelope(deps.fetchImpl, '/api/oliveyoung/inventory', {
|
|
158
|
+
keyword,
|
|
159
|
+
storeKeyword: store.name,
|
|
160
|
+
size: '10',
|
|
161
|
+
});
|
|
162
|
+
if (!isRecord(payload) || payload.success !== true || !isRecord(payload.data)) {
|
|
163
|
+
deps.writeOut('재고 응답을 해석하지 못했습니다.');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const inventory = payload.data.inventory;
|
|
167
|
+
if (!isRecord(inventory) || !Array.isArray(inventory.products)) {
|
|
168
|
+
deps.writeOut('올리브영 상품 데이터가 없습니다.');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const products = inventory.products
|
|
172
|
+
.filter((entry) => isRecord(entry))
|
|
173
|
+
.map((entry) => ({
|
|
174
|
+
goodsNumber: toText(entry.goodsNumber),
|
|
175
|
+
goodsName: toText(entry.goodsName),
|
|
176
|
+
priceToPay: Number.parseInt(toText(entry.priceToPay), 10) || 0,
|
|
177
|
+
o2oRemainQuantity: Number.parseInt(toText(entry.o2oRemainQuantity), 10) || 0,
|
|
178
|
+
}))
|
|
179
|
+
.filter((entry) => entry.goodsName.length > 0);
|
|
180
|
+
deps.writeOut('');
|
|
181
|
+
deps.writeOut('[재고 결과]');
|
|
182
|
+
deps.writeOut(`- 매장: ${store.name}`);
|
|
183
|
+
deps.writeOut(`- 검색어: ${keyword}`);
|
|
184
|
+
if (products.length === 0) {
|
|
185
|
+
deps.writeOut('- 검색된 상품이 없습니다.');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const selected = await pickFromList({
|
|
189
|
+
prompt,
|
|
190
|
+
writeOut: deps.writeOut,
|
|
191
|
+
title: '[상품 선택]',
|
|
192
|
+
emptyText: '검색된 상품이 없습니다.',
|
|
193
|
+
cancelText: '상품 선택을 취소했습니다.',
|
|
194
|
+
items: products,
|
|
195
|
+
renderItem: (product, index) => `${index + 1}. ${product.goodsName} (${product.priceToPay}원, 남은수량 ${product.o2oRemainQuantity})`,
|
|
196
|
+
filterText: (product) => `${product.goodsName} ${product.goodsNumber}`,
|
|
197
|
+
indexText: '입력: 번호 선택 | /키워드 필터 | all 전체보기 | 0 취소',
|
|
198
|
+
});
|
|
199
|
+
if (!selected) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
deps.writeOut(`- 상품: ${selected.goodsName}`);
|
|
203
|
+
deps.writeOut(`- 가격: ${selected.priceToPay}원`);
|
|
204
|
+
deps.writeOut(`- 남은수량: ${selected.o2oRemainQuantity}`);
|
|
205
|
+
}
|
|
206
|
+
export async function runCuItemSearch(deps, prompt, store) {
|
|
207
|
+
const keyword = await askNonEmpty(prompt, '찾을 상품 키워드를 입력하세요: ');
|
|
208
|
+
const payload = await fetchEnvelope(deps.fetchImpl, '/api/cu/inventory', {
|
|
209
|
+
keyword,
|
|
210
|
+
storeKeyword: store.name,
|
|
211
|
+
size: '10',
|
|
212
|
+
storeLimit: '10',
|
|
213
|
+
});
|
|
214
|
+
if (!isRecord(payload) || payload.success !== true || !isRecord(payload.data)) {
|
|
215
|
+
deps.writeOut('재고 응답을 해석하지 못했습니다.');
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const inventory = payload.data.inventory;
|
|
219
|
+
if (!isRecord(inventory) || !Array.isArray(inventory.items)) {
|
|
220
|
+
deps.writeOut('CU 상품 데이터가 없습니다.');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const items = inventory.items
|
|
224
|
+
.filter((entry) => isRecord(entry))
|
|
225
|
+
.map((entry) => ({
|
|
226
|
+
itemCode: toText(entry.itemCode),
|
|
227
|
+
itemName: toText(entry.itemName),
|
|
228
|
+
price: Number.parseInt(toText(entry.price), 10) || 0,
|
|
229
|
+
pickupYn: toText(entry.pickupYn).toLowerCase() === 'true',
|
|
230
|
+
deliveryYn: toText(entry.deliveryYn).toLowerCase() === 'true',
|
|
231
|
+
reserveYn: toText(entry.reserveYn).toLowerCase() === 'true',
|
|
232
|
+
}))
|
|
233
|
+
.filter((entry) => entry.itemName.length > 0);
|
|
234
|
+
deps.writeOut('');
|
|
235
|
+
deps.writeOut('[재고 결과]');
|
|
236
|
+
deps.writeOut(`- 매장: ${store.name}`);
|
|
237
|
+
deps.writeOut(`- 검색어: ${keyword}`);
|
|
238
|
+
if (items.length === 0) {
|
|
239
|
+
deps.writeOut('- 검색된 상품이 없습니다.');
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const selected = await pickFromList({
|
|
243
|
+
prompt,
|
|
244
|
+
writeOut: deps.writeOut,
|
|
245
|
+
title: '[상품 선택]',
|
|
246
|
+
emptyText: '검색된 상품이 없습니다.',
|
|
247
|
+
cancelText: '상품 선택을 취소했습니다.',
|
|
248
|
+
items,
|
|
249
|
+
renderItem: (item, index) => `${index + 1}. ${item.itemName} (${item.price}원)`,
|
|
250
|
+
filterText: (item) => `${item.itemName} ${item.itemCode}`,
|
|
251
|
+
indexText: '입력: 번호 선택 | /키워드 필터 | all 전체보기 | 0 취소',
|
|
252
|
+
});
|
|
253
|
+
if (!selected) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
deps.writeOut(`- 상품: ${selected.itemName}`);
|
|
257
|
+
deps.writeOut(`- 가격: ${selected.price}원`);
|
|
258
|
+
deps.writeOut(`- 픽업 가능: ${selected.pickupYn ? '예' : '아니오'}`);
|
|
259
|
+
deps.writeOut(`- 배달 가능: ${selected.deliveryYn ? '예' : '아니오'}`);
|
|
260
|
+
deps.writeOut(`- 예약 가능: ${selected.reserveYn ? '예' : '아니오'}`);
|
|
261
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 인터랙티브 CLI 입력/출력 헬퍼
|
|
3
|
+
*/
|
|
4
|
+
export async function askMenu(prompt, title, options, writeOut) {
|
|
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
|
+
writeOut?.(`번호로 입력하세요. 0부터 ${options.length} 사이에서 선택할 수 있습니다.`);
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
return picked;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function askYesNo(prompt, question) {
|
|
16
|
+
while (true) {
|
|
17
|
+
const answer = (await prompt.ask(question)).toLowerCase();
|
|
18
|
+
if (answer === 'y' || answer === 'yes') {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (answer === 'n' || answer === 'no') {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function askNonEmpty(prompt, question) {
|
|
27
|
+
while (true) {
|
|
28
|
+
const answer = await prompt.ask(question);
|
|
29
|
+
if (answer.length > 0) {
|
|
30
|
+
return answer;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function askNextAction(prompt, writeOut) {
|
|
35
|
+
writeOut('');
|
|
36
|
+
writeOut('[다음 동작]');
|
|
37
|
+
writeOut('1. 같은 매장에서 다른 상품 찾기');
|
|
38
|
+
writeOut('2. 다른 매장/서비스 다시 선택하기');
|
|
39
|
+
writeOut('3. 종료하기');
|
|
40
|
+
const choice = await askMenu(prompt, '번호를 선택하세요:', ['same-store', 'change-store', 'exit'], writeOut);
|
|
41
|
+
return choice === 1 ? 'same-store' : choice === 2 ? 'change-store' : 'exit';
|
|
42
|
+
}
|
|
43
|
+
export function printStoreDetail(writeOut, store) {
|
|
44
|
+
writeOut('');
|
|
45
|
+
writeOut('[선택한 매장 정보]');
|
|
46
|
+
writeOut(`- 매장명: ${store.name}`);
|
|
47
|
+
writeOut(`- 주소: ${store.address || '정보 없음'}`);
|
|
48
|
+
writeOut(`- 전화: ${store.phone || '정보 없음'}`);
|
|
49
|
+
writeOut('');
|
|
50
|
+
}
|