daiso 1.0.8 → 1.0.10
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 +96 -11
- package/dist/api/actionsProxy.js +5 -0
- package/dist/api/configStatus.js +8 -0
- package/dist/api/feedbackHandlers.js +60 -0
- package/dist/api/healthCheckDefinitions.js +188 -0
- package/dist/api/healthCheckShape.js +106 -0
- package/dist/api/healthCheckTypes.js +4 -0
- package/dist/api/healthChecks.js +13 -272
- package/dist/api/opinetHandlers.js +91 -0
- package/dist/api/routes/feedbackRoutes.js +5 -0
- package/dist/api/routes/healthRoutes.js +17 -1
- package/dist/api/routes/opinetRoutes.js +24 -0
- package/dist/bin.js +0 -0
- package/dist/cli/commandOptions.js +23 -2
- package/dist/cli/commands/apiCommand.js +24 -0
- package/dist/cli/commands/cinema.js +65 -0
- package/dist/cli/commands/convenience.js +57 -100
- package/dist/cli/http.js +1 -1
- package/dist/cli.js +10 -1
- package/dist/cliHelp.js +44 -8
- package/dist/core/outputSchema.js +141 -0
- package/dist/core/registry.js +1 -138
- package/dist/index.js +92 -29
- package/dist/pages/openapiFullSpec.js +5 -0
- package/dist/pages/openapiSpecActions.js +63 -0
- package/dist/pages/openapiSpecPathsFeedback.js +95 -0
- package/dist/pages/openapiSpecPathsOpinet.js +81 -0
- package/dist/pages/promptReferenceText.js +15 -0
- package/dist/services/cgv/client.js +7 -4
- package/dist/services/emart24/tools/searchProducts.js +37 -19
- package/dist/services/feedback/client.js +109 -0
- package/dist/services/feedback/index.js +22 -0
- package/dist/services/feedback/tools/submitDeveloperRequest.js +54 -0
- package/dist/services/feedback/types.js +3 -0
- package/dist/services/gs25/client.js +1 -84
- package/dist/services/gs25/productSearch.js +97 -0
- package/dist/services/oliveyoung/client.js +1 -55
- package/dist/services/oliveyoung/normalize.js +58 -0
- package/dist/services/opinet/client.js +231 -0
- package/dist/services/opinet/index.js +26 -0
- package/dist/services/opinet/location.js +119 -0
- package/dist/services/opinet/tools.js +80 -0
- package/dist/services/opinet/types.js +1 -0
- package/dist/services/places/client.js +29 -8
- package/dist/services/seveneleven/tools/searchProducts.js +36 -17
- package/openapi.json +82 -0
- package/openapi.yaml +55 -0
- package/package.json +24 -20
package/dist/api/healthChecks.js
CHANGED
|
@@ -1,178 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 개별 서비스 헬스 체크 실행기
|
|
3
3
|
*/
|
|
4
|
-
|
|
4
|
+
import { EMART24_UPSTREAM_403_PATTERNS, GS25_CLOUDFRONT_403_PATTERNS, HEALTH_CHECKS, SEVENELEVEN_UPSTREAM_403_PATTERNS, } from './healthCheckDefinitions.js';
|
|
5
|
+
import { hasRequiredRepresentativeFields, toCount, toFirstName } from './healthCheckShape.js';
|
|
6
|
+
const HEALTH_CHECK_CACHE_TTL_MS = 300_000;
|
|
5
7
|
const DEFAULT_HEALTH_CHECK_TIMEOUT_MS = 7000;
|
|
6
8
|
const MAX_HEALTH_CHECK_TIMEOUT_MS = 20_000;
|
|
7
9
|
const DEFAULT_HEALTH_CHECK_CONCURRENCY = 1;
|
|
8
10
|
const DEFAULT_HEALTH_CHECK_SLOW_THRESHOLD_MS = 0;
|
|
9
|
-
const GS25_CLOUDFRONT_403_PATTERNS = [
|
|
10
|
-
'403 Forbidden',
|
|
11
|
-
'The request could not be satisfied',
|
|
12
|
-
'403 ERROR',
|
|
13
|
-
];
|
|
14
|
-
const HEALTH_CHECKS = [
|
|
15
|
-
{
|
|
16
|
-
id: 'cli.contract',
|
|
17
|
-
service: 'cli',
|
|
18
|
-
target: 'api-contract',
|
|
19
|
-
mode: 'deep',
|
|
20
|
-
kind: 'cli-contract',
|
|
21
|
-
path: '/health',
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
id: 'daiso.products',
|
|
25
|
-
service: 'daiso',
|
|
26
|
-
target: 'products',
|
|
27
|
-
mode: 'quick',
|
|
28
|
-
path: '/api/daiso/products?q=%ED%85%8C%EC%9D%B4%ED%94%84&pageSize=1',
|
|
29
|
-
collectionKey: 'products',
|
|
30
|
-
requiredFields: ['id', 'name', 'productName', 'itemName', 'goodsName'],
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
id: 'cu.stores',
|
|
34
|
-
service: 'cu',
|
|
35
|
-
target: 'stores',
|
|
36
|
-
mode: 'quick',
|
|
37
|
-
path: '/api/cu/stores?keyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
38
|
-
collectionKey: 'stores',
|
|
39
|
-
requiredFields: ['storeCode', 'storeName', 'name'],
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
id: 'emart24.products',
|
|
43
|
-
service: 'emart24',
|
|
44
|
-
target: 'products',
|
|
45
|
-
mode: 'quick',
|
|
46
|
-
path: '/api/emart24/products?keyword=%EC%BB%A4%ED%94%BC&pageSize=1',
|
|
47
|
-
collectionKey: 'products',
|
|
48
|
-
requiredFields: ['pluCd', 'goodsName', 'itemName', 'name'],
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
id: 'gs25.products',
|
|
52
|
-
service: 'gs25',
|
|
53
|
-
target: 'products',
|
|
54
|
-
mode: 'quick',
|
|
55
|
-
path: '/api/gs25/products?keyword=%EC%BD%9C%EB%9D%BC&limit=1',
|
|
56
|
-
collectionKey: 'products',
|
|
57
|
-
requiredFields: ['itemCode', 'itemName', 'name'],
|
|
58
|
-
degradedFailurePatterns: GS25_CLOUDFRONT_403_PATTERNS,
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
id: 'gs25.stores',
|
|
62
|
-
service: 'gs25',
|
|
63
|
-
target: 'stores',
|
|
64
|
-
mode: 'quick',
|
|
65
|
-
path: '/api/gs25/stores?keyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
66
|
-
collectionKey: 'stores',
|
|
67
|
-
requiredFields: ['storeCode', 'storeName', 'name'],
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
id: 'seveneleven.products',
|
|
71
|
-
service: 'seveneleven',
|
|
72
|
-
target: 'products',
|
|
73
|
-
mode: 'quick',
|
|
74
|
-
path: '/api/seveneleven/products?query=%EC%BB%A4%ED%94%BC&size=1',
|
|
75
|
-
collectionKey: 'products',
|
|
76
|
-
requiredFields: ['itemCode', 'itemName', 'productNo', 'name'],
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
id: 'lottemart.products',
|
|
80
|
-
service: 'lottemart',
|
|
81
|
-
target: 'products',
|
|
82
|
-
mode: 'quick',
|
|
83
|
-
path: '/api/lottemart/products?keyword=%EC%BD%9C%EB%9D%BC&storeCode=2301&area=%EC%84%9C%EC%9A%B8&pageLimit=1&source=zetta',
|
|
84
|
-
collectionKey: 'products',
|
|
85
|
-
requiredFields: ['productCode', 'name', 'productName'],
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
id: 'oliveyoung.products',
|
|
89
|
-
service: 'oliveyoung',
|
|
90
|
-
target: 'products',
|
|
91
|
-
mode: 'quick',
|
|
92
|
-
path: '/api/oliveyoung/products?keyword=%EB%A6%BD%EB%B0%A4&size=1',
|
|
93
|
-
collectionKey: 'products',
|
|
94
|
-
requiredFields: ['goodsNumber', 'goodsName', 'productNo', 'productName', 'name'],
|
|
95
|
-
timeoutMs: 5000,
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
id: 'megabox.theaters',
|
|
99
|
-
service: 'megabox',
|
|
100
|
-
target: 'theaters',
|
|
101
|
-
mode: 'quick',
|
|
102
|
-
path: '/api/megabox/theaters?keyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
103
|
-
collectionKey: 'theaters',
|
|
104
|
-
requiredFields: ['theaterCode', 'theaterName', 'name'],
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
id: 'lottecinema.theaters',
|
|
108
|
-
service: 'lottecinema',
|
|
109
|
-
target: 'theaters',
|
|
110
|
-
mode: 'quick',
|
|
111
|
-
path: '/api/lottecinema/theaters?keyword=%EC%9E%A0%EC%8B%A4&limit=1',
|
|
112
|
-
collectionKey: 'theaters',
|
|
113
|
-
requiredFields: ['theaterCode', 'theaterName', 'name'],
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
id: 'cgv.theaters',
|
|
117
|
-
service: 'cgv',
|
|
118
|
-
target: 'theaters',
|
|
119
|
-
mode: 'quick',
|
|
120
|
-
path: '/api/cgv/theaters?keyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
121
|
-
collectionKey: 'theaters',
|
|
122
|
-
requiredFields: ['theaterCode', 'theaterName', 'name'],
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
id: 'cu.inventory',
|
|
126
|
-
service: 'cu',
|
|
127
|
-
target: 'inventory',
|
|
128
|
-
mode: 'deep',
|
|
129
|
-
path: '/api/cu/inventory?keyword=%EC%BB%A4%ED%94%BC&size=1&storeLimit=0&storeCheck=false',
|
|
130
|
-
collectionKey: 'inventoryItems',
|
|
131
|
-
requiredFields: ['itemCode', 'itemName', 'name'],
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
id: 'emart24.inventory',
|
|
135
|
-
service: 'emart24',
|
|
136
|
-
target: 'inventory',
|
|
137
|
-
mode: 'deep',
|
|
138
|
-
path: '/api/emart24/inventory?keyword=%EC%BB%A4%ED%94%BC&storeKeyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
139
|
-
collectionKey: 'inventoryItems',
|
|
140
|
-
requiredFields: ['pluCd', 'goodsName', 'itemName', 'name'],
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
id: 'gs25.inventory',
|
|
144
|
-
service: 'gs25',
|
|
145
|
-
target: 'inventory',
|
|
146
|
-
mode: 'deep',
|
|
147
|
-
path: '/api/gs25/inventory?keyword=%EC%BD%9C%EB%9D%BC&storeKeyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
148
|
-
collectionKey: 'inventoryItems',
|
|
149
|
-
requiredFields: ['itemCode', 'itemName', 'name'],
|
|
150
|
-
degradedFailurePatterns: [
|
|
151
|
-
'401 Unauthorized',
|
|
152
|
-
'인증키가 제공되지 않음',
|
|
153
|
-
...GS25_CLOUDFRONT_403_PATTERNS,
|
|
154
|
-
],
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
id: 'seveneleven.inventory',
|
|
158
|
-
service: 'seveneleven',
|
|
159
|
-
target: 'inventory',
|
|
160
|
-
mode: 'deep',
|
|
161
|
-
path: '/api/seveneleven/inventory?keyword=%EC%BB%A4%ED%94%BC&storeKeyword=%EA%B0%95%EB%82%A8&size=1',
|
|
162
|
-
collectionKey: 'inventoryItems',
|
|
163
|
-
requiredFields: ['itemCode', 'itemName', 'productNo', 'name'],
|
|
164
|
-
},
|
|
165
|
-
{
|
|
166
|
-
id: 'oliveyoung.inventory',
|
|
167
|
-
service: 'oliveyoung',
|
|
168
|
-
target: 'inventory',
|
|
169
|
-
mode: 'deep',
|
|
170
|
-
path: '/api/oliveyoung/inventory?keyword=%EC%84%A0%ED%81%AC%EB%A6%BC&storeKeyword=%EB%AA%85%EB%8F%99&size=1&storeLimit=1&stockCheckLimit=0',
|
|
171
|
-
collectionKey: 'inventoryProducts',
|
|
172
|
-
requiredFields: ['goodsNumber', 'goodsName', 'productNo', 'productName', 'name'],
|
|
173
|
-
timeoutMs: 5000,
|
|
174
|
-
},
|
|
175
|
-
];
|
|
176
11
|
const healthCheckCache = new Map();
|
|
177
12
|
function clampTimeout(timeoutMs) {
|
|
178
13
|
if (!Number.isFinite(timeoutMs) || timeoutMs < 1) {
|
|
@@ -220,109 +55,6 @@ async function mapWithConcurrency(items, concurrency, mapper) {
|
|
|
220
55
|
}));
|
|
221
56
|
return results;
|
|
222
57
|
}
|
|
223
|
-
function toCount(data) {
|
|
224
|
-
if (!data || typeof data !== 'object') {
|
|
225
|
-
return null;
|
|
226
|
-
}
|
|
227
|
-
const record = data;
|
|
228
|
-
if (typeof record.count === 'number') {
|
|
229
|
-
return record.count;
|
|
230
|
-
}
|
|
231
|
-
for (const key of ['products', 'stores', 'theaters', 'movies', 'showtimes']) {
|
|
232
|
-
const value = record[key];
|
|
233
|
-
if (Array.isArray(value)) {
|
|
234
|
-
return value.length;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
if (record.inventory && typeof record.inventory === 'object') {
|
|
238
|
-
const inventory = record.inventory;
|
|
239
|
-
for (const key of ['products', 'items']) {
|
|
240
|
-
const value = inventory[key];
|
|
241
|
-
if (Array.isArray(value)) {
|
|
242
|
-
return value.length;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
return null;
|
|
247
|
-
}
|
|
248
|
-
function toFirstName(data) {
|
|
249
|
-
if (!data || typeof data !== 'object') {
|
|
250
|
-
return undefined;
|
|
251
|
-
}
|
|
252
|
-
const record = data;
|
|
253
|
-
for (const key of ['products', 'stores', 'theaters', 'movies', 'showtimes']) {
|
|
254
|
-
const value = record[key];
|
|
255
|
-
if (!Array.isArray(value) || !value[0] || typeof value[0] !== 'object') {
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
const item = value[0];
|
|
259
|
-
for (const nameKey of ['productName', 'itemName', 'goodsName', 'name', 'storeName', 'theaterName', 'movieName']) {
|
|
260
|
-
if (typeof item[nameKey] === 'string' && item[nameKey].trim().length > 0) {
|
|
261
|
-
return item[nameKey].trim();
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
if (record.inventory && typeof record.inventory === 'object') {
|
|
266
|
-
const inventory = record.inventory;
|
|
267
|
-
for (const key of ['products', 'items']) {
|
|
268
|
-
const value = inventory[key];
|
|
269
|
-
if (!Array.isArray(value) || !value[0] || typeof value[0] !== 'object') {
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
const item = value[0];
|
|
273
|
-
for (const nameKey of ['productName', 'itemName', 'goodsName', 'name']) {
|
|
274
|
-
if (typeof item[nameKey] === 'string' && item[nameKey].trim().length > 0) {
|
|
275
|
-
return item[nameKey].trim();
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
return undefined;
|
|
281
|
-
}
|
|
282
|
-
function getCollectionItems(data, collectionKey) {
|
|
283
|
-
if (!data || typeof data !== 'object') {
|
|
284
|
-
return [];
|
|
285
|
-
}
|
|
286
|
-
const record = data;
|
|
287
|
-
if (collectionKey === 'inventoryProducts' || collectionKey === 'inventoryItems') {
|
|
288
|
-
const inventory = record.inventory;
|
|
289
|
-
if (!inventory || typeof inventory !== 'object') {
|
|
290
|
-
return [];
|
|
291
|
-
}
|
|
292
|
-
const key = collectionKey === 'inventoryProducts' ? 'products' : 'items';
|
|
293
|
-
const value = inventory[key];
|
|
294
|
-
return Array.isArray(value) ? value : [];
|
|
295
|
-
}
|
|
296
|
-
if (collectionKey && Array.isArray(record[collectionKey])) {
|
|
297
|
-
return record[collectionKey];
|
|
298
|
-
}
|
|
299
|
-
for (const key of ['products', 'stores', 'theaters', 'movies', 'showtimes']) {
|
|
300
|
-
/* c8 ignore next -- 현재 정의된 체크는 collectionKey를 명시한다. */
|
|
301
|
-
if (Array.isArray(record[key])) {
|
|
302
|
-
return record[key];
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
return [];
|
|
306
|
-
}
|
|
307
|
-
function hasRequiredRepresentativeFields(data, collectionKey, requiredFields = []) {
|
|
308
|
-
/* c8 ignore next -- 현재 정의된 API 체크는 requiredFields를 명시한다. */
|
|
309
|
-
if (requiredFields.length === 0) {
|
|
310
|
-
return true;
|
|
311
|
-
}
|
|
312
|
-
const items = getCollectionItems(data, collectionKey);
|
|
313
|
-
if (items.length === 0) {
|
|
314
|
-
return true;
|
|
315
|
-
}
|
|
316
|
-
const first = items[0];
|
|
317
|
-
if (!first || typeof first !== 'object' || Array.isArray(first)) {
|
|
318
|
-
return false;
|
|
319
|
-
}
|
|
320
|
-
const record = first;
|
|
321
|
-
return requiredFields.some((field) => {
|
|
322
|
-
const value = record[field];
|
|
323
|
-
return typeof value === 'string' ? value.trim().length > 0 : value !== undefined && value !== null;
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
58
|
function aggregateStatus(checks) {
|
|
327
59
|
if (checks.length === 0) {
|
|
328
60
|
return 'skipped';
|
|
@@ -339,7 +71,16 @@ function shouldDegradeFailedResponse(check, message) {
|
|
|
339
71
|
return check.degradedFailurePatterns?.some((pattern) => message.includes(pattern)) ?? false;
|
|
340
72
|
}
|
|
341
73
|
function shouldDegradeCliContractPath(path, message) {
|
|
342
|
-
|
|
74
|
+
if (path.startsWith('/api/gs25/')) {
|
|
75
|
+
return GS25_CLOUDFRONT_403_PATTERNS.some((pattern) => message.includes(pattern));
|
|
76
|
+
}
|
|
77
|
+
if (path.startsWith('/api/emart24/')) {
|
|
78
|
+
return EMART24_UPSTREAM_403_PATTERNS.some((pattern) => message.includes(pattern));
|
|
79
|
+
}
|
|
80
|
+
if (path.startsWith('/api/seveneleven/')) {
|
|
81
|
+
return SEVENELEVEN_UPSTREAM_403_PATTERNS.some((pattern) => message.includes(pattern));
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
343
84
|
}
|
|
344
85
|
function resolveCheckTimeoutMs(check, timeoutMs) {
|
|
345
86
|
if (check.timeoutMs === undefined) {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { getErrorMessage } from '../core/errors.js';
|
|
2
|
+
import { fetchOpinetAveragePrices, fetchOpinetLowestStations, fetchOpinetStationDetail, fetchOpinetStationsAround, } from '../services/opinet/client.js';
|
|
3
|
+
import { errorResponse, successResponse } from './response.js';
|
|
4
|
+
function parseNumber(value) {
|
|
5
|
+
if (!value) {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
const parsed = Number(value);
|
|
9
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
10
|
+
}
|
|
11
|
+
function parseInteger(value) {
|
|
12
|
+
const parsed = parseNumber(value);
|
|
13
|
+
return parsed === undefined ? undefined : Math.trunc(parsed);
|
|
14
|
+
}
|
|
15
|
+
export async function handleOpinetAveragePrices(c) {
|
|
16
|
+
try {
|
|
17
|
+
const result = await fetchOpinetAveragePrices({
|
|
18
|
+
apiKey: c.env?.OPINET_API_KEY,
|
|
19
|
+
timeoutMs: parseInteger(c.req.query('timeoutMs')),
|
|
20
|
+
});
|
|
21
|
+
return successResponse(c, result, { total: result.count, page: 1, pageSize: result.count });
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
return errorResponse(c, 'OPINET_AVERAGE_FAILED', getErrorMessage(error), 500);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function handleOpinetLowestStations(c) {
|
|
28
|
+
try {
|
|
29
|
+
const result = await fetchOpinetLowestStations({
|
|
30
|
+
fuelCode: c.req.query('fuelCode') || c.req.query('prodcd'),
|
|
31
|
+
areaCode: c.req.query('areaCode') || c.req.query('area'),
|
|
32
|
+
count: parseInteger(c.req.query('count') || c.req.query('cnt')),
|
|
33
|
+
}, {
|
|
34
|
+
apiKey: c.env?.OPINET_API_KEY,
|
|
35
|
+
timeoutMs: parseInteger(c.req.query('timeoutMs')),
|
|
36
|
+
});
|
|
37
|
+
return successResponse(c, result, { total: result.count, page: 1, pageSize: result.count });
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
return errorResponse(c, 'OPINET_LOWEST_FAILED', getErrorMessage(error), 500);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function handleOpinetStationsAround(c) {
|
|
44
|
+
const x = parseNumber(c.req.query('x'));
|
|
45
|
+
const y = parseNumber(c.req.query('y'));
|
|
46
|
+
const latitude = parseNumber(c.req.query('latitude') || c.req.query('lat'));
|
|
47
|
+
const longitude = parseNumber(c.req.query('longitude') || c.req.query('lng'));
|
|
48
|
+
const location = c.req.query('location') || c.req.query('keyword') || '';
|
|
49
|
+
const hasKatec = x !== undefined && y !== undefined;
|
|
50
|
+
const hasCoordinates = latitude !== undefined && longitude !== undefined;
|
|
51
|
+
const hasLocation = location.trim().length > 0;
|
|
52
|
+
if (!hasKatec && !hasCoordinates && !hasLocation) {
|
|
53
|
+
return errorResponse(c, 'MISSING_LOCATION', 'KATEC x/y, 위도/경도(lat/lng), 또는 location 중 하나를 입력해주세요.');
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const result = await fetchOpinetStationsAround({
|
|
57
|
+
x,
|
|
58
|
+
y,
|
|
59
|
+
latitude,
|
|
60
|
+
longitude,
|
|
61
|
+
location,
|
|
62
|
+
radiusMeters: parseInteger(c.req.query('radiusMeters') || c.req.query('radius')),
|
|
63
|
+
fuelCode: c.req.query('fuelCode') || c.req.query('prodcd'),
|
|
64
|
+
sort: c.req.query('sort'),
|
|
65
|
+
}, {
|
|
66
|
+
apiKey: c.env?.OPINET_API_KEY,
|
|
67
|
+
googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
|
|
68
|
+
timeoutMs: parseInteger(c.req.query('timeoutMs')),
|
|
69
|
+
});
|
|
70
|
+
return successResponse(c, result, { total: result.count, page: 1, pageSize: result.count });
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
return errorResponse(c, 'OPINET_AROUND_FAILED', getErrorMessage(error), 500);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export async function handleOpinetStationDetail(c) {
|
|
77
|
+
const stationId = c.req.query('stationId') || c.req.query('id') || '';
|
|
78
|
+
if (!stationId.trim()) {
|
|
79
|
+
return errorResponse(c, 'MISSING_STATION_ID', 'stationId 또는 id를 입력해주세요.');
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const result = await fetchOpinetStationDetail(stationId, {
|
|
83
|
+
apiKey: c.env?.OPINET_API_KEY,
|
|
84
|
+
timeoutMs: parseInteger(c.req.query('timeoutMs')),
|
|
85
|
+
});
|
|
86
|
+
return successResponse(c, result, { total: result.station ? 1 : 0, page: 1, pageSize: result.station ? 1 : 0 });
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
return errorResponse(c, 'OPINET_DETAIL_FAILED', getErrorMessage(error), 500);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -14,6 +14,16 @@ function readHealthCheckToken(headers) {
|
|
|
14
14
|
}
|
|
15
15
|
return (headers.get('x-health-check-key') || '').trim();
|
|
16
16
|
}
|
|
17
|
+
function isBetterStackHealthProbe(userAgent) {
|
|
18
|
+
const normalized = (userAgent || '').toLowerCase();
|
|
19
|
+
return normalized.includes('better stack better uptime bot') || normalized === 'nginx-ssl early hints';
|
|
20
|
+
}
|
|
21
|
+
function isEarlyHintsProbe(userAgent) {
|
|
22
|
+
return (userAgent || '').trim().toLowerCase() === 'nginx-ssl early hints';
|
|
23
|
+
}
|
|
24
|
+
function canForceFresh(headers) {
|
|
25
|
+
return parseBoolean(headers.get('x-health-check-force-fresh') || undefined);
|
|
26
|
+
}
|
|
17
27
|
function parseMode(value) {
|
|
18
28
|
if (!value || value === 'quick') {
|
|
19
29
|
return 'quick';
|
|
@@ -25,6 +35,10 @@ function parseMode(value) {
|
|
|
25
35
|
}
|
|
26
36
|
export function registerHealthRoutes(app) {
|
|
27
37
|
app.get('/api/health/checks', async (c) => {
|
|
38
|
+
const userAgent = c.req.header('User-Agent');
|
|
39
|
+
if (isEarlyHintsProbe(userAgent)) {
|
|
40
|
+
return c.body(null, 204);
|
|
41
|
+
}
|
|
28
42
|
const secret = c.env?.HEALTH_CHECK_SECRET?.trim();
|
|
29
43
|
if (!secret) {
|
|
30
44
|
return errorResponse(c, 'HEALTH_CHECK_SECRET_NOT_CONFIGURED', 'HEALTH_CHECK_SECRET이 설정되어 있지 않습니다.', 503);
|
|
@@ -52,7 +66,9 @@ export function registerHealthRoutes(app) {
|
|
|
52
66
|
timeoutMs,
|
|
53
67
|
slowThresholdMs,
|
|
54
68
|
includeSamples: parseBoolean(c.req.query('includeSamples')),
|
|
55
|
-
fresh: parseBoolean(c.req.query('fresh'))
|
|
69
|
+
fresh: parseBoolean(c.req.query('fresh')) &&
|
|
70
|
+
canForceFresh(c.req.raw.headers) &&
|
|
71
|
+
!isBetterStackHealthProbe(userAgent),
|
|
56
72
|
cacheBust,
|
|
57
73
|
fetchImpl,
|
|
58
74
|
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { withEdgeCache } from '../../utils/cache.js';
|
|
2
|
+
import { handleOpinetAveragePrices, handleOpinetLowestStations, handleOpinetStationDetail, handleOpinetStationsAround, } from '../opinetHandlers.js';
|
|
3
|
+
export function registerOpinetRoutes(app) {
|
|
4
|
+
app.get('/api/opinet/average', async (c) => withEdgeCache(c.req.url, {
|
|
5
|
+
ttlSeconds: 60 * 60,
|
|
6
|
+
staleWhileRevalidateSeconds: 60 * 30,
|
|
7
|
+
keyPrefix: 'opinet-average-v1',
|
|
8
|
+
}, () => handleOpinetAveragePrices(c)));
|
|
9
|
+
app.get('/api/opinet/lowest', async (c) => withEdgeCache(c.req.url, {
|
|
10
|
+
ttlSeconds: 60 * 30,
|
|
11
|
+
staleWhileRevalidateSeconds: 60 * 10,
|
|
12
|
+
keyPrefix: 'opinet-lowest-v1',
|
|
13
|
+
}, () => handleOpinetLowestStations(c)));
|
|
14
|
+
app.get('/api/opinet/stations/around', async (c) => withEdgeCache(c.req.url, {
|
|
15
|
+
ttlSeconds: 60 * 20,
|
|
16
|
+
staleWhileRevalidateSeconds: 60 * 5,
|
|
17
|
+
keyPrefix: 'opinet-around-v1',
|
|
18
|
+
}, () => handleOpinetStationsAround(c)));
|
|
19
|
+
app.get('/api/opinet/station', async (c) => withEdgeCache(c.req.url, {
|
|
20
|
+
ttlSeconds: 60 * 60,
|
|
21
|
+
staleWhileRevalidateSeconds: 60 * 10,
|
|
22
|
+
keyPrefix: 'opinet-station-v1',
|
|
23
|
+
}, () => handleOpinetStationDetail(c)));
|
|
24
|
+
}
|
package/dist/bin.js
CHANGED
|
File without changes
|
|
@@ -26,6 +26,18 @@ export const COMMAND_OPTION_ALLOWLISTS = {
|
|
|
26
26
|
'searchSort',
|
|
27
27
|
'storeLimit',
|
|
28
28
|
],
|
|
29
|
+
'cgv-theaters': ['help', 'json', 'keyword', 'regionCode', 'playDate', 'lat', 'lng', 'limit'],
|
|
30
|
+
'cgv-movies': ['help', 'json', 'keyword', 'playDate', 'theaterCode', 'movieCode', 'lat', 'lng'],
|
|
31
|
+
'cgv-timetable': [
|
|
32
|
+
'help',
|
|
33
|
+
'json',
|
|
34
|
+
'keyword',
|
|
35
|
+
'playDate',
|
|
36
|
+
'theaterCode',
|
|
37
|
+
'movieCode',
|
|
38
|
+
'lat',
|
|
39
|
+
'lng',
|
|
40
|
+
],
|
|
29
41
|
'lottecinema-theaters': ['help', 'json', 'keyword', 'lat', 'lng', 'playDate', 'limit'],
|
|
30
42
|
'lottecinema-movies': [
|
|
31
43
|
'help',
|
|
@@ -84,8 +96,17 @@ export const COMMAND_OPTION_ALLOWLISTS = {
|
|
|
84
96
|
'gs25-stores': ['help', 'json', 'keyword', 'lat', 'lng', 'serviceCode', 'limit'],
|
|
85
97
|
'gs25-products': ['help', 'json', 'serviceCode', 'limit'],
|
|
86
98
|
'gs25-inventory': ['help', 'json', 'storeKeyword', 'lat', 'lng', 'serviceCode', 'storeLimit'],
|
|
87
|
-
'seveneleven-products': ['help', 'json', 'page', 'size', 'sort'],
|
|
88
|
-
'seveneleven-stores': ['help', 'json', 'limit'],
|
|
99
|
+
'seveneleven-products': ['help', 'json', 'query', 'page', 'size', 'sort'],
|
|
100
|
+
'seveneleven-stores': ['help', 'json', 'keyword', 'limit'],
|
|
101
|
+
'seveneleven-inventory': [
|
|
102
|
+
'help',
|
|
103
|
+
'json',
|
|
104
|
+
'keyword',
|
|
105
|
+
'storeKeyword',
|
|
106
|
+
'storeLimit',
|
|
107
|
+
'page',
|
|
108
|
+
'size',
|
|
109
|
+
],
|
|
89
110
|
'seveneleven-popwords': ['help', 'json', 'label'],
|
|
90
111
|
'seveneleven-catalog': ['help', 'json', 'includeIssues', 'includeExhibition', 'limit'],
|
|
91
112
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API 기반 CLI 명령 공통 실행기
|
|
3
|
+
*/
|
|
4
|
+
import { printCommandHelp } from '../../cliHelp.js';
|
|
5
|
+
import { applyOptionsToQuery, parseCliArgs, toQueryOptions, toUrl } from '../args.js';
|
|
6
|
+
import { validateCommandOptions } from '../commandOptions.js';
|
|
7
|
+
import { requestAndPrintResponse } from '../http.js';
|
|
8
|
+
export async function runApiCommand(params) {
|
|
9
|
+
const parsed = parseCliArgs(params.options);
|
|
10
|
+
if (parsed.options.help === 'true') {
|
|
11
|
+
return printCommandHelp(params.command, params.deps.writeOut, params.deps.writeErr);
|
|
12
|
+
}
|
|
13
|
+
const errorMessage = params.configure?.(parsed) ?? null;
|
|
14
|
+
if (errorMessage) {
|
|
15
|
+
params.deps.writeErr(errorMessage);
|
|
16
|
+
return 1;
|
|
17
|
+
}
|
|
18
|
+
if (validateCommandOptions(params.command, parsed.options, params.deps.writeErr)) {
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
const targetUrl = toUrl(params.path);
|
|
22
|
+
applyOptionsToQuery(targetUrl, toQueryOptions(parsed.options));
|
|
23
|
+
return await requestAndPrintResponse(params.deps.fetchImpl, params.deps.writeOut, params.deps.writeErr, targetUrl, params.label ?? params.command, parsed.options.json === 'true');
|
|
24
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 영화관 CLI 명령 핸들러
|
|
3
|
+
*/
|
|
4
|
+
import { runApiCommand } from './apiCommand.js';
|
|
5
|
+
function applyKeywordPositional(parsed) {
|
|
6
|
+
const keyword = parsed.positionals[0];
|
|
7
|
+
if (keyword) {
|
|
8
|
+
parsed.options.keyword = keyword;
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
export async function handleLottecinemaTheaters(options, deps) {
|
|
13
|
+
return await runApiCommand({
|
|
14
|
+
command: 'lottecinema-theaters',
|
|
15
|
+
options,
|
|
16
|
+
deps,
|
|
17
|
+
path: '/api/lottecinema/theaters',
|
|
18
|
+
configure: applyKeywordPositional,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export async function handleLottecinemaMovies(options, deps) {
|
|
22
|
+
return await runApiCommand({
|
|
23
|
+
command: 'lottecinema-movies',
|
|
24
|
+
options,
|
|
25
|
+
deps,
|
|
26
|
+
path: '/api/lottecinema/movies',
|
|
27
|
+
configure: applyKeywordPositional,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export async function handleLottecinemaSeats(options, deps) {
|
|
31
|
+
return await runApiCommand({
|
|
32
|
+
command: 'lottecinema-seats',
|
|
33
|
+
options,
|
|
34
|
+
deps,
|
|
35
|
+
path: '/api/lottecinema/seats',
|
|
36
|
+
configure: applyKeywordPositional,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export async function handleCgvTheaters(options, deps) {
|
|
40
|
+
return await runApiCommand({
|
|
41
|
+
command: 'cgv-theaters',
|
|
42
|
+
options,
|
|
43
|
+
deps,
|
|
44
|
+
path: '/api/cgv/theaters',
|
|
45
|
+
configure: applyKeywordPositional,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export async function handleCgvMovies(options, deps) {
|
|
49
|
+
return await runApiCommand({
|
|
50
|
+
command: 'cgv-movies',
|
|
51
|
+
options,
|
|
52
|
+
deps,
|
|
53
|
+
path: '/api/cgv/movies',
|
|
54
|
+
configure: applyKeywordPositional,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export async function handleCgvTimetable(options, deps) {
|
|
58
|
+
return await runApiCommand({
|
|
59
|
+
command: 'cgv-timetable',
|
|
60
|
+
options,
|
|
61
|
+
deps,
|
|
62
|
+
path: '/api/cgv/timetable',
|
|
63
|
+
configure: applyKeywordPositional,
|
|
64
|
+
});
|
|
65
|
+
}
|