@things-factory/weather 9.1.19 → 10.0.0-beta.1
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/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/services/chrome-renderer.js +10 -2
- package/dist-server/services/chrome-renderer.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/client/tsconfig.json +0 -11
- package/server/controllers/open-meteo-adapter.ts +0 -96
- package/server/controllers/types.ts +0 -63
- package/server/controllers/weather-controller.ts +0 -192
- package/server/index.ts +0 -1
- package/server/routers/geo-router.ts +0 -297
- package/server/routers/weather-router.ts +0 -8
- package/server/routes.ts +0 -10
- package/server/services/chrome-renderer.ts +0 -206
- package/server/services/data.json +0 -8438
- package/server/services/headless-pool.ts +0 -34
- package/server/tsconfig.json +0 -10
- package/server/utils/svg-utils.ts +0 -108
- package/server/utils/tile-utils.ts +0 -77
package/server/routes.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { getBrowserPool as getHeadlessPoolForWeather } from './services/headless-pool.js'
|
|
2
|
-
import { weatherRouter } from './routers/weather-router.js'
|
|
3
|
-
import { geoRouter } from './routers/geo-router.js'
|
|
4
|
-
|
|
5
|
-
process.on('bootstrap-module-global-public-route' as any, (app, globalPublicRouter) => {
|
|
6
|
-
getHeadlessPoolForWeather()
|
|
7
|
-
|
|
8
|
-
globalPublicRouter.use('/weather', weatherRouter.routes(), weatherRouter.allowedMethods())
|
|
9
|
-
globalPublicRouter.use('/geo', geoRouter.routes(), geoRouter.allowedMethods())
|
|
10
|
-
})
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import { getBrowserPool } from './headless-pool'
|
|
2
|
-
|
|
3
|
-
export async function renderWeatherMap(
|
|
4
|
-
type: string,
|
|
5
|
-
data: {
|
|
6
|
-
lat: number
|
|
7
|
-
lng: number
|
|
8
|
-
value: number
|
|
9
|
-
description: number
|
|
10
|
-
temperature: number
|
|
11
|
-
humidity: number
|
|
12
|
-
cloudCoverage: number
|
|
13
|
-
solarRadiation: number
|
|
14
|
-
windSpeed: number
|
|
15
|
-
windDirection: number
|
|
16
|
-
precipitation?: number
|
|
17
|
-
}[],
|
|
18
|
-
width: number = 1800,
|
|
19
|
-
height: number = 900
|
|
20
|
-
): Promise<Buffer> {
|
|
21
|
-
console.log(`renderWeatherMap 호출됨: 타입=${type}, 데이터 개수=${data.length}`)
|
|
22
|
-
|
|
23
|
-
if (!data || data.length === 0) {
|
|
24
|
-
throw new Error('데이터가 없습니다. PNG 생성을 중단합니다.')
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const browser = await getBrowserPool().acquire()
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
console.log('브라우저 인스턴스 획득')
|
|
31
|
-
const page = await browser.newPage()
|
|
32
|
-
|
|
33
|
-
// 모든 콘솔 로그를 캡처하여 Node.js 프로세스에 출력
|
|
34
|
-
page.on('console', msg => {
|
|
35
|
-
console.log(`PAGE LOG [${msg.type()}]: ${msg.text()}`)
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
// 오류 이벤트도 추적 (예: Unhandled Promise Rejection)
|
|
39
|
-
page.on('pageerror', err => {
|
|
40
|
-
console.error('BROWSER PAGE ERROR:', err)
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
await page.setViewport({ width, height })
|
|
44
|
-
|
|
45
|
-
await page.setContent(`
|
|
46
|
-
<!DOCTYPE html>
|
|
47
|
-
<html>
|
|
48
|
-
<head>
|
|
49
|
-
<style>body { margin: 0; }</style>
|
|
50
|
-
</head>
|
|
51
|
-
<body>
|
|
52
|
-
<canvas id="weatherCanvas" width="${width}" height="${height}"></canvas>
|
|
53
|
-
</body>
|
|
54
|
-
</html>
|
|
55
|
-
`)
|
|
56
|
-
|
|
57
|
-
console.log('HTML 콘텐츠 설정 완료')
|
|
58
|
-
|
|
59
|
-
await page.evaluate(
|
|
60
|
-
(type, data, width, height) => {
|
|
61
|
-
try {
|
|
62
|
-
console.log('Canvas 렌더링 시작')
|
|
63
|
-
|
|
64
|
-
const canvas = document.getElementById('weatherCanvas') as HTMLCanvasElement
|
|
65
|
-
if (!canvas) {
|
|
66
|
-
throw new Error('캔버스를 찾을 수 없습니다.')
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const ctx = canvas.getContext('2d')
|
|
70
|
-
if (!ctx) {
|
|
71
|
-
throw new Error('CanvasRenderingContext2D를 가져올 수 없습니다.')
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
ctx.clearRect(0, 0, width, height)
|
|
75
|
-
|
|
76
|
-
function latLngToPixel(lat, lng) {
|
|
77
|
-
if (isNaN(lat) || isNaN(lng)) return null
|
|
78
|
-
const x = Math.floor(((lng + 180) / 360) * width)
|
|
79
|
-
const y = Math.floor(((90 - lat) / 180) * height)
|
|
80
|
-
if (x < 0 || x >= width || y < 0 || y >= height) return null
|
|
81
|
-
return { x, y }
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function valueToRGB(type, dataPoint) {
|
|
85
|
-
if (!dataPoint || typeof dataPoint.value !== 'number') {
|
|
86
|
-
console.warn('잘못된 데이터 포인트:', dataPoint)
|
|
87
|
-
return { r: 0, g: 0, b: 0, a: 0 }
|
|
88
|
-
}
|
|
89
|
-
switch (type) {
|
|
90
|
-
case 'temperature':
|
|
91
|
-
return { r: Math.min(255, Math.max(0, (dataPoint.value + 50) * 2.55)), g: 0, b: 0, a: 255 }
|
|
92
|
-
case 'humidity':
|
|
93
|
-
return { r: 0, g: Math.min(255, Math.max(0, dataPoint.humidity * 2.55)), b: 0, a: 255 }
|
|
94
|
-
case 'wind':
|
|
95
|
-
return { r: Math.min(255, Math.max(0, dataPoint.windSpeed * 5)), g: 0, b: 255, a: 255 }
|
|
96
|
-
case 'precipitation':
|
|
97
|
-
return { r: 0, g: 0, b: Math.min(255, Math.max(0, dataPoint.precipitation * 2.55)), a: 255 }
|
|
98
|
-
default:
|
|
99
|
-
return { r: 255, g: 255, b: 255, a: 255 }
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function calculateColor(type, value) {
|
|
104
|
-
switch (type) {
|
|
105
|
-
case 'temperature':
|
|
106
|
-
return { r: Math.min(255, Math.max(0, (value + 50) * 2.55)), g: 0, b: 0, a: 255 }
|
|
107
|
-
case 'humidity':
|
|
108
|
-
return { r: 0, g: Math.min(255, Math.max(0, value * 2.55)), b: 0, a: 255 }
|
|
109
|
-
case 'wind':
|
|
110
|
-
return { r: Math.min(255, Math.max(0, value * 5)), g: 0, b: 255, a: 255 }
|
|
111
|
-
case 'precipitation':
|
|
112
|
-
return { r: 0, g: 0, b: Math.min(255, Math.max(0, value * 2.55)), a: 255 }
|
|
113
|
-
default:
|
|
114
|
-
return { r: 255, g: 255, b: 255, a: 255 }
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function renderInterpolated(ctx, data, type, width, height) {
|
|
119
|
-
const imageData = ctx.createImageData(width, height)
|
|
120
|
-
const maxDistance = width * 0.05 // 최대 영향 거리 (이미지 너비의 5%)
|
|
121
|
-
|
|
122
|
-
// 모든 픽셀에 대해 계산
|
|
123
|
-
for (let y = 0; y < height; y++) {
|
|
124
|
-
for (let x = 0; x < width; x++) {
|
|
125
|
-
// 픽셀의 위/경도 계산
|
|
126
|
-
const lng = (x / width) * 360 - 180
|
|
127
|
-
const lat = 90 - (y / height) * 180
|
|
128
|
-
|
|
129
|
-
// 역거리 가중치 보간법
|
|
130
|
-
let weightedSum = 0
|
|
131
|
-
let totalWeight = 0
|
|
132
|
-
|
|
133
|
-
data.forEach(point => {
|
|
134
|
-
const pixelX = ((point.lng + 180) / 360) * width
|
|
135
|
-
const pixelY = ((90 - point.lat) / 180) * height
|
|
136
|
-
const distance = Math.sqrt(Math.pow(x - pixelX, 2) + Math.pow(y - pixelY, 2))
|
|
137
|
-
|
|
138
|
-
if (distance <= maxDistance && typeof point.value === 'number') {
|
|
139
|
-
const weight = 1 / (Math.pow(distance, 2) + 0.1)
|
|
140
|
-
weightedSum += point.value * weight
|
|
141
|
-
totalWeight += weight
|
|
142
|
-
}
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
const index = (y * width + x) * 4
|
|
146
|
-
if (totalWeight > 0) {
|
|
147
|
-
const value = weightedSum / totalWeight
|
|
148
|
-
const color = calculateColor(type, value)
|
|
149
|
-
|
|
150
|
-
imageData.data[index] = color.r
|
|
151
|
-
imageData.data[index + 1] = color.g
|
|
152
|
-
imageData.data[index + 2] = color.b
|
|
153
|
-
imageData.data[index + 3] = color.a
|
|
154
|
-
} else {
|
|
155
|
-
// 영향 범위 내 데이터 없음
|
|
156
|
-
imageData.data[index + 3] = 0 // 투명하게
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
ctx.putImageData(imageData, 0, 0)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// 보간 렌더링 호출
|
|
165
|
-
renderInterpolated(ctx, data, type, width, height)
|
|
166
|
-
|
|
167
|
-
console.log('Canvas 렌더링 완료')
|
|
168
|
-
;(window as any).renderComplete = true
|
|
169
|
-
} catch (err) {
|
|
170
|
-
console.error('렌더링 오류:', err)
|
|
171
|
-
;(window as any).renderComplete = false
|
|
172
|
-
}
|
|
173
|
-
},
|
|
174
|
-
type,
|
|
175
|
-
data,
|
|
176
|
-
width,
|
|
177
|
-
height
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
console.log('렌더링 완료')
|
|
181
|
-
|
|
182
|
-
const renderComplete = await page
|
|
183
|
-
.waitForFunction('window.renderComplete === true', { timeout: 10000 })
|
|
184
|
-
.catch(err => {
|
|
185
|
-
console.error('렌더링 완료 신호 감지 실패:', err)
|
|
186
|
-
return false
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
if (!renderComplete) {
|
|
190
|
-
throw new Error('렌더링이 완료되지 않았습니다.')
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const pngBuffer = await page.screenshot({
|
|
194
|
-
type: 'png',
|
|
195
|
-
omitBackground: true
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
console.log(`PNG 생성 완료: ${pngBuffer.length} 바이트`)
|
|
199
|
-
return pngBuffer
|
|
200
|
-
} catch (error) {
|
|
201
|
-
console.error('날씨 맵 렌더링 오류:', error)
|
|
202
|
-
throw error
|
|
203
|
-
} finally {
|
|
204
|
-
await getBrowserPool().release(browser)
|
|
205
|
-
}
|
|
206
|
-
}
|