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.
- package/README.md +218 -537
- package/dist/api/actionsProxy.js +82 -0
- package/dist/api/cgvHandlers.js +103 -16
- package/dist/api/daisoHandlers.js +127 -0
- package/dist/api/emart24Handlers.js +176 -0
- package/dist/api/gs25Handlers.js +256 -0
- package/dist/api/handlers.js +184 -108
- package/dist/api/healthChecks.js +332 -0
- package/dist/api/lottecinemaHandlers.js +155 -0
- package/dist/api/lottemartHandlers.js +128 -0
- package/dist/api/megaboxHandlers.js +81 -50
- 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 +44 -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/cli/args.js +54 -0
- package/dist/cli/commands/convenience.js +241 -0
- package/dist/cli/commands/daiso.js +96 -0
- package/dist/cli/constants.js +5 -0
- package/dist/cli/deps.js +48 -0
- package/dist/cli/http.js +98 -0
- package/dist/cli/interactiveFetch.js +31 -0
- package/dist/cli/interactiveItemSearch.js +259 -0
- package/dist/cli/interactivePrompt.js +49 -0
- package/dist/cli/interactiveTypes.js +4 -0
- package/dist/cli/types.js +4 -0
- package/dist/cli.js +66 -181
- package/dist/cliHelp.js +216 -9
- package/dist/cliInteractive.js +174 -0
- package/dist/cliPicker.js +56 -0
- package/dist/cliRenderer.js +139 -0
- package/dist/index.js +147 -8
- 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 +138 -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 +135 -0
- package/dist/pages/openapiSpecPathsLottecinema.js +212 -0
- package/dist/pages/openapiSpecPathsMegabox.js +52 -3
- package/dist/pages/openapiSpecPathsOliveyoung.js +231 -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 +131 -0
- package/dist/pages/promptRetailCinemaText.js +273 -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 +59 -0
- package/dist/services/daiso/index.js +2 -0
- package/dist/services/daiso/product.js +17 -0
- package/dist/services/daiso/tools/checkInventory.js +76 -41
- 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 +173 -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 +63 -0
- package/dist/services/emart24/types.js +4 -0
- package/dist/services/gs25/api.js +9 -0
- package/dist/services/gs25/client.js +198 -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 +50 -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 +245 -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 +62 -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 +150 -9
- package/dist/services/oliveyoung/index.js +3 -1
- package/dist/services/oliveyoung/tools/checkInventory.js +15 -4
- package/dist/services/oliveyoung/tools/searchProducts.js +57 -0
- package/dist/services/seveneleven/api.js +15 -0
- package/dist/services/seveneleven/client.js +279 -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 +56 -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 +16 -2
- package/openapi.json +1977 -734
- package/openapi.yaml +1430 -543
- package/package.json +28 -15
- 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,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GS25 GET API 핸들러
|
|
3
|
+
*/
|
|
4
|
+
/* c8 ignore start */
|
|
5
|
+
import { errorResponse, successResponse } from './response.js';
|
|
6
|
+
import { attachDistanceToGs25Stores, fetchGs25SearchProducts, fetchGs25Stores, filterGs25StoresByKeyword, geocodeGs25Address, selectGs25StoresForKeyword, sortGs25Stores, } from '../services/gs25/client.js';
|
|
7
|
+
const GS25_FALLBACK_STORE_LOOKUP_ITEM_CODE = '8801117752804';
|
|
8
|
+
/**
|
|
9
|
+
* GS25 매장 검색 API 핸들러
|
|
10
|
+
* GET /api/gs25/stores?keyword={키워드}&lat={위도}&lng={경도}
|
|
11
|
+
*/
|
|
12
|
+
export async function handleGs25FindStores(c) {
|
|
13
|
+
const keyword = c.req.query('keyword') || '';
|
|
14
|
+
const serviceCode = c.req.query('serviceCode') || '01';
|
|
15
|
+
const limit = parseInt(c.req.query('limit') || '20', 10);
|
|
16
|
+
const rawLat = c.req.query('lat');
|
|
17
|
+
const rawLng = c.req.query('lng');
|
|
18
|
+
const parsedLat = rawLat ? parseFloat(rawLat) : undefined;
|
|
19
|
+
const parsedLng = rawLng ? parseFloat(rawLng) : undefined;
|
|
20
|
+
let latitude = typeof parsedLat === 'number' && Number.isFinite(parsedLat) ? parsedLat : undefined;
|
|
21
|
+
let longitude = typeof parsedLng === 'number' && Number.isFinite(parsedLng) ? parsedLng : undefined;
|
|
22
|
+
let geocodeUsed = false;
|
|
23
|
+
try {
|
|
24
|
+
if ((typeof latitude !== 'number' || typeof longitude !== 'number') && keyword.trim().length > 0) {
|
|
25
|
+
const geocoded = await geocodeGs25Address(keyword, {
|
|
26
|
+
timeout: 15000,
|
|
27
|
+
googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
|
|
28
|
+
});
|
|
29
|
+
if (geocoded) {
|
|
30
|
+
latitude = geocoded.latitude;
|
|
31
|
+
longitude = geocoded.longitude;
|
|
32
|
+
geocodeUsed = true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
let storeResult = await fetchGs25Stores({
|
|
36
|
+
serviceCode,
|
|
37
|
+
latitude,
|
|
38
|
+
longitude,
|
|
39
|
+
}, {
|
|
40
|
+
timeout: 20000,
|
|
41
|
+
});
|
|
42
|
+
let fallbackUsed = false;
|
|
43
|
+
if (storeResult.stores.length === 0 && typeof latitude === 'number' && typeof longitude === 'number') {
|
|
44
|
+
try {
|
|
45
|
+
const fallbackResult = await fetchGs25Stores({
|
|
46
|
+
serviceCode,
|
|
47
|
+
itemCode: GS25_FALLBACK_STORE_LOOKUP_ITEM_CODE,
|
|
48
|
+
realTimeStockYn: 'Y',
|
|
49
|
+
latitude,
|
|
50
|
+
longitude,
|
|
51
|
+
useCache: false,
|
|
52
|
+
}, {
|
|
53
|
+
timeout: 20000,
|
|
54
|
+
});
|
|
55
|
+
if (fallbackResult.stores.length > 0) {
|
|
56
|
+
storeResult = fallbackResult;
|
|
57
|
+
fallbackUsed = true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
fallbackUsed = false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const selected = selectGs25StoresForKeyword(storeResult.stores, keyword, {
|
|
65
|
+
relaxWhenEmpty: typeof latitude === 'number' && typeof longitude === 'number',
|
|
66
|
+
});
|
|
67
|
+
const withDistance = attachDistanceToGs25Stores(selected.stores, latitude, longitude);
|
|
68
|
+
const stores = sortGs25Stores(withDistance).slice(0, limit);
|
|
69
|
+
return successResponse(c, {
|
|
70
|
+
serviceCode,
|
|
71
|
+
keyword,
|
|
72
|
+
geocodeUsed,
|
|
73
|
+
location: typeof latitude === 'number' && typeof longitude === 'number'
|
|
74
|
+
? { latitude, longitude }
|
|
75
|
+
: null,
|
|
76
|
+
cacheHit: storeResult.cacheHit,
|
|
77
|
+
filterRelaxed: selected.filterRelaxed,
|
|
78
|
+
fallbackUsed,
|
|
79
|
+
stores,
|
|
80
|
+
}, {
|
|
81
|
+
total: selected.stores.length,
|
|
82
|
+
pageSize: limit,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
|
|
87
|
+
return errorResponse(c, 'GS25_STORE_SEARCH_FAILED', message, 500);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* GS25 상품 검색 API 핸들러
|
|
92
|
+
* GET /api/gs25/products?keyword={검색어}
|
|
93
|
+
*/
|
|
94
|
+
export async function handleGs25SearchProducts(c) {
|
|
95
|
+
const keyword = c.req.query('keyword') || '';
|
|
96
|
+
const limit = parseInt(c.req.query('limit') || '20', 10);
|
|
97
|
+
if (keyword.trim().length === 0) {
|
|
98
|
+
return errorResponse(c, 'MISSING_QUERY', '검색어(keyword)를 입력해주세요.');
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const products = await fetchGs25SearchProducts(keyword, { timeout: 20000 });
|
|
102
|
+
const limitedProducts = products.slice(0, limit);
|
|
103
|
+
return successResponse(c, {
|
|
104
|
+
keyword,
|
|
105
|
+
count: limitedProducts.length,
|
|
106
|
+
products: limitedProducts.map((p) => ({
|
|
107
|
+
itemCode: p.itemCode,
|
|
108
|
+
itemName: p.itemName,
|
|
109
|
+
shortItemName: p.shortItemName,
|
|
110
|
+
imageUrl: p.imageUrl,
|
|
111
|
+
rating: p.rating,
|
|
112
|
+
stockCheckEnabled: p.stockCheckEnabled,
|
|
113
|
+
})),
|
|
114
|
+
}, {
|
|
115
|
+
total: limitedProducts.length,
|
|
116
|
+
pageSize: limit,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
|
|
121
|
+
return errorResponse(c, 'GS25_PRODUCT_SEARCH_FAILED', message, 500);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* GS25 재고 조회 API 핸들러
|
|
126
|
+
* GET /api/gs25/inventory?keyword={검색어}&storeKeyword={매장키워드}&lat={위도}&lng={경도}
|
|
127
|
+
*/
|
|
128
|
+
export async function handleGs25CheckInventory(c) {
|
|
129
|
+
const keyword = c.req.query('keyword') || '';
|
|
130
|
+
const itemCode = c.req.query('itemCode') || '';
|
|
131
|
+
const storeKeyword = c.req.query('storeKeyword') || '';
|
|
132
|
+
const serviceCode = c.req.query('serviceCode') || '01';
|
|
133
|
+
const storeLimit = parseInt(c.req.query('storeLimit') || '20', 10);
|
|
134
|
+
const rawLat = c.req.query('lat');
|
|
135
|
+
const rawLng = c.req.query('lng');
|
|
136
|
+
const parsedLat = rawLat ? parseFloat(rawLat) : undefined;
|
|
137
|
+
const parsedLng = rawLng ? parseFloat(rawLng) : undefined;
|
|
138
|
+
let latitude = typeof parsedLat === 'number' && Number.isFinite(parsedLat) ? parsedLat : undefined;
|
|
139
|
+
let longitude = typeof parsedLng === 'number' && Number.isFinite(parsedLng) ? parsedLng : undefined;
|
|
140
|
+
let geocodeUsed = false;
|
|
141
|
+
if (keyword.trim().length === 0 && itemCode.trim().length === 0) {
|
|
142
|
+
return errorResponse(c, 'MISSING_QUERY', '검색어(keyword) 또는 상품 코드(itemCode)를 입력해주세요.');
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
if ((typeof latitude !== 'number' || typeof longitude !== 'number') && storeKeyword.trim().length > 0) {
|
|
146
|
+
const baseStores = await fetchGs25Stores({
|
|
147
|
+
serviceCode,
|
|
148
|
+
}, {
|
|
149
|
+
timeout: 20000,
|
|
150
|
+
});
|
|
151
|
+
const firstAddress = filterGs25StoresByKeyword(baseStores.stores, storeKeyword).find((store) => store.address.trim().length > 0)
|
|
152
|
+
?.address || '';
|
|
153
|
+
if (firstAddress.length > 0) {
|
|
154
|
+
const geocoded = await geocodeGs25Address(firstAddress, {
|
|
155
|
+
timeout: 15000,
|
|
156
|
+
googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
|
|
157
|
+
});
|
|
158
|
+
if (geocoded) {
|
|
159
|
+
latitude = geocoded.latitude;
|
|
160
|
+
longitude = geocoded.longitude;
|
|
161
|
+
geocodeUsed = true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// 1단계: totalSearch API로 키워드 → itemCode 변환
|
|
166
|
+
let firstProduct;
|
|
167
|
+
let stockResult;
|
|
168
|
+
let itemCodeUsed = itemCode.trim().length > 0;
|
|
169
|
+
let resolvedItemCode = itemCode.trim().length > 0 ? itemCode.trim() : null;
|
|
170
|
+
if (resolvedItemCode) {
|
|
171
|
+
stockResult = await fetchGs25Stores({
|
|
172
|
+
serviceCode,
|
|
173
|
+
itemCode: resolvedItemCode,
|
|
174
|
+
realTimeStockYn: 'Y',
|
|
175
|
+
latitude,
|
|
176
|
+
longitude,
|
|
177
|
+
useCache: false,
|
|
178
|
+
}, {
|
|
179
|
+
timeout: 20000,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
const searchProducts = await fetchGs25SearchProducts(keyword, { timeout: 20000 });
|
|
184
|
+
firstProduct = searchProducts.find((p) => p.itemCode.length > 0);
|
|
185
|
+
if (firstProduct) {
|
|
186
|
+
resolvedItemCode = firstProduct.itemCode;
|
|
187
|
+
itemCodeUsed = true;
|
|
188
|
+
stockResult = await fetchGs25Stores({
|
|
189
|
+
serviceCode,
|
|
190
|
+
itemCode: resolvedItemCode,
|
|
191
|
+
realTimeStockYn: 'Y',
|
|
192
|
+
latitude,
|
|
193
|
+
longitude,
|
|
194
|
+
useCache: false,
|
|
195
|
+
}, {
|
|
196
|
+
timeout: 20000,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
stockResult = await fetchGs25Stores({
|
|
201
|
+
serviceCode,
|
|
202
|
+
keyword,
|
|
203
|
+
realTimeStockYn: 'Y',
|
|
204
|
+
latitude,
|
|
205
|
+
longitude,
|
|
206
|
+
useCache: false,
|
|
207
|
+
}, {
|
|
208
|
+
timeout: 20000,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const selected = selectGs25StoresForKeyword(stockResult.stores, storeKeyword, {
|
|
213
|
+
relaxWhenEmpty: typeof latitude === 'number' && typeof longitude === 'number',
|
|
214
|
+
});
|
|
215
|
+
const filtered = selected.stores;
|
|
216
|
+
const withDistance = attachDistanceToGs25Stores(filtered, latitude, longitude);
|
|
217
|
+
const stores = sortGs25Stores(withDistance).slice(0, storeLimit);
|
|
218
|
+
// 상품 정보: searchProducts에서 먼저 가져오고, 없으면 stores에서
|
|
219
|
+
const productName = firstProduct?.itemName || firstProduct?.shortItemName ||
|
|
220
|
+
filtered.find((item) => item.searchItemName.length > 0)?.searchItemName || null;
|
|
221
|
+
const productPrice = filtered.find((item) => item.searchItemSellPrice !== null)?.searchItemSellPrice ?? null;
|
|
222
|
+
const inStockStoreCount = filtered.filter((item) => item.realStockQuantity > 0).length;
|
|
223
|
+
const totalStockQuantity = filtered.reduce((sum, item) => sum + Math.max(item.realStockQuantity, 0), 0);
|
|
224
|
+
return successResponse(c, {
|
|
225
|
+
serviceCode,
|
|
226
|
+
keyword,
|
|
227
|
+
itemCodeUsed,
|
|
228
|
+
itemCode: resolvedItemCode,
|
|
229
|
+
storeKeyword,
|
|
230
|
+
geocodeUsed,
|
|
231
|
+
filterRelaxed: selected.filterRelaxed,
|
|
232
|
+
location: typeof latitude === 'number' && typeof longitude === 'number'
|
|
233
|
+
? { latitude, longitude }
|
|
234
|
+
: null,
|
|
235
|
+
product: {
|
|
236
|
+
name: productName,
|
|
237
|
+
sellPrice: productPrice,
|
|
238
|
+
imageUrl: firstProduct?.imageUrl || null,
|
|
239
|
+
rating: firstProduct?.rating || null,
|
|
240
|
+
},
|
|
241
|
+
inventory: {
|
|
242
|
+
totalStoreCount: stockResult.totalCount,
|
|
243
|
+
matchedStoreCount: filtered.length,
|
|
244
|
+
inStockStoreCount,
|
|
245
|
+
totalStockQuantity,
|
|
246
|
+
count: stores.length,
|
|
247
|
+
stores,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
|
|
253
|
+
return errorResponse(c, 'GS25_INVENTORY_CHECK_FAILED', message, 500);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/* c8 ignore stop */
|
package/dist/api/handlers.js
CHANGED
|
@@ -4,122 +4,44 @@
|
|
|
4
4
|
* MCP 미지원 서비스를 위한 GET 기반 API 핸들러입니다.
|
|
5
5
|
* 기존 도구의 핵심 함수들을 재사용합니다.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
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';
|
|
7
|
+
import { enrichOliveyoungProductsWithNearbyStoreInventory, fetchOliveyoungProducts, fetchOliveyoungStores, } from '../services/oliveyoung/client.js';
|
|
8
|
+
import { fetchCuStock, fetchCuStores, geocodeCuAddress } from '../services/cu/client.js';
|
|
13
9
|
import { errorResponse, successResponse } from './response.js';
|
|
10
|
+
export { handleCheckInventory, handleFindStores, handleGetDisplayLocation, handleGetProduct, handleSearchProducts, } from './daisoHandlers.js';
|
|
14
11
|
/**
|
|
15
|
-
*
|
|
16
|
-
* GET /api/
|
|
12
|
+
* 올리브영 상품 검색 API 핸들러
|
|
13
|
+
* GET /api/oliveyoung/products?keyword={검색어}
|
|
17
14
|
*/
|
|
18
|
-
export async function
|
|
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');
|
|
15
|
+
export async function handleOliveyoungSearchProducts(c) {
|
|
96
16
|
const keyword = c.req.query('keyword') || '';
|
|
97
17
|
const page = parseInt(c.req.query('page') || '1');
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
18
|
+
const size = parseInt(c.req.query('size') || '20');
|
|
19
|
+
const sort = c.req.query('sort') || '01';
|
|
20
|
+
const includeSoldOut = c.req.query('includeSoldOut') === 'true';
|
|
21
|
+
if (keyword.trim().length === 0) {
|
|
22
|
+
return errorResponse(c, 'MISSING_QUERY', '검색어(keyword)를 입력해주세요.');
|
|
101
23
|
}
|
|
102
24
|
try {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
25
|
+
const result = await fetchOliveyoungProducts({
|
|
26
|
+
keyword,
|
|
27
|
+
page,
|
|
28
|
+
size,
|
|
29
|
+
sort,
|
|
30
|
+
includeSoldOut,
|
|
31
|
+
}, {
|
|
32
|
+
apiKey: c.env?.ZYTE_API_KEY,
|
|
33
|
+
});
|
|
34
|
+
return successResponse(c, {
|
|
35
|
+
keyword,
|
|
36
|
+
totalCount: result.totalCount,
|
|
37
|
+
nextPage: result.nextPage,
|
|
38
|
+
count: result.products.length,
|
|
39
|
+
products: result.products,
|
|
40
|
+
}, { total: result.totalCount, page, pageSize: size });
|
|
119
41
|
}
|
|
120
42
|
catch (error) {
|
|
121
43
|
const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
|
|
122
|
-
return errorResponse(c, '
|
|
44
|
+
return errorResponse(c, 'OLIVEYOUNG_PRODUCT_SEARCH_FAILED', message, 500);
|
|
123
45
|
}
|
|
124
46
|
}
|
|
125
47
|
/**
|
|
@@ -139,7 +61,7 @@ export async function handleOliveyoungFindStores(c) {
|
|
|
139
61
|
pageIdx,
|
|
140
62
|
searchWords: keyword,
|
|
141
63
|
}, {
|
|
142
|
-
apiKey: c.env
|
|
64
|
+
apiKey: c.env?.ZYTE_API_KEY,
|
|
143
65
|
});
|
|
144
66
|
return successResponse(c, {
|
|
145
67
|
stores: result.stores.slice(0, limit),
|
|
@@ -187,6 +109,15 @@ export async function handleOliveyoungCheckInventory(c) {
|
|
|
187
109
|
apiKey: c.env.ZYTE_API_KEY,
|
|
188
110
|
}),
|
|
189
111
|
]);
|
|
112
|
+
const enrichedInventory = await enrichOliveyoungProductsWithNearbyStoreInventory(productResult.products, {
|
|
113
|
+
latitude: lat,
|
|
114
|
+
longitude: lng,
|
|
115
|
+
storeKeyword,
|
|
116
|
+
maxProducts: Math.min(productResult.products.length, 5),
|
|
117
|
+
}, {
|
|
118
|
+
apiKey: c.env.ZYTE_API_KEY,
|
|
119
|
+
});
|
|
120
|
+
const inStockCount = enrichedInventory.products.filter((product) => product.inStock).length;
|
|
190
121
|
return successResponse(c, {
|
|
191
122
|
keyword,
|
|
192
123
|
location: { latitude: lat, longitude: lng },
|
|
@@ -197,7 +128,11 @@ export async function handleOliveyoungCheckInventory(c) {
|
|
|
197
128
|
inventory: {
|
|
198
129
|
totalCount: productResult.totalCount,
|
|
199
130
|
nextPage: productResult.nextPage,
|
|
200
|
-
|
|
131
|
+
stockCheckedCount: enrichedInventory.checkedCount,
|
|
132
|
+
stockUncheckedCount: Math.max(0, productResult.products.length - enrichedInventory.checkedCount),
|
|
133
|
+
inStockCount,
|
|
134
|
+
outOfStockCount: enrichedInventory.products.length - inStockCount,
|
|
135
|
+
products: enrichedInventory.products,
|
|
201
136
|
},
|
|
202
137
|
}, { total: productResult.totalCount, page, pageSize: size });
|
|
203
138
|
}
|
|
@@ -206,3 +141,144 @@ export async function handleOliveyoungCheckInventory(c) {
|
|
|
206
141
|
return errorResponse(c, 'OLIVEYOUNG_INVENTORY_CHECK_FAILED', message, 500);
|
|
207
142
|
}
|
|
208
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* CU 매장 검색 API 핸들러
|
|
146
|
+
* GET /api/cu/stores?keyword={키워드}&lat={위도}&lng={경도}
|
|
147
|
+
*/
|
|
148
|
+
export async function handleCuFindStores(c) {
|
|
149
|
+
const keyword = c.req.query('keyword') || '';
|
|
150
|
+
const rawLat = c.req.query('lat');
|
|
151
|
+
const rawLng = c.req.query('lng');
|
|
152
|
+
const limit = parseInt(c.req.query('limit') || '20');
|
|
153
|
+
const parsedLat = rawLat ? parseFloat(rawLat) : undefined;
|
|
154
|
+
const parsedLng = rawLng ? parseFloat(rawLng) : undefined;
|
|
155
|
+
const lat = typeof parsedLat === 'number' && Number.isFinite(parsedLat) ? parsedLat : undefined;
|
|
156
|
+
const lng = typeof parsedLng === 'number' && Number.isFinite(parsedLng) ? parsedLng : undefined;
|
|
157
|
+
try {
|
|
158
|
+
const result = await fetchCuStores({
|
|
159
|
+
latitude: lat,
|
|
160
|
+
longitude: lng,
|
|
161
|
+
searchWord: keyword,
|
|
162
|
+
}, {
|
|
163
|
+
timeout: 15000,
|
|
164
|
+
apiKey: c.env.ZYTE_API_KEY,
|
|
165
|
+
});
|
|
166
|
+
return successResponse(c, {
|
|
167
|
+
location: typeof lat === 'number' && typeof lng === 'number' ? { latitude: lat, longitude: lng } : null,
|
|
168
|
+
keyword,
|
|
169
|
+
stores: result.stores.slice(0, limit),
|
|
170
|
+
}, { total: result.totalCount, pageSize: limit });
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
|
|
174
|
+
return errorResponse(c, 'CU_STORE_SEARCH_FAILED', message, 500);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* CU 재고 검색 API 핸들러
|
|
179
|
+
* GET /api/cu/inventory?keyword={검색어}&lat={위도}&lng={경도}
|
|
180
|
+
*/
|
|
181
|
+
export async function handleCuCheckInventory(c) {
|
|
182
|
+
const keyword = c.req.query('keyword');
|
|
183
|
+
const storeKeyword = c.req.query('storeKeyword') || '';
|
|
184
|
+
const rawLat = c.req.query('lat');
|
|
185
|
+
const rawLng = c.req.query('lng');
|
|
186
|
+
const parsedLat = rawLat ? parseFloat(rawLat) : undefined;
|
|
187
|
+
const parsedLng = rawLng ? parseFloat(rawLng) : undefined;
|
|
188
|
+
const lat = typeof parsedLat === 'number' && Number.isFinite(parsedLat) ? parsedLat : undefined;
|
|
189
|
+
const lng = typeof parsedLng === 'number' && Number.isFinite(parsedLng) ? parsedLng : undefined;
|
|
190
|
+
const size = parseInt(c.req.query('size') || '20');
|
|
191
|
+
const offset = parseInt(c.req.query('offset') || '0');
|
|
192
|
+
const searchSort = c.req.query('searchSort') || 'recom';
|
|
193
|
+
const storeLimit = parseInt(c.req.query('storeLimit') || '10');
|
|
194
|
+
if (!keyword || keyword.trim().length === 0) {
|
|
195
|
+
return errorResponse(c, 'MISSING_QUERY', '검색어(keyword)를 입력해주세요.');
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const stockResult = await fetchCuStock({
|
|
199
|
+
keyword,
|
|
200
|
+
limit: size,
|
|
201
|
+
offset,
|
|
202
|
+
searchSort,
|
|
203
|
+
}, {
|
|
204
|
+
timeout: 15000,
|
|
205
|
+
});
|
|
206
|
+
const firstStockItem = stockResult.items.find((item) => item.itemCode.trim().length > 0) || null;
|
|
207
|
+
const hasInputLocation = typeof lat === 'number' && typeof lng === 'number';
|
|
208
|
+
const resolvedLat = hasInputLocation ? lat : undefined;
|
|
209
|
+
const resolvedLng = hasInputLocation ? lng : undefined;
|
|
210
|
+
let storeResult = null;
|
|
211
|
+
// 좌표 미입력 + 매장 키워드 입력 시, 키워드 기반 매장 검색 결과를 우선 사용합니다.
|
|
212
|
+
if (!hasInputLocation && storeKeyword.trim().length > 0) {
|
|
213
|
+
const keywordStoreResult = await fetchCuStores({
|
|
214
|
+
searchWord: storeKeyword,
|
|
215
|
+
}, {
|
|
216
|
+
timeout: 15000,
|
|
217
|
+
apiKey: c.env?.ZYTE_API_KEY,
|
|
218
|
+
});
|
|
219
|
+
const firstAddress = keywordStoreResult.stores.find((store) => store.address.trim().length > 0)?.address || '';
|
|
220
|
+
if (firstAddress.length > 0) {
|
|
221
|
+
const geocoded = await geocodeCuAddress(firstAddress, {
|
|
222
|
+
timeout: 15000,
|
|
223
|
+
googleMapsApiKey: c.env?.GOOGLE_MAPS_API_KEY,
|
|
224
|
+
});
|
|
225
|
+
if (geocoded) {
|
|
226
|
+
const hasStockSeed = !!firstStockItem?.itemCode;
|
|
227
|
+
storeResult = await fetchCuStores({
|
|
228
|
+
latitude: geocoded.latitude,
|
|
229
|
+
longitude: geocoded.longitude,
|
|
230
|
+
searchWord: storeKeyword,
|
|
231
|
+
itemCd: firstStockItem?.itemCode || '',
|
|
232
|
+
onItemNo: firstStockItem?.onItemNo || '',
|
|
233
|
+
jipCd: firstStockItem?.itemCode || '',
|
|
234
|
+
isRecommend: hasStockSeed ? 'Y' : '',
|
|
235
|
+
recommendId: hasStockSeed ? 'stock' : '',
|
|
236
|
+
pageType: hasStockSeed ? 'search_improve stock_sch_improve' : 'search_improve',
|
|
237
|
+
}, {
|
|
238
|
+
timeout: 15000,
|
|
239
|
+
apiKey: c.env?.ZYTE_API_KEY,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
storeResult ||= keywordStoreResult;
|
|
244
|
+
}
|
|
245
|
+
if (!storeResult) {
|
|
246
|
+
const hasStockSeed = !!firstStockItem?.itemCode;
|
|
247
|
+
storeResult = await fetchCuStores({
|
|
248
|
+
latitude: resolvedLat,
|
|
249
|
+
longitude: resolvedLng,
|
|
250
|
+
searchWord: storeKeyword,
|
|
251
|
+
itemCd: firstStockItem?.itemCode || '',
|
|
252
|
+
onItemNo: firstStockItem?.onItemNo || '',
|
|
253
|
+
jipCd: firstStockItem?.itemCode || '',
|
|
254
|
+
isRecommend: hasStockSeed ? 'Y' : '',
|
|
255
|
+
recommendId: hasStockSeed ? 'stock' : '',
|
|
256
|
+
pageType: hasStockSeed ? 'search_improve stock_sch_improve' : 'search_improve',
|
|
257
|
+
}, {
|
|
258
|
+
timeout: 15000,
|
|
259
|
+
apiKey: c.env?.ZYTE_API_KEY,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return successResponse(c, {
|
|
263
|
+
keyword,
|
|
264
|
+
location: typeof resolvedLat === 'number' && typeof resolvedLng === 'number'
|
|
265
|
+
? { latitude: resolvedLat, longitude: resolvedLng }
|
|
266
|
+
: null,
|
|
267
|
+
nearbyStores: {
|
|
268
|
+
totalCount: storeResult.totalCount,
|
|
269
|
+
stockItemCode: firstStockItem?.itemCode || null,
|
|
270
|
+
stockItemName: firstStockItem?.itemName || null,
|
|
271
|
+
stores: storeResult.stores.slice(0, storeLimit),
|
|
272
|
+
},
|
|
273
|
+
inventory: {
|
|
274
|
+
totalCount: stockResult.totalCount,
|
|
275
|
+
spellModifyYn: stockResult.spellModifyYn,
|
|
276
|
+
items: stockResult.items,
|
|
277
|
+
},
|
|
278
|
+
}, { total: stockResult.totalCount, page: Math.floor(offset / Math.max(size, 1)) + 1, pageSize: size });
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
|
|
282
|
+
return errorResponse(c, 'CU_INVENTORY_CHECK_FAILED', message, 500);
|
|
283
|
+
}
|
|
284
|
+
}
|