@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.
@@ -1,8 +0,0 @@
1
- import Router from 'koa-router'
2
-
3
- import { globalWeatherController } from '../controllers/weather-controller'
4
-
5
- export const weatherRouter = new Router()
6
-
7
- // 전체 날씨 지도 이미지 엔드포인트
8
- weatherRouter.get('/map/:type.png', globalWeatherController)
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
- }