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.
Files changed (166) hide show
  1. package/README.md +249 -533
  2. package/dist/api/actionsProxy.js +82 -0
  3. package/dist/api/cgvHandlers.js +103 -16
  4. package/dist/api/configStatus.js +19 -0
  5. package/dist/api/daisoHandlers.js +127 -0
  6. package/dist/api/emart24Handlers.js +176 -0
  7. package/dist/api/gs25Handlers.js +267 -0
  8. package/dist/api/handlers.js +209 -104
  9. package/dist/api/healthChecks.js +504 -0
  10. package/dist/api/lottecinemaHandlers.js +155 -0
  11. package/dist/api/lottemartHandlers.js +144 -0
  12. package/dist/api/megaboxHandlers.js +81 -50
  13. package/dist/api/response.js +2 -0
  14. package/dist/api/routes/cuRoutes.js +17 -0
  15. package/dist/api/routes/daisoRoutes.js +6 -1
  16. package/dist/api/routes/emart24Routes.js +22 -0
  17. package/dist/api/routes/gs25Routes.js +22 -0
  18. package/dist/api/routes/healthRoutes.js +61 -0
  19. package/dist/api/routes/lottecinemaRoutes.js +22 -0
  20. package/dist/api/routes/lottemartRoutes.js +18 -0
  21. package/dist/api/routes/oliveyoungRoutes.js +6 -1
  22. package/dist/api/routes/sevenelevenRoutes.js +32 -0
  23. package/dist/api/sevenelevenHandlers.js +200 -0
  24. package/dist/bin.js +0 -0
  25. package/dist/cli/args.js +75 -0
  26. package/dist/cli/commandOptions.js +38 -0
  27. package/dist/cli/commands/convenience.js +293 -0
  28. package/dist/cli/commands/daiso.js +112 -0
  29. package/dist/cli/constants.js +5 -0
  30. package/dist/cli/deps.js +48 -0
  31. package/dist/cli/http.js +156 -0
  32. package/dist/cli/interactiveFetch.js +31 -0
  33. package/dist/cli/interactiveItemSearch.js +261 -0
  34. package/dist/cli/interactivePrompt.js +50 -0
  35. package/dist/cli/interactiveTypes.js +4 -0
  36. package/dist/cli/types.js +4 -0
  37. package/dist/cli.js +87 -196
  38. package/dist/cliHelp.js +212 -1
  39. package/dist/cliInteractive.js +243 -0
  40. package/dist/cliPicker.js +57 -0
  41. package/dist/cliRenderer.js +90 -3
  42. package/dist/core/errors.js +66 -0
  43. package/dist/core/registry.js +227 -3
  44. package/dist/index.js +149 -9
  45. package/dist/pages/openapi.js +2 -1
  46. package/dist/pages/openapiFullSpec.js +58 -0
  47. package/dist/pages/openapiResponse.js +28 -0
  48. package/dist/pages/openapiSpec.js +2 -41
  49. package/dist/pages/openapiSpecActions.js +109 -0
  50. package/dist/pages/openapiSpecComponents.js +30 -391
  51. package/dist/pages/openapiSpecComponentsCgv.js +24 -0
  52. package/dist/pages/openapiSpecComponentsCu.js +112 -0
  53. package/dist/pages/openapiSpecComponentsDaisoOliveyoung.js +342 -0
  54. package/dist/pages/openapiSpecComponentsEmart24.js +141 -0
  55. package/dist/pages/openapiSpecComponentsGs25.js +109 -0
  56. package/dist/pages/openapiSpecComponentsLotteMart.js +85 -0
  57. package/dist/pages/openapiSpecComponentsLottecinema.js +163 -0
  58. package/dist/pages/openapiSpecComponentsMegabox.js +144 -0
  59. package/dist/pages/openapiSpecComponentsSeveneleven.js +82 -0
  60. package/dist/pages/openapiSpecPathsCgv.js +66 -3
  61. package/dist/pages/openapiSpecPathsCu.js +145 -0
  62. package/dist/pages/openapiSpecPathsDaiso.js +302 -0
  63. package/dist/pages/openapiSpecPathsDaisoOliveyoung.js +4 -376
  64. package/dist/pages/openapiSpecPathsEmart24.js +223 -0
  65. package/dist/pages/openapiSpecPathsGs25.js +166 -0
  66. package/dist/pages/openapiSpecPathsLotteMart.js +149 -0
  67. package/dist/pages/openapiSpecPathsLottecinema.js +212 -0
  68. package/dist/pages/openapiSpecPathsMegabox.js +52 -3
  69. package/dist/pages/openapiSpecPathsOliveyoung.js +266 -0
  70. package/dist/pages/openapiSpecPathsSeveneleven.js +234 -0
  71. package/dist/pages/prompt.js +8 -402
  72. package/dist/pages/promptCoreText.js +278 -0
  73. package/dist/pages/promptReferenceText.js +137 -0
  74. package/dist/pages/promptRetailCinemaText.js +274 -0
  75. package/dist/services/cgv/client.js +6 -1
  76. package/dist/services/cgv/index.js +3 -3
  77. package/dist/services/cgv/location.js +235 -0
  78. package/dist/services/cgv/tools/findTheaters.js +30 -5
  79. package/dist/services/cgv/tools/getTimetable.js +42 -14
  80. package/dist/services/cgv/tools/searchMovies.js +40 -12
  81. package/dist/services/cgv/transport.js +0 -3
  82. package/dist/services/cu/api.js +11 -0
  83. package/dist/services/cu/client.js +297 -0
  84. package/dist/services/cu/index.js +21 -0
  85. package/dist/services/cu/tools/checkInventory.js +123 -0
  86. package/dist/services/cu/tools/findNearbyStores.js +49 -0
  87. package/dist/services/cu/types.js +4 -0
  88. package/dist/services/daiso/api.js +22 -2
  89. package/dist/services/daiso/client.js +67 -0
  90. package/dist/services/daiso/index.js +4 -0
  91. package/dist/services/daiso/product.js +17 -0
  92. package/dist/services/daiso/tools/checkInventory.js +80 -43
  93. package/dist/services/daiso/tools/findInventoryByName.js +222 -0
  94. package/dist/services/daiso/tools/findStores.js +17 -7
  95. package/dist/services/daiso/tools/getDisplayLocation.js +75 -0
  96. package/dist/services/emart24/api.js +11 -0
  97. package/dist/services/emart24/client.js +182 -0
  98. package/dist/services/emart24/index.js +22 -0
  99. package/dist/services/emart24/inventoryLookup.js +134 -0
  100. package/dist/services/emart24/tools/checkInventory.js +54 -0
  101. package/dist/services/emart24/tools/findNearbyStores.js +72 -0
  102. package/dist/services/emart24/tools/searchProducts.js +73 -0
  103. package/dist/services/emart24/types.js +4 -0
  104. package/dist/services/gs25/api.js +9 -0
  105. package/dist/services/gs25/client.js +208 -0
  106. package/dist/services/gs25/index.js +22 -0
  107. package/dist/services/gs25/storeUtils.js +168 -0
  108. package/dist/services/gs25/tools/checkInventory.js +163 -0
  109. package/dist/services/gs25/tools/findNearbyStores.js +103 -0
  110. package/dist/services/gs25/tools/searchProducts.js +57 -0
  111. package/dist/services/gs25/types.js +4 -0
  112. package/dist/services/lottecinema/api.js +12 -0
  113. package/dist/services/lottecinema/client.js +161 -0
  114. package/dist/services/lottecinema/index.js +30 -0
  115. package/dist/services/lottecinema/location.js +191 -0
  116. package/dist/services/lottecinema/tools/findNearbyTheaters.js +47 -0
  117. package/dist/services/lottecinema/tools/getRemainingSeats.js +85 -0
  118. package/dist/services/lottecinema/tools/listNowShowing.js +79 -0
  119. package/dist/services/lottecinema/types.js +4 -0
  120. package/dist/services/lottemart/api.js +31 -0
  121. package/dist/services/lottemart/client.js +254 -0
  122. package/dist/services/lottemart/clientTypes.js +4 -0
  123. package/dist/services/lottemart/config.js +4 -0
  124. package/dist/services/lottemart/debug.js +92 -0
  125. package/dist/services/lottemart/geocode.js +49 -0
  126. package/dist/services/lottemart/index.js +30 -0
  127. package/dist/services/lottemart/keyword.js +29 -0
  128. package/dist/services/lottemart/parser.js +185 -0
  129. package/dist/services/lottemart/session.js +331 -0
  130. package/dist/services/lottemart/storeSearch.js +52 -0
  131. package/dist/services/lottemart/tools/findNearbyStores.js +67 -0
  132. package/dist/services/lottemart/tools/searchProducts.js +69 -0
  133. package/dist/services/lottemart/types.js +4 -0
  134. package/dist/services/lottemart/zetta.js +105 -0
  135. package/dist/services/megabox/location.js +219 -0
  136. package/dist/services/megabox/tools/findNearbyTheaters.js +13 -44
  137. package/dist/services/megabox/tools/getRemainingSeats.js +31 -3
  138. package/dist/services/megabox/tools/listNowShowing.js +31 -3
  139. package/dist/services/oliveyoung/api.js +2 -0
  140. package/dist/services/oliveyoung/client.js +225 -13
  141. package/dist/services/oliveyoung/index.js +3 -1
  142. package/dist/services/oliveyoung/tools/checkInventory.js +21 -5
  143. package/dist/services/oliveyoung/tools/searchProducts.js +57 -0
  144. package/dist/services/seveneleven/api.js +15 -0
  145. package/dist/services/seveneleven/client.js +286 -0
  146. package/dist/services/seveneleven/index.js +30 -0
  147. package/dist/services/seveneleven/inventory.js +302 -0
  148. package/dist/services/seveneleven/productKeyword.js +141 -0
  149. package/dist/services/seveneleven/tools/checkInventory.js +82 -0
  150. package/dist/services/seveneleven/tools/getCatalogSnapshot.js +55 -0
  151. package/dist/services/seveneleven/tools/getSearchPopwords.js +41 -0
  152. package/dist/services/seveneleven/tools/searchProducts.js +67 -0
  153. package/dist/services/seveneleven/tools/searchStores.js +45 -0
  154. package/dist/services/seveneleven/types.js +4 -0
  155. package/dist/utils/cliInteractiveHelpers.js +63 -0
  156. package/dist/utils/daisoKeyword.js +30 -0
  157. package/dist/utils/http.js +98 -12
  158. package/dist/utils/zyte.js +47 -26
  159. package/openapi.json +1979 -736
  160. package/openapi.yaml +1428 -541
  161. package/package.json +49 -18
  162. package/dist/tools/checkInventory.js +0 -98
  163. package/dist/tools/findStores.js +0 -125
  164. package/dist/tools/getPriceInfo.js +0 -77
  165. package/dist/tools/searchProducts.js +0 -61
  166. package/dist/types/index.js +0 -4
@@ -0,0 +1,144 @@
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
+ function parseLotteMartProductSource(value) {
9
+ if (!value || value === 'auto') {
10
+ return 'auto';
11
+ }
12
+ if (value === 'legacy' || value === 'zetta') {
13
+ return value;
14
+ }
15
+ return null;
16
+ }
17
+ /**
18
+ * 롯데마트 매장 검색 API 핸들러
19
+ * GET /api/lottemart/stores?keyword={키워드}&lat={위도}&lng={경도}
20
+ */
21
+ export async function handleLotteMartFindStores(c) {
22
+ const keyword = c.req.query('keyword') || '';
23
+ const area = c.req.query('area') || '';
24
+ const brandVariant = c.req.query('brandVariant') || '';
25
+ const limit = parseInt(c.req.query('limit') || '20', 10);
26
+ const rawLat = c.req.query('lat');
27
+ const rawLng = c.req.query('lng');
28
+ const parsedLat = rawLat ? parseFloat(rawLat) : undefined;
29
+ const parsedLng = rawLng ? parseFloat(rawLng) : undefined;
30
+ const latitude = typeof parsedLat === 'number' && Number.isFinite(parsedLat) ? parsedLat : undefined;
31
+ const longitude = typeof parsedLng === 'number' && Number.isFinite(parsedLng) ? parsedLng : undefined;
32
+ try {
33
+ const result = await fetchLotteMartStores({
34
+ area,
35
+ keyword,
36
+ brandVariant,
37
+ latitude,
38
+ longitude,
39
+ limit,
40
+ }, {
41
+ timeout: DEFAULT_LOTTEMART_TIMEOUT_MS,
42
+ googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
43
+ zyteApiKey: c.env?.ZYTE_API_KEY,
44
+ });
45
+ return successResponse(c, {
46
+ area: area || null,
47
+ keyword,
48
+ brandVariant: brandVariant || null,
49
+ geocodeUsed: result.geocodeUsed,
50
+ location: result.location,
51
+ count: result.stores.length,
52
+ stores: result.stores,
53
+ }, {
54
+ total: result.stores.length,
55
+ page: 1,
56
+ pageSize: limit,
57
+ });
58
+ }
59
+ catch (error) {
60
+ const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
61
+ return errorResponse(c, 'LOTTEMART_STORE_SEARCH_FAILED', message, 500);
62
+ }
63
+ }
64
+ /**
65
+ * 롯데마트 상품 검색 API 핸들러
66
+ * GET /api/lottemart/products?keyword={검색어}&storeCode={매장코드}
67
+ */
68
+ export async function handleLotteMartSearchProducts(c) {
69
+ const keyword = c.req.query('keyword') || '';
70
+ const area = c.req.query('area') || '';
71
+ const storeCode = c.req.query('storeCode') || '';
72
+ const storeName = c.req.query('storeName') || '';
73
+ const pageLimit = parseInt(c.req.query('pageLimit') || '3', 10);
74
+ const parsedTimeoutMs = parseInt(c.req.query('timeoutMs') || String(DEFAULT_LOTTEMART_TIMEOUT_MS), 10);
75
+ const timeoutMs = Number.isFinite(parsedTimeoutMs) && parsedTimeoutMs > 0 ? parsedTimeoutMs : DEFAULT_LOTTEMART_TIMEOUT_MS;
76
+ const source = parseLotteMartProductSource(c.req.query('source'));
77
+ if (keyword.trim().length === 0) {
78
+ return errorResponse(c, 'MISSING_QUERY', '검색어(keyword)를 입력해주세요.');
79
+ }
80
+ if (!source) {
81
+ return errorResponse(c, 'INVALID_LOTTEMART_SOURCE', 'source는 auto, legacy, zetta 중 하나여야 합니다.');
82
+ }
83
+ if (storeCode.trim().length === 0 && storeName.trim().length === 0) {
84
+ return errorResponse(c, 'MISSING_STORE', 'storeCode 또는 storeName 중 하나를 입력해주세요.');
85
+ }
86
+ try {
87
+ const result = await searchLotteMartProducts({
88
+ area,
89
+ storeCode,
90
+ storeName,
91
+ keyword,
92
+ pageLimit,
93
+ source,
94
+ }, {
95
+ timeout: timeoutMs,
96
+ zyteApiKey: c.env?.ZYTE_API_KEY,
97
+ });
98
+ return successResponse(c, {
99
+ area: result.area,
100
+ storeCode: result.storeCode,
101
+ storeName: result.storeName,
102
+ keyword,
103
+ pageLimit,
104
+ totalPages: result.totalPages,
105
+ products: result.products,
106
+ }, {
107
+ total: result.totalCount,
108
+ page: 1,
109
+ pageSize: result.products.length,
110
+ });
111
+ }
112
+ catch (error) {
113
+ const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
114
+ return errorResponse(c, 'LOTTEMART_PRODUCT_SEARCH_FAILED', message, 500);
115
+ }
116
+ }
117
+ /**
118
+ * 롯데마트 업스트림 단독 진단 API 핸들러
119
+ * GET /api/lottemart/debug?target=market-options|stores|products|product-page
120
+ */
121
+ export async function handleLotteMartDebug(c) {
122
+ const target = (c.req.query('target') || '');
123
+ const allowedTargets = ['market-options', 'stores', 'products', 'product-page'];
124
+ if (!allowedTargets.includes(target)) {
125
+ return errorResponse(c, 'INVALID_TARGET', 'target은 market-options, stores, products, product-page 중 하나여야 합니다.');
126
+ }
127
+ try {
128
+ const result = await probeLotteMartUpstream({
129
+ target,
130
+ area: c.req.query('area') || undefined,
131
+ type: (c.req.query('type') || undefined),
132
+ storeCode: c.req.query('storeCode') || undefined,
133
+ keyword: c.req.query('keyword') || undefined,
134
+ page: c.req.query('page') ? parseInt(c.req.query('page'), 10) : undefined,
135
+ timeout: c.req.query('timeoutMs') ? parseInt(c.req.query('timeoutMs'), 10) : DEFAULT_LOTTEMART_TIMEOUT_MS,
136
+ zyteApiKey: c.env?.ZYTE_API_KEY,
137
+ });
138
+ return successResponse(c, result);
139
+ }
140
+ catch (error) {
141
+ const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
142
+ return errorResponse(c, 'LOTTEMART_DEBUG_FAILED', message, 500);
143
+ }
144
+ }
@@ -1,63 +1,42 @@
1
1
  /**
2
2
  * 메가박스 GET API 핸들러
3
3
  */
4
- import { fetchMegaboxBookingList, fetchMegaboxTheaterInfo, toYyyymmdd, } from '../services/megabox/client.js';
4
+ import { fetchMegaboxBookingList, toYyyymmdd } from '../services/megabox/client.js';
5
+ import { fetchMegaboxNearbyTheaters, resolveMegaboxNearestTheater } from '../services/megabox/location.js';
5
6
  import { errorResponse, successResponse } from './response.js';
6
- function calculateDistanceKm(lat1, lng1, lat2, lng2) {
7
- const toRad = (deg) => (deg * Math.PI) / 180;
8
- const dLat = toRad(lat2 - lat1);
9
- const dLng = toRad(lng2 - lng1);
10
- const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
11
- Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
12
- const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
13
- return 6371 * c;
7
+ function parseOptionalNumber(value) {
8
+ if (!value) {
9
+ return undefined;
10
+ }
11
+ const parsed = Number.parseFloat(value);
12
+ return Number.isFinite(parsed) ? parsed : undefined;
14
13
  }
15
14
  /**
16
15
  * 메가박스 주변 지점 조회 API 핸들러
17
16
  * GET /api/megabox/theaters?lat={위도}&lng={경도}&playDate={YYYYMMDD}&areaCode={지역코드}
18
17
  */
19
18
  export async function handleMegaboxFindNearbyTheaters(c) {
20
- const lat = parseFloat(c.req.query('lat') || '37.5665');
21
- const lng = parseFloat(c.req.query('lng') || '126.978');
19
+ const keyword = c.req.query('keyword') || undefined;
20
+ const lat = parseOptionalNumber(c.req.query('lat'));
21
+ const lng = parseOptionalNumber(c.req.query('lng'));
22
22
  const playDate = c.req.query('playDate') || toYyyymmdd();
23
- const areaCode = c.req.query('areaCode') || '11';
23
+ const areaCode = c.req.query('areaCode') || undefined;
24
24
  const limit = parseInt(c.req.query('limit') || '10');
25
25
  const timeoutMs = parseInt(c.req.query('timeoutMs') || '15000');
26
26
  try {
27
- const { theaters } = await fetchMegaboxBookingList({
27
+ const result = await fetchMegaboxNearbyTheaters({
28
+ keyword,
29
+ latitude: lat,
30
+ longitude: lng,
28
31
  playDate,
29
32
  areaCode,
33
+ limit,
34
+ timeout: timeoutMs,
35
+ }, {
36
+ googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
30
37
  timeout: timeoutMs,
31
38
  });
32
- const infoResults = await Promise.allSettled(theaters.map((theater) => fetchMegaboxTheaterInfo(theater.theaterId, timeoutMs)));
33
- const merged = theaters
34
- .map((theater, index) => {
35
- const infoResult = infoResults[index];
36
- if (infoResult.status !== 'fulfilled') {
37
- return null;
38
- }
39
- if (infoResult.value.latitude === null || infoResult.value.longitude === null) {
40
- return null;
41
- }
42
- const distanceKm = calculateDistanceKm(lat, lng, infoResult.value.latitude, infoResult.value.longitude);
43
- return {
44
- theaterId: theater.theaterId,
45
- theaterName: theater.theaterName,
46
- address: infoResult.value.address,
47
- latitude: infoResult.value.latitude,
48
- longitude: infoResult.value.longitude,
49
- distanceKm: Number(distanceKm.toFixed(2)),
50
- };
51
- })
52
- .filter((theater) => theater !== null)
53
- .sort((a, b) => a.distanceKm - b.distanceKm)
54
- .slice(0, limit);
55
- return successResponse(c, {
56
- location: { latitude: lat, longitude: lng },
57
- playDate,
58
- areaCode,
59
- theaters: merged,
60
- }, { total: merged.length, pageSize: limit });
39
+ return successResponse(c, result, { total: result.count, pageSize: limit });
61
40
  }
62
41
  catch (error) {
63
42
  const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
@@ -70,16 +49,38 @@ export async function handleMegaboxFindNearbyTheaters(c) {
70
49
  */
71
50
  export async function handleMegaboxListNowShowing(c) {
72
51
  const playDate = c.req.query('playDate') || toYyyymmdd();
73
- const theaterId = c.req.query('theaterId') || undefined;
52
+ const keyword = c.req.query('keyword') || undefined;
53
+ const lat = parseOptionalNumber(c.req.query('lat'));
54
+ const lng = parseOptionalNumber(c.req.query('lng'));
55
+ let theaterId = c.req.query('theaterId') || undefined;
74
56
  const movieId = c.req.query('movieId') || undefined;
75
- const areaCode = c.req.query('areaCode') || '11';
57
+ const areaCode = c.req.query('areaCode') || undefined;
76
58
  const timeoutMs = parseInt(c.req.query('timeoutMs') || '15000');
77
59
  try {
60
+ let resolvedTheater = null;
61
+ let resolvedLocation = null;
62
+ if (!theaterId && (keyword || typeof lat === 'number' || typeof lng === 'number')) {
63
+ const resolved = await resolveMegaboxNearestTheater({
64
+ keyword,
65
+ latitude: lat,
66
+ longitude: lng,
67
+ areaCode,
68
+ playDate,
69
+ timeout: timeoutMs,
70
+ }, {
71
+ googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
72
+ timeout: timeoutMs,
73
+ });
74
+ theaterId = resolved.theater?.theaterId;
75
+ resolvedTheater = resolved.theater;
76
+ resolvedLocation = resolved.location;
77
+ }
78
+ const resolvedAreaCode = resolvedLocation?.areaCode || areaCode || '11';
78
79
  const result = await fetchMegaboxBookingList({
79
80
  playDate,
80
81
  theaterId,
81
82
  movieId,
82
- areaCode,
83
+ areaCode: resolvedAreaCode,
83
84
  timeout: timeoutMs,
84
85
  });
85
86
  return successResponse(c, {
@@ -87,8 +88,12 @@ export async function handleMegaboxListNowShowing(c) {
87
88
  filters: {
88
89
  theaterId: theaterId || null,
89
90
  movieId: movieId || null,
90
- areaCode,
91
+ keyword: keyword || null,
92
+ latitude: lat ?? null,
93
+ longitude: lng ?? null,
94
+ areaCode: resolvedAreaCode,
91
95
  },
96
+ resolvedTheater,
92
97
  theaters: result.theaters,
93
98
  movies: result.movies,
94
99
  showtimes: result.showtimes,
@@ -105,17 +110,39 @@ export async function handleMegaboxListNowShowing(c) {
105
110
  */
106
111
  export async function handleMegaboxGetRemainingSeats(c) {
107
112
  const playDate = c.req.query('playDate') || toYyyymmdd();
108
- const theaterId = c.req.query('theaterId') || undefined;
113
+ const keyword = c.req.query('keyword') || undefined;
114
+ const lat = parseOptionalNumber(c.req.query('lat'));
115
+ const lng = parseOptionalNumber(c.req.query('lng'));
116
+ let theaterId = c.req.query('theaterId') || undefined;
109
117
  const movieId = c.req.query('movieId') || undefined;
110
- const areaCode = c.req.query('areaCode') || '11';
118
+ const areaCode = c.req.query('areaCode') || undefined;
111
119
  const limit = parseInt(c.req.query('limit') || '50');
112
120
  const timeoutMs = parseInt(c.req.query('timeoutMs') || '15000');
113
121
  try {
122
+ let resolvedTheater = null;
123
+ let resolvedLocation = null;
124
+ if (!theaterId && (keyword || typeof lat === 'number' || typeof lng === 'number')) {
125
+ const resolved = await resolveMegaboxNearestTheater({
126
+ keyword,
127
+ latitude: lat,
128
+ longitude: lng,
129
+ areaCode,
130
+ playDate,
131
+ timeout: timeoutMs,
132
+ }, {
133
+ googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
134
+ timeout: timeoutMs,
135
+ });
136
+ theaterId = resolved.theater?.theaterId;
137
+ resolvedTheater = resolved.theater;
138
+ resolvedLocation = resolved.location;
139
+ }
140
+ const resolvedAreaCode = resolvedLocation?.areaCode || areaCode || '11';
114
141
  const { showtimes } = await fetchMegaboxBookingList({
115
142
  playDate,
116
143
  theaterId,
117
144
  movieId,
118
- areaCode,
145
+ areaCode: resolvedAreaCode,
119
146
  timeout: timeoutMs,
120
147
  });
121
148
  const seats = showtimes
@@ -133,8 +160,12 @@ export async function handleMegaboxGetRemainingSeats(c) {
133
160
  filters: {
134
161
  theaterId: theaterId || null,
135
162
  movieId: movieId || null,
136
- areaCode,
163
+ keyword: keyword || null,
164
+ latitude: lat ?? null,
165
+ longitude: lng ?? null,
166
+ areaCode: resolvedAreaCode,
137
167
  },
168
+ resolvedTheater,
138
169
  seats,
139
170
  }, { total: seats.length, pageSize: limit });
140
171
  }
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * API 공통 응답 유틸리티
3
3
  */
4
+ import { toStandardErrorDiagnostics } from '../core/errors.js';
4
5
  /**
5
6
  * 성공 응답 생성
6
7
  */
@@ -18,5 +19,6 @@ export function errorResponse(c, code, message, status = 400) {
18
19
  return c.json({
19
20
  success: false,
20
21
  error: { code, message },
22
+ diagnostics: toStandardErrorDiagnostics(code, message, { status }),
21
23
  }, status);
22
24
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * CU GET API 라우트 등록
3
+ */
4
+ import { withEdgeCache } from '../../utils/cache.js';
5
+ import { handleCuFindStores, handleCuCheckInventory } from '../handlers.js';
6
+ export function registerCuRoutes(app) {
7
+ app.get('/api/cu/stores', async (c) => withEdgeCache(c.req.url, {
8
+ ttlSeconds: 60 * 60 * 24,
9
+ staleWhileRevalidateSeconds: 60 * 5,
10
+ keyPrefix: 'cu-stores-v1',
11
+ }, () => handleCuFindStores(c)));
12
+ app.get('/api/cu/inventory', async (c) => withEdgeCache(c.req.url, {
13
+ ttlSeconds: 60 * 10,
14
+ staleWhileRevalidateSeconds: 60,
15
+ keyPrefix: 'cu-inventory-v1',
16
+ }, () => handleCuCheckInventory(c)));
17
+ }
@@ -2,7 +2,7 @@
2
2
  * 다이소 GET API 라우트 등록
3
3
  */
4
4
  import { withEdgeCache } from '../../utils/cache.js';
5
- import { handleSearchProducts, handleGetProduct, handleFindStores, handleCheckInventory, } from '../handlers.js';
5
+ import { handleSearchProducts, handleGetProduct, handleFindStores, handleCheckInventory, handleGetDisplayLocation, } from '../handlers.js';
6
6
  export function registerDaisoRoutes(app) {
7
7
  app.get('/api/daiso/products', async (c) => withEdgeCache(c.req.url, {
8
8
  ttlSeconds: 60 * 30,
@@ -24,4 +24,9 @@ export function registerDaisoRoutes(app) {
24
24
  staleWhileRevalidateSeconds: 60,
25
25
  keyPrefix: 'daiso-inventory-v1',
26
26
  }, () => handleCheckInventory(c)));
27
+ app.get('/api/daiso/display-location', async (c) => withEdgeCache(c.req.url, {
28
+ ttlSeconds: 60 * 10,
29
+ staleWhileRevalidateSeconds: 60,
30
+ keyPrefix: 'daiso-display-location-v1',
31
+ }, () => handleGetDisplayLocation(c)));
27
32
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * 이마트24 GET API 라우트 등록
3
+ */
4
+ import { withEdgeCache } from '../../utils/cache.js';
5
+ import { handleEmart24CheckInventory, handleEmart24FindStores, handleEmart24SearchProducts, } from '../emart24Handlers.js';
6
+ export function registerEmart24Routes(app) {
7
+ app.get('/api/emart24/stores', async (c) => withEdgeCache(c.req.url, {
8
+ ttlSeconds: 60 * 30,
9
+ staleWhileRevalidateSeconds: 60 * 3,
10
+ keyPrefix: 'emart24-stores-v1',
11
+ }, () => handleEmart24FindStores(c)));
12
+ app.get('/api/emart24/products', async (c) => withEdgeCache(c.req.url, {
13
+ ttlSeconds: 60 * 10,
14
+ staleWhileRevalidateSeconds: 60,
15
+ keyPrefix: 'emart24-products-v1',
16
+ }, () => handleEmart24SearchProducts(c)));
17
+ app.get('/api/emart24/inventory', async (c) => withEdgeCache(c.req.url, {
18
+ ttlSeconds: 60 * 5,
19
+ staleWhileRevalidateSeconds: 60,
20
+ keyPrefix: 'emart24-inventory-v1',
21
+ }, () => handleEmart24CheckInventory(c)));
22
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * GS25 GET API 라우트 등록
3
+ */
4
+ import { withEdgeCache } from '../../utils/cache.js';
5
+ import { handleGs25CheckInventory, handleGs25FindStores, handleGs25SearchProducts, } from '../gs25Handlers.js';
6
+ export function registerGs25Routes(app) {
7
+ app.get('/api/gs25/stores', async (c) => withEdgeCache(c.req.url, {
8
+ ttlSeconds: 60 * 10,
9
+ staleWhileRevalidateSeconds: 60,
10
+ keyPrefix: 'gs25-stores-v1',
11
+ }, () => handleGs25FindStores(c)));
12
+ app.get('/api/gs25/products', async (c) => withEdgeCache(c.req.url, {
13
+ ttlSeconds: 60 * 3,
14
+ staleWhileRevalidateSeconds: 60,
15
+ keyPrefix: 'gs25-products-v1',
16
+ }, () => handleGs25SearchProducts(c)));
17
+ app.get('/api/gs25/inventory', async (c) => withEdgeCache(c.req.url, {
18
+ ttlSeconds: 60 * 2,
19
+ staleWhileRevalidateSeconds: 30,
20
+ keyPrefix: 'gs25-inventory-v1',
21
+ }, () => handleGs25CheckInventory(c)));
22
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * 운영용 상세 헬스 체크 라우트
3
+ */
4
+ import { errorResponse } from '../response.js';
5
+ import { runHealthChecks } from '../healthChecks.js';
6
+ function parseBoolean(value) {
7
+ const normalized = (value || '').trim().toLowerCase();
8
+ return normalized === 'true' || normalized === '1' || normalized === 'y';
9
+ }
10
+ function readHealthCheckToken(headers) {
11
+ const authorization = headers.get('Authorization') || '';
12
+ if (authorization.toLowerCase().startsWith('bearer ')) {
13
+ return authorization.slice('bearer '.length).trim();
14
+ }
15
+ return (headers.get('x-health-check-key') || '').trim();
16
+ }
17
+ function parseMode(value) {
18
+ if (!value || value === 'quick') {
19
+ return 'quick';
20
+ }
21
+ if (value === 'deep' || value === 'full') {
22
+ return value;
23
+ }
24
+ return null;
25
+ }
26
+ export function registerHealthRoutes(app) {
27
+ app.get('/api/health/checks', async (c) => {
28
+ const secret = c.env?.HEALTH_CHECK_SECRET?.trim();
29
+ if (!secret) {
30
+ return errorResponse(c, 'HEALTH_CHECK_SECRET_NOT_CONFIGURED', 'HEALTH_CHECK_SECRET이 설정되어 있지 않습니다.', 503);
31
+ }
32
+ if (readHealthCheckToken(c.req.raw.headers) !== secret) {
33
+ return errorResponse(c, 'UNAUTHORIZED_HEALTH_CHECK', '유효한 헬스 체크 시크릿 키가 필요합니다.', 401);
34
+ }
35
+ const mode = parseMode(c.req.query('mode'));
36
+ if (!mode) {
37
+ return errorResponse(c, 'INVALID_HEALTH_CHECK_MODE', 'mode는 quick, deep, full 중 하나여야 합니다.', 400);
38
+ }
39
+ const timeoutMs = Number.parseInt(c.req.query('timeoutMs') || '3000', 10);
40
+ const slowThresholdMs = Number.parseInt(c.req.query('slowThresholdMs') || c.env?.HEALTH_CHECK_SLOW_THRESHOLD_MS || '0', 10);
41
+ const baseUrl = c.req.query('baseUrl')?.trim() || c.env?.HEALTH_CHECK_BASE_URL?.trim() || new URL(c.req.url).origin;
42
+ const transport = c.req.query('transport') || c.env?.HEALTH_CHECK_TRANSPORT || 'internal';
43
+ const cacheBust = parseBoolean(c.req.query('cacheBust')) || transport === 'network';
44
+ const fetchImpl = c.env?.HEALTH_CHECK_BASE_URL && transport !== 'network'
45
+ ? async (input, init) => app.fetch(new Request(input, init), c.env)
46
+ : undefined;
47
+ const result = await runHealthChecks({
48
+ baseUrl,
49
+ service: c.req.query('service') || undefined,
50
+ check: c.req.query('check') || undefined,
51
+ mode,
52
+ timeoutMs,
53
+ slowThresholdMs,
54
+ includeSamples: parseBoolean(c.req.query('includeSamples')),
55
+ fresh: parseBoolean(c.req.query('fresh')),
56
+ cacheBust,
57
+ fetchImpl,
58
+ });
59
+ return c.json(result);
60
+ });
61
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * 롯데시네마 GET API 라우트 등록
3
+ */
4
+ import { withEdgeCache } from '../../utils/cache.js';
5
+ import { handleLotteCinemaFindNearbyTheaters, handleLotteCinemaGetRemainingSeats, handleLotteCinemaListNowShowing, } from '../lottecinemaHandlers.js';
6
+ export function registerLotteCinemaRoutes(app) {
7
+ app.get('/api/lottecinema/theaters', async (c) => withEdgeCache(c.req.url, {
8
+ ttlSeconds: 60 * 60 * 24,
9
+ staleWhileRevalidateSeconds: 60 * 5,
10
+ keyPrefix: 'lottecinema-theaters-v1',
11
+ }, () => handleLotteCinemaFindNearbyTheaters(c)));
12
+ app.get('/api/lottecinema/movies', async (c) => withEdgeCache(c.req.url, {
13
+ ttlSeconds: 60 * 10,
14
+ staleWhileRevalidateSeconds: 60,
15
+ keyPrefix: 'lottecinema-movies-v1',
16
+ }, () => handleLotteCinemaListNowShowing(c)));
17
+ app.get('/api/lottecinema/seats', async (c) => withEdgeCache(c.req.url, {
18
+ ttlSeconds: 60 * 3,
19
+ staleWhileRevalidateSeconds: 30,
20
+ keyPrefix: 'lottecinema-seats-v1',
21
+ }, () => handleLotteCinemaGetRemainingSeats(c)));
22
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * 롯데마트 GET API 라우트 등록
3
+ */
4
+ import { withEdgeCache } from '../../utils/cache.js';
5
+ import { handleLotteMartDebug, handleLotteMartFindStores, handleLotteMartSearchProducts } from '../lottemartHandlers.js';
6
+ export function registerLotteMartRoutes(app) {
7
+ app.get('/api/lottemart/stores', async (c) => withEdgeCache(c.req.url, {
8
+ ttlSeconds: 60 * 30,
9
+ staleWhileRevalidateSeconds: 60 * 3,
10
+ keyPrefix: 'lottemart-stores-v1',
11
+ }, () => handleLotteMartFindStores(c)));
12
+ app.get('/api/lottemart/products', async (c) => withEdgeCache(c.req.url, {
13
+ ttlSeconds: 60 * 5,
14
+ staleWhileRevalidateSeconds: 60,
15
+ keyPrefix: 'lottemart-products-v1',
16
+ }, () => handleLotteMartSearchProducts(c)));
17
+ app.get('/api/lottemart/debug', (c) => handleLotteMartDebug(c));
18
+ }
@@ -2,8 +2,13 @@
2
2
  * 올리브영 GET API 라우트 등록
3
3
  */
4
4
  import { withEdgeCache } from '../../utils/cache.js';
5
- import { handleOliveyoungFindStores, handleOliveyoungCheckInventory } from '../handlers.js';
5
+ import { handleOliveyoungFindStores, handleOliveyoungCheckInventory, handleOliveyoungSearchProducts, } from '../handlers.js';
6
6
  export function registerOliveyoungRoutes(app) {
7
+ app.get('/api/oliveyoung/products', async (c) => withEdgeCache(c.req.url, {
8
+ ttlSeconds: 60 * 5,
9
+ staleWhileRevalidateSeconds: 60,
10
+ keyPrefix: 'oliveyoung-products-v1',
11
+ }, () => handleOliveyoungSearchProducts(c)));
7
12
  app.get('/api/oliveyoung/stores', async (c) => withEdgeCache(c.req.url, {
8
13
  ttlSeconds: 60 * 60 * 24,
9
14
  staleWhileRevalidateSeconds: 60 * 5,
@@ -0,0 +1,32 @@
1
+ /**
2
+ * 세븐일레븐 GET API 라우트 등록
3
+ */
4
+ import { withEdgeCache } from '../../utils/cache.js';
5
+ import { handleSevenElevenCheckInventory, handleSevenElevenGetCatalogSnapshot, handleSevenElevenGetSearchPopwords, handleSevenElevenSearchStores, handleSevenElevenSearchProducts, } from '../sevenelevenHandlers.js';
6
+ export function registerSevenElevenRoutes(app) {
7
+ app.get('/api/seveneleven/products', async (c) => withEdgeCache(c.req.url, {
8
+ ttlSeconds: 60 * 3,
9
+ staleWhileRevalidateSeconds: 60,
10
+ keyPrefix: 'seveneleven-products-v1',
11
+ }, () => handleSevenElevenSearchProducts(c)));
12
+ app.get('/api/seveneleven/stores', async (c) => withEdgeCache(c.req.url, {
13
+ ttlSeconds: 60 * 3,
14
+ staleWhileRevalidateSeconds: 60,
15
+ keyPrefix: 'seveneleven-stores-v1',
16
+ }, () => handleSevenElevenSearchStores(c)));
17
+ app.get('/api/seveneleven/inventory', async (c) => withEdgeCache(c.req.url, {
18
+ ttlSeconds: 60 * 10,
19
+ staleWhileRevalidateSeconds: 60,
20
+ keyPrefix: 'seveneleven-inventory-v1',
21
+ }, () => handleSevenElevenCheckInventory(c)));
22
+ app.get('/api/seveneleven/popwords', async (c) => withEdgeCache(c.req.url, {
23
+ ttlSeconds: 60 * 5,
24
+ staleWhileRevalidateSeconds: 60,
25
+ keyPrefix: 'seveneleven-popwords-v1',
26
+ }, () => handleSevenElevenGetSearchPopwords(c)));
27
+ app.get('/api/seveneleven/catalog', async (c) => withEdgeCache(c.req.url, {
28
+ ttlSeconds: 60 * 5,
29
+ staleWhileRevalidateSeconds: 60,
30
+ keyPrefix: 'seveneleven-catalog-v1',
31
+ }, () => handleSevenElevenGetCatalogSnapshot(c)));
32
+ }