daiso 1.0.3 → 1.0.4
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 +3 -0
- package/dist/cli.js +28 -12
- package/dist/cliHelp.js +10 -8
- package/dist/cliRenderer.js +135 -0
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import path from 'node:path';
|
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
import { spawn } from 'node:child_process';
|
|
11
11
|
import { printCommandHelp, printHelp } from './cliHelp.js';
|
|
12
|
+
import { renderApiEnvelope } from './cliRenderer.js';
|
|
12
13
|
const DEFAULT_BASE_URL = 'https://mcp.aka.page';
|
|
13
14
|
const DEFAULT_MCP_URL = `${DEFAULT_BASE_URL}/mcp`;
|
|
14
15
|
function loadVersion() {
|
|
@@ -89,7 +90,17 @@ function applyOptionsToQuery(url, options) {
|
|
|
89
90
|
url.searchParams.set(key, value);
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
|
-
|
|
93
|
+
function toQueryOptions(options) {
|
|
94
|
+
const queryOptions = {};
|
|
95
|
+
for (const [key, value] of Object.entries(options)) {
|
|
96
|
+
if (key === 'help' || key === 'json') {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
queryOptions[key] = value;
|
|
100
|
+
}
|
|
101
|
+
return queryOptions;
|
|
102
|
+
}
|
|
103
|
+
async function requestAndPrintResponse(fetchImpl, writeOut, writeErr, url, command, asJson) {
|
|
93
104
|
try {
|
|
94
105
|
const response = await fetchImpl(url.toString());
|
|
95
106
|
if (!response.ok) {
|
|
@@ -101,7 +112,12 @@ async function requestAndPrintJson(fetchImpl, writeOut, writeErr, url) {
|
|
|
101
112
|
return 1;
|
|
102
113
|
}
|
|
103
114
|
const payload = (await response.json());
|
|
104
|
-
|
|
115
|
+
if (asJson) {
|
|
116
|
+
writeOut(JSON.stringify(payload, null, 2));
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
writeOut(renderApiEnvelope(command, url, payload));
|
|
120
|
+
}
|
|
105
121
|
return 0;
|
|
106
122
|
}
|
|
107
123
|
catch (error) {
|
|
@@ -179,8 +195,8 @@ export async function runCli(argv, deps) {
|
|
|
179
195
|
return 1;
|
|
180
196
|
}
|
|
181
197
|
const targetUrl = toUrl(targetPath);
|
|
182
|
-
applyOptionsToQuery(targetUrl, parsed.options);
|
|
183
|
-
return await
|
|
198
|
+
applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
|
|
199
|
+
return await requestAndPrintResponse(resolvedDeps.fetchImpl, resolvedDeps.writeOut, resolvedDeps.writeErr, targetUrl, command, parsed.options.json === 'true');
|
|
184
200
|
}
|
|
185
201
|
if (command === 'products') {
|
|
186
202
|
const parsed = parseCliArgs(options);
|
|
@@ -194,8 +210,8 @@ export async function runCli(argv, deps) {
|
|
|
194
210
|
}
|
|
195
211
|
const targetUrl = toUrl('/api/daiso/products');
|
|
196
212
|
targetUrl.searchParams.set('q', query);
|
|
197
|
-
applyOptionsToQuery(targetUrl, parsed.options);
|
|
198
|
-
return await
|
|
213
|
+
applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
|
|
214
|
+
return await requestAndPrintResponse(resolvedDeps.fetchImpl, resolvedDeps.writeOut, resolvedDeps.writeErr, targetUrl, command, parsed.options.json === 'true');
|
|
199
215
|
}
|
|
200
216
|
if (command === 'product') {
|
|
201
217
|
const parsed = parseCliArgs(options);
|
|
@@ -208,8 +224,8 @@ export async function runCli(argv, deps) {
|
|
|
208
224
|
return 1;
|
|
209
225
|
}
|
|
210
226
|
const targetUrl = toUrl(`/api/daiso/products/${productId}`);
|
|
211
|
-
applyOptionsToQuery(targetUrl, parsed.options);
|
|
212
|
-
return await
|
|
227
|
+
applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
|
|
228
|
+
return await requestAndPrintResponse(resolvedDeps.fetchImpl, resolvedDeps.writeOut, resolvedDeps.writeErr, targetUrl, command, parsed.options.json === 'true');
|
|
213
229
|
}
|
|
214
230
|
if (command === 'stores') {
|
|
215
231
|
const parsed = parseCliArgs(options);
|
|
@@ -225,8 +241,8 @@ export async function runCli(argv, deps) {
|
|
|
225
241
|
return 1;
|
|
226
242
|
}
|
|
227
243
|
const targetUrl = toUrl('/api/daiso/stores');
|
|
228
|
-
applyOptionsToQuery(targetUrl, parsed.options);
|
|
229
|
-
return await
|
|
244
|
+
applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
|
|
245
|
+
return await requestAndPrintResponse(resolvedDeps.fetchImpl, resolvedDeps.writeOut, resolvedDeps.writeErr, targetUrl, command, parsed.options.json === 'true');
|
|
230
246
|
}
|
|
231
247
|
if (command === 'inventory') {
|
|
232
248
|
const parsed = parseCliArgs(options);
|
|
@@ -240,8 +256,8 @@ export async function runCli(argv, deps) {
|
|
|
240
256
|
}
|
|
241
257
|
const targetUrl = toUrl('/api/daiso/inventory');
|
|
242
258
|
targetUrl.searchParams.set('productId', productId);
|
|
243
|
-
applyOptionsToQuery(targetUrl, parsed.options);
|
|
244
|
-
return await
|
|
259
|
+
applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
|
|
260
|
+
return await requestAndPrintResponse(resolvedDeps.fetchImpl, resolvedDeps.writeOut, resolvedDeps.writeErr, targetUrl, command, parsed.options.json === 'true');
|
|
245
261
|
}
|
|
246
262
|
resolvedDeps.writeErr(`알 수 없는 명령어: ${command}`);
|
|
247
263
|
resolvedDeps.writeErr('도움말: daiso help');
|
package/dist/cliHelp.js
CHANGED
|
@@ -59,40 +59,42 @@ const COMMAND_DETAIL = {
|
|
|
59
59
|
get: [
|
|
60
60
|
'명령: get',
|
|
61
61
|
'설명: 임의 API 경로를 GET으로 호출합니다.',
|
|
62
|
-
'사용법: daiso get <path|url> [--queryKey value ...]',
|
|
62
|
+
'사용법: daiso get <path|url> [--queryKey value ...] [--json]',
|
|
63
63
|
'필수: <path|url>',
|
|
64
|
+
'옵션: --json 원본 JSON 출력',
|
|
64
65
|
'예시: daiso get /api/cgv/movies --playDate 20260307 --theaterCode 0056',
|
|
65
66
|
],
|
|
66
67
|
products: [
|
|
67
68
|
'명령: products',
|
|
68
69
|
'설명: 다이소 제품 검색 API를 호출합니다.',
|
|
69
|
-
'사용법: daiso products <query> [--page N] [--pageSize N]',
|
|
70
|
+
'사용법: daiso products <query> [--page N] [--pageSize N] [--json]',
|
|
70
71
|
'필수: <query>',
|
|
71
|
-
'옵션: --page, --pageSize',
|
|
72
|
+
'옵션: --page, --pageSize, --json',
|
|
72
73
|
'예시: daiso products 수납박스 --page 1 --pageSize 30',
|
|
73
74
|
],
|
|
74
75
|
product: [
|
|
75
76
|
'명령: product',
|
|
76
77
|
'설명: 다이소 제품 상세 API를 호출합니다.',
|
|
77
|
-
'사용법: daiso product <productId>',
|
|
78
|
+
'사용법: daiso product <productId> [--json]',
|
|
78
79
|
'필수: <productId>',
|
|
80
|
+
'옵션: --json',
|
|
79
81
|
'예시: daiso product 1034604',
|
|
80
82
|
],
|
|
81
83
|
stores: [
|
|
82
84
|
'명령: stores',
|
|
83
85
|
'설명: 다이소 매장 검색 API를 호출합니다.',
|
|
84
|
-
'사용법: daiso stores [keyword] [--sido 값] [--gugun 값] [--dong 값] [--limit N]',
|
|
86
|
+
'사용법: daiso stores [keyword] [--sido 값] [--gugun 값] [--dong 값] [--limit N] [--json]',
|
|
85
87
|
'필수: keyword 또는 --sido',
|
|
86
|
-
'옵션: --keyword, --sido, --gugun, --dong, --limit',
|
|
88
|
+
'옵션: --keyword, --sido, --gugun, --dong, --limit, --json',
|
|
87
89
|
'예시: daiso stores 강남역',
|
|
88
90
|
'예시: daiso stores --sido 서울 --gugun 강남구',
|
|
89
91
|
],
|
|
90
92
|
inventory: [
|
|
91
93
|
'명령: inventory',
|
|
92
94
|
'설명: 다이소 재고 API를 호출합니다.',
|
|
93
|
-
'사용법: daiso inventory <productId> [--keyword 값] [--lat 값] [--lng 값] [--page N] [--pageSize N]',
|
|
95
|
+
'사용법: daiso inventory <productId> [--keyword 값] [--lat 값] [--lng 값] [--page N] [--pageSize N] [--json]',
|
|
94
96
|
'필수: <productId>',
|
|
95
|
-
'옵션: --keyword, --lat, --lng, --page, --pageSize',
|
|
97
|
+
'옵션: --keyword, --lat, --lng, --page, --pageSize, --json',
|
|
96
98
|
'예시: daiso inventory 1034604 --keyword 강남역',
|
|
97
99
|
],
|
|
98
100
|
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI 응답 렌더러
|
|
3
|
+
*/
|
|
4
|
+
function isRecord(value) {
|
|
5
|
+
return typeof value === 'object' && value !== null;
|
|
6
|
+
}
|
|
7
|
+
function toText(value) {
|
|
8
|
+
if (value === null || value === undefined) {
|
|
9
|
+
return '-';
|
|
10
|
+
}
|
|
11
|
+
if (typeof value === 'string') {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
15
|
+
return String(value);
|
|
16
|
+
}
|
|
17
|
+
return JSON.stringify(value);
|
|
18
|
+
}
|
|
19
|
+
function formatMeta(meta) {
|
|
20
|
+
if (!isRecord(meta)) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
const pairs = Object.entries(meta)
|
|
24
|
+
.filter(([, value]) => typeof value === 'string' || typeof value === 'number')
|
|
25
|
+
.map(([key, value]) => `${key}=${value}`);
|
|
26
|
+
if (pairs.length === 0) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
return [`메타: ${pairs.join(', ')}`];
|
|
30
|
+
}
|
|
31
|
+
function detectName(item) {
|
|
32
|
+
const keys = ['name', 'productName', 'storeName', 'theaterName', 'movieName'];
|
|
33
|
+
for (const key of keys) {
|
|
34
|
+
if (typeof item[key] === 'string' && item[key].trim().length > 0) {
|
|
35
|
+
return item[key];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return '이름 없음';
|
|
39
|
+
}
|
|
40
|
+
function formatCollection(title, items) {
|
|
41
|
+
if (!Array.isArray(items)) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const lines = [`${title}: ${items.length}건`];
|
|
45
|
+
const preview = items.slice(0, 5);
|
|
46
|
+
for (const entry of preview) {
|
|
47
|
+
if (!isRecord(entry)) {
|
|
48
|
+
lines.push(`- ${toText(entry)}`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const name = detectName(entry);
|
|
52
|
+
const id = entry.id ?? entry.productId ?? entry.PD_NO ?? entry.storeCode ?? entry.theaterId ?? entry.movieId;
|
|
53
|
+
const price = entry.price ?? entry.prc ?? entry.PD_PRC;
|
|
54
|
+
let detail = `- ${name}`;
|
|
55
|
+
if (id !== undefined) {
|
|
56
|
+
detail += ` [${toText(id)}]`;
|
|
57
|
+
}
|
|
58
|
+
if (price !== undefined) {
|
|
59
|
+
detail += ` / ${toText(price)}원`;
|
|
60
|
+
}
|
|
61
|
+
lines.push(detail);
|
|
62
|
+
}
|
|
63
|
+
if (items.length > preview.length) {
|
|
64
|
+
lines.push(`...외 ${items.length - preview.length}건`);
|
|
65
|
+
}
|
|
66
|
+
return lines;
|
|
67
|
+
}
|
|
68
|
+
function formatProductDetail(data) {
|
|
69
|
+
const id = data.id ?? data.productId ?? '-';
|
|
70
|
+
const name = data.name ?? data.productName ?? '-';
|
|
71
|
+
const price = data.price ?? '-';
|
|
72
|
+
const soldOut = data.soldOut;
|
|
73
|
+
const lines = [`제품 상세: ${toText(name)} (${toText(id)})`, `가격: ${toText(price)}원`];
|
|
74
|
+
if (typeof soldOut === 'boolean') {
|
|
75
|
+
lines.push(`품절 여부: ${soldOut ? '품절' : '재고 있음'}`);
|
|
76
|
+
}
|
|
77
|
+
return lines;
|
|
78
|
+
}
|
|
79
|
+
function formatInventory(data) {
|
|
80
|
+
const lines = [];
|
|
81
|
+
const onlineStock = data.onlineStock;
|
|
82
|
+
if (onlineStock !== undefined) {
|
|
83
|
+
lines.push(`온라인 재고: ${toText(onlineStock)}`);
|
|
84
|
+
}
|
|
85
|
+
const storeInventory = data.storeInventory;
|
|
86
|
+
if (isRecord(storeInventory)) {
|
|
87
|
+
const total = storeInventory.totalStores;
|
|
88
|
+
const inStock = storeInventory.inStockCount;
|
|
89
|
+
lines.push(`매장 재고: 재고보유 ${toText(inStock)} / 전체 ${toText(total)}`);
|
|
90
|
+
lines.push(...formatCollection('매장 목록', storeInventory.stores));
|
|
91
|
+
}
|
|
92
|
+
return lines;
|
|
93
|
+
}
|
|
94
|
+
export function renderApiEnvelope(command, url, payload) {
|
|
95
|
+
if (!isRecord(payload)) {
|
|
96
|
+
return `요청 성공: ${url.pathname}\n${toText(payload)}\n\n원본 JSON은 --json 옵션으로 확인하세요.`;
|
|
97
|
+
}
|
|
98
|
+
const envelope = payload;
|
|
99
|
+
if (envelope.success !== true) {
|
|
100
|
+
return JSON.stringify(payload, null, 2);
|
|
101
|
+
}
|
|
102
|
+
const lines = [`요청 성공: ${url.pathname}`];
|
|
103
|
+
lines.push(...formatMeta(envelope.meta));
|
|
104
|
+
if (!isRecord(envelope.data)) {
|
|
105
|
+
lines.push(`응답 데이터: ${toText(envelope.data)}`);
|
|
106
|
+
lines.push('', '원본 JSON은 --json 옵션으로 확인하세요.');
|
|
107
|
+
return lines.join('\n');
|
|
108
|
+
}
|
|
109
|
+
const data = envelope.data;
|
|
110
|
+
if (command === 'product') {
|
|
111
|
+
lines.push(...formatProductDetail(data));
|
|
112
|
+
}
|
|
113
|
+
else if (command === 'inventory') {
|
|
114
|
+
lines.push(...formatInventory(data));
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
lines.push(...formatCollection('제품 목록', data.products));
|
|
118
|
+
lines.push(...formatCollection('매장 목록', data.stores));
|
|
119
|
+
lines.push(...formatCollection('극장 목록', data.theaters));
|
|
120
|
+
lines.push(...formatCollection('영화 목록', data.movies));
|
|
121
|
+
lines.push(...formatCollection('시간표', data.timetable));
|
|
122
|
+
lines.push(...formatCollection('좌석 목록', data.seats));
|
|
123
|
+
lines.push(...formatCollection('상영 목록', data.showtimes));
|
|
124
|
+
if (lines.length <= 2) {
|
|
125
|
+
const summaryEntries = Object.entries(data)
|
|
126
|
+
.filter(([, value]) => typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
|
|
127
|
+
.slice(0, 8);
|
|
128
|
+
for (const [key, value] of summaryEntries) {
|
|
129
|
+
lines.push(`${key}: ${toText(value)}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
lines.push('', '원본 JSON은 --json 옵션으로 확인하세요.');
|
|
134
|
+
return lines.join('\n');
|
|
135
|
+
}
|