daiso 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +218 -537
  2. package/dist/api/actionsProxy.js +82 -0
  3. package/dist/api/cgvHandlers.js +103 -16
  4. package/dist/api/daisoHandlers.js +127 -0
  5. package/dist/api/emart24Handlers.js +176 -0
  6. package/dist/api/gs25Handlers.js +256 -0
  7. package/dist/api/handlers.js +184 -108
  8. package/dist/api/healthChecks.js +332 -0
  9. package/dist/api/lottecinemaHandlers.js +155 -0
  10. package/dist/api/lottemartHandlers.js +128 -0
  11. package/dist/api/megaboxHandlers.js +81 -50
  12. package/dist/api/routes/cuRoutes.js +17 -0
  13. package/dist/api/routes/daisoRoutes.js +6 -1
  14. package/dist/api/routes/emart24Routes.js +22 -0
  15. package/dist/api/routes/gs25Routes.js +22 -0
  16. package/dist/api/routes/healthRoutes.js +44 -0
  17. package/dist/api/routes/lottecinemaRoutes.js +22 -0
  18. package/dist/api/routes/lottemartRoutes.js +18 -0
  19. package/dist/api/routes/oliveyoungRoutes.js +6 -1
  20. package/dist/api/routes/sevenelevenRoutes.js +32 -0
  21. package/dist/api/sevenelevenHandlers.js +200 -0
  22. package/dist/cli/args.js +54 -0
  23. package/dist/cli/commands/convenience.js +241 -0
  24. package/dist/cli/commands/daiso.js +96 -0
  25. package/dist/cli/constants.js +5 -0
  26. package/dist/cli/deps.js +48 -0
  27. package/dist/cli/http.js +98 -0
  28. package/dist/cli/interactiveFetch.js +31 -0
  29. package/dist/cli/interactiveItemSearch.js +259 -0
  30. package/dist/cli/interactivePrompt.js +49 -0
  31. package/dist/cli/interactiveTypes.js +4 -0
  32. package/dist/cli/types.js +4 -0
  33. package/dist/cli.js +66 -181
  34. package/dist/cliHelp.js +216 -9
  35. package/dist/cliInteractive.js +174 -0
  36. package/dist/cliPicker.js +56 -0
  37. package/dist/cliRenderer.js +139 -0
  38. package/dist/index.js +147 -8
  39. package/dist/pages/openapi.js +2 -1
  40. package/dist/pages/openapiFullSpec.js +58 -0
  41. package/dist/pages/openapiResponse.js +28 -0
  42. package/dist/pages/openapiSpec.js +2 -41
  43. package/dist/pages/openapiSpecActions.js +109 -0
  44. package/dist/pages/openapiSpecComponents.js +30 -391
  45. package/dist/pages/openapiSpecComponentsCgv.js +24 -0
  46. package/dist/pages/openapiSpecComponentsCu.js +112 -0
  47. package/dist/pages/openapiSpecComponentsDaisoOliveyoung.js +342 -0
  48. package/dist/pages/openapiSpecComponentsEmart24.js +141 -0
  49. package/dist/pages/openapiSpecComponentsGs25.js +109 -0
  50. package/dist/pages/openapiSpecComponentsLotteMart.js +85 -0
  51. package/dist/pages/openapiSpecComponentsLottecinema.js +163 -0
  52. package/dist/pages/openapiSpecComponentsMegabox.js +144 -0
  53. package/dist/pages/openapiSpecComponentsSeveneleven.js +82 -0
  54. package/dist/pages/openapiSpecPathsCgv.js +66 -3
  55. package/dist/pages/openapiSpecPathsCu.js +138 -0
  56. package/dist/pages/openapiSpecPathsDaiso.js +302 -0
  57. package/dist/pages/openapiSpecPathsDaisoOliveyoung.js +4 -376
  58. package/dist/pages/openapiSpecPathsEmart24.js +223 -0
  59. package/dist/pages/openapiSpecPathsGs25.js +166 -0
  60. package/dist/pages/openapiSpecPathsLotteMart.js +135 -0
  61. package/dist/pages/openapiSpecPathsLottecinema.js +212 -0
  62. package/dist/pages/openapiSpecPathsMegabox.js +52 -3
  63. package/dist/pages/openapiSpecPathsOliveyoung.js +231 -0
  64. package/dist/pages/openapiSpecPathsSeveneleven.js +234 -0
  65. package/dist/pages/prompt.js +8 -402
  66. package/dist/pages/promptCoreText.js +278 -0
  67. package/dist/pages/promptReferenceText.js +131 -0
  68. package/dist/pages/promptRetailCinemaText.js +273 -0
  69. package/dist/services/cgv/client.js +6 -1
  70. package/dist/services/cgv/index.js +3 -3
  71. package/dist/services/cgv/location.js +235 -0
  72. package/dist/services/cgv/tools/findTheaters.js +30 -5
  73. package/dist/services/cgv/tools/getTimetable.js +42 -14
  74. package/dist/services/cgv/tools/searchMovies.js +40 -12
  75. package/dist/services/cgv/transport.js +0 -3
  76. package/dist/services/cu/api.js +11 -0
  77. package/dist/services/cu/client.js +297 -0
  78. package/dist/services/cu/index.js +21 -0
  79. package/dist/services/cu/tools/checkInventory.js +123 -0
  80. package/dist/services/cu/tools/findNearbyStores.js +49 -0
  81. package/dist/services/cu/types.js +4 -0
  82. package/dist/services/daiso/api.js +22 -2
  83. package/dist/services/daiso/client.js +59 -0
  84. package/dist/services/daiso/index.js +2 -0
  85. package/dist/services/daiso/product.js +17 -0
  86. package/dist/services/daiso/tools/checkInventory.js +76 -41
  87. package/dist/services/daiso/tools/findStores.js +17 -7
  88. package/dist/services/daiso/tools/getDisplayLocation.js +75 -0
  89. package/dist/services/emart24/api.js +11 -0
  90. package/dist/services/emart24/client.js +173 -0
  91. package/dist/services/emart24/index.js +22 -0
  92. package/dist/services/emart24/inventoryLookup.js +134 -0
  93. package/dist/services/emart24/tools/checkInventory.js +54 -0
  94. package/dist/services/emart24/tools/findNearbyStores.js +72 -0
  95. package/dist/services/emart24/tools/searchProducts.js +63 -0
  96. package/dist/services/emart24/types.js +4 -0
  97. package/dist/services/gs25/api.js +9 -0
  98. package/dist/services/gs25/client.js +198 -0
  99. package/dist/services/gs25/index.js +22 -0
  100. package/dist/services/gs25/storeUtils.js +168 -0
  101. package/dist/services/gs25/tools/checkInventory.js +163 -0
  102. package/dist/services/gs25/tools/findNearbyStores.js +103 -0
  103. package/dist/services/gs25/tools/searchProducts.js +50 -0
  104. package/dist/services/gs25/types.js +4 -0
  105. package/dist/services/lottecinema/api.js +12 -0
  106. package/dist/services/lottecinema/client.js +161 -0
  107. package/dist/services/lottecinema/index.js +30 -0
  108. package/dist/services/lottecinema/location.js +191 -0
  109. package/dist/services/lottecinema/tools/findNearbyTheaters.js +47 -0
  110. package/dist/services/lottecinema/tools/getRemainingSeats.js +85 -0
  111. package/dist/services/lottecinema/tools/listNowShowing.js +79 -0
  112. package/dist/services/lottecinema/types.js +4 -0
  113. package/dist/services/lottemart/api.js +31 -0
  114. package/dist/services/lottemart/client.js +245 -0
  115. package/dist/services/lottemart/clientTypes.js +4 -0
  116. package/dist/services/lottemart/config.js +4 -0
  117. package/dist/services/lottemart/debug.js +92 -0
  118. package/dist/services/lottemart/geocode.js +49 -0
  119. package/dist/services/lottemart/index.js +30 -0
  120. package/dist/services/lottemart/keyword.js +29 -0
  121. package/dist/services/lottemart/parser.js +185 -0
  122. package/dist/services/lottemart/session.js +331 -0
  123. package/dist/services/lottemart/storeSearch.js +52 -0
  124. package/dist/services/lottemart/tools/findNearbyStores.js +67 -0
  125. package/dist/services/lottemart/tools/searchProducts.js +62 -0
  126. package/dist/services/lottemart/types.js +4 -0
  127. package/dist/services/lottemart/zetta.js +105 -0
  128. package/dist/services/megabox/location.js +219 -0
  129. package/dist/services/megabox/tools/findNearbyTheaters.js +13 -44
  130. package/dist/services/megabox/tools/getRemainingSeats.js +31 -3
  131. package/dist/services/megabox/tools/listNowShowing.js +31 -3
  132. package/dist/services/oliveyoung/api.js +2 -0
  133. package/dist/services/oliveyoung/client.js +150 -9
  134. package/dist/services/oliveyoung/index.js +3 -1
  135. package/dist/services/oliveyoung/tools/checkInventory.js +15 -4
  136. package/dist/services/oliveyoung/tools/searchProducts.js +57 -0
  137. package/dist/services/seveneleven/api.js +15 -0
  138. package/dist/services/seveneleven/client.js +279 -0
  139. package/dist/services/seveneleven/index.js +30 -0
  140. package/dist/services/seveneleven/inventory.js +302 -0
  141. package/dist/services/seveneleven/productKeyword.js +141 -0
  142. package/dist/services/seveneleven/tools/checkInventory.js +82 -0
  143. package/dist/services/seveneleven/tools/getCatalogSnapshot.js +55 -0
  144. package/dist/services/seveneleven/tools/getSearchPopwords.js +41 -0
  145. package/dist/services/seveneleven/tools/searchProducts.js +56 -0
  146. package/dist/services/seveneleven/tools/searchStores.js +45 -0
  147. package/dist/services/seveneleven/types.js +4 -0
  148. package/dist/utils/cliInteractiveHelpers.js +63 -0
  149. package/dist/utils/daisoKeyword.js +30 -0
  150. package/dist/utils/http.js +16 -2
  151. package/openapi.json +1977 -734
  152. package/openapi.yaml +1430 -543
  153. package/package.json +28 -15
  154. package/dist/tools/checkInventory.js +0 -98
  155. package/dist/tools/findStores.js +0 -125
  156. package/dist/tools/getPriceInfo.js +0 -77
  157. package/dist/tools/searchProducts.js +0 -61
  158. package/dist/types/index.js +0 -4
@@ -0,0 +1,72 @@
1
+ /**
2
+ * 이마트24 주변 매장 탐색 도구
3
+ */
4
+ import * as z from 'zod';
5
+ import { calculateDistanceM, fetchEmart24Stores } from '../client.js';
6
+ async function findNearbyStores(args) {
7
+ const { latitude, longitude, keyword = '', area1 = '', area2 = '', service24h = false, page = 1, limit = 20, timeoutMs = 15000, } = args;
8
+ const result = await fetchEmart24Stores({
9
+ keyword,
10
+ area1,
11
+ area2,
12
+ service24h,
13
+ page,
14
+ }, {
15
+ timeout: timeoutMs,
16
+ });
17
+ const withDistance = result.stores.map((store) => {
18
+ if (typeof latitude !== 'number' ||
19
+ typeof longitude !== 'number' ||
20
+ store.latitude === 0 ||
21
+ store.longitude === 0) {
22
+ return store;
23
+ }
24
+ return {
25
+ ...store,
26
+ distanceM: calculateDistanceM(latitude, longitude, store.latitude, store.longitude),
27
+ };
28
+ });
29
+ const sortedStores = [...withDistance].sort((a, b) => (a.distanceM ?? Number.MAX_SAFE_INTEGER) - (b.distanceM ?? Number.MAX_SAFE_INTEGER));
30
+ const stores = sortedStores.slice(0, limit);
31
+ return {
32
+ content: [
33
+ {
34
+ type: 'text',
35
+ text: JSON.stringify({
36
+ location: typeof latitude === 'number' && typeof longitude === 'number'
37
+ ? { latitude, longitude }
38
+ : null,
39
+ keyword,
40
+ appliedKeyword: result.appliedKeyword,
41
+ area1,
42
+ area2,
43
+ service24h,
44
+ totalCount: result.totalCount,
45
+ count: stores.length,
46
+ stores,
47
+ }, null, 2),
48
+ },
49
+ ],
50
+ };
51
+ }
52
+ export function createFindNearbyStoresTool() {
53
+ return {
54
+ name: 'emart24_find_nearby_stores',
55
+ metadata: {
56
+ title: '이마트24 주변 매장 탐색',
57
+ description: '키워드/지역 조건으로 이마트24 매장을 조회하고 좌표가 있으면 거리순으로 정렬합니다.',
58
+ inputSchema: {
59
+ latitude: z.number().optional().describe('위도 (선택)'),
60
+ longitude: z.number().optional().describe('경도 (선택)'),
61
+ keyword: z.string().optional().describe('매장명/지역 키워드 (예: 강남)'),
62
+ area1: z.string().optional().describe('시/도 (예: 서울특별시)'),
63
+ area2: z.string().optional().describe('구/군 (예: 강남구)'),
64
+ service24h: z.boolean().optional().default(false).describe('24시간 매장만 조회할지 여부'),
65
+ page: z.number().optional().default(1).describe('조회 페이지 (기본값: 1)'),
66
+ limit: z.number().optional().default(20).describe('반환할 매장 수 (기본값: 20)'),
67
+ timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
68
+ },
69
+ },
70
+ handler: findNearbyStores,
71
+ };
72
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * 이마트24 상품 검색 도구
3
+ */
4
+ import * as z from 'zod';
5
+ import { searchEmart24Products } from '../client.js';
6
+ async function searchProducts(args) {
7
+ const { keyword, page = 1, pageSize = 10, sortType = 'SALE', saleProductYn = 'N', timeoutMs = 15000, } = args;
8
+ if (!keyword || keyword.trim().length === 0) {
9
+ throw new Error('상품 검색어(keyword)를 입력해주세요.');
10
+ }
11
+ const result = await searchEmart24Products({
12
+ keyword,
13
+ page,
14
+ pageSize,
15
+ sortType,
16
+ saleProductYn,
17
+ }, {
18
+ timeout: timeoutMs,
19
+ });
20
+ return {
21
+ content: [
22
+ {
23
+ type: 'text',
24
+ text: JSON.stringify({
25
+ keyword,
26
+ page,
27
+ pageSize,
28
+ sortType,
29
+ saleProductYn,
30
+ totalCount: result.totalCount,
31
+ count: result.products.length,
32
+ products: result.products,
33
+ }, null, 2),
34
+ },
35
+ ],
36
+ };
37
+ }
38
+ export function createSearchProductsTool() {
39
+ return {
40
+ name: 'emart24_search_products',
41
+ metadata: {
42
+ title: '이마트24 상품 검색',
43
+ description: '키워드로 이마트24 상품 목록을 조회합니다.',
44
+ inputSchema: {
45
+ keyword: z.string().describe('상품 검색어 (예: 두바이, 도시락, 음료)'),
46
+ page: z.number().optional().default(1).describe('조회 페이지 (기본값: 1)'),
47
+ pageSize: z.number().optional().default(10).describe('페이지당 결과 수 (기본값: 10)'),
48
+ sortType: z
49
+ .enum(['SALE', 'LATEST', 'PRICE_ASC', 'PRICE_DESC'])
50
+ .optional()
51
+ .default('SALE')
52
+ .describe('정렬 기준 (기본값: SALE)'),
53
+ saleProductYn: z
54
+ .enum(['Y', 'N'])
55
+ .optional()
56
+ .default('N')
57
+ .describe('행사 상품 여부 필터 (기본값: N)'),
58
+ timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
59
+ },
60
+ },
61
+ handler: searchProducts,
62
+ };
63
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * 이마트24 서비스 전용 타입 정의
3
+ */
4
+ export {};
@@ -0,0 +1,9 @@
1
+ /**
2
+ * GS25 API 엔드포인트 중앙 관리
3
+ */
4
+ export const GS25_API = {
5
+ BFF_BASE_URL: 'https://b2c-bff.woodongs.com',
6
+ STORE_STOCK_PATH: '/api/bff/v2/store/stock',
7
+ APIGW_BASE_URL: 'https://b2c-apigw.woodongs.com',
8
+ TOTAL_SEARCH_PATH: '/search/v3/totalSearch',
9
+ };
@@ -0,0 +1,198 @@
1
+ /**
2
+ * GS25 API 클라이언트
3
+ */
4
+ /* c8 ignore start */
5
+ import { fetchJson } from '../../utils/http.js';
6
+ import { GS25_API } from './api.js';
7
+ import { normalizeStore, toNumber } from './storeUtils.js';
8
+ export { attachDistanceToGs25Stores, calculateDistanceM, extractGs25ProductCandidates, filterGs25StoresByKeyword, selectGs25StoresForKeyword, sortGs25Stores, } from './storeUtils.js';
9
+ const GS25_DEFAULT_HEADERS = {
10
+ Accept: 'application/json, text/plain, */*',
11
+ };
12
+ const GS25_STORES_CACHE_TTL_MS = 60 * 5 * 1000;
13
+ const gs25StoresCache = new Map();
14
+ function buildCacheKey(params) {
15
+ return [
16
+ params.serviceCode,
17
+ params.itemCode?.trim() || params.keyword,
18
+ params.storeCode,
19
+ typeof params.latitude === 'number' ? params.latitude : '',
20
+ typeof params.longitude === 'number' ? params.longitude : '',
21
+ ].join(':');
22
+ }
23
+ export async function geocodeGs25Address(address, options = {}) {
24
+ const keyword = address.trim();
25
+ if (keyword.length === 0) {
26
+ return null;
27
+ }
28
+ const { timeout = 15000, googleMapsApiKey } = options;
29
+ const apiKey = (googleMapsApiKey || '').trim();
30
+ if (apiKey.length === 0) {
31
+ return null;
32
+ }
33
+ const endpoint = new URL('https://maps.googleapis.com/maps/api/geocode/json');
34
+ endpoint.searchParams.set('address', keyword);
35
+ endpoint.searchParams.set('key', apiKey);
36
+ const body = await fetchJson(endpoint.toString(), {
37
+ method: 'GET',
38
+ timeout,
39
+ headers: {
40
+ Accept: 'application/json',
41
+ },
42
+ });
43
+ if (body.status !== 'OK') {
44
+ return null;
45
+ }
46
+ const location = body.results?.[0]?.geometry?.location;
47
+ if (!location) {
48
+ return null;
49
+ }
50
+ const latitude = toNumber(location.lat);
51
+ const longitude = toNumber(location.lng);
52
+ if (latitude === 0 || longitude === 0) {
53
+ return null;
54
+ }
55
+ return { latitude, longitude };
56
+ }
57
+ export async function fetchGs25Stores(params = {}, options = {}) {
58
+ const { serviceCode = '01', keyword = '', itemCode = '', storeCode = '', realTimeStockYn = 'Y', latitude, longitude, pageNumber = 0, pageCount = 20000, useCache = true, } = params;
59
+ const { timeout = 20000 } = options;
60
+ const cacheKey = buildCacheKey({
61
+ serviceCode,
62
+ keyword: itemCode.trim() || keyword.trim(),
63
+ storeCode: storeCode.trim(),
64
+ itemCode: itemCode.trim(),
65
+ latitude,
66
+ longitude,
67
+ });
68
+ if (useCache) {
69
+ const cached = gs25StoresCache.get(cacheKey);
70
+ if (cached && cached.expiresAt > Date.now()) {
71
+ return {
72
+ totalCount: cached.stores.length,
73
+ stores: cached.stores,
74
+ cacheHit: true,
75
+ };
76
+ }
77
+ }
78
+ const endpoint = new URL(GS25_API.STORE_STOCK_PATH, GS25_API.BFF_BASE_URL);
79
+ endpoint.searchParams.set('serviceCode', serviceCode);
80
+ endpoint.searchParams.set('pageNumber', String(pageNumber));
81
+ endpoint.searchParams.set('pageCount', String(pageCount));
82
+ endpoint.searchParams.set('realTimeStockYn', realTimeStockYn);
83
+ // itemCode가 있으면 itemCode 사용, 없으면 keyword 사용
84
+ // 주의: itemCode 사용 시 좌표도 필수
85
+ if (itemCode.trim().length > 0) {
86
+ endpoint.searchParams.set('itemCode', itemCode.trim());
87
+ }
88
+ else if (keyword.trim().length > 0) {
89
+ endpoint.searchParams.set('keyword', keyword.trim());
90
+ }
91
+ if (storeCode.trim().length > 0) {
92
+ endpoint.searchParams.set('storeCode', storeCode.trim());
93
+ }
94
+ // 좌표 파라미터: 앱은 myPosition + centerPosition 모두 필요
95
+ if (typeof latitude === 'number' && Number.isFinite(latitude)) {
96
+ endpoint.searchParams.set('myPositionYCoordination', String(latitude));
97
+ endpoint.searchParams.set('centerPositionYCoordination', String(latitude));
98
+ }
99
+ if (typeof longitude === 'number' && Number.isFinite(longitude)) {
100
+ endpoint.searchParams.set('myPositionXCoordination', String(longitude));
101
+ endpoint.searchParams.set('centerPositionXCoordination', String(longitude));
102
+ }
103
+ // 반경 조건 (미터 단위, 1km로 설정하여 인근 매장 포함)
104
+ if (typeof latitude === 'number' && typeof longitude === 'number') {
105
+ endpoint.searchParams.set('radiusCondition', '1000');
106
+ // 앱에서 사용하는 추가 플래그
107
+ endpoint.searchParams.set('pickupStoreYn', 'N');
108
+ endpoint.searchParams.set('isSuperDlvyStoreSelected', 'N');
109
+ endpoint.searchParams.set('isGs25DlvyStoreSelected', 'N');
110
+ }
111
+ const body = await fetchJson(endpoint.toString(), {
112
+ method: 'GET',
113
+ timeout,
114
+ headers: GS25_DEFAULT_HEADERS,
115
+ });
116
+ const stores = (body.stores || []).map(normalizeStore).filter((store) => store.storeCode.length > 0);
117
+ if (useCache) {
118
+ gs25StoresCache.set(cacheKey, {
119
+ stores,
120
+ expiresAt: Date.now() + GS25_STORES_CACHE_TTL_MS,
121
+ });
122
+ }
123
+ return {
124
+ totalCount: stores.length,
125
+ stores,
126
+ cacheHit: false,
127
+ };
128
+ }
129
+ export async function fetchGs25NormalizedKeyword(keyword, options = {}) {
130
+ const query = keyword.trim();
131
+ if (query.length === 0) {
132
+ return null;
133
+ }
134
+ const { timeout = 20000 } = options;
135
+ const endpoint = new URL(GS25_API.TOTAL_SEARCH_PATH, GS25_API.APIGW_BASE_URL);
136
+ const body = await fetchJson(endpoint.toString(), {
137
+ method: 'POST',
138
+ timeout,
139
+ headers: {
140
+ Accept: 'application/json, text/plain, */*',
141
+ 'Content-Type': 'application/json',
142
+ },
143
+ body: JSON.stringify({ query }),
144
+ });
145
+ const normalizedKeyword = body.SearchQueryResult?.keywordInfo?.keyword?.trim() || '';
146
+ const normalizedSearchKeyword = body.SearchQueryResult?.keywordInfo?.searchKeyword?.trim() || '';
147
+ if (normalizedKeyword.length === 0 && normalizedSearchKeyword.length === 0) {
148
+ return null;
149
+ }
150
+ return {
151
+ keyword: normalizedKeyword,
152
+ searchKeyword: normalizedSearchKeyword,
153
+ };
154
+ }
155
+ /**
156
+ * 키워드로 상품을 검색하여 itemCode 목록을 반환합니다.
157
+ * 재고 조회 API는 itemCode가 필요하므로 이 함수로 먼저 변환해야 합니다.
158
+ */
159
+ export async function fetchGs25SearchProducts(keyword, options = {}) {
160
+ const query = keyword.trim();
161
+ if (query.length === 0) {
162
+ return [];
163
+ }
164
+ const { timeout = 20000 } = options;
165
+ const endpoint = new URL(GS25_API.TOTAL_SEARCH_PATH, GS25_API.APIGW_BASE_URL);
166
+ const body = await fetchJson(endpoint.toString(), {
167
+ method: 'POST',
168
+ timeout,
169
+ headers: {
170
+ Accept: 'application/json, text/plain, */*',
171
+ 'Content-Type': 'application/json',
172
+ },
173
+ body: JSON.stringify({ query }),
174
+ });
175
+ const products = [];
176
+ const collections = body.SearchQueryResult?.Collection || [];
177
+ for (const collection of collections) {
178
+ const documents = collection.Documentset?.Document || [];
179
+ for (const doc of documents) {
180
+ const field = doc.field;
181
+ if (!field?.itemCode)
182
+ continue;
183
+ products.push({
184
+ itemCode: field.itemCode,
185
+ itemName: field.itemName || '',
186
+ shortItemName: field.shortItemName || '',
187
+ imageUrl: field.itemImageUrl || '',
188
+ rating: toNumber(field.starPoint),
189
+ stockCheckEnabled: field.stockCheckYn === 'Y',
190
+ });
191
+ }
192
+ }
193
+ return products;
194
+ }
195
+ export function clearGs25StoresCache() {
196
+ gs25StoresCache.clear();
197
+ }
198
+ /* c8 ignore stop */
@@ -0,0 +1,22 @@
1
+ /**
2
+ * GS25 서비스 프로바이더
3
+ */
4
+ import { createFindNearbyStoresTool } from './tools/findNearbyStores.js';
5
+ import { createSearchProductsTool } from './tools/searchProducts.js';
6
+ import { createCheckInventoryTool } from './tools/checkInventory.js';
7
+ const GS25_METADATA = {
8
+ id: 'gs25',
9
+ name: 'GS25',
10
+ version: '1.0.0',
11
+ description: 'GS25 매장 탐색, 상품 키워드 검색, 재고 조회 서비스',
12
+ };
13
+ class Gs25Service {
14
+ metadata = GS25_METADATA;
15
+ getTools() {
16
+ return [createFindNearbyStoresTool(), createSearchProductsTool(), createCheckInventoryTool()];
17
+ }
18
+ }
19
+ export function createGs25Service() {
20
+ return new Gs25Service();
21
+ }
22
+ export * from './types.js';
@@ -0,0 +1,168 @@
1
+ /**
2
+ * GS25 매장 정규화와 정렬 유틸리티
3
+ */
4
+ export function toNumber(value) {
5
+ if (typeof value === 'number') {
6
+ return Number.isFinite(value) ? value : 0;
7
+ }
8
+ if (typeof value === 'string') {
9
+ const parsed = Number.parseFloat(value);
10
+ return Number.isFinite(parsed) ? parsed : 0;
11
+ }
12
+ return 0;
13
+ }
14
+ function toInteger(value) {
15
+ if (typeof value === 'number') {
16
+ return Number.isFinite(value) ? Math.trunc(value) : 0;
17
+ }
18
+ if (typeof value === 'string') {
19
+ const parsed = Number.parseInt(value, 10);
20
+ return Number.isFinite(parsed) ? parsed : 0;
21
+ }
22
+ return 0;
23
+ }
24
+ function toBooleanYn(value) {
25
+ const normalized = String(value || '').trim().toUpperCase();
26
+ return normalized === 'Y' || normalized === 'TRUE' || normalized === '1';
27
+ }
28
+ function normalizeProperty(raw) {
29
+ return {
30
+ code: raw.storePropertyCode || '',
31
+ name: raw.storePropertyName || '',
32
+ type: raw.storePropertyType || '',
33
+ imageUrl: raw.storePropertyImage || '',
34
+ };
35
+ }
36
+ export function normalizeStore(raw) {
37
+ const properties = (raw.propertyList || []).map(normalizeProperty).filter((item) => item.name.length > 0);
38
+ return {
39
+ storeCode: raw.storeCode || '',
40
+ storeName: raw.storeName || '',
41
+ address: raw.storeAddress || '',
42
+ phone: raw.storeTelephoneNumber || '',
43
+ longitude: toNumber(raw.storeXCoordination),
44
+ latitude: toNumber(raw.storeYCoordination),
45
+ serviceCode: raw.serviceCode || '',
46
+ realStockQuantity: toInteger(raw.realStockQuantity),
47
+ pickupStockQuantity: toInteger(raw.pickupStkQty),
48
+ deliveryStockQuantity: toInteger(raw.dlvyStkQty),
49
+ isSoldOut: toBooleanYn(raw.isSoldOutYn),
50
+ searchItemName: String(raw.searchItemName || '').trim(),
51
+ searchItemSellPrice: raw.searchItemSellPrice === null || raw.searchItemSellPrice === undefined
52
+ ? null
53
+ : toInteger(raw.searchItemSellPrice),
54
+ propertyNames: properties.map((item) => item.name),
55
+ properties,
56
+ distanceM: null,
57
+ };
58
+ }
59
+ export function calculateDistanceM(lat1, lng1, lat2, lng2) {
60
+ const toRad = (deg) => (deg * Math.PI) / 180;
61
+ const dLat = toRad(lat2 - lat1);
62
+ const dLng = toRad(lng2 - lng1);
63
+ const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
64
+ Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
65
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
66
+ return Math.round(6371000 * c);
67
+ }
68
+ export function filterGs25StoresByKeyword(stores, keyword) {
69
+ const trimmed = keyword.trim();
70
+ if (trimmed.length === 0) {
71
+ return stores;
72
+ }
73
+ const normalized = trimmed.toLowerCase();
74
+ const noSpaceKeyword = normalized.replace(/\s+/g, '');
75
+ const keywordTokens = normalized.split(/\s+/).filter((token) => token.length > 0);
76
+ const normalizedVariants = new Set([normalized, noSpaceKeyword]);
77
+ for (const suffix of ['역', '점']) {
78
+ for (const variant of [...normalizedVariants]) {
79
+ if (variant.endsWith(suffix) && variant.length > suffix.length) {
80
+ normalizedVariants.add(variant.slice(0, -suffix.length));
81
+ }
82
+ }
83
+ }
84
+ return stores.filter((store) => {
85
+ const target = `${store.storeName} ${store.address} ${store.propertyNames.join(' ')}`.toLowerCase();
86
+ const targetNoSpace = target.replace(/\s+/g, '');
87
+ if (target.includes(normalized)) {
88
+ return true;
89
+ }
90
+ for (const variant of normalizedVariants) {
91
+ if (variant.length > 0 && targetNoSpace.includes(variant.replace(/\s+/g, ''))) {
92
+ return true;
93
+ }
94
+ }
95
+ if (keywordTokens.length > 1) {
96
+ return keywordTokens.every((token) => target.includes(token));
97
+ }
98
+ return false;
99
+ });
100
+ }
101
+ export function selectGs25StoresForKeyword(stores, keyword, options = {}) {
102
+ const filtered = filterGs25StoresByKeyword(stores, keyword);
103
+ if (filtered.length > 0 || !options.relaxWhenEmpty || keyword.trim().length === 0 || stores.length === 0) {
104
+ return { stores: filtered, filterRelaxed: false };
105
+ }
106
+ return { stores, filterRelaxed: true };
107
+ }
108
+ export function attachDistanceToGs25Stores(stores, latitude, longitude) {
109
+ if (typeof latitude !== 'number' || typeof longitude !== 'number') {
110
+ return stores;
111
+ }
112
+ return stores.map((store) => {
113
+ if (store.latitude === 0 || store.longitude === 0) {
114
+ return store;
115
+ }
116
+ return {
117
+ ...store,
118
+ distanceM: calculateDistanceM(latitude, longitude, store.latitude, store.longitude),
119
+ };
120
+ });
121
+ }
122
+ export function sortGs25Stores(stores) {
123
+ return [...stores].sort((a, b) => {
124
+ const distanceA = a.distanceM ?? Number.MAX_SAFE_INTEGER;
125
+ const distanceB = b.distanceM ?? Number.MAX_SAFE_INTEGER;
126
+ if (distanceA !== distanceB) {
127
+ return distanceA - distanceB;
128
+ }
129
+ if (b.realStockQuantity !== a.realStockQuantity) {
130
+ return b.realStockQuantity - a.realStockQuantity;
131
+ }
132
+ return a.storeName.localeCompare(b.storeName, 'ko');
133
+ });
134
+ }
135
+ export function extractGs25ProductCandidates(stores) {
136
+ const map = new Map();
137
+ for (const store of stores) {
138
+ const name = store.searchItemName.trim();
139
+ if (name.length === 0) {
140
+ continue;
141
+ }
142
+ const key = `${name}::${store.searchItemSellPrice ?? 'null'}`;
143
+ const prev = map.get(key);
144
+ const inStock = store.realStockQuantity > 0 ? 1 : 0;
145
+ if (!prev) {
146
+ map.set(key, {
147
+ name,
148
+ sellPrice: store.searchItemSellPrice,
149
+ matchedStoreCount: 1,
150
+ inStockStoreCount: inStock,
151
+ totalStockQuantity: Math.max(store.realStockQuantity, 0),
152
+ });
153
+ continue;
154
+ }
155
+ prev.matchedStoreCount += 1;
156
+ prev.inStockStoreCount += inStock;
157
+ prev.totalStockQuantity += Math.max(store.realStockQuantity, 0);
158
+ }
159
+ return [...map.values()].sort((a, b) => {
160
+ if (b.inStockStoreCount !== a.inStockStoreCount) {
161
+ return b.inStockStoreCount - a.inStockStoreCount;
162
+ }
163
+ if (b.totalStockQuantity !== a.totalStockQuantity) {
164
+ return b.totalStockQuantity - a.totalStockQuantity;
165
+ }
166
+ return a.name.localeCompare(b.name, 'ko');
167
+ });
168
+ }