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,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CU API 클라이언트
|
|
3
|
+
*/
|
|
4
|
+
import { fetchJson } from '../../utils/http.js';
|
|
5
|
+
import { decodeBase64, requestByZyte } from '../../utils/zyte.js';
|
|
6
|
+
import { CU_API } from './api.js';
|
|
7
|
+
const CU_DEFAULT_HEADERS = {
|
|
8
|
+
'Content-Type': 'application/json',
|
|
9
|
+
Accept: 'application/json, text/javascript, */*; q=0.01',
|
|
10
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
11
|
+
};
|
|
12
|
+
const CU_WEB_DEFAULT_HEADERS = {
|
|
13
|
+
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
14
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
15
|
+
Accept: 'text/html, */*; q=0.01',
|
|
16
|
+
'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
|
|
17
|
+
Origin: 'https://cu.bgfretail.com',
|
|
18
|
+
Referer: 'https://cu.bgfretail.com/store/list.do?category=store',
|
|
19
|
+
};
|
|
20
|
+
function toNumber(value) {
|
|
21
|
+
if (typeof value === 'number') {
|
|
22
|
+
return Number.isFinite(value) ? value : 0;
|
|
23
|
+
}
|
|
24
|
+
if (typeof value === 'string') {
|
|
25
|
+
const parsed = Number.parseFloat(value);
|
|
26
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
27
|
+
}
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
function toYnBoolean(value) {
|
|
31
|
+
if (typeof value === 'boolean') {
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
if (typeof value === 'string') {
|
|
35
|
+
const normalized = value.trim().toUpperCase();
|
|
36
|
+
return normalized === 'Y' || normalized === 'TRUE';
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
function toStoreAddress(raw) {
|
|
41
|
+
const road = [raw.doroStoreAddr1 || '', raw.doroStoreAddr2 || ''].join(' ').trim();
|
|
42
|
+
if (road.length > 0) {
|
|
43
|
+
return road;
|
|
44
|
+
}
|
|
45
|
+
return [raw.addrFst || '', raw.addrDetail || ''].join(' ').trim();
|
|
46
|
+
}
|
|
47
|
+
async function requestCuJson(path, body, timeout = 15000) {
|
|
48
|
+
return fetchJson(`${CU_API.BASE_URL}${path}`, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
timeout,
|
|
51
|
+
headers: CU_DEFAULT_HEADERS,
|
|
52
|
+
body: JSON.stringify(body),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async function requestCuWebHtml(path, body, apiKey, timeout = 15000) {
|
|
56
|
+
const form = new URLSearchParams(body);
|
|
57
|
+
const formText = form.toString();
|
|
58
|
+
const targetUrl = `${CU_API.WEB_BASE_URL}${path}`;
|
|
59
|
+
const response = await fetch(`${CU_API.WEB_BASE_URL}${path}`, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: CU_WEB_DEFAULT_HEADERS,
|
|
62
|
+
body: formText,
|
|
63
|
+
signal: AbortSignal.timeout(timeout),
|
|
64
|
+
});
|
|
65
|
+
if (response.ok) {
|
|
66
|
+
return response.text();
|
|
67
|
+
}
|
|
68
|
+
if (response.status === 400 || response.status === 403 || response.status === 429) {
|
|
69
|
+
try {
|
|
70
|
+
const result = await requestByZyte({
|
|
71
|
+
apiKey,
|
|
72
|
+
url: targetUrl,
|
|
73
|
+
timeout,
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: Object.entries(CU_WEB_DEFAULT_HEADERS).map(([name, value]) => ({ name, value })),
|
|
76
|
+
bodyText: formText,
|
|
77
|
+
});
|
|
78
|
+
if (result.statusCode === 200 && result.httpResponseBody) {
|
|
79
|
+
return decodeBase64(result.httpResponseBody);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// 직접 호출 실패 시 Zyte 재시도도 실패하면 원본 에러를 반환합니다.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
throw new Error(`API 요청 실패: ${response.status} ${response.statusText}`);
|
|
87
|
+
}
|
|
88
|
+
export async function geocodeCuAddress(address, options = {}) {
|
|
89
|
+
const keyword = address.trim();
|
|
90
|
+
if (keyword.length === 0) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const { timeout = 15000, googleMapsApiKey } = options;
|
|
94
|
+
const apiKey = (googleMapsApiKey || '').trim();
|
|
95
|
+
if (apiKey.length === 0) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const endpoint = new URL('https://maps.googleapis.com/maps/api/geocode/json');
|
|
99
|
+
endpoint.searchParams.set('address', keyword);
|
|
100
|
+
endpoint.searchParams.set('key', apiKey);
|
|
101
|
+
const body = await fetchJson(endpoint.toString(), {
|
|
102
|
+
method: 'GET',
|
|
103
|
+
timeout,
|
|
104
|
+
headers: {
|
|
105
|
+
Accept: 'application/json',
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
if (body.status !== 'OK') {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const firstLocation = body.results?.[0]?.geometry?.location;
|
|
112
|
+
if (!firstLocation) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const latitude = toNumber(firstLocation.lat);
|
|
116
|
+
const longitude = toNumber(firstLocation.lng);
|
|
117
|
+
if (latitude === 0 || longitude === 0) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
latitude,
|
|
122
|
+
longitude,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function decodeHtmlEntities(value) {
|
|
126
|
+
return value
|
|
127
|
+
.replace(/ /g, ' ')
|
|
128
|
+
.replace(/</g, '<')
|
|
129
|
+
.replace(/>/g, '>')
|
|
130
|
+
.replace(/"/g, '"')
|
|
131
|
+
.replace(/'/g, "'")
|
|
132
|
+
.replace(/&/g, '&');
|
|
133
|
+
}
|
|
134
|
+
function sanitizeHtmlText(value) {
|
|
135
|
+
const stripped = value.replace(/<[^>]*>/g, ' ');
|
|
136
|
+
return decodeHtmlEntities(stripped).replace(/\s+/g, ' ').trim();
|
|
137
|
+
}
|
|
138
|
+
function parseCuWebStores(html) {
|
|
139
|
+
const rows = html.match(/<tr>[\s\S]*?<\/tr>/g) || [];
|
|
140
|
+
const stores = [];
|
|
141
|
+
for (const row of rows) {
|
|
142
|
+
const nameMatch = row.match(/<span class="name">([\s\S]*?)<\/span>/);
|
|
143
|
+
if (!nameMatch) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const phoneMatch = row.match(/<span class="tel">([\s\S]*?)<\/span>/);
|
|
147
|
+
const addressMatch = row.match(/<address>[\s\S]*?<a [^>]*>([\s\S]*?)<\/a>/);
|
|
148
|
+
const codeMatch = row.match(/searchLatLng\('[^']*',\s*'([^']+)'\)/);
|
|
149
|
+
stores.push({
|
|
150
|
+
storeCode: codeMatch?.[1] || '',
|
|
151
|
+
storeName: sanitizeHtmlText(nameMatch[1]),
|
|
152
|
+
phone: sanitizeHtmlText(phoneMatch?.[1] || ''),
|
|
153
|
+
address: sanitizeHtmlText(addressMatch?.[1] || ''),
|
|
154
|
+
latitude: 0,
|
|
155
|
+
longitude: 0,
|
|
156
|
+
distanceM: 0,
|
|
157
|
+
stock: 0,
|
|
158
|
+
deliveryYn: false,
|
|
159
|
+
pickupYn: false,
|
|
160
|
+
reserveYn: false,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return stores;
|
|
164
|
+
}
|
|
165
|
+
function normalizeCuStore(raw) {
|
|
166
|
+
return {
|
|
167
|
+
storeCode: raw.storeCd || '',
|
|
168
|
+
storeName: raw.storeNm || '',
|
|
169
|
+
phone: raw.storeTelNo || '',
|
|
170
|
+
address: toStoreAddress(raw),
|
|
171
|
+
latitude: toNumber(raw.latVal),
|
|
172
|
+
longitude: toNumber(raw.longVal),
|
|
173
|
+
distanceM: toNumber(raw.distance),
|
|
174
|
+
stock: toNumber(raw.stock),
|
|
175
|
+
deliveryYn: toYnBoolean(raw.deliveryYn) || toYnBoolean(raw.deliveryPickYn),
|
|
176
|
+
pickupYn: toYnBoolean(raw.jumpoPickYn),
|
|
177
|
+
reserveYn: toYnBoolean(raw.reserveYn),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* 좌표 기반 CU 점포 목록을 조회합니다.
|
|
182
|
+
*/
|
|
183
|
+
export async function fetchCuStores(params, options = {}) {
|
|
184
|
+
const { timeout = 15000, apiKey } = options;
|
|
185
|
+
const searchWord = (params.searchWord || '').trim();
|
|
186
|
+
const hasLatitude = typeof params.latitude === 'number' && Number.isFinite(params.latitude);
|
|
187
|
+
const hasLongitude = typeof params.longitude === 'number' && Number.isFinite(params.longitude);
|
|
188
|
+
// 좌표가 없고 검색어가 있으면 웹 매장 검색으로 폴백합니다.
|
|
189
|
+
if (searchWord.length > 0 && (!hasLatitude || !hasLongitude)) {
|
|
190
|
+
const html = await requestCuWebHtml(CU_API.WEB_STORE_LIST_PATH, {
|
|
191
|
+
pageIndex: '1',
|
|
192
|
+
listType: '',
|
|
193
|
+
jumpoCode: '',
|
|
194
|
+
jumpoLotto: '',
|
|
195
|
+
jumpoToto: '',
|
|
196
|
+
jumpoCash: '',
|
|
197
|
+
jumpoHour: '',
|
|
198
|
+
jumpoCafe: '',
|
|
199
|
+
jumpoDelivery: '',
|
|
200
|
+
jumpoBakery: '',
|
|
201
|
+
jumpoFry: '',
|
|
202
|
+
jumpoMultiDevice: '',
|
|
203
|
+
jumpoPosCash: '',
|
|
204
|
+
jumpoBattery: '',
|
|
205
|
+
jumpoAdderss: '',
|
|
206
|
+
jumpoSido: '',
|
|
207
|
+
jumpoGugun: '',
|
|
208
|
+
jumpodong: '',
|
|
209
|
+
searchWord,
|
|
210
|
+
}, apiKey, timeout);
|
|
211
|
+
const stores = parseCuWebStores(html);
|
|
212
|
+
return {
|
|
213
|
+
totalCount: stores.length,
|
|
214
|
+
stores,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const latitude = hasLatitude ? params.latitude : 37.5665;
|
|
218
|
+
const longitude = hasLongitude ? params.longitude : 126.978;
|
|
219
|
+
const payload = {
|
|
220
|
+
latVal: String(latitude),
|
|
221
|
+
longVal: String(longitude),
|
|
222
|
+
baseLatVal: String(latitude),
|
|
223
|
+
baseLongVal: String(longitude),
|
|
224
|
+
tabId: '2',
|
|
225
|
+
filterSvcList: [],
|
|
226
|
+
filterAdtList: [],
|
|
227
|
+
stockCdcYn: 'N',
|
|
228
|
+
searchStock: false,
|
|
229
|
+
pickupType: 'change',
|
|
230
|
+
getRoute: 'IOS',
|
|
231
|
+
areaTplNo: '0',
|
|
232
|
+
childMealPickUpYn: 'N',
|
|
233
|
+
isCurrentSearch: 'N',
|
|
234
|
+
pageType: params.pageType || 'search_improve',
|
|
235
|
+
searchWord,
|
|
236
|
+
isRecommend: params.isRecommend || '',
|
|
237
|
+
recommendId: params.recommendId || '',
|
|
238
|
+
jipCd: params.jipCd || params.itemCd || '',
|
|
239
|
+
itemCd: params.itemCd || '',
|
|
240
|
+
item_cd: params.itemCd || '',
|
|
241
|
+
onItemNo: params.onItemNo || '',
|
|
242
|
+
};
|
|
243
|
+
const body = await requestCuJson(CU_API.STORE_PATH, payload, timeout);
|
|
244
|
+
const stores = (body.storeList || []).map((store) => normalizeCuStore(store));
|
|
245
|
+
return {
|
|
246
|
+
totalCount: toNumber(body.totalCnt) || stores.length,
|
|
247
|
+
stores,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* 재고 검색 전 초기 화면 데이터를 조회합니다.
|
|
252
|
+
* 정책 변경 시 사전 호출이 필요한 경우를 대비한 워밍업 요청입니다.
|
|
253
|
+
*/
|
|
254
|
+
export async function primeCuStockDisplay(options = {}) {
|
|
255
|
+
const { timeout = 15000 } = options;
|
|
256
|
+
await requestCuJson(CU_API.STOCK_DISPLAY_PATH, {}, timeout);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* CU 상품 재고 검색 결과를 조회합니다.
|
|
260
|
+
*/
|
|
261
|
+
export async function fetchCuStock(params, options = {}) {
|
|
262
|
+
const { timeout = 15000 } = options;
|
|
263
|
+
try {
|
|
264
|
+
await primeCuStockDisplay({ timeout });
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// 사전 워밍업 실패는 본 검색으로 재시도합니다.
|
|
268
|
+
}
|
|
269
|
+
const payload = {
|
|
270
|
+
searchWord: params.keyword,
|
|
271
|
+
prevSearchWord: '',
|
|
272
|
+
spellModifyUseYn: 'Y',
|
|
273
|
+
offset: params.offset,
|
|
274
|
+
limit: params.limit,
|
|
275
|
+
searchSort: params.searchSort,
|
|
276
|
+
};
|
|
277
|
+
const body = await requestCuJson(CU_API.STOCK_MAIN_PATH, payload, timeout);
|
|
278
|
+
const result = body.data?.stockResult?.result;
|
|
279
|
+
const rows = result?.rows || [];
|
|
280
|
+
const items = rows
|
|
281
|
+
.map((row) => row.fields)
|
|
282
|
+
.filter((fields) => Boolean(fields))
|
|
283
|
+
.map((fields) => ({
|
|
284
|
+
itemCode: fields.item_cd || '',
|
|
285
|
+
onItemNo: fields.on_item_no || '',
|
|
286
|
+
itemName: fields.item_nm || '',
|
|
287
|
+
price: toNumber(fields.hyun_maega),
|
|
288
|
+
pickupYn: toYnBoolean(fields.pickup_yn),
|
|
289
|
+
deliveryYn: toYnBoolean(fields.deliv_yn),
|
|
290
|
+
reserveYn: toYnBoolean(fields.reserv_yn),
|
|
291
|
+
}));
|
|
292
|
+
return {
|
|
293
|
+
totalCount: toNumber(result?.total_count) || items.length,
|
|
294
|
+
spellModifyYn: body.spellModifyYn || 'N',
|
|
295
|
+
items,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CU 서비스 프로바이더
|
|
3
|
+
*/
|
|
4
|
+
import { createFindNearbyStoresTool } from './tools/findNearbyStores.js';
|
|
5
|
+
import { createCheckInventoryTool } from './tools/checkInventory.js';
|
|
6
|
+
const CU_METADATA = {
|
|
7
|
+
id: 'cu',
|
|
8
|
+
name: 'CU 편의점',
|
|
9
|
+
version: '1.0.0',
|
|
10
|
+
description: 'CU 매장 탐색 및 상품 재고 조회 서비스',
|
|
11
|
+
};
|
|
12
|
+
class CuService {
|
|
13
|
+
metadata = CU_METADATA;
|
|
14
|
+
getTools() {
|
|
15
|
+
return [createFindNearbyStoresTool(), createCheckInventoryTool()];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function createCuService() {
|
|
19
|
+
return new CuService();
|
|
20
|
+
}
|
|
21
|
+
export * from './types.js';
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CU 재고 확인 도구
|
|
3
|
+
*/
|
|
4
|
+
import * as z from 'zod';
|
|
5
|
+
import { fetchCuStock, fetchCuStores, geocodeCuAddress } from '../client.js';
|
|
6
|
+
async function checkInventory(args) {
|
|
7
|
+
const { keyword, latitude, longitude, storeKeyword = '', size = 20, offset = 0, searchSort = 'recom', storeLimit = 10, timeoutMs = 15000, } = args;
|
|
8
|
+
if (!keyword || keyword.trim().length === 0) {
|
|
9
|
+
throw new Error('상품 검색어(keyword)를 입력해주세요.');
|
|
10
|
+
}
|
|
11
|
+
const stockResult = await fetchCuStock({
|
|
12
|
+
keyword,
|
|
13
|
+
limit: size,
|
|
14
|
+
offset,
|
|
15
|
+
searchSort,
|
|
16
|
+
}, {
|
|
17
|
+
timeout: timeoutMs,
|
|
18
|
+
});
|
|
19
|
+
const firstStockItem = stockResult.items.find((item) => item.itemCode.trim().length > 0) || null;
|
|
20
|
+
const hasInputLocation = typeof latitude === 'number' && typeof longitude === 'number';
|
|
21
|
+
const resolvedLatitude = hasInputLocation ? latitude : undefined;
|
|
22
|
+
const resolvedLongitude = hasInputLocation ? longitude : undefined;
|
|
23
|
+
let storeResult = null;
|
|
24
|
+
// 좌표 미입력 + 매장 키워드 입력 시, 키워드 기반 매장 검색 결과를 우선 사용합니다.
|
|
25
|
+
if (!hasInputLocation && storeKeyword.trim().length > 0) {
|
|
26
|
+
const keywordStoreResult = await fetchCuStores({
|
|
27
|
+
searchWord: storeKeyword,
|
|
28
|
+
}, {
|
|
29
|
+
timeout: timeoutMs,
|
|
30
|
+
});
|
|
31
|
+
const firstAddress = keywordStoreResult.stores.find((store) => store.address.trim().length > 0)?.address || '';
|
|
32
|
+
if (firstAddress.length > 0) {
|
|
33
|
+
const googleMapsApiKey = process.env.GOOGLE_MAPS_API_KEY;
|
|
34
|
+
const geocoded = await geocodeCuAddress(firstAddress, {
|
|
35
|
+
timeout: timeoutMs,
|
|
36
|
+
googleMapsApiKey,
|
|
37
|
+
});
|
|
38
|
+
if (geocoded) {
|
|
39
|
+
const hasStockSeed = !!firstStockItem?.itemCode;
|
|
40
|
+
storeResult = await fetchCuStores({
|
|
41
|
+
latitude: geocoded.latitude,
|
|
42
|
+
longitude: geocoded.longitude,
|
|
43
|
+
searchWord: storeKeyword,
|
|
44
|
+
itemCd: firstStockItem?.itemCode || '',
|
|
45
|
+
onItemNo: firstStockItem?.onItemNo || '',
|
|
46
|
+
jipCd: firstStockItem?.itemCode || '',
|
|
47
|
+
isRecommend: hasStockSeed ? 'Y' : '',
|
|
48
|
+
recommendId: hasStockSeed ? 'stock' : '',
|
|
49
|
+
pageType: hasStockSeed ? 'search_improve stock_sch_improve' : 'search_improve',
|
|
50
|
+
}, {
|
|
51
|
+
timeout: timeoutMs,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
storeResult ||= keywordStoreResult;
|
|
56
|
+
}
|
|
57
|
+
if (!storeResult) {
|
|
58
|
+
const hasStockSeed = !!firstStockItem?.itemCode;
|
|
59
|
+
storeResult = await fetchCuStores({
|
|
60
|
+
latitude: resolvedLatitude,
|
|
61
|
+
longitude: resolvedLongitude,
|
|
62
|
+
searchWord: storeKeyword,
|
|
63
|
+
itemCd: firstStockItem?.itemCode || '',
|
|
64
|
+
onItemNo: firstStockItem?.onItemNo || '',
|
|
65
|
+
jipCd: firstStockItem?.itemCode || '',
|
|
66
|
+
isRecommend: hasStockSeed ? 'Y' : '',
|
|
67
|
+
recommendId: hasStockSeed ? 'stock' : '',
|
|
68
|
+
pageType: hasStockSeed ? 'search_improve stock_sch_improve' : 'search_improve',
|
|
69
|
+
}, {
|
|
70
|
+
timeout: timeoutMs,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const limitedStores = storeResult.stores.slice(0, storeLimit);
|
|
74
|
+
const result = {
|
|
75
|
+
keyword,
|
|
76
|
+
searchOptions: {
|
|
77
|
+
size,
|
|
78
|
+
offset,
|
|
79
|
+
searchSort,
|
|
80
|
+
storeKeyword,
|
|
81
|
+
},
|
|
82
|
+
location: typeof resolvedLatitude === 'number' && typeof resolvedLongitude === 'number'
|
|
83
|
+
? { latitude: resolvedLatitude, longitude: resolvedLongitude }
|
|
84
|
+
: null,
|
|
85
|
+
nearbyStores: {
|
|
86
|
+
totalCount: storeResult.totalCount,
|
|
87
|
+
count: limitedStores.length,
|
|
88
|
+
stockItemCode: firstStockItem?.itemCode || null,
|
|
89
|
+
stockItemName: firstStockItem?.itemName || null,
|
|
90
|
+
stores: limitedStores,
|
|
91
|
+
},
|
|
92
|
+
inventory: {
|
|
93
|
+
totalCount: stockResult.totalCount,
|
|
94
|
+
count: stockResult.items.length,
|
|
95
|
+
spellModifyYn: stockResult.spellModifyYn,
|
|
96
|
+
items: stockResult.items,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export function createCheckInventoryTool() {
|
|
104
|
+
return {
|
|
105
|
+
name: 'cu_check_inventory',
|
|
106
|
+
metadata: {
|
|
107
|
+
title: 'CU 재고 확인',
|
|
108
|
+
description: '상품 키워드로 CU 재고를 조회하고 주변 매장 정보를 함께 반환합니다.',
|
|
109
|
+
inputSchema: {
|
|
110
|
+
keyword: z.string().describe('재고를 확인할 상품 검색어 (예: 과자, 커피, 도시락)'),
|
|
111
|
+
latitude: z.number().optional().describe('위도 (없으면 storeKeyword 기반으로 좌표 추정 시도)'),
|
|
112
|
+
longitude: z.number().optional().describe('경도 (없으면 storeKeyword 기반으로 좌표 추정 시도)'),
|
|
113
|
+
storeKeyword: z.string().optional().describe('주변 매장 필터 키워드 (예: 강남, 안산)'),
|
|
114
|
+
size: z.number().optional().default(20).describe('재고 검색 결과 수 (기본값: 20)'),
|
|
115
|
+
offset: z.number().optional().default(0).describe('재고 검색 시작 오프셋 (기본값: 0)'),
|
|
116
|
+
searchSort: z.string().optional().default('recom').describe('재고 정렬 방식 (기본값: recom)'),
|
|
117
|
+
storeLimit: z.number().optional().default(10).describe('반환할 주변 매장 최대 수 (기본값: 10)'),
|
|
118
|
+
timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
handler: checkInventory,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CU 주변 매장 탐색 도구
|
|
3
|
+
*/
|
|
4
|
+
import * as z from 'zod';
|
|
5
|
+
import { fetchCuStores } from '../client.js';
|
|
6
|
+
async function findNearbyStores(args) {
|
|
7
|
+
const { latitude, longitude, keyword = '', limit = 20, timeoutMs = 15000, } = args;
|
|
8
|
+
const { totalCount, stores } = await fetchCuStores({
|
|
9
|
+
latitude,
|
|
10
|
+
longitude,
|
|
11
|
+
searchWord: keyword,
|
|
12
|
+
}, {
|
|
13
|
+
timeout: timeoutMs,
|
|
14
|
+
});
|
|
15
|
+
const limitedStores = stores.slice(0, limit);
|
|
16
|
+
return {
|
|
17
|
+
content: [
|
|
18
|
+
{
|
|
19
|
+
type: 'text',
|
|
20
|
+
text: JSON.stringify({
|
|
21
|
+
location: typeof latitude === 'number' && typeof longitude === 'number'
|
|
22
|
+
? { latitude, longitude }
|
|
23
|
+
: null,
|
|
24
|
+
keyword,
|
|
25
|
+
totalCount,
|
|
26
|
+
count: limitedStores.length,
|
|
27
|
+
stores: limitedStores,
|
|
28
|
+
}, null, 2),
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function createFindNearbyStoresTool() {
|
|
34
|
+
return {
|
|
35
|
+
name: 'cu_find_nearby_stores',
|
|
36
|
+
metadata: {
|
|
37
|
+
title: 'CU 주변 매장 탐색',
|
|
38
|
+
description: '위치(위도/경도)와 키워드로 CU 매장을 검색합니다.',
|
|
39
|
+
inputSchema: {
|
|
40
|
+
latitude: z.number().optional().describe('위도 (없으면 키워드 기반 검색)'),
|
|
41
|
+
longitude: z.number().optional().describe('경도 (없으면 키워드 기반 검색)'),
|
|
42
|
+
keyword: z.string().optional().describe('매장 검색 키워드 (예: 강남, 명동, 안산)'),
|
|
43
|
+
limit: z.number().optional().default(20).describe('반환할 최대 매장 수 (기본값: 20)'),
|
|
44
|
+
timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
handler: findNearbyStores,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -12,10 +12,18 @@ export const DAISOMALL_API = {
|
|
|
12
12
|
SEARCH_PRODUCTS: 'https://prdm.daisomall.co.kr/ssn/search/FindStoreGoods',
|
|
13
13
|
/** 온라인 재고 조회 API */
|
|
14
14
|
ONLINE_STOCK: 'https://mapi.daisomall.co.kr/ms/msg/selOnlStck',
|
|
15
|
-
/**
|
|
15
|
+
/** 구 재고 조회 API (현재 403 Unauthorized 발생) */
|
|
16
16
|
STORE_INVENTORY: 'https://mapi.daisomall.co.kr/ms/msg/newIntSelStr',
|
|
17
|
+
/** 현재 사용 중인 인증 토큰 요청 API */
|
|
18
|
+
AUTH_REQUEST: 'https://fapi.daisomall.co.kr/auth/request',
|
|
19
|
+
/** 현재 사용 중인 위치 기반 매장 조회 API */
|
|
20
|
+
STORE_SEARCH_V2: 'https://fapi.daisomall.co.kr/ms/msg/selStr',
|
|
21
|
+
/** 현재 사용 중인 매장별 재고 조회 API */
|
|
22
|
+
STORE_INVENTORY_V2: 'https://fapi.daisomall.co.kr/pd/pdh/selStrPkupStck',
|
|
23
|
+
/** 매장 내 상품 진열 위치 조회 API */
|
|
24
|
+
DISPLAY_LOCATION: 'https://fapi.daisomall.co.kr/pdo/selIntPdStDispInfo',
|
|
17
25
|
/** 이미지 CDN 베이스 URL */
|
|
18
|
-
IMAGE_BASE_URL: 'https://
|
|
26
|
+
IMAGE_BASE_URL: 'https://cdn.daisomall.co.kr',
|
|
19
27
|
};
|
|
20
28
|
/**
|
|
21
29
|
* 다이소 공식 사이트 API (daiso.co.kr)
|
|
@@ -37,6 +45,18 @@ export const DAISO_WEB_API = {
|
|
|
37
45
|
export function getImageUrl(path) {
|
|
38
46
|
if (!path)
|
|
39
47
|
return undefined;
|
|
48
|
+
if (path.startsWith('http://') || path.startsWith('https://')) {
|
|
49
|
+
try {
|
|
50
|
+
const url = new URL(path);
|
|
51
|
+
if (url.hostname === 'img.daisomall.co.kr') {
|
|
52
|
+
url.hostname = 'cdn.daisomall.co.kr';
|
|
53
|
+
}
|
|
54
|
+
return url.toString();
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return path;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
40
60
|
return `${DAISOMALL_API.IMAGE_BASE_URL}${path}`;
|
|
41
61
|
}
|
|
42
62
|
/**
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* 다이소 API 호출 시 필요한 기본 헤더를 적용합니다.
|
|
5
5
|
*/
|
|
6
6
|
import { fetchJson, fetchText, fetchWithTimeout } from '../../utils/http.js';
|
|
7
|
+
import { DAISOMALL_API } from './api.js';
|
|
8
|
+
const DAISO_AUTH_KEY = 'PRE_AUTH_ENC_KEY';
|
|
7
9
|
const DAISO_DEFAULT_HEADERS = {
|
|
8
10
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
9
11
|
Accept: 'application/json, text/html, */*',
|
|
@@ -33,3 +35,60 @@ export async function fetchDaisoHtml(url, options = {}) {
|
|
|
33
35
|
headers: withDaisoHeaders(options.headers),
|
|
34
36
|
});
|
|
35
37
|
}
|
|
38
|
+
function base64FromBytes(bytes) {
|
|
39
|
+
/* c8 ignore next -- Node 20 CI always provides Buffer; the fallback below is for browser-like runtimes. */
|
|
40
|
+
if (typeof Buffer !== 'undefined') {
|
|
41
|
+
return Buffer.from(bytes).toString('base64');
|
|
42
|
+
}
|
|
43
|
+
/* c8 ignore start -- Node 20 CI always provides Buffer; this fallback is for browser-like runtimes. */
|
|
44
|
+
let binary = '';
|
|
45
|
+
for (const byte of bytes) {
|
|
46
|
+
binary += String.fromCharCode(byte);
|
|
47
|
+
}
|
|
48
|
+
if (typeof btoa === 'function') {
|
|
49
|
+
return btoa(binary);
|
|
50
|
+
}
|
|
51
|
+
throw new Error('Base64 인코딩을 지원하지 않는 환경입니다.');
|
|
52
|
+
/* c8 ignore stop */
|
|
53
|
+
}
|
|
54
|
+
async function createDaisoAuthHeader(token) {
|
|
55
|
+
const encoder = new TextEncoder();
|
|
56
|
+
const iv = crypto.getRandomValues(new Uint8Array(16));
|
|
57
|
+
const key = await crypto.subtle.importKey('raw', encoder.encode(DAISO_AUTH_KEY), { name: 'AES-CBC' }, false, ['encrypt']);
|
|
58
|
+
const encrypted = await crypto.subtle.encrypt({ name: 'AES-CBC', iv }, key, encoder.encode(token));
|
|
59
|
+
return `${base64FromBytes(iv)}${base64FromBytes(new Uint8Array(encrypted))}`;
|
|
60
|
+
}
|
|
61
|
+
export async function createDaisoAuthContext() {
|
|
62
|
+
const response = await daisoFetch(DAISOMALL_API.AUTH_REQUEST, {
|
|
63
|
+
method: 'GET',
|
|
64
|
+
});
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
throw new Error(`다이소 인증 토큰 요청 실패: ${response.status} ${response.statusText}`);
|
|
67
|
+
}
|
|
68
|
+
const token = (await response.text()).trim();
|
|
69
|
+
const dmUid = response.headers.get('x-dm-uid')?.trim() || '';
|
|
70
|
+
if (!token) {
|
|
71
|
+
throw new Error('다이소 인증 토큰이 비어 있습니다.');
|
|
72
|
+
}
|
|
73
|
+
if (!dmUid) {
|
|
74
|
+
throw new Error('다이소 인증 응답에 X-DM-UID 헤더가 없습니다.');
|
|
75
|
+
}
|
|
76
|
+
const authorization = await createDaisoAuthHeader(token);
|
|
77
|
+
return {
|
|
78
|
+
authorization: `Bearer ${authorization}`,
|
|
79
|
+
dmUid,
|
|
80
|
+
cookie: `DM_UID=${dmUid}`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export async function fetchDaisoJsonWithAuth(url, options = {}) {
|
|
84
|
+
const auth = await createDaisoAuthContext();
|
|
85
|
+
return fetchJson(url, {
|
|
86
|
+
...options,
|
|
87
|
+
headers: withDaisoHeaders({
|
|
88
|
+
...options.headers,
|
|
89
|
+
Authorization: auth.authorization,
|
|
90
|
+
'X-DM-UID': auth.dmUid,
|
|
91
|
+
Cookie: auth.cookie,
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -7,6 +7,7 @@ import { createSearchProductsTool } from './tools/searchProducts.js';
|
|
|
7
7
|
import { createFindStoresTool } from './tools/findStores.js';
|
|
8
8
|
import { createCheckInventoryTool } from './tools/checkInventory.js';
|
|
9
9
|
import { createGetPriceInfoTool } from './tools/getPriceInfo.js';
|
|
10
|
+
import { createGetDisplayLocationTool } from './tools/getDisplayLocation.js';
|
|
10
11
|
/**
|
|
11
12
|
* 다이소 서비스 메타데이터
|
|
12
13
|
*/
|
|
@@ -27,6 +28,7 @@ class DaisoService {
|
|
|
27
28
|
createFindStoresTool(),
|
|
28
29
|
createCheckInventoryTool(),
|
|
29
30
|
createGetPriceInfoTool(),
|
|
31
|
+
createGetDisplayLocationTool(),
|
|
30
32
|
];
|
|
31
33
|
}
|
|
32
34
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 다이소 상품 매핑 유틸리티
|
|
3
|
+
*/
|
|
4
|
+
import { getImageUrl } from './api.js';
|
|
5
|
+
/**
|
|
6
|
+
* 검색/상세 응답의 상품 문서를 공통 요약 형태로 변환합니다.
|
|
7
|
+
*/
|
|
8
|
+
export function toProductSummary(product) {
|
|
9
|
+
return {
|
|
10
|
+
id: product.PD_NO,
|
|
11
|
+
name: product.PDNM || product.EXH_PD_NM || '',
|
|
12
|
+
imageUrl: getImageUrl(product.ATCH_FILE_URL),
|
|
13
|
+
brand: product.BRND_NM || undefined,
|
|
14
|
+
soldOut: product.SOLD_OUT_YN === 'Y',
|
|
15
|
+
isNew: product.NEW_PD_YN === 'Y',
|
|
16
|
+
};
|
|
17
|
+
}
|