daiso 1.0.8 → 1.0.9

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 (44) hide show
  1. package/README.md +83 -7
  2. package/dist/api/actionsProxy.js +5 -0
  3. package/dist/api/configStatus.js +8 -0
  4. package/dist/api/feedbackHandlers.js +60 -0
  5. package/dist/api/healthCheckDefinitions.js +170 -0
  6. package/dist/api/healthCheckShape.js +106 -0
  7. package/dist/api/healthCheckTypes.js +4 -0
  8. package/dist/api/healthChecks.js +3 -271
  9. package/dist/api/opinetHandlers.js +91 -0
  10. package/dist/api/routes/feedbackRoutes.js +5 -0
  11. package/dist/api/routes/healthRoutes.js +17 -1
  12. package/dist/api/routes/opinetRoutes.js +24 -0
  13. package/dist/bin.js +0 -0
  14. package/dist/cli/commandOptions.js +23 -2
  15. package/dist/cli/commands/apiCommand.js +24 -0
  16. package/dist/cli/commands/cinema.js +65 -0
  17. package/dist/cli/commands/convenience.js +57 -100
  18. package/dist/cli/http.js +1 -1
  19. package/dist/cli.js +10 -1
  20. package/dist/cliHelp.js +44 -8
  21. package/dist/core/outputSchema.js +141 -0
  22. package/dist/core/registry.js +1 -138
  23. package/dist/index.js +92 -29
  24. package/dist/pages/openapiFullSpec.js +5 -0
  25. package/dist/pages/openapiSpecActions.js +63 -0
  26. package/dist/pages/openapiSpecPathsFeedback.js +95 -0
  27. package/dist/pages/openapiSpecPathsOpinet.js +81 -0
  28. package/dist/pages/promptReferenceText.js +15 -0
  29. package/dist/services/feedback/client.js +109 -0
  30. package/dist/services/feedback/index.js +22 -0
  31. package/dist/services/feedback/tools/submitDeveloperRequest.js +54 -0
  32. package/dist/services/feedback/types.js +3 -0
  33. package/dist/services/gs25/client.js +1 -84
  34. package/dist/services/gs25/productSearch.js +97 -0
  35. package/dist/services/oliveyoung/client.js +1 -55
  36. package/dist/services/oliveyoung/normalize.js +58 -0
  37. package/dist/services/opinet/client.js +231 -0
  38. package/dist/services/opinet/index.js +26 -0
  39. package/dist/services/opinet/location.js +119 -0
  40. package/dist/services/opinet/tools.js +80 -0
  41. package/dist/services/opinet/types.js +1 -0
  42. package/openapi.json +82 -0
  43. package/openapi.yaml +55 -0
  44. package/package.json +18 -17
@@ -0,0 +1,119 @@
1
+ import proj4 from 'proj4';
2
+ const WGS84 = 'EPSG:4326';
3
+ const KATEC = 'KATEC';
4
+ const KATEC_PROJ4 = '+proj=tmerc +lat_0=38 +lon_0=128 +k=0.9999 +x_0=400000 +y_0=600000 +ellps=bessel +units=m +no_defs +towgs84=-115.80,474.99,674.11,1.16,-2.31,-1.63,6.43';
5
+ proj4.defs(KATEC, KATEC_PROJ4);
6
+ const GEOCODE_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
7
+ const geocodeCache = new Map();
8
+ function getProcessEnvValue(name) {
9
+ /* c8 ignore next -- Worker 런타임은 bindings로 키를 주입합니다. */
10
+ return typeof process !== 'undefined' ? process.env[name] : undefined;
11
+ }
12
+ function isFiniteNumber(value) {
13
+ return typeof value === 'number' && Number.isFinite(value);
14
+ }
15
+ export function wgs84ToKatec(latitude, longitude) {
16
+ if (!isFiniteNumber(latitude) || !isFiniteNumber(longitude)) {
17
+ throw new Error('유효한 위도/경도를 입력해주세요.');
18
+ }
19
+ const [x, y] = proj4(WGS84, KATEC, [longitude, latitude]);
20
+ return {
21
+ x: Number(x.toFixed(3)),
22
+ y: Number(y.toFixed(3)),
23
+ };
24
+ }
25
+ async function geocodeLocation(location, options) {
26
+ const keyword = location.trim();
27
+ const cached = geocodeCache.get(keyword);
28
+ if (cached && cached.expiresAt > Date.now()) {
29
+ return cached.value;
30
+ }
31
+ const apiKey = options.googleMapsApiKey?.trim() || getProcessEnvValue('GOOGLE_MAPS_API_KEY')?.trim();
32
+ if (!apiKey) {
33
+ throw new Error('location 검색에는 GOOGLE_MAPS_API_KEY가 필요합니다.');
34
+ }
35
+ const endpoint = new URL('https://maps.googleapis.com/maps/api/geocode/json');
36
+ endpoint.searchParams.set('address', keyword);
37
+ endpoint.searchParams.set('key', apiKey);
38
+ const response = await (options.fetchImpl ?? fetch)(endpoint.toString(), {
39
+ headers: { Accept: 'application/json' },
40
+ signal: AbortSignal.timeout(options.timeoutMs ?? 10000),
41
+ });
42
+ const bodyText = await response.text();
43
+ let parsed;
44
+ try {
45
+ parsed = JSON.parse(bodyText);
46
+ }
47
+ catch {
48
+ parsed = { status: 'INVALID_JSON' };
49
+ }
50
+ if (!response.ok) {
51
+ throw new Error(`Google Geocoding 호출 실패: HTTP ${response.status} ${bodyText}`);
52
+ }
53
+ if (parsed.status !== 'OK') {
54
+ geocodeCache.set(keyword, {
55
+ expiresAt: Date.now() + GEOCODE_CACHE_TTL_MS,
56
+ value: null,
57
+ });
58
+ return null;
59
+ }
60
+ const first = parsed.results?.[0];
61
+ const latitude = first?.geometry?.location?.lat;
62
+ const longitude = first?.geometry?.location?.lng;
63
+ const value = isFiniteNumber(latitude) && isFiniteNumber(longitude)
64
+ ? {
65
+ latitude,
66
+ longitude,
67
+ formattedAddress: first?.formatted_address ?? null,
68
+ }
69
+ : null;
70
+ geocodeCache.set(keyword, {
71
+ expiresAt: Date.now() + GEOCODE_CACHE_TTL_MS,
72
+ value,
73
+ });
74
+ return value;
75
+ }
76
+ export async function resolveOpinetLocation(input, options = {}) {
77
+ if (isFiniteNumber(input.x) && isFiniteNumber(input.y)) {
78
+ return {
79
+ katec: { x: input.x, y: input.y },
80
+ latitude: null,
81
+ longitude: null,
82
+ location: input.location?.trim() || null,
83
+ formattedAddress: null,
84
+ geocodeUsed: false,
85
+ inputType: 'katec',
86
+ };
87
+ }
88
+ if (isFiniteNumber(input.latitude) && isFiniteNumber(input.longitude)) {
89
+ return {
90
+ katec: wgs84ToKatec(input.latitude, input.longitude),
91
+ latitude: input.latitude,
92
+ longitude: input.longitude,
93
+ location: input.location?.trim() || null,
94
+ formattedAddress: null,
95
+ geocodeUsed: false,
96
+ inputType: 'coordinates',
97
+ };
98
+ }
99
+ const location = input.location?.trim() || '';
100
+ if (location.length > 0) {
101
+ const geocoded = await geocodeLocation(location, options);
102
+ if (!geocoded) {
103
+ throw new Error(`위치를 좌표로 변환하지 못했습니다: ${location}`);
104
+ }
105
+ return {
106
+ katec: wgs84ToKatec(geocoded.latitude, geocoded.longitude),
107
+ latitude: geocoded.latitude,
108
+ longitude: geocoded.longitude,
109
+ location,
110
+ formattedAddress: geocoded.formattedAddress,
111
+ geocodeUsed: true,
112
+ inputType: 'location',
113
+ };
114
+ }
115
+ throw new Error('KATEC x/y, 위도/경도(latitude/longitude), 또는 location 중 하나를 입력해주세요.');
116
+ }
117
+ export function __testOnlyClearOpinetGeocodeCache() {
118
+ geocodeCache.clear();
119
+ }
@@ -0,0 +1,80 @@
1
+ import * as z from 'zod';
2
+ import { fetchOpinetAveragePrices, fetchOpinetLowestStations, fetchOpinetStationDetail, fetchOpinetStationsAround, } from './client.js';
3
+ function buildTextResponse(payload) {
4
+ return {
5
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
6
+ structuredContent: payload,
7
+ };
8
+ }
9
+ export function createOpinetAveragePricesTool(apiKey) {
10
+ return {
11
+ name: 'opinet_get_average_prices',
12
+ metadata: {
13
+ title: '오피넷 전국 평균 유가',
14
+ description: '한국석유공사 오피넷의 전국 평균 유가를 조회합니다.',
15
+ inputSchema: {
16
+ timeoutMs: z.number().optional().default(10000).describe('요청 제한 시간(ms)'),
17
+ },
18
+ },
19
+ handler: (async (args) => buildTextResponse(await fetchOpinetAveragePrices({ apiKey, timeoutMs: args.timeoutMs }))),
20
+ };
21
+ }
22
+ export function createOpinetLowestStationsTool(apiKey) {
23
+ return {
24
+ name: 'opinet_get_lowest_price_stations',
25
+ metadata: {
26
+ title: '오피넷 최저가 주유소',
27
+ description: '전국 또는 지역별 최저가 주유소를 유종별로 조회합니다.',
28
+ inputSchema: {
29
+ fuelCode: z
30
+ .enum(['B027', 'D047', 'B034', 'C004', 'K015'])
31
+ .optional()
32
+ .default('B027')
33
+ .describe('유종 코드: B027 휘발유, D047 경유, B034 고급휘발유, C004 실내등유, K015 자동차부탄'),
34
+ areaCode: z.string().optional().describe('오피넷 지역 코드. 미입력 시 전국 기준'),
35
+ count: z.number().optional().default(10).describe('최저가 결과 수(1~20)'),
36
+ timeoutMs: z.number().optional().default(10000).describe('요청 제한 시간(ms)'),
37
+ },
38
+ },
39
+ handler: (async (args) => buildTextResponse(await fetchOpinetLowestStations(args, { apiKey, timeoutMs: args.timeoutMs }))),
40
+ };
41
+ }
42
+ export function createOpinetStationsAroundTool(apiKey, googleMapsApiKey) {
43
+ return {
44
+ name: 'opinet_search_stations_around',
45
+ metadata: {
46
+ title: '오피넷 반경 내 주유소',
47
+ description: 'KATEC x/y, 위도/경도, 또는 장소 키워드 기준 반경 내 주유소를 가격순 또는 거리순으로 조회합니다.',
48
+ inputSchema: {
49
+ x: z.number().optional().describe('기준 위치 X좌표(KATEC)'),
50
+ y: z.number().optional().describe('기준 위치 Y좌표(KATEC)'),
51
+ latitude: z.number().optional().describe('위도(WGS84)'),
52
+ longitude: z.number().optional().describe('경도(WGS84)'),
53
+ location: z.string().optional().describe('장소/주소 키워드. 예: 강남역, 서울역'),
54
+ radiusMeters: z.number().optional().default(3000).describe('검색 반경 m(100~5000)'),
55
+ fuelCode: z
56
+ .enum(['B027', 'D047', 'B034', 'C004', 'K015'])
57
+ .optional()
58
+ .default('B027')
59
+ .describe('유종 코드'),
60
+ sort: z.enum(['price', 'distance']).optional().default('price').describe('정렬 기준'),
61
+ timeoutMs: z.number().optional().default(10000).describe('요청 제한 시간(ms)'),
62
+ },
63
+ },
64
+ handler: (async (args) => buildTextResponse(await fetchOpinetStationsAround(args, { apiKey, googleMapsApiKey, timeoutMs: args.timeoutMs }))),
65
+ };
66
+ }
67
+ export function createOpinetStationDetailTool(apiKey) {
68
+ return {
69
+ name: 'opinet_get_station_detail',
70
+ metadata: {
71
+ title: '오피넷 주유소 상세정보',
72
+ description: '오피넷 주유소 ID로 상세 정보, 부가시설, 유종별 가격을 조회합니다.',
73
+ inputSchema: {
74
+ stationId: z.string().describe('오피넷 주유소 ID'),
75
+ timeoutMs: z.number().optional().default(10000).describe('요청 제한 시간(ms)'),
76
+ },
77
+ },
78
+ handler: (async (args) => buildTextResponse(await fetchOpinetStationDetail(args.stationId, { apiKey, timeoutMs: args.timeoutMs }))),
79
+ };
80
+ }
@@ -0,0 +1 @@
1
+ export {};
package/openapi.json CHANGED
@@ -55,6 +55,7 @@
55
55
  "sevenelevenGetCatalog",
56
56
  "compareProducts",
57
57
  "placesSearchNearby",
58
+ "submitDeveloperRequest",
58
59
  "megaboxFindTheaters",
59
60
  "megaboxListMovies",
60
61
  "megaboxGetSeats",
@@ -139,6 +140,87 @@
139
140
  "type": "string"
140
141
  }
141
142
  },
143
+ {
144
+ "name": "type",
145
+ "in": "query",
146
+ "required": false,
147
+ "description": "개발자 요청 유형(bug, improvement, feature, docs, other)",
148
+ "schema": {
149
+ "type": "string"
150
+ }
151
+ },
152
+ {
153
+ "name": "severity",
154
+ "in": "query",
155
+ "required": false,
156
+ "description": "개발자 요청 영향도(low, medium, high, critical)",
157
+ "schema": {
158
+ "type": "string"
159
+ }
160
+ },
161
+ {
162
+ "name": "title",
163
+ "in": "query",
164
+ "required": false,
165
+ "description": "개발자 요청 제목",
166
+ "schema": {
167
+ "type": "string"
168
+ }
169
+ },
170
+ {
171
+ "name": "description",
172
+ "in": "query",
173
+ "required": false,
174
+ "description": "개발자 요청 상세 설명",
175
+ "schema": {
176
+ "type": "string"
177
+ }
178
+ },
179
+ {
180
+ "name": "toolName",
181
+ "in": "query",
182
+ "required": false,
183
+ "description": "문제가 발생했거나 개선이 필요한 MCP 도구 이름",
184
+ "schema": {
185
+ "type": "string"
186
+ }
187
+ },
188
+ {
189
+ "name": "reproduction",
190
+ "in": "query",
191
+ "required": false,
192
+ "description": "오류 재현 방법",
193
+ "schema": {
194
+ "type": "string"
195
+ }
196
+ },
197
+ {
198
+ "name": "expected",
199
+ "in": "query",
200
+ "required": false,
201
+ "description": "기대 결과",
202
+ "schema": {
203
+ "type": "string"
204
+ }
205
+ },
206
+ {
207
+ "name": "actual",
208
+ "in": "query",
209
+ "required": false,
210
+ "description": "실제 결과",
211
+ "schema": {
212
+ "type": "string"
213
+ }
214
+ },
215
+ {
216
+ "name": "source",
217
+ "in": "query",
218
+ "required": false,
219
+ "description": "요청 출처(mcp, chatgpt, claude, cli, api, other)",
220
+ "schema": {
221
+ "type": "string"
222
+ }
223
+ },
142
224
  {
143
225
  "name": "sido",
144
226
  "in": "query",
package/openapi.yaml CHANGED
@@ -56,6 +56,7 @@ paths:
56
56
  - sevenelevenGetCatalog
57
57
  - compareProducts
58
58
  - placesSearchNearby
59
+ - submitDeveloperRequest
59
60
  - megaboxFindTheaters
60
61
  - megaboxListMovies
61
62
  - megaboxGetSeats
@@ -113,6 +114,60 @@ paths:
113
114
  description: 통합 비교 서비스 목록(daiso,gs25,seveneleven,emart24)
114
115
  schema:
115
116
  type: string
117
+ - name: type
118
+ in: query
119
+ required: false
120
+ description: 개발자 요청 유형(bug, improvement, feature, docs, other)
121
+ schema:
122
+ type: string
123
+ - name: severity
124
+ in: query
125
+ required: false
126
+ description: 개발자 요청 영향도(low, medium, high, critical)
127
+ schema:
128
+ type: string
129
+ - name: title
130
+ in: query
131
+ required: false
132
+ description: 개발자 요청 제목
133
+ schema:
134
+ type: string
135
+ - name: description
136
+ in: query
137
+ required: false
138
+ description: 개발자 요청 상세 설명
139
+ schema:
140
+ type: string
141
+ - name: toolName
142
+ in: query
143
+ required: false
144
+ description: 문제가 발생했거나 개선이 필요한 MCP 도구 이름
145
+ schema:
146
+ type: string
147
+ - name: reproduction
148
+ in: query
149
+ required: false
150
+ description: 오류 재현 방법
151
+ schema:
152
+ type: string
153
+ - name: expected
154
+ in: query
155
+ required: false
156
+ description: 기대 결과
157
+ schema:
158
+ type: string
159
+ - name: actual
160
+ in: query
161
+ required: false
162
+ description: 실제 결과
163
+ schema:
164
+ type: string
165
+ - name: source
166
+ in: query
167
+ required: false
168
+ description: 요청 출처(mcp, chatgpt, claude, cli, api, other)
169
+ schema:
170
+ type: string
116
171
  - name: sido
117
172
  in: query
118
173
  required: false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "daiso",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "Korean retail and cinema MCP server, CLI, and Codex Skill for Daiso, convenience stores, marts, Olive Young, and theaters",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -39,7 +39,8 @@
39
39
  "test": "vitest run",
40
40
  "test:watch": "vitest",
41
41
  "test:coverage": "vitest run --coverage",
42
- "check": "npm run format:check && npm run lint && npm run typecheck && npm test"
42
+ "check": "npm run format:check && npm run lint && npm run lint:biome && npm run typecheck && npm test",
43
+ "lint:biome": "biome lint"
43
44
  },
44
45
  "keywords": [
45
46
  "mcp",
@@ -78,24 +79,23 @@
78
79
  "node": ">=20"
79
80
  },
80
81
  "devDependencies": {
81
- "@cloudflare/workers-types": "^4.20260316.1",
82
+ "@biomejs/biome": "^2.4.15",
83
+ "@cloudflare/workers-types": "^4.20260526.1",
82
84
  "@eslint/js": "^10.0.1",
83
- "@types/node": "^25.5.0",
84
- "@vitest/coverage-v8": "^4.1.0",
85
- "chart.js": "^4.5.1",
86
- "chartjs-node-canvas": "^5.0.0",
87
- "chartjs-plugin-datalabels": "^2.2.0",
88
- "eslint": "^10.0.2",
85
+ "@types/node": "^25.9.1",
86
+ "@vitest/coverage-v8": "^4.1.7",
87
+ "eslint": "^10.4.0",
89
88
  "eslint-config-prettier": "^10.1.8",
90
89
  "globals": "^17.4.0",
91
- "jiti": "^2.6.1",
92
- "playwright": "^1.58.2",
93
- "prettier": "^3.8.1",
94
- "tsx": "^4.20.6",
90
+ "jiti": "^2.7.0",
91
+ "playwright": "^1.60.0",
92
+ "prettier": "^3.8.3",
93
+ "sharp": "^0.34.5",
94
+ "tsx": "^4.22.3",
95
95
  "typescript": "^6.0.3",
96
- "typescript-eslint": "^8.59.3",
97
- "vitest": "^4.1.0",
98
- "wrangler": "^4.73.0"
96
+ "typescript-eslint": "^8.60.0",
97
+ "vitest": "^4.1.7",
98
+ "wrangler": "^4.95.0"
99
99
  },
100
100
  "overrides": {
101
101
  "flatted": "3.4.2",
@@ -105,7 +105,8 @@
105
105
  },
106
106
  "dependencies": {
107
107
  "@modelcontextprotocol/sdk": "^1.27.1",
108
- "hono": "^4.12.8",
108
+ "hono": "^4.12.23",
109
+ "proj4": "^2.20.8",
109
110
  "workers-mcp": "^0.0.13"
110
111
  }
111
112
  }