daiso 1.0.3 → 1.0.5

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.
Files changed (158) hide show
  1. package/README.md +218 -537
  2. package/dist/api/actionsProxy.js +82 -0
  3. package/dist/api/cgvHandlers.js +103 -16
  4. package/dist/api/daisoHandlers.js +127 -0
  5. package/dist/api/emart24Handlers.js +176 -0
  6. package/dist/api/gs25Handlers.js +256 -0
  7. package/dist/api/handlers.js +184 -108
  8. package/dist/api/healthChecks.js +332 -0
  9. package/dist/api/lottecinemaHandlers.js +155 -0
  10. package/dist/api/lottemartHandlers.js +128 -0
  11. package/dist/api/megaboxHandlers.js +81 -50
  12. package/dist/api/routes/cuRoutes.js +17 -0
  13. package/dist/api/routes/daisoRoutes.js +6 -1
  14. package/dist/api/routes/emart24Routes.js +22 -0
  15. package/dist/api/routes/gs25Routes.js +22 -0
  16. package/dist/api/routes/healthRoutes.js +44 -0
  17. package/dist/api/routes/lottecinemaRoutes.js +22 -0
  18. package/dist/api/routes/lottemartRoutes.js +18 -0
  19. package/dist/api/routes/oliveyoungRoutes.js +6 -1
  20. package/dist/api/routes/sevenelevenRoutes.js +32 -0
  21. package/dist/api/sevenelevenHandlers.js +200 -0
  22. package/dist/cli/args.js +54 -0
  23. package/dist/cli/commands/convenience.js +241 -0
  24. package/dist/cli/commands/daiso.js +96 -0
  25. package/dist/cli/constants.js +5 -0
  26. package/dist/cli/deps.js +48 -0
  27. package/dist/cli/http.js +98 -0
  28. package/dist/cli/interactiveFetch.js +31 -0
  29. package/dist/cli/interactiveItemSearch.js +259 -0
  30. package/dist/cli/interactivePrompt.js +49 -0
  31. package/dist/cli/interactiveTypes.js +4 -0
  32. package/dist/cli/types.js +4 -0
  33. package/dist/cli.js +66 -181
  34. package/dist/cliHelp.js +216 -9
  35. package/dist/cliInteractive.js +174 -0
  36. package/dist/cliPicker.js +56 -0
  37. package/dist/cliRenderer.js +139 -0
  38. package/dist/index.js +147 -8
  39. package/dist/pages/openapi.js +2 -1
  40. package/dist/pages/openapiFullSpec.js +58 -0
  41. package/dist/pages/openapiResponse.js +28 -0
  42. package/dist/pages/openapiSpec.js +2 -41
  43. package/dist/pages/openapiSpecActions.js +109 -0
  44. package/dist/pages/openapiSpecComponents.js +30 -391
  45. package/dist/pages/openapiSpecComponentsCgv.js +24 -0
  46. package/dist/pages/openapiSpecComponentsCu.js +112 -0
  47. package/dist/pages/openapiSpecComponentsDaisoOliveyoung.js +342 -0
  48. package/dist/pages/openapiSpecComponentsEmart24.js +141 -0
  49. package/dist/pages/openapiSpecComponentsGs25.js +109 -0
  50. package/dist/pages/openapiSpecComponentsLotteMart.js +85 -0
  51. package/dist/pages/openapiSpecComponentsLottecinema.js +163 -0
  52. package/dist/pages/openapiSpecComponentsMegabox.js +144 -0
  53. package/dist/pages/openapiSpecComponentsSeveneleven.js +82 -0
  54. package/dist/pages/openapiSpecPathsCgv.js +66 -3
  55. package/dist/pages/openapiSpecPathsCu.js +138 -0
  56. package/dist/pages/openapiSpecPathsDaiso.js +302 -0
  57. package/dist/pages/openapiSpecPathsDaisoOliveyoung.js +4 -376
  58. package/dist/pages/openapiSpecPathsEmart24.js +223 -0
  59. package/dist/pages/openapiSpecPathsGs25.js +166 -0
  60. package/dist/pages/openapiSpecPathsLotteMart.js +135 -0
  61. package/dist/pages/openapiSpecPathsLottecinema.js +212 -0
  62. package/dist/pages/openapiSpecPathsMegabox.js +52 -3
  63. package/dist/pages/openapiSpecPathsOliveyoung.js +231 -0
  64. package/dist/pages/openapiSpecPathsSeveneleven.js +234 -0
  65. package/dist/pages/prompt.js +8 -402
  66. package/dist/pages/promptCoreText.js +278 -0
  67. package/dist/pages/promptReferenceText.js +131 -0
  68. package/dist/pages/promptRetailCinemaText.js +273 -0
  69. package/dist/services/cgv/client.js +6 -1
  70. package/dist/services/cgv/index.js +3 -3
  71. package/dist/services/cgv/location.js +235 -0
  72. package/dist/services/cgv/tools/findTheaters.js +30 -5
  73. package/dist/services/cgv/tools/getTimetable.js +42 -14
  74. package/dist/services/cgv/tools/searchMovies.js +40 -12
  75. package/dist/services/cgv/transport.js +0 -3
  76. package/dist/services/cu/api.js +11 -0
  77. package/dist/services/cu/client.js +297 -0
  78. package/dist/services/cu/index.js +21 -0
  79. package/dist/services/cu/tools/checkInventory.js +123 -0
  80. package/dist/services/cu/tools/findNearbyStores.js +49 -0
  81. package/dist/services/cu/types.js +4 -0
  82. package/dist/services/daiso/api.js +22 -2
  83. package/dist/services/daiso/client.js +59 -0
  84. package/dist/services/daiso/index.js +2 -0
  85. package/dist/services/daiso/product.js +17 -0
  86. package/dist/services/daiso/tools/checkInventory.js +76 -41
  87. package/dist/services/daiso/tools/findStores.js +17 -7
  88. package/dist/services/daiso/tools/getDisplayLocation.js +75 -0
  89. package/dist/services/emart24/api.js +11 -0
  90. package/dist/services/emart24/client.js +173 -0
  91. package/dist/services/emart24/index.js +22 -0
  92. package/dist/services/emart24/inventoryLookup.js +134 -0
  93. package/dist/services/emart24/tools/checkInventory.js +54 -0
  94. package/dist/services/emart24/tools/findNearbyStores.js +72 -0
  95. package/dist/services/emart24/tools/searchProducts.js +63 -0
  96. package/dist/services/emart24/types.js +4 -0
  97. package/dist/services/gs25/api.js +9 -0
  98. package/dist/services/gs25/client.js +198 -0
  99. package/dist/services/gs25/index.js +22 -0
  100. package/dist/services/gs25/storeUtils.js +168 -0
  101. package/dist/services/gs25/tools/checkInventory.js +163 -0
  102. package/dist/services/gs25/tools/findNearbyStores.js +103 -0
  103. package/dist/services/gs25/tools/searchProducts.js +50 -0
  104. package/dist/services/gs25/types.js +4 -0
  105. package/dist/services/lottecinema/api.js +12 -0
  106. package/dist/services/lottecinema/client.js +161 -0
  107. package/dist/services/lottecinema/index.js +30 -0
  108. package/dist/services/lottecinema/location.js +191 -0
  109. package/dist/services/lottecinema/tools/findNearbyTheaters.js +47 -0
  110. package/dist/services/lottecinema/tools/getRemainingSeats.js +85 -0
  111. package/dist/services/lottecinema/tools/listNowShowing.js +79 -0
  112. package/dist/services/lottecinema/types.js +4 -0
  113. package/dist/services/lottemart/api.js +31 -0
  114. package/dist/services/lottemart/client.js +245 -0
  115. package/dist/services/lottemart/clientTypes.js +4 -0
  116. package/dist/services/lottemart/config.js +4 -0
  117. package/dist/services/lottemart/debug.js +92 -0
  118. package/dist/services/lottemart/geocode.js +49 -0
  119. package/dist/services/lottemart/index.js +30 -0
  120. package/dist/services/lottemart/keyword.js +29 -0
  121. package/dist/services/lottemart/parser.js +185 -0
  122. package/dist/services/lottemart/session.js +331 -0
  123. package/dist/services/lottemart/storeSearch.js +52 -0
  124. package/dist/services/lottemart/tools/findNearbyStores.js +67 -0
  125. package/dist/services/lottemart/tools/searchProducts.js +62 -0
  126. package/dist/services/lottemart/types.js +4 -0
  127. package/dist/services/lottemart/zetta.js +105 -0
  128. package/dist/services/megabox/location.js +219 -0
  129. package/dist/services/megabox/tools/findNearbyTheaters.js +13 -44
  130. package/dist/services/megabox/tools/getRemainingSeats.js +31 -3
  131. package/dist/services/megabox/tools/listNowShowing.js +31 -3
  132. package/dist/services/oliveyoung/api.js +2 -0
  133. package/dist/services/oliveyoung/client.js +150 -9
  134. package/dist/services/oliveyoung/index.js +3 -1
  135. package/dist/services/oliveyoung/tools/checkInventory.js +15 -4
  136. package/dist/services/oliveyoung/tools/searchProducts.js +57 -0
  137. package/dist/services/seveneleven/api.js +15 -0
  138. package/dist/services/seveneleven/client.js +279 -0
  139. package/dist/services/seveneleven/index.js +30 -0
  140. package/dist/services/seveneleven/inventory.js +302 -0
  141. package/dist/services/seveneleven/productKeyword.js +141 -0
  142. package/dist/services/seveneleven/tools/checkInventory.js +82 -0
  143. package/dist/services/seveneleven/tools/getCatalogSnapshot.js +55 -0
  144. package/dist/services/seveneleven/tools/getSearchPopwords.js +41 -0
  145. package/dist/services/seveneleven/tools/searchProducts.js +56 -0
  146. package/dist/services/seveneleven/tools/searchStores.js +45 -0
  147. package/dist/services/seveneleven/types.js +4 -0
  148. package/dist/utils/cliInteractiveHelpers.js +63 -0
  149. package/dist/utils/daisoKeyword.js +30 -0
  150. package/dist/utils/http.js +16 -2
  151. package/openapi.json +1977 -734
  152. package/openapi.yaml +1430 -543
  153. package/package.json +28 -15
  154. package/dist/tools/checkInventory.js +0 -98
  155. package/dist/tools/findStores.js +0 -125
  156. package/dist/tools/getPriceInfo.js +0 -77
  157. package/dist/tools/searchProducts.js +0 -61
  158. package/dist/types/index.js +0 -4
@@ -0,0 +1,332 @@
1
+ /**
2
+ * 개별 서비스 헬스 체크 실행기
3
+ */
4
+ const HEALTH_CHECK_CACHE_TTL_MS = 60_000;
5
+ const DEFAULT_HEALTH_CHECK_TIMEOUT_MS = 3000;
6
+ const MAX_HEALTH_CHECK_TIMEOUT_MS = 10_000;
7
+ const HEALTH_CHECKS = [
8
+ {
9
+ id: 'cli.contract',
10
+ service: 'cli',
11
+ target: 'api-contract',
12
+ mode: 'deep',
13
+ kind: 'cli-contract',
14
+ path: '/health',
15
+ },
16
+ {
17
+ id: 'daiso.products',
18
+ service: 'daiso',
19
+ target: 'products',
20
+ mode: 'quick',
21
+ path: '/api/daiso/products?q=%ED%85%8C%EC%9D%B4%ED%94%84&pageSize=1',
22
+ },
23
+ {
24
+ id: 'cu.stores',
25
+ service: 'cu',
26
+ target: 'stores',
27
+ mode: 'quick',
28
+ path: '/api/cu/stores?keyword=%EA%B0%95%EB%82%A8&limit=1',
29
+ },
30
+ {
31
+ id: 'emart24.products',
32
+ service: 'emart24',
33
+ target: 'products',
34
+ mode: 'quick',
35
+ path: '/api/emart24/products?keyword=%EC%BB%A4%ED%94%BC&pageSize=1',
36
+ },
37
+ {
38
+ id: 'gs25.products',
39
+ service: 'gs25',
40
+ target: 'products',
41
+ mode: 'quick',
42
+ path: '/api/gs25/products?keyword=%EC%BD%9C%EB%9D%BC&limit=1',
43
+ },
44
+ {
45
+ id: 'gs25.stores',
46
+ service: 'gs25',
47
+ target: 'stores',
48
+ mode: 'quick',
49
+ path: '/api/gs25/stores?keyword=%EA%B0%95%EB%82%A8&limit=1',
50
+ },
51
+ {
52
+ id: 'seveneleven.products',
53
+ service: 'seveneleven',
54
+ target: 'products',
55
+ mode: 'quick',
56
+ path: '/api/seveneleven/products?query=%EC%BB%A4%ED%94%BC&size=1',
57
+ },
58
+ {
59
+ id: 'lottemart.products',
60
+ service: 'lottemart',
61
+ target: 'products',
62
+ mode: 'quick',
63
+ path: '/api/lottemart/products?keyword=%EC%BD%9C%EB%9D%BC&storeCode=2301&area=%EC%84%9C%EC%9A%B8&pageLimit=1',
64
+ },
65
+ {
66
+ id: 'oliveyoung.products',
67
+ service: 'oliveyoung',
68
+ target: 'products',
69
+ mode: 'quick',
70
+ path: '/api/oliveyoung/products?keyword=%EC%84%A0%ED%81%AC%EB%A6%BC&size=1',
71
+ },
72
+ {
73
+ id: 'megabox.theaters',
74
+ service: 'megabox',
75
+ target: 'theaters',
76
+ mode: 'quick',
77
+ path: '/api/megabox/theaters?keyword=%EA%B0%95%EB%82%A8&limit=1',
78
+ },
79
+ {
80
+ id: 'lottecinema.theaters',
81
+ service: 'lottecinema',
82
+ target: 'theaters',
83
+ mode: 'quick',
84
+ path: '/api/lottecinema/theaters?keyword=%EC%9E%A0%EC%8B%A4&limit=1',
85
+ },
86
+ {
87
+ id: 'cgv.theaters',
88
+ service: 'cgv',
89
+ target: 'theaters',
90
+ mode: 'quick',
91
+ path: '/api/cgv/theaters?keyword=%EA%B0%95%EB%82%A8&limit=1',
92
+ },
93
+ ];
94
+ const healthCheckCache = new Map();
95
+ function clampTimeout(timeoutMs) {
96
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 1) {
97
+ return DEFAULT_HEALTH_CHECK_TIMEOUT_MS;
98
+ }
99
+ return Math.min(Math.trunc(timeoutMs), MAX_HEALTH_CHECK_TIMEOUT_MS);
100
+ }
101
+ function createCacheKey(params) {
102
+ return [
103
+ params.baseUrl,
104
+ params.service || '',
105
+ params.check || '',
106
+ params.mode,
107
+ params.timeoutMs,
108
+ params.includeSamples ? 'samples' : 'no-samples',
109
+ ].join('|');
110
+ }
111
+ function selectChecks(params) {
112
+ const mode = params.mode || 'quick';
113
+ return HEALTH_CHECKS.filter((check) => {
114
+ if (check.mode !== mode) {
115
+ return false;
116
+ }
117
+ if (params.service && check.service !== params.service) {
118
+ return false;
119
+ }
120
+ if (params.check && check.id !== params.check) {
121
+ return false;
122
+ }
123
+ return true;
124
+ });
125
+ }
126
+ function toCount(data) {
127
+ if (!data || typeof data !== 'object') {
128
+ return null;
129
+ }
130
+ const record = data;
131
+ if (typeof record.count === 'number') {
132
+ return record.count;
133
+ }
134
+ for (const key of ['products', 'stores', 'theaters', 'movies', 'showtimes']) {
135
+ const value = record[key];
136
+ if (Array.isArray(value)) {
137
+ return value.length;
138
+ }
139
+ }
140
+ return null;
141
+ }
142
+ function toFirstName(data) {
143
+ if (!data || typeof data !== 'object') {
144
+ return undefined;
145
+ }
146
+ const record = data;
147
+ for (const key of ['products', 'stores', 'theaters', 'movies', 'showtimes']) {
148
+ const value = record[key];
149
+ if (!Array.isArray(value) || !value[0] || typeof value[0] !== 'object') {
150
+ continue;
151
+ }
152
+ const item = value[0];
153
+ for (const nameKey of ['productName', 'itemName', 'goodsName', 'name', 'storeName', 'theaterName', 'movieName']) {
154
+ if (typeof item[nameKey] === 'string' && item[nameKey].trim().length > 0) {
155
+ return item[nameKey].trim();
156
+ }
157
+ }
158
+ }
159
+ return undefined;
160
+ }
161
+ function aggregateStatus(checks) {
162
+ if (checks.length === 0) {
163
+ return 'skipped';
164
+ }
165
+ if (checks.some((check) => check.status === 'fail')) {
166
+ return 'fail';
167
+ }
168
+ if (checks.some((check) => check.status === 'degraded')) {
169
+ return 'degraded';
170
+ }
171
+ return 'ok';
172
+ }
173
+ function buildCheckUrl(baseUrl, check, timeoutMs) {
174
+ const url = new URL(check.path, baseUrl);
175
+ url.searchParams.set('timeoutMs', String(timeoutMs));
176
+ return url.toString();
177
+ }
178
+ const CLI_CONTRACT_PATHS = [
179
+ '/health',
180
+ '/api/daiso/products?q=%ED%85%8C%EC%9D%B4%ED%94%84&pageSize=1',
181
+ '/api/daiso/stores?keyword=%EA%B0%95%EB%82%A8&pageSize=1',
182
+ '/api/gs25/products?keyword=%EC%BD%9C%EB%9D%BC&limit=1',
183
+ '/api/gs25/stores?keyword=%EA%B0%95%EB%82%A8&limit=1',
184
+ '/api/seveneleven/products?query=%EC%BB%A4%ED%94%BC&size=1',
185
+ '/api/emart24/products?keyword=%EC%BB%A4%ED%94%BC&pageSize=1',
186
+ '/api/lottemart/products?keyword=%EC%BD%9C%EB%9D%BC&storeCode=2301&area=%EC%84%9C%EC%9A%B8&pageLimit=1',
187
+ '/api/oliveyoung/products?keyword=%EC%84%A0%ED%81%AC%EB%A6%BC&size=1',
188
+ '/api/megabox/theaters?keyword=%EA%B0%95%EB%82%A8&limit=1',
189
+ '/api/lottecinema/theaters?keyword=%EC%9E%A0%EC%8B%A4&limit=1',
190
+ '/api/cgv/theaters?keyword=%EA%B0%95%EB%82%A8&limit=1',
191
+ ];
192
+ function isCliCompatibleEnvelope(path, body) {
193
+ if (!body || typeof body !== 'object') {
194
+ return false;
195
+ }
196
+ const record = body;
197
+ if (path === '/health') {
198
+ return record.status === 'ok';
199
+ }
200
+ return record.success === true && typeof record.data === 'object' && record.data !== null;
201
+ }
202
+ async function runCliContractCheck(check, params) {
203
+ const startedAt = params.now();
204
+ for (const path of CLI_CONTRACT_PATHS) {
205
+ const syntheticCheck = { ...check, path };
206
+ try {
207
+ const response = await params.fetchImpl(buildCheckUrl(params.baseUrl, syntheticCheck, params.timeoutMs), {
208
+ signal: AbortSignal.timeout(params.timeoutMs),
209
+ });
210
+ const body = (await response.json().catch(() => ({})));
211
+ if (!response.ok || !isCliCompatibleEnvelope(path, body)) {
212
+ return {
213
+ id: check.id,
214
+ service: check.service,
215
+ target: check.target,
216
+ status: 'fail',
217
+ durationMs: params.now() - startedAt,
218
+ httpStatus: response.status,
219
+ message: body.error?.message || `${path} CLI 계약 응답이 올바르지 않습니다.`,
220
+ };
221
+ }
222
+ }
223
+ catch (error) {
224
+ return {
225
+ id: check.id,
226
+ service: check.service,
227
+ target: check.target,
228
+ status: 'fail',
229
+ durationMs: params.now() - startedAt,
230
+ message: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.',
231
+ };
232
+ }
233
+ }
234
+ return {
235
+ id: check.id,
236
+ service: check.service,
237
+ target: check.target,
238
+ status: 'ok',
239
+ durationMs: params.now() - startedAt,
240
+ message: `${CLI_CONTRACT_PATHS.length} CLI contract endpoint(s) passed`,
241
+ };
242
+ }
243
+ async function runSingleCheck(check, params) {
244
+ if (check.kind === 'cli-contract') {
245
+ return runCliContractCheck(check, params);
246
+ }
247
+ const timeoutMs = params.timeoutMs;
248
+ const startedAt = params.now();
249
+ try {
250
+ const response = await params.fetchImpl(buildCheckUrl(params.baseUrl, check, timeoutMs), {
251
+ signal: AbortSignal.timeout(timeoutMs),
252
+ });
253
+ const body = (await response.json().catch(() => ({})));
254
+ const durationMs = params.now() - startedAt;
255
+ if (!response.ok || body.success === false) {
256
+ return {
257
+ id: check.id,
258
+ service: check.service,
259
+ target: check.target,
260
+ status: 'fail',
261
+ durationMs,
262
+ httpStatus: response.status,
263
+ message: body.error?.message || `HTTP ${response.status}`,
264
+ };
265
+ }
266
+ const count = typeof body.meta?.total === 'number' ? body.meta.total : toCount(body.data);
267
+ const status = count === 0 ? 'degraded' : 'ok';
268
+ const first = params.includeSamples ? toFirstName(body.data) : undefined;
269
+ return {
270
+ id: check.id,
271
+ service: check.service,
272
+ target: check.target,
273
+ status,
274
+ durationMs,
275
+ httpStatus: response.status,
276
+ message: count === null ? 'response ok' : `${count} item(s) returned`,
277
+ ...(first ? { sample: { first } } : {}),
278
+ };
279
+ }
280
+ catch (error) {
281
+ return {
282
+ id: check.id,
283
+ service: check.service,
284
+ target: check.target,
285
+ status: 'fail',
286
+ durationMs: params.now() - startedAt,
287
+ message: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.',
288
+ };
289
+ }
290
+ }
291
+ export async function runHealthChecks(params) {
292
+ const now = params.now || Date.now;
293
+ const fetchImpl = params.fetchImpl || ((input, init) => globalThis.fetch(input, init));
294
+ const mode = params.mode || 'quick';
295
+ const timeoutMs = clampTimeout(params.timeoutMs ?? DEFAULT_HEALTH_CHECK_TIMEOUT_MS);
296
+ const cacheKey = createCacheKey({ ...params, mode, timeoutMs, baseUrl: params.baseUrl });
297
+ const cached = healthCheckCache.get(cacheKey);
298
+ const startedAt = now();
299
+ if (!params.fresh && cached && cached.expiresAt > startedAt) {
300
+ return {
301
+ ...cached.summary,
302
+ cached: true,
303
+ };
304
+ }
305
+ const checks = await Promise.all(selectChecks(params).map((check) => runSingleCheck(check, {
306
+ baseUrl: params.baseUrl,
307
+ fetchImpl,
308
+ now,
309
+ timeoutMs,
310
+ includeSamples: params.includeSamples,
311
+ })));
312
+ const summary = {
313
+ status: aggregateStatus(checks),
314
+ checkedAt: new Date(startedAt).toISOString(),
315
+ durationMs: now() - startedAt,
316
+ cached: false,
317
+ filters: {
318
+ service: params.service || null,
319
+ check: params.check || null,
320
+ mode,
321
+ },
322
+ checks,
323
+ };
324
+ healthCheckCache.set(cacheKey, {
325
+ expiresAt: startedAt + HEALTH_CHECK_CACHE_TTL_MS,
326
+ summary,
327
+ });
328
+ return summary;
329
+ }
330
+ export function __testOnlyClearHealthCheckCache() {
331
+ healthCheckCache.clear();
332
+ }
@@ -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
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * 롯데마트 GET API 핸들러
3
+ */
4
+ import { errorResponse, successResponse } from './response.js';
5
+ import { fetchLotteMartStores, searchLotteMartProducts } from '../services/lottemart/client.js';
6
+ import { DEFAULT_LOTTEMART_TIMEOUT_MS } from '../services/lottemart/config.js';
7
+ import { probeLotteMartUpstream } from '../services/lottemart/debug.js';
8
+ /**
9
+ * 롯데마트 매장 검색 API 핸들러
10
+ * GET /api/lottemart/stores?keyword={키워드}&lat={위도}&lng={경도}
11
+ */
12
+ export async function handleLotteMartFindStores(c) {
13
+ const keyword = c.req.query('keyword') || '';
14
+ const area = c.req.query('area') || '';
15
+ const brandVariant = c.req.query('brandVariant') || '';
16
+ const limit = parseInt(c.req.query('limit') || '20', 10);
17
+ const rawLat = c.req.query('lat');
18
+ const rawLng = c.req.query('lng');
19
+ const parsedLat = rawLat ? parseFloat(rawLat) : undefined;
20
+ const parsedLng = rawLng ? parseFloat(rawLng) : undefined;
21
+ const latitude = typeof parsedLat === 'number' && Number.isFinite(parsedLat) ? parsedLat : undefined;
22
+ const longitude = typeof parsedLng === 'number' && Number.isFinite(parsedLng) ? parsedLng : undefined;
23
+ try {
24
+ const result = await fetchLotteMartStores({
25
+ area,
26
+ keyword,
27
+ brandVariant,
28
+ latitude,
29
+ longitude,
30
+ limit,
31
+ }, {
32
+ timeout: DEFAULT_LOTTEMART_TIMEOUT_MS,
33
+ googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
34
+ zyteApiKey: c.env?.ZYTE_API_KEY,
35
+ });
36
+ return successResponse(c, {
37
+ area: area || null,
38
+ keyword,
39
+ brandVariant: brandVariant || null,
40
+ geocodeUsed: result.geocodeUsed,
41
+ location: result.location,
42
+ count: result.stores.length,
43
+ stores: result.stores,
44
+ }, {
45
+ total: result.stores.length,
46
+ page: 1,
47
+ pageSize: limit,
48
+ });
49
+ }
50
+ catch (error) {
51
+ const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
52
+ return errorResponse(c, 'LOTTEMART_STORE_SEARCH_FAILED', message, 500);
53
+ }
54
+ }
55
+ /**
56
+ * 롯데마트 상품 검색 API 핸들러
57
+ * GET /api/lottemart/products?keyword={검색어}&storeCode={매장코드}
58
+ */
59
+ export async function handleLotteMartSearchProducts(c) {
60
+ const keyword = c.req.query('keyword') || '';
61
+ const area = c.req.query('area') || '';
62
+ const storeCode = c.req.query('storeCode') || '';
63
+ const storeName = c.req.query('storeName') || '';
64
+ const pageLimit = parseInt(c.req.query('pageLimit') || '3', 10);
65
+ if (keyword.trim().length === 0) {
66
+ return errorResponse(c, 'MISSING_QUERY', '검색어(keyword)를 입력해주세요.');
67
+ }
68
+ if (storeCode.trim().length === 0 && storeName.trim().length === 0) {
69
+ return errorResponse(c, 'MISSING_STORE', 'storeCode 또는 storeName 중 하나를 입력해주세요.');
70
+ }
71
+ try {
72
+ const result = await searchLotteMartProducts({
73
+ area,
74
+ storeCode,
75
+ storeName,
76
+ keyword,
77
+ pageLimit,
78
+ }, {
79
+ timeout: DEFAULT_LOTTEMART_TIMEOUT_MS,
80
+ zyteApiKey: c.env?.ZYTE_API_KEY,
81
+ });
82
+ return successResponse(c, {
83
+ area: result.area,
84
+ storeCode: result.storeCode,
85
+ storeName: result.storeName,
86
+ keyword,
87
+ pageLimit,
88
+ totalPages: result.totalPages,
89
+ products: result.products,
90
+ }, {
91
+ total: result.totalCount,
92
+ page: 1,
93
+ pageSize: result.products.length,
94
+ });
95
+ }
96
+ catch (error) {
97
+ const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
98
+ return errorResponse(c, 'LOTTEMART_PRODUCT_SEARCH_FAILED', message, 500);
99
+ }
100
+ }
101
+ /**
102
+ * 롯데마트 업스트림 단독 진단 API 핸들러
103
+ * GET /api/lottemart/debug?target=market-options|stores|products|product-page
104
+ */
105
+ export async function handleLotteMartDebug(c) {
106
+ const target = (c.req.query('target') || '');
107
+ const allowedTargets = ['market-options', 'stores', 'products', 'product-page'];
108
+ if (!allowedTargets.includes(target)) {
109
+ return errorResponse(c, 'INVALID_TARGET', 'target은 market-options, stores, products, product-page 중 하나여야 합니다.');
110
+ }
111
+ try {
112
+ const result = await probeLotteMartUpstream({
113
+ target,
114
+ area: c.req.query('area') || undefined,
115
+ type: (c.req.query('type') || undefined),
116
+ storeCode: c.req.query('storeCode') || undefined,
117
+ keyword: c.req.query('keyword') || undefined,
118
+ page: c.req.query('page') ? parseInt(c.req.query('page'), 10) : undefined,
119
+ timeout: c.req.query('timeoutMs') ? parseInt(c.req.query('timeoutMs'), 10) : DEFAULT_LOTTEMART_TIMEOUT_MS,
120
+ zyteApiKey: c.env?.ZYTE_API_KEY,
121
+ });
122
+ return successResponse(c, result);
123
+ }
124
+ catch (error) {
125
+ const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
126
+ return errorResponse(c, 'LOTTEMART_DEBUG_FAILED', message, 500);
127
+ }
128
+ }