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
@@ -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
  }
@@ -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,44 @@
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
+ export function registerHealthRoutes(app) {
18
+ app.get('/api/health/checks', async (c) => {
19
+ const secret = c.env?.HEALTH_CHECK_SECRET?.trim();
20
+ if (!secret) {
21
+ return errorResponse(c, 'HEALTH_CHECK_SECRET_NOT_CONFIGURED', 'HEALTH_CHECK_SECRET이 설정되어 있지 않습니다.', 503);
22
+ }
23
+ if (readHealthCheckToken(c.req.raw.headers) !== secret) {
24
+ return errorResponse(c, 'UNAUTHORIZED_HEALTH_CHECK', '유효한 헬스 체크 시크릿 키가 필요합니다.', 401);
25
+ }
26
+ const mode = c.req.query('mode') === 'deep' ? 'deep' : 'quick';
27
+ const timeoutMs = Number.parseInt(c.req.query('timeoutMs') || '3000', 10);
28
+ const baseUrl = c.env?.HEALTH_CHECK_BASE_URL?.trim() || new URL(c.req.url).origin;
29
+ const fetchImpl = c.env?.HEALTH_CHECK_BASE_URL && c.env?.HEALTH_CHECK_TRANSPORT !== 'network'
30
+ ? async (input, init) => app.fetch(new Request(input, init), c.env)
31
+ : undefined;
32
+ const result = await runHealthChecks({
33
+ baseUrl,
34
+ service: c.req.query('service') || undefined,
35
+ check: c.req.query('check') || undefined,
36
+ mode,
37
+ timeoutMs,
38
+ includeSamples: parseBoolean(c.req.query('includeSamples')),
39
+ fresh: parseBoolean(c.req.query('fresh')),
40
+ fetchImpl,
41
+ });
42
+ return c.json(result);
43
+ });
44
+ }
@@ -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
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * 세븐일레븐 GET API 핸들러
3
+ */
4
+ /* c8 ignore start */
5
+ import { errorResponse, successResponse } from './response.js';
6
+ import { fetchSevenElevenStoresByKeyword, fetchSevenElevenCatalogSnapshot, fetchSevenElevenSearchPopwords, searchSevenElevenProducts, } from '../services/seveneleven/client.js';
7
+ import { checkSevenElevenInventory } from '../services/seveneleven/inventory.js';
8
+ function parseBoolean(value, defaultValue) {
9
+ if (typeof value !== 'string') {
10
+ return defaultValue;
11
+ }
12
+ const normalized = value.trim().toLowerCase();
13
+ if (normalized === 'true' || normalized === '1' || normalized === 'y') {
14
+ return true;
15
+ }
16
+ if (normalized === 'false' || normalized === '0' || normalized === 'n') {
17
+ return false;
18
+ }
19
+ return defaultValue;
20
+ }
21
+ /**
22
+ * 세븐일레븐 상품 검색 API 핸들러
23
+ * GET /api/seveneleven/products?query={검색어}&page={페이지}&size={개수}&sort={정렬}
24
+ */
25
+ export async function handleSevenElevenSearchProducts(c) {
26
+ const query = c.req.query('query') || '';
27
+ const page = parseInt(c.req.query('page') || '1', 10);
28
+ const size = parseInt(c.req.query('size') || '20', 10);
29
+ const sort = c.req.query('sort') || 'recommend';
30
+ if (query.trim().length === 0) {
31
+ return errorResponse(c, 'MISSING_QUERY', '검색어(query)를 입력해주세요.');
32
+ }
33
+ try {
34
+ const result = await searchSevenElevenProducts({
35
+ query,
36
+ page,
37
+ size,
38
+ sort,
39
+ });
40
+ return successResponse(c, {
41
+ query: result.query,
42
+ count: result.products.length,
43
+ totalCount: result.totalCount,
44
+ collectionIds: result.collectionIds,
45
+ products: result.products,
46
+ }, {
47
+ total: result.totalCount,
48
+ page,
49
+ pageSize: size,
50
+ });
51
+ }
52
+ catch (error) {
53
+ const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
54
+ return errorResponse(c, 'SEVENELEVEN_PRODUCT_SEARCH_FAILED', message, 500);
55
+ }
56
+ }
57
+ /**
58
+ * 세븐일레븐 매장 검색 API 핸들러
59
+ * GET /api/seveneleven/stores?keyword={검색어}&limit={개수}
60
+ */
61
+ export async function handleSevenElevenSearchStores(c) {
62
+ const keyword = c.req.query('keyword') || '';
63
+ const limit = parseInt(c.req.query('limit') || '20', 10);
64
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? limit : 20;
65
+ if (keyword.trim().length === 0) {
66
+ return errorResponse(c, 'MISSING_KEYWORD', '매장 검색어(keyword)를 입력해주세요.');
67
+ }
68
+ try {
69
+ const result = await fetchSevenElevenStoresByKeyword({
70
+ keyword,
71
+ limit: safeLimit,
72
+ });
73
+ return successResponse(c, {
74
+ keyword: result.query,
75
+ count: result.stores.length,
76
+ totalCount: result.totalCount,
77
+ stores: result.stores,
78
+ }, {
79
+ total: result.totalCount,
80
+ page: 1,
81
+ pageSize: safeLimit,
82
+ });
83
+ }
84
+ catch (error) {
85
+ const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
86
+ return errorResponse(c, 'SEVENELEVEN_STORE_SEARCH_FAILED', message, 500);
87
+ }
88
+ }
89
+ /**
90
+ * 세븐일레븐 재고 확인 API 핸들러
91
+ * GET /api/seveneleven/inventory?keyword={검색어}&storeKeyword={매장키워드}
92
+ */
93
+ export async function handleSevenElevenCheckInventory(c) {
94
+ const keyword = c.req.query('keyword') || '';
95
+ const storeKeyword = c.req.query('storeKeyword') || '';
96
+ const storeLimit = parseInt(c.req.query('storeLimit') || '20', 10);
97
+ const timeoutMs = parseInt(c.req.query('timeoutMs') || '20000', 10);
98
+ const safeStoreLimit = Number.isFinite(storeLimit) && storeLimit > 0 ? storeLimit : 20;
99
+ const safeTimeoutMs = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 20000;
100
+ if (keyword.trim().length === 0) {
101
+ return errorResponse(c, 'MISSING_QUERY', '검색어(keyword)를 입력해주세요.');
102
+ }
103
+ try {
104
+ const result = await checkSevenElevenInventory({
105
+ productKeyword: keyword,
106
+ storeKeyword,
107
+ storeLimit: safeStoreLimit,
108
+ }, {
109
+ timeout: safeTimeoutMs,
110
+ });
111
+ const note = result.stockAvailable
112
+ ? '실시간 재고 데이터가 포함되어 있습니다.'
113
+ : result.stockError
114
+ ? `실시간 재고 API 호출에 실패했습니다: ${result.stockError.message}`
115
+ : '실시간 재고 API가 현재 제한되어 있어 매장 목록만 제공됩니다.';
116
+ return successResponse(c, {
117
+ keyword,
118
+ storeKeyword,
119
+ product: result.product,
120
+ stockAvailable: result.stockAvailable,
121
+ stockError: result.stockError,
122
+ note,
123
+ inventory: {
124
+ totalStoreCount: result.totalStoreCount,
125
+ inStockStoreCount: result.inStockStoreCount,
126
+ count: result.stores.length,
127
+ stores: result.stores,
128
+ },
129
+ }, {
130
+ total: result.totalStoreCount,
131
+ pageSize: safeStoreLimit,
132
+ });
133
+ }
134
+ catch (error) {
135
+ const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
136
+ return errorResponse(c, 'SEVENELEVEN_INVENTORY_CHECK_FAILED', message, 500);
137
+ }
138
+ }
139
+ /**
140
+ * 세븐일레븐 인기 검색어 API 핸들러
141
+ * GET /api/seveneleven/popwords?label={라벨}
142
+ */
143
+ export async function handleSevenElevenGetSearchPopwords(c) {
144
+ const label = c.req.query('label') || 'home';
145
+ try {
146
+ const keywords = await fetchSevenElevenSearchPopwords(label);
147
+ return successResponse(c, {
148
+ label,
149
+ available: keywords.length > 0,
150
+ count: keywords.length,
151
+ keywords,
152
+ note: keywords.length === 0
153
+ ? '현재 응답에서 인기 검색어 목록을 찾지 못했습니다.'
154
+ : '홈 인기 검색어를 조회했습니다.',
155
+ });
156
+ }
157
+ catch (error) {
158
+ const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
159
+ return errorResponse(c, 'SEVENELEVEN_POPWORD_FETCH_FAILED', message, 500);
160
+ }
161
+ }
162
+ /**
163
+ * 세븐일레븐 카탈로그 스냅샷 API 핸들러
164
+ * GET /api/seveneleven/catalog?includeIssues={bool}&includeExhibition={bool}&limit={개수}
165
+ */
166
+ export async function handleSevenElevenGetCatalogSnapshot(c) {
167
+ const includeIssues = parseBoolean(c.req.query('includeIssues'), true);
168
+ const includeExhibition = parseBoolean(c.req.query('includeExhibition'), true);
169
+ const limit = parseInt(c.req.query('limit') || '20', 10);
170
+ try {
171
+ const result = await fetchSevenElevenCatalogSnapshot({
172
+ includeIssues,
173
+ includeExhibition,
174
+ });
175
+ return successResponse(c, {
176
+ options: {
177
+ includeIssues,
178
+ includeExhibition,
179
+ limit,
180
+ },
181
+ pages: {
182
+ totalCount: result.pages.length,
183
+ items: result.pages.slice(0, limit),
184
+ },
185
+ issues: {
186
+ totalCount: result.issues.length,
187
+ items: result.issues.slice(0, limit),
188
+ },
189
+ exhibitions: {
190
+ totalCount: result.exhibitions.length,
191
+ items: result.exhibitions.slice(0, limit),
192
+ },
193
+ });
194
+ }
195
+ catch (error) {
196
+ const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
197
+ return errorResponse(c, 'SEVENELEVEN_CATALOG_FETCH_FAILED', message, 500);
198
+ }
199
+ }
200
+ /* c8 ignore stop */
@@ -0,0 +1,54 @@
1
+ /**
2
+ * CLI 인자 파싱 및 URL 조작 유틸리티
3
+ */
4
+ import { DEFAULT_BASE_URL } from './constants.js';
5
+ export function parseCliArgs(args) {
6
+ const positionals = [];
7
+ const options = {};
8
+ for (let index = 0; index < args.length; index += 1) {
9
+ const token = args[index];
10
+ if (!token.startsWith('--')) {
11
+ positionals.push(token);
12
+ continue;
13
+ }
14
+ const withoutPrefix = token.slice(2);
15
+ const equalIndex = withoutPrefix.indexOf('=');
16
+ if (equalIndex >= 0) {
17
+ const key = withoutPrefix.slice(0, equalIndex);
18
+ const value = withoutPrefix.slice(equalIndex + 1);
19
+ options[key] = value;
20
+ continue;
21
+ }
22
+ const key = withoutPrefix;
23
+ const nextValue = args[index + 1];
24
+ if (!nextValue || nextValue.startsWith('--')) {
25
+ options[key] = 'true';
26
+ continue;
27
+ }
28
+ options[key] = nextValue;
29
+ index += 1;
30
+ }
31
+ return { positionals, options };
32
+ }
33
+ export function toUrl(pathOrUrl) {
34
+ if (pathOrUrl.startsWith('http://') || pathOrUrl.startsWith('https://')) {
35
+ return new URL(pathOrUrl);
36
+ }
37
+ const normalizedPath = pathOrUrl.startsWith('/') ? pathOrUrl : `/${pathOrUrl}`;
38
+ return new URL(normalizedPath, DEFAULT_BASE_URL);
39
+ }
40
+ export function applyOptionsToQuery(url, options) {
41
+ for (const [key, value] of Object.entries(options)) {
42
+ url.searchParams.set(key, value);
43
+ }
44
+ }
45
+ export function toQueryOptions(options) {
46
+ const queryOptions = {};
47
+ for (const [key, value] of Object.entries(options)) {
48
+ if (key === 'help' || key === 'json') {
49
+ continue;
50
+ }
51
+ queryOptions[key] = value;
52
+ }
53
+ return queryOptions;
54
+ }