daiso 1.0.0

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 (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +741 -0
  3. package/dist/api/cgvHandlers.js +97 -0
  4. package/dist/api/handlers.js +208 -0
  5. package/dist/api/megaboxHandlers.js +145 -0
  6. package/dist/api/response.js +22 -0
  7. package/dist/api/routes/cgvRoutes.js +22 -0
  8. package/dist/api/routes/daisoRoutes.js +27 -0
  9. package/dist/api/routes/megaboxRoutes.js +22 -0
  10. package/dist/api/routes/oliveyoungRoutes.js +17 -0
  11. package/dist/cli.js +140 -0
  12. package/dist/core/interfaces.js +6 -0
  13. package/dist/core/registry.js +121 -0
  14. package/dist/core/types.js +6 -0
  15. package/dist/index.js +123 -0
  16. package/dist/pages/openapi.js +6 -0
  17. package/dist/pages/openapiResponse.js +32 -0
  18. package/dist/pages/openapiSpec.js +43 -0
  19. package/dist/pages/openapiSpecComponents.js +419 -0
  20. package/dist/pages/openapiSpecComponentsCgv.js +123 -0
  21. package/dist/pages/openapiSpecPathsCgv.js +149 -0
  22. package/dist/pages/openapiSpecPathsDaisoOliveyoung.js +381 -0
  23. package/dist/pages/openapiSpecPathsMegabox.js +184 -0
  24. package/dist/pages/openapiYaml.js +58 -0
  25. package/dist/pages/privacy.js +262 -0
  26. package/dist/pages/prompt.js +425 -0
  27. package/dist/services/cgv/api.js +15 -0
  28. package/dist/services/cgv/client.js +146 -0
  29. package/dist/services/cgv/index.js +30 -0
  30. package/dist/services/cgv/timetable.js +16 -0
  31. package/dist/services/cgv/tools/findTheaters.js +43 -0
  32. package/dist/services/cgv/tools/getTimetable.js +47 -0
  33. package/dist/services/cgv/tools/searchMovies.js +40 -0
  34. package/dist/services/cgv/transport.js +91 -0
  35. package/dist/services/cgv/types.js +4 -0
  36. package/dist/services/daiso/api.js +53 -0
  37. package/dist/services/daiso/client.js +35 -0
  38. package/dist/services/daiso/index.js +42 -0
  39. package/dist/services/daiso/tools/checkInventory.js +122 -0
  40. package/dist/services/daiso/tools/findStores.js +145 -0
  41. package/dist/services/daiso/tools/getPriceInfo.js +90 -0
  42. package/dist/services/daiso/tools/searchProducts.js +74 -0
  43. package/dist/services/daiso/types.js +4 -0
  44. package/dist/services/megabox/api.js +8 -0
  45. package/dist/services/megabox/client.js +140 -0
  46. package/dist/services/megabox/index.js +26 -0
  47. package/dist/services/megabox/tools/findNearbyTheaters.js +74 -0
  48. package/dist/services/megabox/tools/getRemainingSeats.js +57 -0
  49. package/dist/services/megabox/tools/listNowShowing.js +51 -0
  50. package/dist/services/megabox/types.js +4 -0
  51. package/dist/services/oliveyoung/api.js +8 -0
  52. package/dist/services/oliveyoung/client.js +90 -0
  53. package/dist/services/oliveyoung/index.js +28 -0
  54. package/dist/services/oliveyoung/tools/checkInventory.js +87 -0
  55. package/dist/services/oliveyoung/tools/findNearbyStores.js +53 -0
  56. package/dist/services/oliveyoung/types.js +4 -0
  57. package/dist/tools/checkInventory.js +98 -0
  58. package/dist/tools/findStores.js +125 -0
  59. package/dist/tools/getPriceInfo.js +77 -0
  60. package/dist/tools/searchProducts.js +61 -0
  61. package/dist/types/index.js +4 -0
  62. package/dist/utils/cache.js +54 -0
  63. package/dist/utils/fetch.js +14 -0
  64. package/dist/utils/format.js +31 -0
  65. package/dist/utils/http.js +35 -0
  66. package/dist/utils/zyte.js +76 -0
  67. package/openapi.json +1938 -0
  68. package/openapi.yaml +1338 -0
  69. package/package.json +79 -0
@@ -0,0 +1,97 @@
1
+ /**
2
+ * CGV GET API 핸들러
3
+ */
4
+ import { fetchCgvMovies, fetchCgvTheaters, fetchCgvTimetable, toYyyymmdd } from '../services/cgv/client.js';
5
+ import { filterAndSortTimetable } from '../services/cgv/timetable.js';
6
+ import { errorResponse, successResponse } from './response.js';
7
+ /**
8
+ * CGV 극장 목록 조회 API 핸들러
9
+ * GET /api/cgv/theaters?playDate={YYYYMMDD}&regionCode={지역코드}
10
+ */
11
+ export async function handleCgvFindTheaters(c) {
12
+ const playDate = c.req.query('playDate') || toYyyymmdd();
13
+ const regionCode = c.req.query('regionCode') || undefined;
14
+ const limit = parseInt(c.req.query('limit') || '30');
15
+ const timeoutMs = parseInt(c.req.query('timeoutMs') || '15000');
16
+ try {
17
+ const theaters = await fetchCgvTheaters({
18
+ playDate,
19
+ regionCode,
20
+ timeout: timeoutMs,
21
+ zyteApiKey: c.env?.ZYTE_API_KEY,
22
+ });
23
+ const sliced = theaters.slice(0, limit);
24
+ return successResponse(c, {
25
+ playDate,
26
+ filters: {
27
+ regionCode: regionCode || null,
28
+ },
29
+ theaters: sliced,
30
+ }, { total: sliced.length, pageSize: limit });
31
+ }
32
+ catch (error) {
33
+ const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
34
+ return errorResponse(c, 'CGV_THEATER_SEARCH_FAILED', message, 500);
35
+ }
36
+ }
37
+ /**
38
+ * CGV 영화 목록 조회 API 핸들러
39
+ * GET /api/cgv/movies?playDate={YYYYMMDD}&theaterCode={극장코드}
40
+ */
41
+ export async function handleCgvSearchMovies(c) {
42
+ const playDate = c.req.query('playDate') || toYyyymmdd();
43
+ const theaterCode = c.req.query('theaterCode') || undefined;
44
+ const timeoutMs = parseInt(c.req.query('timeoutMs') || '15000');
45
+ try {
46
+ const movies = await fetchCgvMovies({
47
+ playDate,
48
+ theaterCode,
49
+ timeout: timeoutMs,
50
+ zyteApiKey: c.env?.ZYTE_API_KEY,
51
+ });
52
+ return successResponse(c, {
53
+ playDate,
54
+ filters: {
55
+ theaterCode: theaterCode || null,
56
+ },
57
+ movies,
58
+ }, { total: movies.length });
59
+ }
60
+ catch (error) {
61
+ const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
62
+ return errorResponse(c, 'CGV_MOVIE_SEARCH_FAILED', message, 500);
63
+ }
64
+ }
65
+ /**
66
+ * CGV 시간표 조회 API 핸들러
67
+ * GET /api/cgv/timetable?playDate={YYYYMMDD}&theaterCode={극장코드}&movieCode={영화코드}
68
+ */
69
+ export async function handleCgvGetTimetable(c) {
70
+ const playDate = c.req.query('playDate') || toYyyymmdd();
71
+ const theaterCode = c.req.query('theaterCode') || undefined;
72
+ const movieCode = c.req.query('movieCode') || undefined;
73
+ const limit = parseInt(c.req.query('limit') || '50');
74
+ const timeoutMs = parseInt(c.req.query('timeoutMs') || '15000');
75
+ try {
76
+ const timetable = await fetchCgvTimetable({
77
+ playDate,
78
+ theaterCode,
79
+ movieCode,
80
+ timeout: timeoutMs,
81
+ zyteApiKey: c.env?.ZYTE_API_KEY,
82
+ });
83
+ const filtered = filterAndSortTimetable(timetable, { theaterCode, movieCode, limit });
84
+ return successResponse(c, {
85
+ playDate,
86
+ filters: {
87
+ theaterCode: theaterCode || null,
88
+ movieCode: movieCode || null,
89
+ },
90
+ timetable: filtered,
91
+ }, { total: filtered.length, pageSize: limit });
92
+ }
93
+ catch (error) {
94
+ const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
95
+ return errorResponse(c, 'CGV_TIMETABLE_FETCH_FAILED', message, 500);
96
+ }
97
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * GET API 핸들러
3
+ *
4
+ * MCP 미지원 서비스를 위한 GET 기반 API 핸들러입니다.
5
+ * 기존 도구의 핵심 함수들을 재사용합니다.
6
+ */
7
+ import { fetchProducts } from '../services/daiso/tools/searchProducts.js';
8
+ import { fetchStores } from '../services/daiso/tools/findStores.js';
9
+ import { fetchOnlineStock, fetchStoreInventory, } from '../services/daiso/tools/checkInventory.js';
10
+ import { fetchProductById } from '../services/daiso/tools/getPriceInfo.js';
11
+ import { getImageUrl } from '../services/daiso/api.js';
12
+ import { fetchOliveyoungProducts, fetchOliveyoungStores, } from '../services/oliveyoung/client.js';
13
+ import { errorResponse, successResponse } from './response.js';
14
+ /**
15
+ * 제품 검색 API 핸들러
16
+ * GET /api/daiso/products?q={검색어}&page={페이지}&pageSize={개수}
17
+ */
18
+ export async function handleSearchProducts(c) {
19
+ const query = c.req.query('q');
20
+ const page = parseInt(c.req.query('page') || '1');
21
+ const pageSize = parseInt(c.req.query('pageSize') || '30');
22
+ if (!query || query.trim().length === 0) {
23
+ return errorResponse(c, 'MISSING_QUERY', '검색어(q)를 입력해주세요.');
24
+ }
25
+ try {
26
+ const { products, totalCount } = await fetchProducts(query, page, pageSize);
27
+ return successResponse(c, { products }, { total: totalCount, page, pageSize });
28
+ }
29
+ catch (error) {
30
+ const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
31
+ return errorResponse(c, 'SEARCH_FAILED', message, 500);
32
+ }
33
+ }
34
+ /**
35
+ * 제품 상세 정보 API 핸들러
36
+ * GET /api/daiso/products/:id
37
+ */
38
+ export async function handleGetProduct(c) {
39
+ const productId = c.req.param('id');
40
+ if (!productId) {
41
+ return errorResponse(c, 'MISSING_ID', '제품 ID가 필요합니다.');
42
+ }
43
+ try {
44
+ const product = await fetchProductById(productId);
45
+ if (!product) {
46
+ return errorResponse(c, 'NOT_FOUND', '제품을 찾을 수 없습니다.', 404);
47
+ }
48
+ const result = {
49
+ id: product.PD_NO,
50
+ name: product.PDNM || product.EXH_PD_NM,
51
+ price: parseInt(product.PD_PRC) || 0,
52
+ currency: 'KRW',
53
+ imageUrl: getImageUrl(product.ATCH_FILE_URL),
54
+ brand: product.BRND_NM || undefined,
55
+ soldOut: product.SOLD_OUT_YN === 'Y',
56
+ isNew: product.NEW_PD_YN === 'Y',
57
+ };
58
+ return successResponse(c, result);
59
+ }
60
+ catch (error) {
61
+ const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
62
+ return errorResponse(c, 'FETCH_FAILED', message, 500);
63
+ }
64
+ }
65
+ /**
66
+ * 매장 검색 API 핸들러
67
+ * GET /api/daiso/stores?keyword={키워드}&sido={시도}&gugun={구군}&dong={동}&limit={개수}
68
+ */
69
+ export async function handleFindStores(c) {
70
+ const keyword = c.req.query('keyword');
71
+ const sido = c.req.query('sido');
72
+ const gugun = c.req.query('gugun');
73
+ const dong = c.req.query('dong');
74
+ const limit = parseInt(c.req.query('limit') || '50');
75
+ if (!keyword && !sido) {
76
+ return errorResponse(c, 'MISSING_PARAMS', '검색어(keyword) 또는 지역(sido)을 입력해주세요.');
77
+ }
78
+ try {
79
+ const stores = await fetchStores(keyword, sido, gugun, dong);
80
+ const limitedStores = stores.slice(0, limit);
81
+ return successResponse(c, { stores: limitedStores }, { total: stores.length });
82
+ }
83
+ catch (error) {
84
+ const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
85
+ return errorResponse(c, 'SEARCH_FAILED', message, 500);
86
+ }
87
+ }
88
+ /**
89
+ * 재고 확인 API 핸들러
90
+ * GET /api/daiso/inventory?productId={제품ID}&lat={위도}&lng={경도}&keyword={매장검색어}
91
+ */
92
+ export async function handleCheckInventory(c) {
93
+ const productId = c.req.query('productId');
94
+ const lat = parseFloat(c.req.query('lat') || '37.5665');
95
+ const lng = parseFloat(c.req.query('lng') || '126.978');
96
+ const keyword = c.req.query('keyword') || '';
97
+ const page = parseInt(c.req.query('page') || '1');
98
+ const pageSize = parseInt(c.req.query('pageSize') || '30');
99
+ if (!productId) {
100
+ return errorResponse(c, 'MISSING_PRODUCT_ID', '제품 ID(productId)를 입력해주세요.');
101
+ }
102
+ try {
103
+ const [onlineStock, storeResult] = await Promise.all([
104
+ fetchOnlineStock(productId),
105
+ fetchStoreInventory(productId, lat, lng, page, pageSize, keyword),
106
+ ]);
107
+ const inStockStores = storeResult.stores.filter((s) => s.quantity > 0);
108
+ const result = {
109
+ productId,
110
+ location: { latitude: lat, longitude: lng },
111
+ onlineStock,
112
+ storeInventory: {
113
+ totalStores: storeResult.totalCount,
114
+ inStockCount: inStockStores.length,
115
+ stores: storeResult.stores,
116
+ },
117
+ };
118
+ return successResponse(c, result, { total: storeResult.totalCount, page, pageSize });
119
+ }
120
+ catch (error) {
121
+ const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
122
+ return errorResponse(c, 'INVENTORY_CHECK_FAILED', message, 500);
123
+ }
124
+ }
125
+ /**
126
+ * 올리브영 매장 검색 API 핸들러
127
+ * GET /api/oliveyoung/stores?keyword={키워드}&lat={위도}&lng={경도}
128
+ */
129
+ export async function handleOliveyoungFindStores(c) {
130
+ const keyword = c.req.query('keyword') || '';
131
+ const lat = parseFloat(c.req.query('lat') || '37.5665');
132
+ const lng = parseFloat(c.req.query('lng') || '126.978');
133
+ const pageIdx = parseInt(c.req.query('pageIdx') || '1');
134
+ const limit = parseInt(c.req.query('limit') || '20');
135
+ try {
136
+ const result = await fetchOliveyoungStores({
137
+ latitude: lat,
138
+ longitude: lng,
139
+ pageIdx,
140
+ searchWords: keyword,
141
+ }, {
142
+ apiKey: c.env.ZYTE_API_KEY,
143
+ });
144
+ return successResponse(c, {
145
+ stores: result.stores.slice(0, limit),
146
+ }, { total: result.totalCount, page: pageIdx, pageSize: limit });
147
+ }
148
+ catch (error) {
149
+ const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
150
+ return errorResponse(c, 'OLIVEYOUNG_STORE_SEARCH_FAILED', message, 500);
151
+ }
152
+ }
153
+ /**
154
+ * 올리브영 재고 확인 API 핸들러
155
+ * GET /api/oliveyoung/inventory?keyword={검색어}&lat={위도}&lng={경도}
156
+ */
157
+ export async function handleOliveyoungCheckInventory(c) {
158
+ const keyword = c.req.query('keyword');
159
+ const lat = parseFloat(c.req.query('lat') || '37.5665');
160
+ const lng = parseFloat(c.req.query('lng') || '126.978');
161
+ const storeKeyword = c.req.query('storeKeyword') || '';
162
+ const page = parseInt(c.req.query('page') || '1');
163
+ const size = parseInt(c.req.query('size') || '20');
164
+ const sort = c.req.query('sort') || '01';
165
+ const includeSoldOut = c.req.query('includeSoldOut') === 'true';
166
+ const storeLimit = parseInt(c.req.query('storeLimit') || '10');
167
+ if (!keyword || keyword.trim().length === 0) {
168
+ return errorResponse(c, 'MISSING_QUERY', '검색어(keyword)를 입력해주세요.');
169
+ }
170
+ try {
171
+ const [storeResult, productResult] = await Promise.all([
172
+ fetchOliveyoungStores({
173
+ latitude: lat,
174
+ longitude: lng,
175
+ pageIdx: 1,
176
+ searchWords: storeKeyword,
177
+ }, {
178
+ apiKey: c.env.ZYTE_API_KEY,
179
+ }),
180
+ fetchOliveyoungProducts({
181
+ keyword,
182
+ page,
183
+ size,
184
+ sort,
185
+ includeSoldOut,
186
+ }, {
187
+ apiKey: c.env.ZYTE_API_KEY,
188
+ }),
189
+ ]);
190
+ return successResponse(c, {
191
+ keyword,
192
+ location: { latitude: lat, longitude: lng },
193
+ nearbyStores: {
194
+ totalCount: storeResult.totalCount,
195
+ stores: storeResult.stores.slice(0, storeLimit),
196
+ },
197
+ inventory: {
198
+ totalCount: productResult.totalCount,
199
+ nextPage: productResult.nextPage,
200
+ products: productResult.products,
201
+ },
202
+ }, { total: productResult.totalCount, page, pageSize: size });
203
+ }
204
+ catch (error) {
205
+ const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
206
+ return errorResponse(c, 'OLIVEYOUNG_INVENTORY_CHECK_FAILED', message, 500);
207
+ }
208
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * 메가박스 GET API 핸들러
3
+ */
4
+ import { fetchMegaboxBookingList, fetchMegaboxTheaterInfo, toYyyymmdd, } from '../services/megabox/client.js';
5
+ 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;
14
+ }
15
+ /**
16
+ * 메가박스 주변 지점 조회 API 핸들러
17
+ * GET /api/megabox/theaters?lat={위도}&lng={경도}&playDate={YYYYMMDD}&areaCode={지역코드}
18
+ */
19
+ 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');
22
+ const playDate = c.req.query('playDate') || toYyyymmdd();
23
+ const areaCode = c.req.query('areaCode') || '11';
24
+ const limit = parseInt(c.req.query('limit') || '10');
25
+ const timeoutMs = parseInt(c.req.query('timeoutMs') || '15000');
26
+ try {
27
+ const { theaters } = await fetchMegaboxBookingList({
28
+ playDate,
29
+ areaCode,
30
+ timeout: timeoutMs,
31
+ });
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 });
61
+ }
62
+ catch (error) {
63
+ const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
64
+ return errorResponse(c, 'MEGABOX_THEATER_SEARCH_FAILED', message, 500);
65
+ }
66
+ }
67
+ /**
68
+ * 메가박스 영화/회차 목록 조회 API 핸들러
69
+ * GET /api/megabox/movies?playDate={YYYYMMDD}&theaterId={지점ID}&movieId={영화ID}
70
+ */
71
+ export async function handleMegaboxListNowShowing(c) {
72
+ const playDate = c.req.query('playDate') || toYyyymmdd();
73
+ const theaterId = c.req.query('theaterId') || undefined;
74
+ const movieId = c.req.query('movieId') || undefined;
75
+ const areaCode = c.req.query('areaCode') || '11';
76
+ const timeoutMs = parseInt(c.req.query('timeoutMs') || '15000');
77
+ try {
78
+ const result = await fetchMegaboxBookingList({
79
+ playDate,
80
+ theaterId,
81
+ movieId,
82
+ areaCode,
83
+ timeout: timeoutMs,
84
+ });
85
+ return successResponse(c, {
86
+ playDate,
87
+ filters: {
88
+ theaterId: theaterId || null,
89
+ movieId: movieId || null,
90
+ areaCode,
91
+ },
92
+ theaters: result.theaters,
93
+ movies: result.movies,
94
+ showtimes: result.showtimes,
95
+ }, { total: result.showtimes.length });
96
+ }
97
+ catch (error) {
98
+ const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
99
+ return errorResponse(c, 'MEGABOX_MOVIE_LIST_FAILED', message, 500);
100
+ }
101
+ }
102
+ /**
103
+ * 메가박스 잔여 좌석 조회 API 핸들러
104
+ * GET /api/megabox/seats?playDate={YYYYMMDD}&theaterId={지점ID}&movieId={영화ID}
105
+ */
106
+ export async function handleMegaboxGetRemainingSeats(c) {
107
+ const playDate = c.req.query('playDate') || toYyyymmdd();
108
+ const theaterId = c.req.query('theaterId') || undefined;
109
+ const movieId = c.req.query('movieId') || undefined;
110
+ const areaCode = c.req.query('areaCode') || '11';
111
+ const limit = parseInt(c.req.query('limit') || '50');
112
+ const timeoutMs = parseInt(c.req.query('timeoutMs') || '15000');
113
+ try {
114
+ const { showtimes } = await fetchMegaboxBookingList({
115
+ playDate,
116
+ theaterId,
117
+ movieId,
118
+ areaCode,
119
+ timeout: timeoutMs,
120
+ });
121
+ const seats = showtimes
122
+ .filter((item) => (theaterId ? item.theaterId === theaterId : true))
123
+ .filter((item) => (movieId ? item.movieId === movieId : true))
124
+ .sort((a, b) => {
125
+ if (a.startTime === b.startTime) {
126
+ return a.theaterName.localeCompare(b.theaterName);
127
+ }
128
+ return a.startTime.localeCompare(b.startTime);
129
+ })
130
+ .slice(0, limit);
131
+ return successResponse(c, {
132
+ playDate,
133
+ filters: {
134
+ theaterId: theaterId || null,
135
+ movieId: movieId || null,
136
+ areaCode,
137
+ },
138
+ seats,
139
+ }, { total: seats.length, pageSize: limit });
140
+ }
141
+ catch (error) {
142
+ const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
143
+ return errorResponse(c, 'MEGABOX_SEAT_LIST_FAILED', message, 500);
144
+ }
145
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * API 공통 응답 유틸리티
3
+ */
4
+ /**
5
+ * 성공 응답 생성
6
+ */
7
+ export function successResponse(c, data, meta) {
8
+ return c.json({
9
+ success: true,
10
+ data,
11
+ meta,
12
+ });
13
+ }
14
+ /**
15
+ * 에러 응답 생성
16
+ */
17
+ export function errorResponse(c, code, message, status = 400) {
18
+ return c.json({
19
+ success: false,
20
+ error: { code, message },
21
+ }, status);
22
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * CGV GET API 라우트 등록
3
+ */
4
+ import { withEdgeCache } from '../../utils/cache.js';
5
+ import { handleCgvFindTheaters, handleCgvGetTimetable, handleCgvSearchMovies } from '../cgvHandlers.js';
6
+ export function registerCgvRoutes(app) {
7
+ app.get('/api/cgv/theaters', async (c) => withEdgeCache(c.req.url, {
8
+ ttlSeconds: 60 * 60 * 24,
9
+ staleWhileRevalidateSeconds: 60 * 5,
10
+ keyPrefix: 'cgv-theaters-v1',
11
+ }, () => handleCgvFindTheaters(c)));
12
+ app.get('/api/cgv/movies', async (c) => withEdgeCache(c.req.url, {
13
+ ttlSeconds: 60 * 10,
14
+ staleWhileRevalidateSeconds: 60,
15
+ keyPrefix: 'cgv-movies-v1',
16
+ }, () => handleCgvSearchMovies(c)));
17
+ app.get('/api/cgv/timetable', async (c) => withEdgeCache(c.req.url, {
18
+ ttlSeconds: 60 * 3,
19
+ staleWhileRevalidateSeconds: 30,
20
+ keyPrefix: 'cgv-timetable-v1',
21
+ }, () => handleCgvGetTimetable(c)));
22
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 다이소 GET API 라우트 등록
3
+ */
4
+ import { withEdgeCache } from '../../utils/cache.js';
5
+ import { handleSearchProducts, handleGetProduct, handleFindStores, handleCheckInventory, } from '../handlers.js';
6
+ export function registerDaisoRoutes(app) {
7
+ app.get('/api/daiso/products', async (c) => withEdgeCache(c.req.url, {
8
+ ttlSeconds: 60 * 30,
9
+ staleWhileRevalidateSeconds: 60 * 3,
10
+ keyPrefix: 'daiso-products-v1',
11
+ }, () => handleSearchProducts(c)));
12
+ app.get('/api/daiso/products/:id', async (c) => withEdgeCache(c.req.url, {
13
+ ttlSeconds: 60 * 60,
14
+ staleWhileRevalidateSeconds: 60 * 5,
15
+ keyPrefix: 'daiso-product-detail-v1',
16
+ }, () => handleGetProduct(c)));
17
+ app.get('/api/daiso/stores', async (c) => withEdgeCache(c.req.url, {
18
+ ttlSeconds: 60 * 60 * 24,
19
+ staleWhileRevalidateSeconds: 60 * 5,
20
+ keyPrefix: 'daiso-stores-v1',
21
+ }, () => handleFindStores(c)));
22
+ app.get('/api/daiso/inventory', async (c) => withEdgeCache(c.req.url, {
23
+ ttlSeconds: 60 * 10,
24
+ staleWhileRevalidateSeconds: 60,
25
+ keyPrefix: 'daiso-inventory-v1',
26
+ }, () => handleCheckInventory(c)));
27
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * 메가박스 GET API 라우트 등록
3
+ */
4
+ import { withEdgeCache } from '../../utils/cache.js';
5
+ import { handleMegaboxFindNearbyTheaters, handleMegaboxListNowShowing, handleMegaboxGetRemainingSeats, } from '../megaboxHandlers.js';
6
+ export function registerMegaboxRoutes(app) {
7
+ app.get('/api/megabox/theaters', async (c) => withEdgeCache(c.req.url, {
8
+ ttlSeconds: 60 * 60 * 24,
9
+ staleWhileRevalidateSeconds: 60 * 5,
10
+ keyPrefix: 'megabox-theaters-v1',
11
+ }, () => handleMegaboxFindNearbyTheaters(c)));
12
+ app.get('/api/megabox/movies', async (c) => withEdgeCache(c.req.url, {
13
+ ttlSeconds: 60 * 10,
14
+ staleWhileRevalidateSeconds: 60,
15
+ keyPrefix: 'megabox-movies-v1',
16
+ }, () => handleMegaboxListNowShowing(c)));
17
+ app.get('/api/megabox/seats', async (c) => withEdgeCache(c.req.url, {
18
+ ttlSeconds: 60 * 3,
19
+ staleWhileRevalidateSeconds: 30,
20
+ keyPrefix: 'megabox-seats-v1',
21
+ }, () => handleMegaboxGetRemainingSeats(c)));
22
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 올리브영 GET API 라우트 등록
3
+ */
4
+ import { withEdgeCache } from '../../utils/cache.js';
5
+ import { handleOliveyoungFindStores, handleOliveyoungCheckInventory } from '../handlers.js';
6
+ export function registerOliveyoungRoutes(app) {
7
+ app.get('/api/oliveyoung/stores', async (c) => withEdgeCache(c.req.url, {
8
+ ttlSeconds: 60 * 60 * 24,
9
+ staleWhileRevalidateSeconds: 60 * 5,
10
+ keyPrefix: 'oliveyoung-stores-v1',
11
+ }, () => handleOliveyoungFindStores(c)));
12
+ app.get('/api/oliveyoung/inventory', async (c) => withEdgeCache(c.req.url, {
13
+ ttlSeconds: 60 * 10,
14
+ staleWhileRevalidateSeconds: 60,
15
+ keyPrefix: 'oliveyoung-inventory-v1',
16
+ }, () => handleOliveyoungCheckInventory(c)));
17
+ }