daiso 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +249 -533
- package/dist/api/actionsProxy.js +82 -0
- package/dist/api/cgvHandlers.js +103 -16
- package/dist/api/configStatus.js +19 -0
- package/dist/api/daisoHandlers.js +127 -0
- package/dist/api/emart24Handlers.js +176 -0
- package/dist/api/gs25Handlers.js +267 -0
- package/dist/api/handlers.js +209 -104
- package/dist/api/healthChecks.js +504 -0
- package/dist/api/lottecinemaHandlers.js +155 -0
- package/dist/api/lottemartHandlers.js +144 -0
- package/dist/api/megaboxHandlers.js +81 -50
- package/dist/api/response.js +2 -0
- package/dist/api/routes/cuRoutes.js +17 -0
- package/dist/api/routes/daisoRoutes.js +6 -1
- package/dist/api/routes/emart24Routes.js +22 -0
- package/dist/api/routes/gs25Routes.js +22 -0
- package/dist/api/routes/healthRoutes.js +61 -0
- package/dist/api/routes/lottecinemaRoutes.js +22 -0
- package/dist/api/routes/lottemartRoutes.js +18 -0
- package/dist/api/routes/oliveyoungRoutes.js +6 -1
- package/dist/api/routes/sevenelevenRoutes.js +32 -0
- package/dist/api/sevenelevenHandlers.js +200 -0
- package/dist/bin.js +0 -0
- package/dist/cli/args.js +75 -0
- package/dist/cli/commandOptions.js +38 -0
- package/dist/cli/commands/convenience.js +293 -0
- package/dist/cli/commands/daiso.js +112 -0
- package/dist/cli/constants.js +5 -0
- package/dist/cli/deps.js +48 -0
- package/dist/cli/http.js +156 -0
- package/dist/cli/interactiveFetch.js +31 -0
- package/dist/cli/interactiveItemSearch.js +261 -0
- package/dist/cli/interactivePrompt.js +50 -0
- package/dist/cli/interactiveTypes.js +4 -0
- package/dist/cli/types.js +4 -0
- package/dist/cli.js +87 -196
- package/dist/cliHelp.js +212 -1
- package/dist/cliInteractive.js +243 -0
- package/dist/cliPicker.js +57 -0
- package/dist/cliRenderer.js +90 -3
- package/dist/core/errors.js +66 -0
- package/dist/core/registry.js +227 -3
- package/dist/index.js +149 -9
- package/dist/pages/openapi.js +2 -1
- package/dist/pages/openapiFullSpec.js +58 -0
- package/dist/pages/openapiResponse.js +28 -0
- package/dist/pages/openapiSpec.js +2 -41
- package/dist/pages/openapiSpecActions.js +109 -0
- package/dist/pages/openapiSpecComponents.js +30 -391
- package/dist/pages/openapiSpecComponentsCgv.js +24 -0
- package/dist/pages/openapiSpecComponentsCu.js +112 -0
- package/dist/pages/openapiSpecComponentsDaisoOliveyoung.js +342 -0
- package/dist/pages/openapiSpecComponentsEmart24.js +141 -0
- package/dist/pages/openapiSpecComponentsGs25.js +109 -0
- package/dist/pages/openapiSpecComponentsLotteMart.js +85 -0
- package/dist/pages/openapiSpecComponentsLottecinema.js +163 -0
- package/dist/pages/openapiSpecComponentsMegabox.js +144 -0
- package/dist/pages/openapiSpecComponentsSeveneleven.js +82 -0
- package/dist/pages/openapiSpecPathsCgv.js +66 -3
- package/dist/pages/openapiSpecPathsCu.js +145 -0
- package/dist/pages/openapiSpecPathsDaiso.js +302 -0
- package/dist/pages/openapiSpecPathsDaisoOliveyoung.js +4 -376
- package/dist/pages/openapiSpecPathsEmart24.js +223 -0
- package/dist/pages/openapiSpecPathsGs25.js +166 -0
- package/dist/pages/openapiSpecPathsLotteMart.js +149 -0
- package/dist/pages/openapiSpecPathsLottecinema.js +212 -0
- package/dist/pages/openapiSpecPathsMegabox.js +52 -3
- package/dist/pages/openapiSpecPathsOliveyoung.js +266 -0
- package/dist/pages/openapiSpecPathsSeveneleven.js +234 -0
- package/dist/pages/prompt.js +8 -402
- package/dist/pages/promptCoreText.js +278 -0
- package/dist/pages/promptReferenceText.js +137 -0
- package/dist/pages/promptRetailCinemaText.js +274 -0
- package/dist/services/cgv/client.js +6 -1
- package/dist/services/cgv/index.js +3 -3
- package/dist/services/cgv/location.js +235 -0
- package/dist/services/cgv/tools/findTheaters.js +30 -5
- package/dist/services/cgv/tools/getTimetable.js +42 -14
- package/dist/services/cgv/tools/searchMovies.js +40 -12
- package/dist/services/cgv/transport.js +0 -3
- package/dist/services/cu/api.js +11 -0
- package/dist/services/cu/client.js +297 -0
- package/dist/services/cu/index.js +21 -0
- package/dist/services/cu/tools/checkInventory.js +123 -0
- package/dist/services/cu/tools/findNearbyStores.js +49 -0
- package/dist/services/cu/types.js +4 -0
- package/dist/services/daiso/api.js +22 -2
- package/dist/services/daiso/client.js +67 -0
- package/dist/services/daiso/index.js +4 -0
- package/dist/services/daiso/product.js +17 -0
- package/dist/services/daiso/tools/checkInventory.js +80 -43
- package/dist/services/daiso/tools/findInventoryByName.js +222 -0
- package/dist/services/daiso/tools/findStores.js +17 -7
- package/dist/services/daiso/tools/getDisplayLocation.js +75 -0
- package/dist/services/emart24/api.js +11 -0
- package/dist/services/emart24/client.js +182 -0
- package/dist/services/emart24/index.js +22 -0
- package/dist/services/emart24/inventoryLookup.js +134 -0
- package/dist/services/emart24/tools/checkInventory.js +54 -0
- package/dist/services/emart24/tools/findNearbyStores.js +72 -0
- package/dist/services/emart24/tools/searchProducts.js +73 -0
- package/dist/services/emart24/types.js +4 -0
- package/dist/services/gs25/api.js +9 -0
- package/dist/services/gs25/client.js +208 -0
- package/dist/services/gs25/index.js +22 -0
- package/dist/services/gs25/storeUtils.js +168 -0
- package/dist/services/gs25/tools/checkInventory.js +163 -0
- package/dist/services/gs25/tools/findNearbyStores.js +103 -0
- package/dist/services/gs25/tools/searchProducts.js +57 -0
- package/dist/services/gs25/types.js +4 -0
- package/dist/services/lottecinema/api.js +12 -0
- package/dist/services/lottecinema/client.js +161 -0
- package/dist/services/lottecinema/index.js +30 -0
- package/dist/services/lottecinema/location.js +191 -0
- package/dist/services/lottecinema/tools/findNearbyTheaters.js +47 -0
- package/dist/services/lottecinema/tools/getRemainingSeats.js +85 -0
- package/dist/services/lottecinema/tools/listNowShowing.js +79 -0
- package/dist/services/lottecinema/types.js +4 -0
- package/dist/services/lottemart/api.js +31 -0
- package/dist/services/lottemart/client.js +254 -0
- package/dist/services/lottemart/clientTypes.js +4 -0
- package/dist/services/lottemart/config.js +4 -0
- package/dist/services/lottemart/debug.js +92 -0
- package/dist/services/lottemart/geocode.js +49 -0
- package/dist/services/lottemart/index.js +30 -0
- package/dist/services/lottemart/keyword.js +29 -0
- package/dist/services/lottemart/parser.js +185 -0
- package/dist/services/lottemart/session.js +331 -0
- package/dist/services/lottemart/storeSearch.js +52 -0
- package/dist/services/lottemart/tools/findNearbyStores.js +67 -0
- package/dist/services/lottemart/tools/searchProducts.js +69 -0
- package/dist/services/lottemart/types.js +4 -0
- package/dist/services/lottemart/zetta.js +105 -0
- package/dist/services/megabox/location.js +219 -0
- package/dist/services/megabox/tools/findNearbyTheaters.js +13 -44
- package/dist/services/megabox/tools/getRemainingSeats.js +31 -3
- package/dist/services/megabox/tools/listNowShowing.js +31 -3
- package/dist/services/oliveyoung/api.js +2 -0
- package/dist/services/oliveyoung/client.js +225 -13
- package/dist/services/oliveyoung/index.js +3 -1
- package/dist/services/oliveyoung/tools/checkInventory.js +21 -5
- package/dist/services/oliveyoung/tools/searchProducts.js +57 -0
- package/dist/services/seveneleven/api.js +15 -0
- package/dist/services/seveneleven/client.js +286 -0
- package/dist/services/seveneleven/index.js +30 -0
- package/dist/services/seveneleven/inventory.js +302 -0
- package/dist/services/seveneleven/productKeyword.js +141 -0
- package/dist/services/seveneleven/tools/checkInventory.js +82 -0
- package/dist/services/seveneleven/tools/getCatalogSnapshot.js +55 -0
- package/dist/services/seveneleven/tools/getSearchPopwords.js +41 -0
- package/dist/services/seveneleven/tools/searchProducts.js +67 -0
- package/dist/services/seveneleven/tools/searchStores.js +45 -0
- package/dist/services/seveneleven/types.js +4 -0
- package/dist/utils/cliInteractiveHelpers.js +63 -0
- package/dist/utils/daisoKeyword.js +30 -0
- package/dist/utils/http.js +98 -12
- package/dist/utils/zyte.js +47 -26
- package/openapi.json +1979 -736
- package/openapi.yaml +1428 -541
- package/package.json +49 -18
- package/dist/tools/checkInventory.js +0 -98
- package/dist/tools/findStores.js +0 -125
- package/dist/tools/getPriceInfo.js +0 -77
- package/dist/tools/searchProducts.js +0 -61
- package/dist/types/index.js +0 -4
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Actions용 공통 GET 프록시 정의
|
|
3
|
+
*/
|
|
4
|
+
export const ACTION_QUERY_DEFINITIONS = [
|
|
5
|
+
{ action: 'daisoSearchProducts', summary: '다이소 제품 검색', targetPath: '/api/daiso/products' },
|
|
6
|
+
{ action: 'daisoGetProduct', summary: '다이소 제품 상세 조회', targetPath: '/api/daiso/products/:productId', pathParam: 'productId' },
|
|
7
|
+
{ action: 'daisoFindStores', summary: '다이소 매장 검색', targetPath: '/api/daiso/stores' },
|
|
8
|
+
{ action: 'daisoCheckInventory', summary: '다이소 재고 조회', targetPath: '/api/daiso/inventory' },
|
|
9
|
+
{ action: 'daisoGetDisplayLocation', summary: '다이소 진열 위치 조회', targetPath: '/api/daiso/display-location' },
|
|
10
|
+
{ action: 'oliveyoungSearchProducts', summary: '올리브영 상품 검색', targetPath: '/api/oliveyoung/products' },
|
|
11
|
+
{ action: 'oliveyoungFindStores', summary: '올리브영 매장 검색', targetPath: '/api/oliveyoung/stores' },
|
|
12
|
+
{ action: 'oliveyoungCheckInventory', summary: '올리브영 재고 조회', targetPath: '/api/oliveyoung/inventory' },
|
|
13
|
+
{ action: 'cuFindStores', summary: 'CU 매장 검색', targetPath: '/api/cu/stores' },
|
|
14
|
+
{ action: 'cuCheckInventory', summary: 'CU 재고 조회', targetPath: '/api/cu/inventory' },
|
|
15
|
+
{ action: 'emart24FindStores', summary: '이마트24 매장 검색', targetPath: '/api/emart24/stores' },
|
|
16
|
+
{ action: 'emart24SearchProducts', summary: '이마트24 상품 검색', targetPath: '/api/emart24/products' },
|
|
17
|
+
{ action: 'emart24CheckInventory', summary: '이마트24 재고 조회', targetPath: '/api/emart24/inventory' },
|
|
18
|
+
{ action: 'lottemartFindStores', summary: '롯데마트 매장 검색', targetPath: '/api/lottemart/stores' },
|
|
19
|
+
{ action: 'lottemartSearchProducts', summary: '롯데마트 상품 검색', targetPath: '/api/lottemart/products' },
|
|
20
|
+
{ action: 'gs25FindStores', summary: 'GS25 매장 검색', targetPath: '/api/gs25/stores' },
|
|
21
|
+
{ action: 'gs25SearchProducts', summary: 'GS25 상품 검색', targetPath: '/api/gs25/products' },
|
|
22
|
+
{ action: 'gs25CheckInventory', summary: 'GS25 재고 조회', targetPath: '/api/gs25/inventory' },
|
|
23
|
+
{ action: 'sevenelevenSearchProducts', summary: '세븐일레븐 상품 검색', targetPath: '/api/seveneleven/products' },
|
|
24
|
+
{ action: 'sevenelevenSearchStores', summary: '세븐일레븐 매장 검색', targetPath: '/api/seveneleven/stores' },
|
|
25
|
+
{ action: 'sevenelevenCheckInventory', summary: '세븐일레븐 재고 조회', targetPath: '/api/seveneleven/inventory' },
|
|
26
|
+
{ action: 'sevenelevenGetPopwords', summary: '세븐일레븐 인기 검색어 조회', targetPath: '/api/seveneleven/popwords' },
|
|
27
|
+
{ action: 'sevenelevenGetCatalog', summary: '세븐일레븐 카탈로그 조회', targetPath: '/api/seveneleven/catalog' },
|
|
28
|
+
{ action: 'megaboxFindTheaters', summary: '메가박스 지점 검색', targetPath: '/api/megabox/theaters' },
|
|
29
|
+
{ action: 'megaboxListMovies', summary: '메가박스 영화 목록 조회', targetPath: '/api/megabox/movies' },
|
|
30
|
+
{ action: 'megaboxGetSeats', summary: '메가박스 잔여 좌석 조회', targetPath: '/api/megabox/seats' },
|
|
31
|
+
{ action: 'lottecinemaFindTheaters', summary: '롯데시네마 지점 검색', targetPath: '/api/lottecinema/theaters' },
|
|
32
|
+
{ action: 'lottecinemaListMovies', summary: '롯데시네마 영화 목록 조회', targetPath: '/api/lottecinema/movies' },
|
|
33
|
+
{ action: 'lottecinemaGetSeats', summary: '롯데시네마 잔여 좌석 조회', targetPath: '/api/lottecinema/seats' },
|
|
34
|
+
{ action: 'cgvFindTheaters', summary: 'CGV 극장 검색', targetPath: '/api/cgv/theaters' },
|
|
35
|
+
{ action: 'cgvSearchMovies', summary: 'CGV 영화 목록 조회', targetPath: '/api/cgv/movies' },
|
|
36
|
+
{ action: 'cgvGetTimetable', summary: 'CGV 시간표 조회', targetPath: '/api/cgv/timetable' },
|
|
37
|
+
];
|
|
38
|
+
const ACTION_QUERY_MAP = new Map(ACTION_QUERY_DEFINITIONS.map((definition) => [definition.action, definition]));
|
|
39
|
+
export const ACTION_QUERY_ACTIONS = ACTION_QUERY_DEFINITIONS.map((definition) => definition.action);
|
|
40
|
+
function toAbsoluteUrl(value) {
|
|
41
|
+
try {
|
|
42
|
+
return new URL(value);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return new URL(value, 'https://actions-proxy.local');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function getActionQueryDefinition(action) {
|
|
49
|
+
return ACTION_QUERY_MAP.get(action);
|
|
50
|
+
}
|
|
51
|
+
export function buildActionQueryTargetUrl(requestUrl) {
|
|
52
|
+
const sourceUrl = toAbsoluteUrl(requestUrl);
|
|
53
|
+
const action = (sourceUrl.searchParams.get('action') || '').trim();
|
|
54
|
+
if (action.length === 0) {
|
|
55
|
+
throw new Error('action 파라미터를 입력해주세요.');
|
|
56
|
+
}
|
|
57
|
+
const definition = getActionQueryDefinition(action);
|
|
58
|
+
if (!definition) {
|
|
59
|
+
throw new Error(`지원하지 않는 action 입니다: ${action}`);
|
|
60
|
+
}
|
|
61
|
+
let pathname = definition.targetPath;
|
|
62
|
+
const consumedParams = new Set(['action']);
|
|
63
|
+
if (definition.pathParam) {
|
|
64
|
+
const pathValue = (sourceUrl.searchParams.get(definition.pathParam) || '').trim();
|
|
65
|
+
if (pathValue.length === 0) {
|
|
66
|
+
throw new Error(`${definition.pathParam} 파라미터를 입력해주세요.`);
|
|
67
|
+
}
|
|
68
|
+
pathname = pathname.replace(`:${definition.pathParam}`, encodeURIComponent(pathValue));
|
|
69
|
+
consumedParams.add(definition.pathParam);
|
|
70
|
+
}
|
|
71
|
+
const targetUrl = new URL(pathname, sourceUrl.origin);
|
|
72
|
+
for (const [key, value] of sourceUrl.searchParams.entries()) {
|
|
73
|
+
if (consumedParams.has(key)) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
targetUrl.searchParams.append(key, value);
|
|
77
|
+
}
|
|
78
|
+
return targetUrl;
|
|
79
|
+
}
|
|
80
|
+
export function createActionQueryDescriptionList() {
|
|
81
|
+
return ACTION_QUERY_DEFINITIONS.map((definition) => `- \`${definition.action}\`: ${definition.summary}`).join('\n');
|
|
82
|
+
}
|
package/dist/api/cgvHandlers.js
CHANGED
|
@@ -2,8 +2,16 @@
|
|
|
2
2
|
* CGV GET API 핸들러
|
|
3
3
|
*/
|
|
4
4
|
import { fetchCgvMovies, fetchCgvTheaters, fetchCgvTimetable, toYyyymmdd } from '../services/cgv/client.js';
|
|
5
|
+
import { fetchCgvNearbyTheaters, resolveCgvNearestTheater } from '../services/cgv/location.js';
|
|
5
6
|
import { filterAndSortTimetable } from '../services/cgv/timetable.js';
|
|
6
7
|
import { errorResponse, successResponse } from './response.js';
|
|
8
|
+
function parseOptionalNumber(value) {
|
|
9
|
+
if (!value) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
const parsed = Number.parseFloat(value);
|
|
13
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
14
|
+
}
|
|
7
15
|
/**
|
|
8
16
|
* CGV 극장 목록 조회 API 핸들러
|
|
9
17
|
* GET /api/cgv/theaters?playDate={YYYYMMDD}®ionCode={지역코드}
|
|
@@ -11,9 +19,28 @@ import { errorResponse, successResponse } from './response.js';
|
|
|
11
19
|
export async function handleCgvFindTheaters(c) {
|
|
12
20
|
const playDate = c.req.query('playDate') || toYyyymmdd();
|
|
13
21
|
const regionCode = c.req.query('regionCode') || undefined;
|
|
22
|
+
const keyword = c.req.query('keyword') || undefined;
|
|
23
|
+
const latitude = parseOptionalNumber(c.req.query('lat'));
|
|
24
|
+
const longitude = parseOptionalNumber(c.req.query('lng'));
|
|
14
25
|
const limit = parseInt(c.req.query('limit') || '30');
|
|
15
26
|
const timeoutMs = parseInt(c.req.query('timeoutMs') || '15000');
|
|
16
27
|
try {
|
|
28
|
+
if (keyword || typeof latitude === 'number' || typeof longitude === 'number') {
|
|
29
|
+
const result = await fetchCgvNearbyTheaters({
|
|
30
|
+
playDate,
|
|
31
|
+
regionCode,
|
|
32
|
+
keyword,
|
|
33
|
+
latitude,
|
|
34
|
+
longitude,
|
|
35
|
+
limit,
|
|
36
|
+
timeout: timeoutMs,
|
|
37
|
+
}, {
|
|
38
|
+
timeout: timeoutMs,
|
|
39
|
+
zyteApiKey: c.env?.ZYTE_API_KEY,
|
|
40
|
+
googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
|
|
41
|
+
});
|
|
42
|
+
return successResponse(c, result, { total: result.count, pageSize: limit });
|
|
43
|
+
}
|
|
17
44
|
const theaters = await fetchCgvTheaters({
|
|
18
45
|
playDate,
|
|
19
46
|
regionCode,
|
|
@@ -25,6 +52,9 @@ export async function handleCgvFindTheaters(c) {
|
|
|
25
52
|
playDate,
|
|
26
53
|
filters: {
|
|
27
54
|
regionCode: regionCode || null,
|
|
55
|
+
keyword: keyword || null,
|
|
56
|
+
latitude: latitude ?? null,
|
|
57
|
+
longitude: longitude ?? null,
|
|
28
58
|
},
|
|
29
59
|
theaters: sliced,
|
|
30
60
|
}, { total: sliced.length, pageSize: limit });
|
|
@@ -40,20 +70,46 @@ export async function handleCgvFindTheaters(c) {
|
|
|
40
70
|
*/
|
|
41
71
|
export async function handleCgvSearchMovies(c) {
|
|
42
72
|
const playDate = c.req.query('playDate') || toYyyymmdd();
|
|
43
|
-
|
|
73
|
+
let theaterCode = c.req.query('theaterCode') || undefined;
|
|
74
|
+
const keyword = c.req.query('keyword') || undefined;
|
|
75
|
+
const latitude = parseOptionalNumber(c.req.query('lat'));
|
|
76
|
+
const longitude = parseOptionalNumber(c.req.query('lng'));
|
|
44
77
|
const timeoutMs = parseInt(c.req.query('timeoutMs') || '15000');
|
|
45
78
|
try {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
79
|
+
let resolvedTheater = null;
|
|
80
|
+
if (!theaterCode && (keyword || typeof latitude === 'number' || typeof longitude === 'number')) {
|
|
81
|
+
const resolved = await resolveCgvNearestTheater({
|
|
82
|
+
playDate,
|
|
83
|
+
keyword,
|
|
84
|
+
latitude,
|
|
85
|
+
longitude,
|
|
86
|
+
timeout: timeoutMs,
|
|
87
|
+
}, {
|
|
88
|
+
timeout: timeoutMs,
|
|
89
|
+
zyteApiKey: c.env?.ZYTE_API_KEY,
|
|
90
|
+
googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
|
|
91
|
+
});
|
|
92
|
+
resolvedTheater = resolved.theater;
|
|
93
|
+
theaterCode = resolved.theater?.theaterCode;
|
|
94
|
+
}
|
|
95
|
+
const shouldReturnEmpty = !theaterCode && (keyword || typeof latitude === 'number' || typeof longitude === 'number');
|
|
96
|
+
const movies = shouldReturnEmpty
|
|
97
|
+
? []
|
|
98
|
+
: await fetchCgvMovies({
|
|
99
|
+
playDate,
|
|
100
|
+
theaterCode,
|
|
101
|
+
timeout: timeoutMs,
|
|
102
|
+
zyteApiKey: c.env?.ZYTE_API_KEY,
|
|
103
|
+
});
|
|
52
104
|
return successResponse(c, {
|
|
53
105
|
playDate,
|
|
54
106
|
filters: {
|
|
55
107
|
theaterCode: theaterCode || null,
|
|
108
|
+
keyword: keyword || null,
|
|
109
|
+
latitude: latitude ?? null,
|
|
110
|
+
longitude: longitude ?? null,
|
|
56
111
|
},
|
|
112
|
+
resolvedTheater,
|
|
57
113
|
movies,
|
|
58
114
|
}, { total: movies.length });
|
|
59
115
|
}
|
|
@@ -68,25 +124,56 @@ export async function handleCgvSearchMovies(c) {
|
|
|
68
124
|
*/
|
|
69
125
|
export async function handleCgvGetTimetable(c) {
|
|
70
126
|
const playDate = c.req.query('playDate') || toYyyymmdd();
|
|
71
|
-
|
|
127
|
+
let theaterCode = c.req.query('theaterCode') || undefined;
|
|
72
128
|
const movieCode = c.req.query('movieCode') || undefined;
|
|
129
|
+
const keyword = c.req.query('keyword') || undefined;
|
|
130
|
+
const latitude = parseOptionalNumber(c.req.query('lat'));
|
|
131
|
+
const longitude = parseOptionalNumber(c.req.query('lng'));
|
|
73
132
|
const limit = parseInt(c.req.query('limit') || '50');
|
|
74
133
|
const timeoutMs = parseInt(c.req.query('timeoutMs') || '15000');
|
|
75
134
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
135
|
+
let resolvedTheater = null;
|
|
136
|
+
if (!theaterCode && (keyword || typeof latitude === 'number' || typeof longitude === 'number')) {
|
|
137
|
+
const resolved = await resolveCgvNearestTheater({
|
|
138
|
+
playDate,
|
|
139
|
+
keyword,
|
|
140
|
+
latitude,
|
|
141
|
+
longitude,
|
|
142
|
+
timeout: timeoutMs,
|
|
143
|
+
}, {
|
|
144
|
+
timeout: timeoutMs,
|
|
145
|
+
zyteApiKey: c.env?.ZYTE_API_KEY,
|
|
146
|
+
googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
|
|
147
|
+
});
|
|
148
|
+
resolvedTheater = resolved.theater;
|
|
149
|
+
theaterCode = resolved.theater?.theaterCode;
|
|
150
|
+
}
|
|
151
|
+
const shouldReturnEmpty = !theaterCode && (keyword || typeof latitude === 'number' || typeof longitude === 'number');
|
|
152
|
+
const timetable = shouldReturnEmpty
|
|
153
|
+
? []
|
|
154
|
+
: await fetchCgvTimetable({
|
|
155
|
+
playDate,
|
|
156
|
+
theaterCode,
|
|
157
|
+
movieCode,
|
|
158
|
+
timeout: timeoutMs,
|
|
159
|
+
zyteApiKey: c.env?.ZYTE_API_KEY,
|
|
160
|
+
});
|
|
161
|
+
const exactFiltered = filterAndSortTimetable(timetable, { theaterCode, movieCode, limit });
|
|
162
|
+
const filterRelaxed = Boolean(movieCode && exactFiltered.length === 0 && timetable.length > 0);
|
|
163
|
+
const filtered = filterRelaxed
|
|
164
|
+
? filterAndSortTimetable(timetable, { theaterCode, limit })
|
|
165
|
+
: exactFiltered;
|
|
84
166
|
return successResponse(c, {
|
|
85
167
|
playDate,
|
|
86
168
|
filters: {
|
|
87
169
|
theaterCode: theaterCode || null,
|
|
88
170
|
movieCode: movieCode || null,
|
|
171
|
+
keyword: keyword || null,
|
|
172
|
+
latitude: latitude ?? null,
|
|
173
|
+
longitude: longitude ?? null,
|
|
89
174
|
},
|
|
175
|
+
resolvedTheater,
|
|
176
|
+
filterRelaxed,
|
|
90
177
|
timetable: filtered,
|
|
91
178
|
}, { total: filtered.length, pageSize: limit });
|
|
92
179
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function isConfigured(value) {
|
|
2
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
3
|
+
}
|
|
4
|
+
export function buildConfigStatus(bindings) {
|
|
5
|
+
return {
|
|
6
|
+
googleMapsApiKey: {
|
|
7
|
+
configured: isConfigured(bindings?.GOOGLE_MAPS_API_KEY),
|
|
8
|
+
usedBy: ['gs25', 'cu', 'lottemart', 'megabox', 'lottecinema', 'cgv'],
|
|
9
|
+
},
|
|
10
|
+
zyteApiKey: {
|
|
11
|
+
configured: isConfigured(bindings?.ZYTE_API_KEY),
|
|
12
|
+
usedBy: ['oliveyoung', 'lottemart', 'cgv'],
|
|
13
|
+
},
|
|
14
|
+
healthCheckSecret: {
|
|
15
|
+
configured: isConfigured(bindings?.HEALTH_CHECK_SECRET),
|
|
16
|
+
usedBy: ['health-checks'],
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 다이소 GET API 핸들러
|
|
3
|
+
*/
|
|
4
|
+
import { getImageUrl } from '../services/daiso/api.js';
|
|
5
|
+
import { toProductSummary } from '../services/daiso/product.js';
|
|
6
|
+
import { fetchOnlineStock, fetchStoreInventory } from '../services/daiso/tools/checkInventory.js';
|
|
7
|
+
import { fetchDisplayLocation } from '../services/daiso/tools/getDisplayLocation.js';
|
|
8
|
+
import { fetchProductById } from '../services/daiso/tools/getPriceInfo.js';
|
|
9
|
+
import { fetchStores } from '../services/daiso/tools/findStores.js';
|
|
10
|
+
import { fetchProducts } from '../services/daiso/tools/searchProducts.js';
|
|
11
|
+
import { errorResponse, successResponse } from './response.js';
|
|
12
|
+
export async function handleSearchProducts(c) {
|
|
13
|
+
const query = c.req.query('q');
|
|
14
|
+
const page = parseInt(c.req.query('page') || '1');
|
|
15
|
+
const pageSize = parseInt(c.req.query('pageSize') || '30');
|
|
16
|
+
if (!query || query.trim().length === 0) {
|
|
17
|
+
return errorResponse(c, 'MISSING_QUERY', '검색어(q)를 입력해주세요.');
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const { products, totalCount } = await fetchProducts(query, page, pageSize);
|
|
21
|
+
return successResponse(c, { products }, { total: totalCount, page, pageSize });
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
|
|
25
|
+
return errorResponse(c, 'SEARCH_FAILED', message, 500);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export async function handleGetProduct(c) {
|
|
29
|
+
const productId = c.req.param('id');
|
|
30
|
+
if (!productId) {
|
|
31
|
+
return errorResponse(c, 'MISSING_ID', '제품 ID가 필요합니다.');
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const product = await fetchProductById(productId);
|
|
35
|
+
if (!product) {
|
|
36
|
+
return errorResponse(c, 'NOT_FOUND', '제품을 찾을 수 없습니다.', 404);
|
|
37
|
+
}
|
|
38
|
+
const result = {
|
|
39
|
+
id: product.PD_NO,
|
|
40
|
+
name: product.PDNM || product.EXH_PD_NM,
|
|
41
|
+
price: parseInt(product.PD_PRC) || 0,
|
|
42
|
+
currency: 'KRW',
|
|
43
|
+
imageUrl: getImageUrl(product.ATCH_FILE_URL),
|
|
44
|
+
brand: product.BRND_NM || undefined,
|
|
45
|
+
soldOut: product.SOLD_OUT_YN === 'Y',
|
|
46
|
+
isNew: product.NEW_PD_YN === 'Y',
|
|
47
|
+
};
|
|
48
|
+
return successResponse(c, result);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
|
|
52
|
+
return errorResponse(c, 'FETCH_FAILED', message, 500);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export async function handleFindStores(c) {
|
|
56
|
+
const keyword = c.req.query('keyword');
|
|
57
|
+
const sido = c.req.query('sido');
|
|
58
|
+
const gugun = c.req.query('gugun');
|
|
59
|
+
const dong = c.req.query('dong');
|
|
60
|
+
const limit = parseInt(c.req.query('limit') || '50');
|
|
61
|
+
if (!keyword && !sido) {
|
|
62
|
+
return errorResponse(c, 'MISSING_PARAMS', '검색어(keyword) 또는 지역(sido)을 입력해주세요.');
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const stores = await fetchStores(keyword, sido, gugun, dong);
|
|
66
|
+
const limitedStores = stores.slice(0, limit);
|
|
67
|
+
return successResponse(c, { stores: limitedStores }, { total: stores.length });
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
|
|
71
|
+
return errorResponse(c, 'SEARCH_FAILED', message, 500);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export async function handleCheckInventory(c) {
|
|
75
|
+
const productId = c.req.query('productId');
|
|
76
|
+
const lat = parseFloat(c.req.query('lat') || '37.5665');
|
|
77
|
+
const lng = parseFloat(c.req.query('lng') || '126.978');
|
|
78
|
+
const keyword = c.req.query('keyword') || '';
|
|
79
|
+
const page = parseInt(c.req.query('page') || '1');
|
|
80
|
+
const pageSize = parseInt(c.req.query('pageSize') || '30');
|
|
81
|
+
if (!productId) {
|
|
82
|
+
return errorResponse(c, 'MISSING_PRODUCT_ID', '제품 ID(productId)를 입력해주세요.');
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const [onlineStock, storeResult, productDoc] = await Promise.all([
|
|
86
|
+
fetchOnlineStock(productId),
|
|
87
|
+
fetchStoreInventory(productId, lat, lng, page, pageSize, keyword),
|
|
88
|
+
fetchProductById(productId).catch(() => null),
|
|
89
|
+
]);
|
|
90
|
+
const inStockStores = storeResult.stores.filter((s) => s.quantity > 0);
|
|
91
|
+
const product = productDoc ? toProductSummary(productDoc) : undefined;
|
|
92
|
+
const result = {
|
|
93
|
+
productId,
|
|
94
|
+
product,
|
|
95
|
+
location: { latitude: lat, longitude: lng },
|
|
96
|
+
onlineStock,
|
|
97
|
+
storeInventory: {
|
|
98
|
+
totalStores: storeResult.totalCount,
|
|
99
|
+
inStockCount: inStockStores.length,
|
|
100
|
+
stores: storeResult.stores,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
return successResponse(c, result, { total: storeResult.totalCount, page, pageSize });
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
|
|
107
|
+
return errorResponse(c, 'INVENTORY_CHECK_FAILED', message, 500);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export async function handleGetDisplayLocation(c) {
|
|
111
|
+
const productId = c.req.query('productId')?.trim();
|
|
112
|
+
const storeCode = c.req.query('storeCode')?.trim();
|
|
113
|
+
if (!productId || productId.length === 0) {
|
|
114
|
+
return errorResponse(c, 'MISSING_PRODUCT_ID', '상품 ID(productId)를 입력해주세요.');
|
|
115
|
+
}
|
|
116
|
+
if (!storeCode || storeCode.length === 0) {
|
|
117
|
+
return errorResponse(c, 'MISSING_STORE_CODE', '매장 코드(storeCode)를 입력해주세요.');
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const result = await fetchDisplayLocation(productId, storeCode);
|
|
121
|
+
return successResponse(c, result);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
|
|
125
|
+
return errorResponse(c, 'DISPLAY_LOCATION_FAILED', message, 500);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 이마트24 GET API 핸들러
|
|
3
|
+
*/
|
|
4
|
+
import { errorResponse, successResponse } from './response.js';
|
|
5
|
+
import { calculateDistanceM, fetchEmart24Stores, searchEmart24Products } from '../services/emart24/client.js';
|
|
6
|
+
import { lookupEmart24Inventory } from '../services/emart24/inventoryLookup.js';
|
|
7
|
+
/**
|
|
8
|
+
* 이마트24 매장 검색 API 핸들러
|
|
9
|
+
* GET /api/emart24/stores?keyword={키워드}&lat={위도}&lng={경도}
|
|
10
|
+
*/
|
|
11
|
+
export async function handleEmart24FindStores(c) {
|
|
12
|
+
const keyword = c.req.query('keyword') || '';
|
|
13
|
+
const area1 = c.req.query('area1') || '';
|
|
14
|
+
const area2 = c.req.query('area2') || '';
|
|
15
|
+
const service24h = c.req.query('service24h') === 'true';
|
|
16
|
+
const page = parseInt(c.req.query('page') || '1', 10);
|
|
17
|
+
const limit = parseInt(c.req.query('limit') || '20', 10);
|
|
18
|
+
const rawLat = c.req.query('lat');
|
|
19
|
+
const rawLng = c.req.query('lng');
|
|
20
|
+
const parsedLat = rawLat ? parseFloat(rawLat) : undefined;
|
|
21
|
+
const parsedLng = rawLng ? parseFloat(rawLng) : undefined;
|
|
22
|
+
const lat = typeof parsedLat === 'number' && Number.isFinite(parsedLat) ? parsedLat : undefined;
|
|
23
|
+
const lng = typeof parsedLng === 'number' && Number.isFinite(parsedLng) ? parsedLng : undefined;
|
|
24
|
+
try {
|
|
25
|
+
const result = await fetchEmart24Stores({
|
|
26
|
+
keyword,
|
|
27
|
+
area1,
|
|
28
|
+
area2,
|
|
29
|
+
page,
|
|
30
|
+
service24h,
|
|
31
|
+
}, {
|
|
32
|
+
timeout: 15000,
|
|
33
|
+
});
|
|
34
|
+
const stores = result.stores
|
|
35
|
+
.map((store) => {
|
|
36
|
+
if (typeof lat !== 'number' || typeof lng !== 'number' || store.latitude === 0 || store.longitude === 0) {
|
|
37
|
+
return store;
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
...store,
|
|
41
|
+
distanceM: calculateDistanceM(lat, lng, store.latitude, store.longitude),
|
|
42
|
+
};
|
|
43
|
+
})
|
|
44
|
+
.sort((a, b) => (a.distanceM ?? Number.MAX_SAFE_INTEGER) - (b.distanceM ?? Number.MAX_SAFE_INTEGER))
|
|
45
|
+
.slice(0, limit);
|
|
46
|
+
return successResponse(c, {
|
|
47
|
+
location: typeof lat === 'number' && typeof lng === 'number' ? { latitude: lat, longitude: lng } : null,
|
|
48
|
+
keyword,
|
|
49
|
+
appliedKeyword: result.appliedKeyword,
|
|
50
|
+
area1,
|
|
51
|
+
area2,
|
|
52
|
+
service24h,
|
|
53
|
+
stores,
|
|
54
|
+
}, {
|
|
55
|
+
total: result.totalCount,
|
|
56
|
+
page,
|
|
57
|
+
pageSize: limit,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
|
|
62
|
+
return errorResponse(c, 'EMART24_STORE_SEARCH_FAILED', message, 500);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 이마트24 상품 검색 API 핸들러
|
|
67
|
+
* GET /api/emart24/products?keyword={검색어}
|
|
68
|
+
*/
|
|
69
|
+
export async function handleEmart24SearchProducts(c) {
|
|
70
|
+
const keyword = c.req.query('keyword') || '';
|
|
71
|
+
const page = parseInt(c.req.query('page') || '1', 10);
|
|
72
|
+
const pageSize = parseInt(c.req.query('pageSize') || '10', 10);
|
|
73
|
+
const sortType = c.req.query('sortType') || 'SALE';
|
|
74
|
+
const saleProductYn = c.req.query('saleProductYn') || 'N';
|
|
75
|
+
if (keyword.trim().length === 0) {
|
|
76
|
+
return errorResponse(c, 'MISSING_QUERY', '검색어(keyword)를 입력해주세요.');
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const result = await searchEmart24Products({
|
|
80
|
+
keyword,
|
|
81
|
+
page,
|
|
82
|
+
pageSize,
|
|
83
|
+
sortType,
|
|
84
|
+
saleProductYn,
|
|
85
|
+
}, {
|
|
86
|
+
timeout: 15000,
|
|
87
|
+
});
|
|
88
|
+
return successResponse(c, {
|
|
89
|
+
keyword,
|
|
90
|
+
sortType,
|
|
91
|
+
saleProductYn,
|
|
92
|
+
products: result.products,
|
|
93
|
+
}, {
|
|
94
|
+
total: result.totalCount,
|
|
95
|
+
page,
|
|
96
|
+
pageSize,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
|
|
101
|
+
return errorResponse(c, 'EMART24_PRODUCT_SEARCH_FAILED', message, 500);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 이마트24 재고 조회 API 핸들러
|
|
106
|
+
* GET /api/emart24/inventory?pluCd={PLU코드}&bizNoArr={매장코드들}
|
|
107
|
+
*/
|
|
108
|
+
export async function handleEmart24CheckInventory(c) {
|
|
109
|
+
const pluCd = c.req.query('pluCd') || '';
|
|
110
|
+
const keyword = c.req.query('keyword') || '';
|
|
111
|
+
const bizNoArr = c.req.query('bizNoArr') || c.req.query('storeCodeArr') || '';
|
|
112
|
+
const storeCode = c.req.query('storeCode') || '';
|
|
113
|
+
const storeKeyword = c.req.query('storeKeyword') || '';
|
|
114
|
+
const area1 = c.req.query('area1') || '';
|
|
115
|
+
const area2 = c.req.query('area2') || '';
|
|
116
|
+
const service24h = c.req.query('service24h') === 'true';
|
|
117
|
+
const productPage = parseInt(c.req.query('page') || '1', 10);
|
|
118
|
+
const productPageSize = parseInt(c.req.query('pageSize') || '10', 10);
|
|
119
|
+
const storeLimit = parseInt(c.req.query('storeLimit') || '10', 10);
|
|
120
|
+
const timeoutMs = parseInt(c.req.query('timeoutMs') || '15000', 10);
|
|
121
|
+
const rawLat = c.req.query('lat');
|
|
122
|
+
const rawLng = c.req.query('lng');
|
|
123
|
+
const parsedLat = rawLat ? parseFloat(rawLat) : undefined;
|
|
124
|
+
const parsedLng = rawLng ? parseFloat(rawLng) : undefined;
|
|
125
|
+
const lat = typeof parsedLat === 'number' && Number.isFinite(parsedLat) ? parsedLat : undefined;
|
|
126
|
+
const lng = typeof parsedLng === 'number' && Number.isFinite(parsedLng) ? parsedLng : undefined;
|
|
127
|
+
if (pluCd.trim().length === 0 && keyword.trim().length === 0) {
|
|
128
|
+
return errorResponse(c, 'MISSING_PLU_CD', '상품 PLU 코드(pluCd) 또는 검색어(keyword)를 입력해주세요.');
|
|
129
|
+
}
|
|
130
|
+
const bizNos = bizNoArr
|
|
131
|
+
.split(',')
|
|
132
|
+
.map((value) => value.trim())
|
|
133
|
+
.concat(storeCode.trim().length > 0 ? [storeCode.trim()] : [])
|
|
134
|
+
.filter((value, index, array) => value.length > 0 && array.indexOf(value) === index);
|
|
135
|
+
const hasStoreFilters = storeKeyword.trim().length > 0 ||
|
|
136
|
+
area1.trim().length > 0 ||
|
|
137
|
+
area2.trim().length > 0 ||
|
|
138
|
+
typeof lat === 'number' ||
|
|
139
|
+
typeof lng === 'number';
|
|
140
|
+
if (bizNos.length === 0 && !hasStoreFilters) {
|
|
141
|
+
return errorResponse(c, 'MISSING_BIZ_NO_ARR', '매장 코드 목록(bizNoArr) 또는 매장 검색 조건(storeKeyword/area1/area2/lat/lng)을 입력해주세요.');
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const result = await lookupEmart24Inventory({
|
|
145
|
+
pluCd,
|
|
146
|
+
keyword,
|
|
147
|
+
latitude: lat,
|
|
148
|
+
longitude: lng,
|
|
149
|
+
storeKeyword,
|
|
150
|
+
area1,
|
|
151
|
+
area2,
|
|
152
|
+
service24h,
|
|
153
|
+
productPage,
|
|
154
|
+
productPageSize,
|
|
155
|
+
storeLimit,
|
|
156
|
+
timeoutMs,
|
|
157
|
+
bizNos,
|
|
158
|
+
includeRequestedStoresWithoutQty: bizNos.length > 0,
|
|
159
|
+
});
|
|
160
|
+
return successResponse(c, {
|
|
161
|
+
keyword: result.keyword,
|
|
162
|
+
pluCd: result.pluCd,
|
|
163
|
+
productCandidates: result.productCandidates,
|
|
164
|
+
location: result.location,
|
|
165
|
+
storeFilters: result.storeFilters,
|
|
166
|
+
nearbyStores: result.nearbyStores,
|
|
167
|
+
goodsInfo: result.inventory.goodsInfo,
|
|
168
|
+
count: result.inventory.count,
|
|
169
|
+
stores: result.inventory.stores,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
|
|
174
|
+
return errorResponse(c, 'EMART24_INVENTORY_CHECK_FAILED', message, 500);
|
|
175
|
+
}
|
|
176
|
+
}
|