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,504 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 개별 서비스 헬스 체크 실행기
|
|
3
|
+
*/
|
|
4
|
+
const HEALTH_CHECK_CACHE_TTL_MS = 60_000;
|
|
5
|
+
const DEFAULT_HEALTH_CHECK_TIMEOUT_MS = 7000;
|
|
6
|
+
const MAX_HEALTH_CHECK_TIMEOUT_MS = 20_000;
|
|
7
|
+
const DEFAULT_HEALTH_CHECK_CONCURRENCY = 6;
|
|
8
|
+
const DEFAULT_HEALTH_CHECK_SLOW_THRESHOLD_MS = 0;
|
|
9
|
+
const HEALTH_CHECKS = [
|
|
10
|
+
{
|
|
11
|
+
id: 'cli.contract',
|
|
12
|
+
service: 'cli',
|
|
13
|
+
target: 'api-contract',
|
|
14
|
+
mode: 'deep',
|
|
15
|
+
kind: 'cli-contract',
|
|
16
|
+
path: '/health',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'daiso.products',
|
|
20
|
+
service: 'daiso',
|
|
21
|
+
target: 'products',
|
|
22
|
+
mode: 'quick',
|
|
23
|
+
path: '/api/daiso/products?q=%ED%85%8C%EC%9D%B4%ED%94%84&pageSize=1',
|
|
24
|
+
collectionKey: 'products',
|
|
25
|
+
requiredFields: ['id', 'name', 'productName', 'itemName', 'goodsName'],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'cu.stores',
|
|
29
|
+
service: 'cu',
|
|
30
|
+
target: 'stores',
|
|
31
|
+
mode: 'quick',
|
|
32
|
+
path: '/api/cu/stores?keyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
33
|
+
collectionKey: 'stores',
|
|
34
|
+
requiredFields: ['storeCode', 'storeName', 'name'],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'emart24.products',
|
|
38
|
+
service: 'emart24',
|
|
39
|
+
target: 'products',
|
|
40
|
+
mode: 'quick',
|
|
41
|
+
path: '/api/emart24/products?keyword=%EC%BB%A4%ED%94%BC&pageSize=1',
|
|
42
|
+
collectionKey: 'products',
|
|
43
|
+
requiredFields: ['pluCd', 'goodsName', 'itemName', 'name'],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'gs25.products',
|
|
47
|
+
service: 'gs25',
|
|
48
|
+
target: 'products',
|
|
49
|
+
mode: 'quick',
|
|
50
|
+
path: '/api/gs25/products?keyword=%EC%BD%9C%EB%9D%BC&limit=1',
|
|
51
|
+
collectionKey: 'products',
|
|
52
|
+
requiredFields: ['itemCode', 'itemName', 'name'],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'gs25.stores',
|
|
56
|
+
service: 'gs25',
|
|
57
|
+
target: 'stores',
|
|
58
|
+
mode: 'quick',
|
|
59
|
+
path: '/api/gs25/stores?keyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
60
|
+
collectionKey: 'stores',
|
|
61
|
+
requiredFields: ['storeCode', 'storeName', 'name'],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'seveneleven.products',
|
|
65
|
+
service: 'seveneleven',
|
|
66
|
+
target: 'products',
|
|
67
|
+
mode: 'quick',
|
|
68
|
+
path: '/api/seveneleven/products?query=%EC%BB%A4%ED%94%BC&size=1',
|
|
69
|
+
collectionKey: 'products',
|
|
70
|
+
requiredFields: ['itemCode', 'itemName', 'productNo', 'name'],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'lottemart.products',
|
|
74
|
+
service: 'lottemart',
|
|
75
|
+
target: 'products',
|
|
76
|
+
mode: 'quick',
|
|
77
|
+
path: '/api/lottemart/products?keyword=%EC%BD%9C%EB%9D%BC&storeCode=2301&area=%EC%84%9C%EC%9A%B8&pageLimit=1&source=zetta',
|
|
78
|
+
collectionKey: 'products',
|
|
79
|
+
requiredFields: ['productCode', 'name', 'productName'],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'oliveyoung.products',
|
|
83
|
+
service: 'oliveyoung',
|
|
84
|
+
target: 'products',
|
|
85
|
+
mode: 'quick',
|
|
86
|
+
path: '/api/oliveyoung/products?keyword=%EB%A6%BD%EB%B0%A4&size=1',
|
|
87
|
+
collectionKey: 'products',
|
|
88
|
+
requiredFields: ['goodsNumber', 'goodsName', 'productNo', 'productName', 'name'],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 'megabox.theaters',
|
|
92
|
+
service: 'megabox',
|
|
93
|
+
target: 'theaters',
|
|
94
|
+
mode: 'quick',
|
|
95
|
+
path: '/api/megabox/theaters?keyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
96
|
+
collectionKey: 'theaters',
|
|
97
|
+
requiredFields: ['theaterCode', 'theaterName', 'name'],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'lottecinema.theaters',
|
|
101
|
+
service: 'lottecinema',
|
|
102
|
+
target: 'theaters',
|
|
103
|
+
mode: 'quick',
|
|
104
|
+
path: '/api/lottecinema/theaters?keyword=%EC%9E%A0%EC%8B%A4&limit=1',
|
|
105
|
+
collectionKey: 'theaters',
|
|
106
|
+
requiredFields: ['theaterCode', 'theaterName', 'name'],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: 'cgv.theaters',
|
|
110
|
+
service: 'cgv',
|
|
111
|
+
target: 'theaters',
|
|
112
|
+
mode: 'quick',
|
|
113
|
+
path: '/api/cgv/theaters?keyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
114
|
+
collectionKey: 'theaters',
|
|
115
|
+
requiredFields: ['theaterCode', 'theaterName', 'name'],
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: 'cu.inventory',
|
|
119
|
+
service: 'cu',
|
|
120
|
+
target: 'inventory',
|
|
121
|
+
mode: 'deep',
|
|
122
|
+
path: '/api/cu/inventory?keyword=%EC%BB%A4%ED%94%BC&size=1&storeLimit=0&storeCheck=false',
|
|
123
|
+
collectionKey: 'inventoryItems',
|
|
124
|
+
requiredFields: ['itemCode', 'itemName', 'name'],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: 'emart24.inventory',
|
|
128
|
+
service: 'emart24',
|
|
129
|
+
target: 'inventory',
|
|
130
|
+
mode: 'deep',
|
|
131
|
+
path: '/api/emart24/inventory?keyword=%EC%BB%A4%ED%94%BC&storeKeyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
132
|
+
collectionKey: 'inventoryItems',
|
|
133
|
+
requiredFields: ['pluCd', 'goodsName', 'itemName', 'name'],
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: 'gs25.inventory',
|
|
137
|
+
service: 'gs25',
|
|
138
|
+
target: 'inventory',
|
|
139
|
+
mode: 'deep',
|
|
140
|
+
path: '/api/gs25/inventory?keyword=%EC%BD%9C%EB%9D%BC&storeKeyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
141
|
+
collectionKey: 'inventoryItems',
|
|
142
|
+
requiredFields: ['itemCode', 'itemName', 'name'],
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: 'seveneleven.inventory',
|
|
146
|
+
service: 'seveneleven',
|
|
147
|
+
target: 'inventory',
|
|
148
|
+
mode: 'deep',
|
|
149
|
+
path: '/api/seveneleven/inventory?keyword=%EC%BB%A4%ED%94%BC&storeKeyword=%EA%B0%95%EB%82%A8&size=1',
|
|
150
|
+
collectionKey: 'inventoryItems',
|
|
151
|
+
requiredFields: ['itemCode', 'itemName', 'productNo', 'name'],
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: 'oliveyoung.inventory',
|
|
155
|
+
service: 'oliveyoung',
|
|
156
|
+
target: 'inventory',
|
|
157
|
+
mode: 'deep',
|
|
158
|
+
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',
|
|
159
|
+
collectionKey: 'inventoryProducts',
|
|
160
|
+
requiredFields: ['goodsNumber', 'goodsName', 'productNo', 'productName', 'name'],
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
const healthCheckCache = new Map();
|
|
164
|
+
function clampTimeout(timeoutMs) {
|
|
165
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 1) {
|
|
166
|
+
return DEFAULT_HEALTH_CHECK_TIMEOUT_MS;
|
|
167
|
+
}
|
|
168
|
+
return Math.min(Math.trunc(timeoutMs), MAX_HEALTH_CHECK_TIMEOUT_MS);
|
|
169
|
+
}
|
|
170
|
+
function createCacheKey(params) {
|
|
171
|
+
return [
|
|
172
|
+
params.baseUrl,
|
|
173
|
+
params.service || '',
|
|
174
|
+
params.check || '',
|
|
175
|
+
params.mode,
|
|
176
|
+
params.timeoutMs,
|
|
177
|
+
params.slowThresholdMs || 0,
|
|
178
|
+
params.cacheBust ? 'cache-bust' : 'cache',
|
|
179
|
+
params.includeSamples ? 'samples' : 'no-samples',
|
|
180
|
+
].join('|');
|
|
181
|
+
}
|
|
182
|
+
function selectChecks(params) {
|
|
183
|
+
const mode = params.mode || 'quick';
|
|
184
|
+
return HEALTH_CHECKS.filter((check) => {
|
|
185
|
+
if (mode !== 'full' && check.mode !== mode) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
if (params.service && check.service !== params.service) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
if (params.check && check.id !== params.check) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
return true;
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
async function mapWithConcurrency(items, concurrency, mapper) {
|
|
198
|
+
const results = new Array(items.length);
|
|
199
|
+
let nextIndex = 0;
|
|
200
|
+
const workerCount = Math.min(Math.max(1, concurrency), items.length);
|
|
201
|
+
await Promise.all(Array.from({ length: workerCount }, async () => {
|
|
202
|
+
while (nextIndex < items.length) {
|
|
203
|
+
const currentIndex = nextIndex;
|
|
204
|
+
nextIndex += 1;
|
|
205
|
+
results[currentIndex] = await mapper(items[currentIndex]);
|
|
206
|
+
}
|
|
207
|
+
}));
|
|
208
|
+
return results;
|
|
209
|
+
}
|
|
210
|
+
function toCount(data) {
|
|
211
|
+
if (!data || typeof data !== 'object') {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
const record = data;
|
|
215
|
+
if (typeof record.count === 'number') {
|
|
216
|
+
return record.count;
|
|
217
|
+
}
|
|
218
|
+
for (const key of ['products', 'stores', 'theaters', 'movies', 'showtimes']) {
|
|
219
|
+
const value = record[key];
|
|
220
|
+
if (Array.isArray(value)) {
|
|
221
|
+
return value.length;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (record.inventory && typeof record.inventory === 'object') {
|
|
225
|
+
const inventory = record.inventory;
|
|
226
|
+
for (const key of ['products', 'items']) {
|
|
227
|
+
const value = inventory[key];
|
|
228
|
+
if (Array.isArray(value)) {
|
|
229
|
+
return value.length;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
function toFirstName(data) {
|
|
236
|
+
if (!data || typeof data !== 'object') {
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
const record = data;
|
|
240
|
+
for (const key of ['products', 'stores', 'theaters', 'movies', 'showtimes']) {
|
|
241
|
+
const value = record[key];
|
|
242
|
+
if (!Array.isArray(value) || !value[0] || typeof value[0] !== 'object') {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const item = value[0];
|
|
246
|
+
for (const nameKey of ['productName', 'itemName', 'goodsName', 'name', 'storeName', 'theaterName', 'movieName']) {
|
|
247
|
+
if (typeof item[nameKey] === 'string' && item[nameKey].trim().length > 0) {
|
|
248
|
+
return item[nameKey].trim();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (record.inventory && typeof record.inventory === 'object') {
|
|
253
|
+
const inventory = record.inventory;
|
|
254
|
+
for (const key of ['products', 'items']) {
|
|
255
|
+
const value = inventory[key];
|
|
256
|
+
if (!Array.isArray(value) || !value[0] || typeof value[0] !== 'object') {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const item = value[0];
|
|
260
|
+
for (const nameKey of ['productName', 'itemName', 'goodsName', 'name']) {
|
|
261
|
+
if (typeof item[nameKey] === 'string' && item[nameKey].trim().length > 0) {
|
|
262
|
+
return item[nameKey].trim();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
function getCollectionItems(data, collectionKey) {
|
|
270
|
+
if (!data || typeof data !== 'object') {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
const record = data;
|
|
274
|
+
if (collectionKey === 'inventoryProducts' || collectionKey === 'inventoryItems') {
|
|
275
|
+
const inventory = record.inventory;
|
|
276
|
+
if (!inventory || typeof inventory !== 'object') {
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
const key = collectionKey === 'inventoryProducts' ? 'products' : 'items';
|
|
280
|
+
const value = inventory[key];
|
|
281
|
+
return Array.isArray(value) ? value : [];
|
|
282
|
+
}
|
|
283
|
+
if (collectionKey && Array.isArray(record[collectionKey])) {
|
|
284
|
+
return record[collectionKey];
|
|
285
|
+
}
|
|
286
|
+
for (const key of ['products', 'stores', 'theaters', 'movies', 'showtimes']) {
|
|
287
|
+
/* c8 ignore next -- 현재 정의된 체크는 collectionKey를 명시한다. */
|
|
288
|
+
if (Array.isArray(record[key])) {
|
|
289
|
+
return record[key];
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
function hasRequiredRepresentativeFields(data, collectionKey, requiredFields = []) {
|
|
295
|
+
/* c8 ignore next -- 현재 정의된 API 체크는 requiredFields를 명시한다. */
|
|
296
|
+
if (requiredFields.length === 0) {
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
const items = getCollectionItems(data, collectionKey);
|
|
300
|
+
if (items.length === 0) {
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
const first = items[0];
|
|
304
|
+
if (!first || typeof first !== 'object' || Array.isArray(first)) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
const record = first;
|
|
308
|
+
return requiredFields.some((field) => {
|
|
309
|
+
const value = record[field];
|
|
310
|
+
return typeof value === 'string' ? value.trim().length > 0 : value !== undefined && value !== null;
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
function aggregateStatus(checks) {
|
|
314
|
+
if (checks.length === 0) {
|
|
315
|
+
return 'skipped';
|
|
316
|
+
}
|
|
317
|
+
if (checks.some((check) => check.status === 'fail')) {
|
|
318
|
+
return 'fail';
|
|
319
|
+
}
|
|
320
|
+
if (checks.some((check) => check.status === 'degraded')) {
|
|
321
|
+
return 'degraded';
|
|
322
|
+
}
|
|
323
|
+
return 'ok';
|
|
324
|
+
}
|
|
325
|
+
function buildCheckUrl(baseUrl, check, timeoutMs, cacheBustValue) {
|
|
326
|
+
const url = new URL(check.path, baseUrl);
|
|
327
|
+
url.searchParams.set('timeoutMs', String(timeoutMs));
|
|
328
|
+
if (typeof cacheBustValue === 'number') {
|
|
329
|
+
url.searchParams.set('_healthCheck', String(cacheBustValue));
|
|
330
|
+
}
|
|
331
|
+
return url.toString();
|
|
332
|
+
}
|
|
333
|
+
const CLI_CONTRACT_PATHS = [
|
|
334
|
+
'/health',
|
|
335
|
+
'/api/daiso/products?q=%ED%85%8C%EC%9D%B4%ED%94%84&pageSize=1',
|
|
336
|
+
'/api/daiso/stores?keyword=%EA%B0%95%EB%82%A8&pageSize=1',
|
|
337
|
+
'/api/gs25/products?keyword=%EC%BD%9C%EB%9D%BC&limit=1',
|
|
338
|
+
'/api/gs25/stores?keyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
339
|
+
'/api/seveneleven/products?query=%EC%BB%A4%ED%94%BC&size=1',
|
|
340
|
+
'/api/emart24/products?keyword=%EC%BB%A4%ED%94%BC&pageSize=1',
|
|
341
|
+
'/api/lottemart/products?keyword=%EC%BD%9C%EB%9D%BC&storeCode=2301&area=%EC%84%9C%EC%9A%B8&pageLimit=1',
|
|
342
|
+
'/api/oliveyoung/products?keyword=%EC%84%A0%ED%81%AC%EB%A6%BC&size=1',
|
|
343
|
+
'/api/megabox/theaters?keyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
344
|
+
'/api/lottecinema/theaters?keyword=%EC%9E%A0%EC%8B%A4&limit=1',
|
|
345
|
+
'/api/cgv/theaters?keyword=%EA%B0%95%EB%82%A8&limit=1',
|
|
346
|
+
];
|
|
347
|
+
function isCliCompatibleEnvelope(path, body) {
|
|
348
|
+
if (!body || typeof body !== 'object') {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
const record = body;
|
|
352
|
+
if (path === '/health') {
|
|
353
|
+
return record.status === 'ok';
|
|
354
|
+
}
|
|
355
|
+
return record.success === true && typeof record.data === 'object' && record.data !== null;
|
|
356
|
+
}
|
|
357
|
+
async function runCliContractCheck(check, params) {
|
|
358
|
+
const startedAt = params.now();
|
|
359
|
+
const cacheBustValue = params.cacheBust ? startedAt : undefined;
|
|
360
|
+
for (const path of CLI_CONTRACT_PATHS) {
|
|
361
|
+
const syntheticCheck = { ...check, path };
|
|
362
|
+
try {
|
|
363
|
+
const response = await params.fetchImpl(buildCheckUrl(params.baseUrl, syntheticCheck, params.timeoutMs, cacheBustValue), {
|
|
364
|
+
signal: AbortSignal.timeout(params.timeoutMs),
|
|
365
|
+
});
|
|
366
|
+
const body = (await response.json().catch(() => ({})));
|
|
367
|
+
if (!response.ok || !isCliCompatibleEnvelope(path, body)) {
|
|
368
|
+
return {
|
|
369
|
+
id: check.id,
|
|
370
|
+
service: check.service,
|
|
371
|
+
target: check.target,
|
|
372
|
+
status: 'fail',
|
|
373
|
+
durationMs: params.now() - startedAt,
|
|
374
|
+
httpStatus: response.status,
|
|
375
|
+
message: body.error?.message || `${path} CLI 계약 응답이 올바르지 않습니다.`,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
return {
|
|
381
|
+
id: check.id,
|
|
382
|
+
service: check.service,
|
|
383
|
+
target: check.target,
|
|
384
|
+
status: 'fail',
|
|
385
|
+
durationMs: params.now() - startedAt,
|
|
386
|
+
message: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.',
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
id: check.id,
|
|
392
|
+
service: check.service,
|
|
393
|
+
target: check.target,
|
|
394
|
+
status: 'ok',
|
|
395
|
+
durationMs: params.now() - startedAt,
|
|
396
|
+
message: `${CLI_CONTRACT_PATHS.length} CLI contract endpoint(s) passed`,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
async function runSingleCheck(check, params) {
|
|
400
|
+
if (check.kind === 'cli-contract') {
|
|
401
|
+
return runCliContractCheck(check, params);
|
|
402
|
+
}
|
|
403
|
+
const timeoutMs = params.timeoutMs;
|
|
404
|
+
const startedAt = params.now();
|
|
405
|
+
const cacheBustValue = params.cacheBust ? startedAt : undefined;
|
|
406
|
+
try {
|
|
407
|
+
const response = await params.fetchImpl(buildCheckUrl(params.baseUrl, check, timeoutMs, cacheBustValue), {
|
|
408
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
409
|
+
});
|
|
410
|
+
const body = (await response.json().catch(() => ({})));
|
|
411
|
+
const durationMs = params.now() - startedAt;
|
|
412
|
+
if (!response.ok || body.success === false) {
|
|
413
|
+
return {
|
|
414
|
+
id: check.id,
|
|
415
|
+
service: check.service,
|
|
416
|
+
target: check.target,
|
|
417
|
+
status: 'fail',
|
|
418
|
+
durationMs,
|
|
419
|
+
httpStatus: response.status,
|
|
420
|
+
message: body.error?.message || `HTTP ${response.status}`,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
const count = typeof body.meta?.total === 'number' ? body.meta.total : toCount(body.data);
|
|
424
|
+
const shapeOk = hasRequiredRepresentativeFields(body.data, check.collectionKey, check.requiredFields);
|
|
425
|
+
const slowThresholdMs = params.slowThresholdMs || DEFAULT_HEALTH_CHECK_SLOW_THRESHOLD_MS;
|
|
426
|
+
const slow = slowThresholdMs > 0 && durationMs > slowThresholdMs;
|
|
427
|
+
const status = count === 0 || !shapeOk || slow ? 'degraded' : 'ok';
|
|
428
|
+
const first = params.includeSamples ? toFirstName(body.data) : undefined;
|
|
429
|
+
const message = slow
|
|
430
|
+
? `slow response: ${durationMs}ms > ${slowThresholdMs}ms`
|
|
431
|
+
: !shapeOk && count !== 0
|
|
432
|
+
? `response missing required fields: ${check.requiredFields.join(', ')}`
|
|
433
|
+
: count === null
|
|
434
|
+
? 'response ok'
|
|
435
|
+
: `${count} item(s) returned`;
|
|
436
|
+
return {
|
|
437
|
+
id: check.id,
|
|
438
|
+
service: check.service,
|
|
439
|
+
target: check.target,
|
|
440
|
+
status,
|
|
441
|
+
durationMs,
|
|
442
|
+
httpStatus: response.status,
|
|
443
|
+
message,
|
|
444
|
+
...(first ? { sample: { first } } : {}),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
return {
|
|
449
|
+
id: check.id,
|
|
450
|
+
service: check.service,
|
|
451
|
+
target: check.target,
|
|
452
|
+
status: 'fail',
|
|
453
|
+
durationMs: params.now() - startedAt,
|
|
454
|
+
message: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.',
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
export async function runHealthChecks(params) {
|
|
459
|
+
const now = params.now || Date.now;
|
|
460
|
+
const fetchImpl = params.fetchImpl || ((input, init) => globalThis.fetch(input, init));
|
|
461
|
+
const mode = params.mode || 'quick';
|
|
462
|
+
const timeoutMs = clampTimeout(params.timeoutMs ?? DEFAULT_HEALTH_CHECK_TIMEOUT_MS);
|
|
463
|
+
const slowThresholdMs = Number.isFinite(params.slowThresholdMs) && params.slowThresholdMs && params.slowThresholdMs > 0
|
|
464
|
+
? Math.trunc(params.slowThresholdMs)
|
|
465
|
+
: DEFAULT_HEALTH_CHECK_SLOW_THRESHOLD_MS;
|
|
466
|
+
const cacheKey = createCacheKey({ ...params, mode, timeoutMs, slowThresholdMs, baseUrl: params.baseUrl });
|
|
467
|
+
const cached = healthCheckCache.get(cacheKey);
|
|
468
|
+
const startedAt = now();
|
|
469
|
+
if (!params.fresh && cached && cached.expiresAt > startedAt) {
|
|
470
|
+
return {
|
|
471
|
+
...cached.summary,
|
|
472
|
+
cached: true,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
const checks = await mapWithConcurrency(selectChecks(params), DEFAULT_HEALTH_CHECK_CONCURRENCY, (check) => runSingleCheck(check, {
|
|
476
|
+
baseUrl: params.baseUrl,
|
|
477
|
+
fetchImpl,
|
|
478
|
+
now,
|
|
479
|
+
timeoutMs,
|
|
480
|
+
slowThresholdMs,
|
|
481
|
+
includeSamples: params.includeSamples,
|
|
482
|
+
cacheBust: params.cacheBust,
|
|
483
|
+
}));
|
|
484
|
+
const summary = {
|
|
485
|
+
status: aggregateStatus(checks),
|
|
486
|
+
checkedAt: new Date(startedAt).toISOString(),
|
|
487
|
+
durationMs: now() - startedAt,
|
|
488
|
+
cached: false,
|
|
489
|
+
filters: {
|
|
490
|
+
service: params.service || null,
|
|
491
|
+
check: params.check || null,
|
|
492
|
+
mode,
|
|
493
|
+
},
|
|
494
|
+
checks,
|
|
495
|
+
};
|
|
496
|
+
healthCheckCache.set(cacheKey, {
|
|
497
|
+
expiresAt: startedAt + HEALTH_CHECK_CACHE_TTL_MS,
|
|
498
|
+
summary,
|
|
499
|
+
});
|
|
500
|
+
return summary;
|
|
501
|
+
}
|
|
502
|
+
export function __testOnlyClearHealthCheckCache() {
|
|
503
|
+
healthCheckCache.clear();
|
|
504
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 롯데시네마 GET API 핸들러
|
|
3
|
+
*/
|
|
4
|
+
import { fetchLotteCinemaNowShowing, toYyyymmdd, } from '../services/lottecinema/client.js';
|
|
5
|
+
import { fetchLotteCinemaNearbyTheaters, resolveLotteCinemaNearestTheater, } from '../services/lottecinema/location.js';
|
|
6
|
+
import { errorResponse, successResponse } from './response.js';
|
|
7
|
+
function parseOptionalNumber(value) {
|
|
8
|
+
if (!value) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
const parsed = Number.parseFloat(value);
|
|
12
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
13
|
+
}
|
|
14
|
+
export async function handleLotteCinemaFindNearbyTheaters(c) {
|
|
15
|
+
const keyword = c.req.query('keyword') || undefined;
|
|
16
|
+
const latitude = parseOptionalNumber(c.req.query('lat'));
|
|
17
|
+
const longitude = parseOptionalNumber(c.req.query('lng'));
|
|
18
|
+
const playDate = c.req.query('playDate') || toYyyymmdd();
|
|
19
|
+
const limit = parseInt(c.req.query('limit') || '10', 10);
|
|
20
|
+
const timeoutMs = parseInt(c.req.query('timeoutMs') || '15000', 10);
|
|
21
|
+
try {
|
|
22
|
+
const hasCoordinates = typeof latitude === 'number' && typeof longitude === 'number';
|
|
23
|
+
const result = await fetchLotteCinemaNearbyTheaters({
|
|
24
|
+
keyword,
|
|
25
|
+
latitude: hasCoordinates ? latitude : keyword ? undefined : 37.5665,
|
|
26
|
+
longitude: hasCoordinates ? longitude : keyword ? undefined : 126.978,
|
|
27
|
+
playDate,
|
|
28
|
+
limit,
|
|
29
|
+
timeout: timeoutMs,
|
|
30
|
+
}, {
|
|
31
|
+
timeout: timeoutMs,
|
|
32
|
+
googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
|
|
33
|
+
});
|
|
34
|
+
return successResponse(c, result, { total: result.count, pageSize: limit });
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
|
|
38
|
+
return errorResponse(c, 'LOTTECINEMA_THEATER_SEARCH_FAILED', message, 500);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function handleLotteCinemaListNowShowing(c) {
|
|
42
|
+
const playDate = c.req.query('playDate') || toYyyymmdd();
|
|
43
|
+
let theaterId = c.req.query('theaterId') || undefined;
|
|
44
|
+
const movieId = c.req.query('movieId') || undefined;
|
|
45
|
+
const keyword = c.req.query('keyword') || undefined;
|
|
46
|
+
const latitude = parseOptionalNumber(c.req.query('lat'));
|
|
47
|
+
const longitude = parseOptionalNumber(c.req.query('lng'));
|
|
48
|
+
const timeoutMs = parseInt(c.req.query('timeoutMs') || '15000', 10);
|
|
49
|
+
try {
|
|
50
|
+
let resolvedTheater = null;
|
|
51
|
+
if (!theaterId && (keyword || typeof latitude === 'number' || typeof longitude === 'number')) {
|
|
52
|
+
const resolved = await resolveLotteCinemaNearestTheater({
|
|
53
|
+
playDate,
|
|
54
|
+
keyword,
|
|
55
|
+
latitude,
|
|
56
|
+
longitude,
|
|
57
|
+
timeout: timeoutMs,
|
|
58
|
+
}, {
|
|
59
|
+
timeout: timeoutMs,
|
|
60
|
+
googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
|
|
61
|
+
});
|
|
62
|
+
resolvedTheater = resolved.theater;
|
|
63
|
+
theaterId = resolved.theater?.theaterId;
|
|
64
|
+
}
|
|
65
|
+
const shouldReturnEmpty = !theaterId && (keyword || typeof latitude === 'number' || typeof longitude === 'number');
|
|
66
|
+
const { theaters, movies, showtimes } = shouldReturnEmpty
|
|
67
|
+
? { theaters: [], movies: [], showtimes: [] }
|
|
68
|
+
: await fetchLotteCinemaNowShowing({
|
|
69
|
+
playDate,
|
|
70
|
+
theaterId,
|
|
71
|
+
movieId,
|
|
72
|
+
timeout: timeoutMs,
|
|
73
|
+
});
|
|
74
|
+
return successResponse(c, {
|
|
75
|
+
playDate,
|
|
76
|
+
filters: {
|
|
77
|
+
theaterId: theaterId || null,
|
|
78
|
+
movieId: movieId || null,
|
|
79
|
+
keyword: keyword || null,
|
|
80
|
+
latitude: latitude ?? null,
|
|
81
|
+
longitude: longitude ?? null,
|
|
82
|
+
},
|
|
83
|
+
resolvedTheater,
|
|
84
|
+
theaters,
|
|
85
|
+
movies,
|
|
86
|
+
showtimes,
|
|
87
|
+
}, { total: showtimes.length });
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
|
|
91
|
+
return errorResponse(c, 'LOTTECINEMA_MOVIE_LIST_FAILED', message, 500);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export async function handleLotteCinemaGetRemainingSeats(c) {
|
|
95
|
+
const playDate = c.req.query('playDate') || toYyyymmdd();
|
|
96
|
+
let theaterId = c.req.query('theaterId') || undefined;
|
|
97
|
+
const movieId = c.req.query('movieId') || undefined;
|
|
98
|
+
const keyword = c.req.query('keyword') || undefined;
|
|
99
|
+
const latitude = parseOptionalNumber(c.req.query('lat'));
|
|
100
|
+
const longitude = parseOptionalNumber(c.req.query('lng'));
|
|
101
|
+
const limit = parseInt(c.req.query('limit') || '50', 10);
|
|
102
|
+
const timeoutMs = parseInt(c.req.query('timeoutMs') || '15000', 10);
|
|
103
|
+
try {
|
|
104
|
+
let resolvedTheater = null;
|
|
105
|
+
if (!theaterId && (keyword || typeof latitude === 'number' || typeof longitude === 'number')) {
|
|
106
|
+
const resolved = await resolveLotteCinemaNearestTheater({
|
|
107
|
+
playDate,
|
|
108
|
+
keyword,
|
|
109
|
+
latitude,
|
|
110
|
+
longitude,
|
|
111
|
+
timeout: timeoutMs,
|
|
112
|
+
}, {
|
|
113
|
+
timeout: timeoutMs,
|
|
114
|
+
googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
|
|
115
|
+
});
|
|
116
|
+
resolvedTheater = resolved.theater;
|
|
117
|
+
theaterId = resolved.theater?.theaterId;
|
|
118
|
+
}
|
|
119
|
+
const shouldReturnEmpty = !theaterId && (keyword || typeof latitude === 'number' || typeof longitude === 'number');
|
|
120
|
+
const { showtimes } = shouldReturnEmpty
|
|
121
|
+
? { showtimes: [] }
|
|
122
|
+
: await fetchLotteCinemaNowShowing({
|
|
123
|
+
playDate,
|
|
124
|
+
theaterId,
|
|
125
|
+
movieId,
|
|
126
|
+
timeout: timeoutMs,
|
|
127
|
+
});
|
|
128
|
+
const seats = showtimes
|
|
129
|
+
.filter((item) => (theaterId ? item.theaterId === theaterId : true))
|
|
130
|
+
.filter((item) => (movieId ? item.movieId === movieId : true))
|
|
131
|
+
.sort((a, b) => {
|
|
132
|
+
if (a.startTime === b.startTime) {
|
|
133
|
+
return a.theaterName.localeCompare(b.theaterName);
|
|
134
|
+
}
|
|
135
|
+
return a.startTime.localeCompare(b.startTime);
|
|
136
|
+
})
|
|
137
|
+
.slice(0, limit);
|
|
138
|
+
return successResponse(c, {
|
|
139
|
+
playDate,
|
|
140
|
+
filters: {
|
|
141
|
+
theaterId: theaterId || null,
|
|
142
|
+
movieId: movieId || null,
|
|
143
|
+
keyword: keyword || null,
|
|
144
|
+
latitude: latitude ?? null,
|
|
145
|
+
longitude: longitude ?? null,
|
|
146
|
+
},
|
|
147
|
+
resolvedTheater,
|
|
148
|
+
seats,
|
|
149
|
+
}, { total: seats.length, pageSize: limit });
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
|
|
153
|
+
return errorResponse(c, 'LOTTECINEMA_SEAT_LIST_FAILED', message, 500);
|
|
154
|
+
}
|
|
155
|
+
}
|