@things-factory/weather 9.0.0-9.0.0-beta.59.0

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 (90) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/assets/images/wind.png +0 -0
  3. package/assets/images/wind2.png +0 -0
  4. package/client/bootstrap.ts +1 -0
  5. package/client/index.ts +0 -0
  6. package/client/ol-components/webgl-canvas-layer.ts +413 -0
  7. package/client/ol-components/wind-data-tile-layer.ts +485 -0
  8. package/client/ol-components/wind-rasterized-layer.ts +437 -0
  9. package/client/pages/weather-page.ts +353 -0
  10. package/client/route.ts +7 -0
  11. package/client/tsconfig.json +11 -0
  12. package/config/config.development.js +28 -0
  13. package/config/config.production.js +10 -0
  14. package/data/geo-json/countries-max-geo.json +1 -0
  15. package/dist-client/bootstrap.d.ts +1 -0
  16. package/dist-client/bootstrap.js +2 -0
  17. package/dist-client/bootstrap.js.map +1 -0
  18. package/dist-client/index.d.ts +0 -0
  19. package/dist-client/index.js +2 -0
  20. package/dist-client/index.js.map +1 -0
  21. package/dist-client/ol-components/webgl-canvas-layer.d.ts +61 -0
  22. package/dist-client/ol-components/webgl-canvas-layer.js +364 -0
  23. package/dist-client/ol-components/webgl-canvas-layer.js.map +1 -0
  24. package/dist-client/ol-components/wind-data-tile-layer.d.ts +59 -0
  25. package/dist-client/ol-components/wind-data-tile-layer.js +414 -0
  26. package/dist-client/ol-components/wind-data-tile-layer.js.map +1 -0
  27. package/dist-client/ol-components/wind-rasterized-layer.d.ts +59 -0
  28. package/dist-client/ol-components/wind-rasterized-layer.js +363 -0
  29. package/dist-client/ol-components/wind-rasterized-layer.js.map +1 -0
  30. package/dist-client/pages/weather-page.d.ts +23 -0
  31. package/dist-client/pages/weather-page.js +348 -0
  32. package/dist-client/pages/weather-page.js.map +1 -0
  33. package/dist-client/route.d.ts +1 -0
  34. package/dist-client/route.js +8 -0
  35. package/dist-client/route.js.map +1 -0
  36. package/dist-client/tsconfig.tsbuildinfo +1 -0
  37. package/dist-server/controllers/open-meteo-adapter.d.ts +4 -0
  38. package/dist-server/controllers/open-meteo-adapter.js +87 -0
  39. package/dist-server/controllers/open-meteo-adapter.js.map +1 -0
  40. package/dist-server/controllers/types.d.ts +55 -0
  41. package/dist-server/controllers/types.js +3 -0
  42. package/dist-server/controllers/types.js.map +1 -0
  43. package/dist-server/controllers/weather-controller.d.ts +12 -0
  44. package/dist-server/controllers/weather-controller.js +179 -0
  45. package/dist-server/controllers/weather-controller.js.map +1 -0
  46. package/dist-server/index.d.ts +1 -0
  47. package/dist-server/index.js +4 -0
  48. package/dist-server/index.js.map +1 -0
  49. package/dist-server/routers/geo-router.d.ts +59 -0
  50. package/dist-server/routers/geo-router.js +277 -0
  51. package/dist-server/routers/geo-router.js.map +1 -0
  52. package/dist-server/routers/weather-router.d.ts +1 -0
  53. package/dist-server/routers/weather-router.js +10 -0
  54. package/dist-server/routers/weather-router.js.map +1 -0
  55. package/dist-server/routes.d.ts +1 -0
  56. package/dist-server/routes.js +11 -0
  57. package/dist-server/routes.js.map +1 -0
  58. package/dist-server/services/chrome-renderer.d.ts +13 -0
  59. package/dist-server/services/chrome-renderer.js +162 -0
  60. package/dist-server/services/chrome-renderer.js.map +1 -0
  61. package/dist-server/services/headless-pool.d.ts +4 -0
  62. package/dist-server/services/headless-pool.js +89 -0
  63. package/dist-server/services/headless-pool.js.map +1 -0
  64. package/dist-server/tsconfig.tsbuildinfo +1 -0
  65. package/dist-server/utils/svg-utils.d.ts +35 -0
  66. package/dist-server/utils/svg-utils.js +89 -0
  67. package/dist-server/utils/svg-utils.js.map +1 -0
  68. package/dist-server/utils/tile-utils.d.ts +38 -0
  69. package/dist-server/utils/tile-utils.js +64 -0
  70. package/dist-server/utils/tile-utils.js.map +1 -0
  71. package/package.json +46 -0
  72. package/server/controllers/open-meteo-adapter.ts +96 -0
  73. package/server/controllers/types.ts +63 -0
  74. package/server/controllers/weather-controller.ts +192 -0
  75. package/server/index.ts +1 -0
  76. package/server/routers/geo-router.ts +297 -0
  77. package/server/routers/weather-router.ts +8 -0
  78. package/server/routes.ts +10 -0
  79. package/server/services/chrome-renderer.ts +206 -0
  80. package/server/services/data.json +8438 -0
  81. package/server/services/headless-pool.ts +97 -0
  82. package/server/tsconfig.json +10 -0
  83. package/server/utils/svg-utils.ts +108 -0
  84. package/server/utils/tile-utils.ts +77 -0
  85. package/things-factory.config.js +8 -0
  86. package/translations/en.json +1 -0
  87. package/translations/ja.json +1 -0
  88. package/translations/ko.json +1 -0
  89. package/translations/ms.json +1 -0
  90. package/translations/zh.json +1 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"weather-controller.js","sourceRoot":"","sources":["../../server/controllers/weather-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import { Context } from 'koa'\n\nimport { config } from '@things-factory/env'\nimport { cacheService } from '@things-factory/cache-service'\n\nimport { WeatherType } from './types.js'\nimport { getGlobalWeatherData } from './open-meteo-adapter.js'\nimport { renderWeatherMap } from '../services/chrome-renderer.js'\n\n/**\n * 전체 날씨 지도 이미지 컨트롤러\n */\nexport async function globalWeatherController(ctx: Context) {\n const type = ctx.params.type\n const forceRefresh = ctx.query.refresh === 'true'\n\n try {\n // 유효한 날씨 타입인지 확인\n if (!['temperature', 'humidity', 'wind', 'precipitation'].includes(type)) {\n ctx.status = 400\n ctx.body = {\n error: `유효하지 않은 날씨 타입: ${type}`,\n validTypes: ['temperature', 'humidity', 'wind', 'precipitation']\n }\n return\n }\n\n // 날씨 이미지 가져오기\n const imageBuffer = await getGlobalWeatherImage(type, forceRefresh)\n\n // 이미지 데이터 응답\n ctx.body = Buffer.from(imageBuffer)\n\n // 이미지 응답 헤더 설정\n ctx.status = 200\n ctx.set('Content-Type', 'image/png')\n ctx.set('Cache-Control', 'public, max-age=3600') // 1시간 캐싱\n } catch (error) {\n console.error(`날씨 지도 생성 오류 (${type}):`, error)\n ctx.status = 500\n ctx.body = {\n error: '날씨 지도 생성 중 오류가 발생했습니다',\n message: error.message\n }\n }\n}\n\n/**\n * 전체 날씨 이미지 가져오기\n * @param type 날씨 타입 (temperature, humidity, wind, precipitation)\n * @param forceRefresh 캐시 무시하고 새로 가져오기\n */\nexport async function getGlobalWeatherImage(type: WeatherType, forceRefresh: boolean = false): Promise<Buffer> {\n if (!['temperature', 'humidity', 'wind', 'precipitation'].includes(type)) {\n throw new Error(`유효하지 않은 날씨 타입: ${type}`)\n }\n\n // 캐시 유효 시간 (분)\n const cacheLifetime = config.get('WEATHER_CACHE_LIFETIME') || 60\n\n try {\n // 캐시에서 데이터 확인\n if (!forceRefresh) {\n const { value: cachedDataJson } = (await cacheService.getFromCache(`global-weather-image`, { type })) || {}\n\n // Buffer.from이 객체를 직접 처리할 수 없으므로 타입에 따라 적절하게 처리\n let cachedData = null\n if (cachedDataJson) {\n if (Buffer.isBuffer(cachedDataJson)) {\n cachedData = cachedDataJson\n } else if (typeof cachedDataJson === 'string') {\n // 문자열이면 버퍼로 변환\n cachedData = cachedDataJson\n } else if (Array.isArray(cachedDataJson)) {\n // 배열이면 버퍼로 변환\n cachedData = cachedDataJson\n } else if (typeof cachedDataJson === 'object') {\n // 객체인 경우 처리 (JSON.stringify된 버퍼일 수 있음)\n try {\n // JSON 문자열로 변환했다가 다시 파싱하면 배열로 변환될 수 있음\n if ((cachedDataJson.type = 'Buffer')) {\n cachedData = cachedDataJson.data\n }\n } catch (conversionError) {\n console.error('캐시 데이터 변환 실패:', conversionError)\n }\n }\n }\n\n if (cachedData && cachedData.length > 0) {\n console.log(`전체 날씨 이미지 캐시 사용 (타입: ${type})`)\n return cachedData\n }\n }\n\n console.log(`전체 날씨 이미지 생성 (타입: ${type})`)\n\n var { value: weatherPoints } = (await cacheService.getFromCache(`weather-points`, {})) || {}\n\n if (!weatherPoints || weatherPoints.length === 0) {\n const weatherData = await getGlobalWeatherData(10)\n\n const flattenedWeatherPoints = weatherData.reduce((acc, area) => {\n return acc.concat(area.weatherPoints)\n }, [])\n\n weatherPoints = flattenedWeatherPoints.map(point => {\n let value\n switch (type) {\n case 'temperature':\n value = point.temperature?.current\n break\n case 'humidity':\n value = point.humidity\n break\n case 'wind':\n value = point.wind?.speed\n break\n case 'precipitation':\n value = 0\n break // 현재 데이터 없음\n }\n\n return {\n lat: point.location.latitude,\n lng: point.location.longitude,\n value,\n // value: point.temperature?.current,\n description: point.weatherDescription,\n temperature: point.temperature?.current,\n humidity: point.humidity,\n cloudCoverage: point.cloudCoverage,\n solarRadiation: point.solarRadiation,\n windSpeed: point.wind?.speed,\n windDirection: point.wind?.direction\n // precipitation: point.precipitation.current\n }\n })\n\n await cacheService.setInCache(`weather-points`, {}, weatherPoints, cacheLifetime * 60)\n }\n\n // Headless Chrome으로 이미지 생성\n const imageBuffer = await renderWeatherMap(type, weatherPoints, 1800, 900)\n\n // 캐시에 저장 (초 단위로 TTL 설정)\n await cacheService.setInCache(`global-weather-image`, { type }, imageBuffer, cacheLifetime * 60)\n\n return imageBuffer\n } catch (error) {\n console.error(`전체 날씨 이미지 생성 실패 (타입: ${type}):`, error)\n\n // 에러 발생 시 캐시에서 데이터 확인\n const { value: cachedDataJson } = (await cacheService.getFromCache(`global-weather-image`, { type })) || {}\n\n // Buffer.from이 객체를 직접 처리할 수 없으므로 타입에 따라 적절하게 처리\n let cachedData = null\n if (cachedDataJson) {\n if (Buffer.isBuffer(cachedDataJson)) {\n cachedData = cachedDataJson\n } else if (typeof cachedDataJson === 'string') {\n // 문자열이면 버퍼로 변환\n cachedData = Buffer.from(cachedDataJson)\n } else if (Array.isArray(cachedDataJson)) {\n // 배열이면 버퍼로 변환\n cachedData = Buffer.from(cachedDataJson)\n } else if (typeof cachedDataJson === 'object') {\n // 객체인 경우 처리 (JSON.stringify된 버퍼일 수 있음)\n try {\n // JSON 문자열로 변환했다가 다시 파싱하면 배열로 변환될 수 있음\n const dataArray = Object.keys(cachedDataJson)\n .filter(key => !isNaN(Number(key)))\n .sort((a, b) => Number(a) - Number(b))\n .map(key => cachedDataJson[key])\n\n if (dataArray.length > 0) {\n cachedData = Buffer.from(dataArray)\n }\n } catch (conversionError) {\n console.error('캐시 데이터 변환 실패:', conversionError)\n }\n }\n }\n\n if (cachedData) {\n console.log(`에러로 인해 캐시된 날씨 이미지 사용 (타입: ${type})`)\n return cachedData\n }\n\n throw error\n }\n}\n"]}
@@ -0,0 +1 @@
1
+ import './routes';
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ require("./routes");
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../server/index.ts"],"names":[],"mappings":";;AAAA,oBAAiB","sourcesContent":["import './routes'\n"]}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * @fileoverview
3
+ * Koa 기반의 GeoJSON → MBTiles 변환 및 VectorTile(MVT) 서비스 제공 라우터
4
+ *
5
+ * 이 라우터는 GeoJSON 데이터를 MBTiles 형식으로 변환하고, OpenLayers 등의 클라이언트에서
6
+ * Vector Tile(MVT) 형식으로 요청할 수 있도록 API 엔드포인트를 제공합니다.
7
+ *
8
+ * 📌 주요 기능:
9
+ * 1️⃣ `/tiles/regenerate` (POST) → GeoJSON을 다시 MBTiles로 변환
10
+ * 2️⃣ `/tiles/:z/:x/:y.mvt` (GET) → 특정 타일 데이터를 반환
11
+ * 3️⃣ `/tiles/metadata` (GET) → MBTiles의 메타데이터 반환
12
+ *
13
+ * 🚀 서비스 초기화 흐름:
14
+ * 1. 서버 시작 시 `initializeTileService()` 호출
15
+ * - MBTiles 파일이 없으면 `convertGeoJsonToMBTiles()` 실행하여 생성
16
+ * - MBTiles가 존재하면 `loadTileSource()`를 호출하여 데이터 로드
17
+ * 2. 클라이언트가 MVT 타일을 요청하면 `tileSource.getTile(z, x, y, callback)`을 통해 데이터 반환
18
+ *
19
+ * 📌 주요 처리 로직:
20
+ * ✅ **GeoJSON → MBTiles 변환 (`convertGeoJsonToMBTiles`)**
21
+ * - `tippecanoe` CLI를 사용하여 GeoJSON을 MBTiles로 변환
22
+ * - 기존 MBTiles 파일이 있으면 삭제 후 재생성
23
+ * - `spawn`을 사용하여 비동기적으로 실행하며 로그 출력
24
+ *
25
+ * ✅ **MBTiles 파일을 로드 (`loadTileSource`)**
26
+ * - `@mapbox/mbtiles` 라이브러리를 사용하여 MBTiles 데이터베이스를 로드
27
+ * - 타일 요청이 있을 때 해당 타일을 `tileSource.getTile()`을 통해 제공
28
+ *
29
+ * ✅ **타일 요청 처리 (`/tiles/:z/:x/:y.mvt`)**
30
+ * - MBTiles에서 특정 타일을 찾아 바이너리 MVT 데이터로 반환
31
+ * - `Content-Encoding: gzip` 설정으로 압축된 타일 제공
32
+ * - `TMS 좌표 변환` 문제 제거하여 직접 요청 가능하도록 개선
33
+ *
34
+ * ✅ **타일셋 메타데이터 반환 (`/tiles/metadata`)**
35
+ * - MBTiles 파일에서 메타데이터 정보를 가져와 JSON 형태로 반환
36
+ *
37
+ * 📌 종속성:
38
+ * - `koa-router`: Koa 기반 API 라우팅
39
+ * - `fs`, `path`: 파일 시스템 접근 및 경로 처리
40
+ * - `child_process`, `util`: `tippecanoe` 실행을 위한 비동기 프로세스 실행
41
+ * - `@mapbox/mbtiles`: MBTiles 데이터베이스 접근 라이브러리
42
+ * - `@things-factory/env`: 환경 변수 관리
43
+ *
44
+ * 📌 디렉토리 구조:
45
+ * ```
46
+ * /cache/mvt-tiles/ # 생성된 MBTiles 저장
47
+ * /data/geo-json/ # 원본 GeoJSON 파일 저장
48
+ * ```
49
+ *
50
+ * 📌 실행 방법:
51
+ * 1. GeoJSON 데이터 준비
52
+ * 2. Koa 서버 실행 → 자동으로 MBTiles 변환 후 서비스 시작
53
+ * 3. 클라이언트(OpenLayers 등)에서 VectorTile 요청 가능
54
+ * 4. 필요 시 `/tiles/regenerate` 엔드포인트로 타일 재생성
55
+ *
56
+ * @module geoRouter
57
+ */
58
+ export declare const geoRouter: any;
59
+ export default geoRouter;
@@ -0,0 +1,277 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview
4
+ * Koa 기반의 GeoJSON → MBTiles 변환 및 VectorTile(MVT) 서비스 제공 라우터
5
+ *
6
+ * 이 라우터는 GeoJSON 데이터를 MBTiles 형식으로 변환하고, OpenLayers 등의 클라이언트에서
7
+ * Vector Tile(MVT) 형식으로 요청할 수 있도록 API 엔드포인트를 제공합니다.
8
+ *
9
+ * 📌 주요 기능:
10
+ * 1️⃣ `/tiles/regenerate` (POST) → GeoJSON을 다시 MBTiles로 변환
11
+ * 2️⃣ `/tiles/:z/:x/:y.mvt` (GET) → 특정 타일 데이터를 반환
12
+ * 3️⃣ `/tiles/metadata` (GET) → MBTiles의 메타데이터 반환
13
+ *
14
+ * 🚀 서비스 초기화 흐름:
15
+ * 1. 서버 시작 시 `initializeTileService()` 호출
16
+ * - MBTiles 파일이 없으면 `convertGeoJsonToMBTiles()` 실행하여 생성
17
+ * - MBTiles가 존재하면 `loadTileSource()`를 호출하여 데이터 로드
18
+ * 2. 클라이언트가 MVT 타일을 요청하면 `tileSource.getTile(z, x, y, callback)`을 통해 데이터 반환
19
+ *
20
+ * 📌 주요 처리 로직:
21
+ * ✅ **GeoJSON → MBTiles 변환 (`convertGeoJsonToMBTiles`)**
22
+ * - `tippecanoe` CLI를 사용하여 GeoJSON을 MBTiles로 변환
23
+ * - 기존 MBTiles 파일이 있으면 삭제 후 재생성
24
+ * - `spawn`을 사용하여 비동기적으로 실행하며 로그 출력
25
+ *
26
+ * ✅ **MBTiles 파일을 로드 (`loadTileSource`)**
27
+ * - `@mapbox/mbtiles` 라이브러리를 사용하여 MBTiles 데이터베이스를 로드
28
+ * - 타일 요청이 있을 때 해당 타일을 `tileSource.getTile()`을 통해 제공
29
+ *
30
+ * ✅ **타일 요청 처리 (`/tiles/:z/:x/:y.mvt`)**
31
+ * - MBTiles에서 특정 타일을 찾아 바이너리 MVT 데이터로 반환
32
+ * - `Content-Encoding: gzip` 설정으로 압축된 타일 제공
33
+ * - `TMS 좌표 변환` 문제 제거하여 직접 요청 가능하도록 개선
34
+ *
35
+ * ✅ **타일셋 메타데이터 반환 (`/tiles/metadata`)**
36
+ * - MBTiles 파일에서 메타데이터 정보를 가져와 JSON 형태로 반환
37
+ *
38
+ * 📌 종속성:
39
+ * - `koa-router`: Koa 기반 API 라우팅
40
+ * - `fs`, `path`: 파일 시스템 접근 및 경로 처리
41
+ * - `child_process`, `util`: `tippecanoe` 실행을 위한 비동기 프로세스 실행
42
+ * - `@mapbox/mbtiles`: MBTiles 데이터베이스 접근 라이브러리
43
+ * - `@things-factory/env`: 환경 변수 관리
44
+ *
45
+ * 📌 디렉토리 구조:
46
+ * ```
47
+ * /cache/mvt-tiles/ # 생성된 MBTiles 저장
48
+ * /data/geo-json/ # 원본 GeoJSON 파일 저장
49
+ * ```
50
+ *
51
+ * 📌 실행 방법:
52
+ * 1. GeoJSON 데이터 준비
53
+ * 2. Koa 서버 실행 → 자동으로 MBTiles 변환 후 서비스 시작
54
+ * 3. 클라이언트(OpenLayers 등)에서 VectorTile 요청 가능
55
+ * 4. 필요 시 `/tiles/regenerate` 엔드포인트로 타일 재생성
56
+ *
57
+ * @module geoRouter
58
+ */
59
+ Object.defineProperty(exports, "__esModule", { value: true });
60
+ exports.geoRouter = void 0;
61
+ const tslib_1 = require("tslib");
62
+ const koa_router_1 = tslib_1.__importDefault(require("koa-router"));
63
+ const fs = tslib_1.__importStar(require("fs"));
64
+ const path = tslib_1.__importStar(require("path"));
65
+ const child_process = tslib_1.__importStar(require("child_process"));
66
+ const util = tslib_1.__importStar(require("util"));
67
+ const env_1 = require("@things-factory/env");
68
+ const mbtiles_1 = tslib_1.__importDefault(require("@mapbox/mbtiles"));
69
+ const exec = util.promisify(child_process.exec);
70
+ exports.geoRouter = new koa_router_1.default();
71
+ const TILE_CACHE_DIR = path.resolve(env_1.config.get('directory', './cache'), 'mvt-tiles');
72
+ const GEO_DATA_DIR = path.resolve(__dirname, '..', '..', 'data', 'geo-json');
73
+ if (!fs.existsSync(TILE_CACHE_DIR)) {
74
+ fs.mkdirSync(TILE_CACHE_DIR, { recursive: true });
75
+ }
76
+ if (!fs.existsSync(GEO_DATA_DIR)) {
77
+ fs.mkdirSync(GEO_DATA_DIR, { recursive: true });
78
+ }
79
+ const COUNTRIES_GEO_JSON = path.resolve(GEO_DATA_DIR, 'countries-max-geo.json');
80
+ const MBTILES_PATH = path.resolve(TILE_CACHE_DIR, 'countries.mbtiles');
81
+ const child_process_1 = require("child_process");
82
+ const convertGeoJsonToMBTiles = async () => {
83
+ // tippecanoe -o ./packages/operato-ecs/cache/mvt-tiles/countries.mbtiles -z10 -Z5 --force --maximum-zoom=10 --minimum-zoom=0 --drop-smallest-as-needed --layer=countries ./packages/weather/data/geo-json/countries-max-geo.json
84
+ try {
85
+ if (!fs.existsSync(COUNTRIES_GEO_JSON)) {
86
+ console.error('GeoJSON 파일을 찾을 수 없습니다:', COUNTRIES_GEO_JSON);
87
+ return false;
88
+ }
89
+ if (fs.existsSync(MBTILES_PATH)) {
90
+ fs.unlinkSync(MBTILES_PATH);
91
+ console.log('기존 MBTiles 파일 삭제됨');
92
+ }
93
+ const tippecanoeCmd = [
94
+ '-o',
95
+ MBTILES_PATH,
96
+ '-z10',
97
+ '-Z5',
98
+ '--force',
99
+ '--maximum-zoom=10',
100
+ '--minimum-zoom=0',
101
+ '--drop-smallest-as-needed',
102
+ '--layer=countries',
103
+ COUNTRIES_GEO_JSON
104
+ ];
105
+ console.log('Tippecanoe 명령 실행 중...');
106
+ console.log(`tippecanoe ${tippecanoeCmd.join(' ')}`);
107
+ return new Promise((resolve, reject) => {
108
+ const process = (0, child_process_1.spawn)('tippecanoe', tippecanoeCmd);
109
+ process.stdout.on('data', data => {
110
+ console.log(`[tippecanoe]: ${data.toString()}`);
111
+ });
112
+ process.stderr.on('data', data => {
113
+ console.error(`[tippecanoe ERROR]: ${data.toString()}`);
114
+ });
115
+ process.on('close', code => {
116
+ if (code === 0) {
117
+ console.log('MBTiles 변환 완료 ✅');
118
+ resolve(true);
119
+ }
120
+ else {
121
+ console.error(`Tippecanoe 종료 코드: ${code}`);
122
+ resolve(false);
123
+ }
124
+ });
125
+ process.on('error', err => {
126
+ console.error(`Tippecanoe 실행 오류: ${err}`);
127
+ reject(err);
128
+ });
129
+ });
130
+ }
131
+ catch (error) {
132
+ console.error('MBTiles 변환 오류:', error);
133
+ return false;
134
+ }
135
+ };
136
+ // MBTiles 소스
137
+ let tileSource = null;
138
+ // MBTiles 소스 로드
139
+ const loadTileSource = async () => {
140
+ if (tileSource)
141
+ return tileSource;
142
+ return new Promise((resolve, reject) => {
143
+ new mbtiles_1.default(`mbtiles://${MBTILES_PATH}`, (err, source) => {
144
+ if (err) {
145
+ console.error('MBTiles 로드 오류:', err);
146
+ return reject(err);
147
+ }
148
+ tileSource = source;
149
+ console.log('MBTiles 소스 로드 완료');
150
+ resolve(source);
151
+ });
152
+ });
153
+ };
154
+ // 타일 서비스 초기화
155
+ const initializeTileService = async () => {
156
+ try {
157
+ // MBTiles 파일이 없으면 생성
158
+ if (!fs.existsSync(MBTILES_PATH)) {
159
+ console.log('MBTiles 파일이 없습니다. 생성을 시작합니다...');
160
+ const result = await convertGeoJsonToMBTiles();
161
+ if (!result)
162
+ return false;
163
+ }
164
+ // MBTiles 소스 로드
165
+ await loadTileSource();
166
+ return true;
167
+ }
168
+ catch (error) {
169
+ console.error('타일 서비스 초기화 오류:', error);
170
+ return false;
171
+ }
172
+ };
173
+ // API 엔드포인트: 타일 재생성 트리거
174
+ exports.geoRouter.post('/tiles/regenerate', async (ctx) => {
175
+ try {
176
+ console.log('타일 재생성 요청 받음');
177
+ const result = await convertGeoJsonToMBTiles();
178
+ if (result) {
179
+ // 새 타일소스 로드
180
+ await loadTileSource();
181
+ ctx.body = { success: true, message: 'MBTiles 타일 생성 완료' };
182
+ }
183
+ else {
184
+ ctx.status = 500;
185
+ ctx.body = { success: false, message: 'MBTiles 타일 생성 실패' };
186
+ }
187
+ }
188
+ catch (error) {
189
+ console.error('타일 재생성 오류:', error);
190
+ ctx.status = 500;
191
+ ctx.body = { success: false, message: '서버 오류 발생' };
192
+ }
193
+ });
194
+ exports.geoRouter.get('/tiles/:z/:x/:y.mvt', async (ctx) => {
195
+ try {
196
+ const z = parseInt(ctx.params.z, 10);
197
+ const x = parseInt(ctx.params.x, 10);
198
+ const y = parseInt(ctx.params.y, 10);
199
+ // 타일 서비스 로드 확인
200
+ if (!tileSource) {
201
+ console.log(`🚀 타일 서비스 로드 중...`);
202
+ const initialized = await initializeTileService();
203
+ if (!initialized) {
204
+ ctx.status = 500;
205
+ ctx.body = { error: '타일 서비스를 초기화할 수 없습니다.' };
206
+ return;
207
+ }
208
+ }
209
+ await new Promise((resolve, reject) => {
210
+ tileSource.getTile(z, x, y, (err, data, headers) => {
211
+ if (err) {
212
+ if (err.message.includes('Tile does not exist')) {
213
+ ctx.status = 204;
214
+ console.log(`🚨 타일 없음: z=${z}, x=${x}, y=${y}`);
215
+ return resolve(null);
216
+ }
217
+ console.error(`❌ 타일 로드 오류 (z:${z}, x:${x}, y:${y}):`, err);
218
+ ctx.status = 500;
219
+ ctx.body = { error: '타일을 로드할 수 없습니다.' };
220
+ return resolve(null);
221
+ }
222
+ ctx.set('Content-Type', headers['Content-Type'] || 'application/x-protobuf');
223
+ ctx.set({
224
+ 'Access-Control-Allow-Origin': '*',
225
+ 'Cache-Control': 'no-cache, no-store, must-revalidate, proxy-revalidate',
226
+ 'Content-Encoding': 'gzip'
227
+ });
228
+ ctx.body = data;
229
+ resolve(null);
230
+ });
231
+ });
232
+ }
233
+ catch (error) {
234
+ console.error('❌ 타일 서비스 오류:', error);
235
+ ctx.status = 500;
236
+ ctx.body = { error: '서버 오류가 발생했습니다.' };
237
+ }
238
+ });
239
+ exports.geoRouter.get('/tiles/metadata', async (ctx) => {
240
+ try {
241
+ if (!tileSource) {
242
+ const initialized = await initializeTileService();
243
+ if (!initialized) {
244
+ ctx.status = 500;
245
+ ctx.body = { error: '타일 서비스를 초기화할 수 없습니다.' };
246
+ return;
247
+ }
248
+ }
249
+ await new Promise((resolve, reject) => {
250
+ tileSource.getInfo((err, info) => {
251
+ if (err) {
252
+ console.error('메타데이터 로드 오류:', err);
253
+ ctx.status = 500;
254
+ ctx.body = { error: '메타데이터를 로드할 수 없습니다.' };
255
+ return resolve(null);
256
+ }
257
+ ctx.body = info;
258
+ resolve(null);
259
+ });
260
+ });
261
+ }
262
+ catch (error) {
263
+ console.error('메타데이터 서비스 오류:', error);
264
+ ctx.status = 500;
265
+ ctx.body = { error: '서버 오류가 발생했습니다.' };
266
+ }
267
+ });
268
+ initializeTileService().then(success => {
269
+ if (success) {
270
+ console.log('🗺️ 타일 서비스가 준비되었습니다.');
271
+ }
272
+ else {
273
+ console.error('❌ 타일 서비스 초기화에 실패했습니다.');
274
+ }
275
+ });
276
+ exports.default = exports.geoRouter;
277
+ //# sourceMappingURL=geo-router.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geo-router.js","sourceRoot":"","sources":["../../server/routers/geo-router.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview\n * Koa 기반의 GeoJSON → MBTiles 변환 및 VectorTile(MVT) 서비스 제공 라우터\n *\n * 이 라우터는 GeoJSON 데이터를 MBTiles 형식으로 변환하고, OpenLayers 등의 클라이언트에서\n * Vector Tile(MVT) 형식으로 요청할 수 있도록 API 엔드포인트를 제공합니다.\n *\n * 📌 주요 기능:\n * 1️⃣ `/tiles/regenerate` (POST) → GeoJSON을 다시 MBTiles로 변환\n * 2️⃣ `/tiles/:z/:x/:y.mvt` (GET) → 특정 타일 데이터를 반환\n * 3️⃣ `/tiles/metadata` (GET) → MBTiles의 메타데이터 반환\n *\n * 🚀 서비스 초기화 흐름:\n * 1. 서버 시작 시 `initializeTileService()` 호출\n * - MBTiles 파일이 없으면 `convertGeoJsonToMBTiles()` 실행하여 생성\n * - MBTiles가 존재하면 `loadTileSource()`를 호출하여 데이터 로드\n * 2. 클라이언트가 MVT 타일을 요청하면 `tileSource.getTile(z, x, y, callback)`을 통해 데이터 반환\n *\n * 📌 주요 처리 로직:\n * ✅ **GeoJSON → MBTiles 변환 (`convertGeoJsonToMBTiles`)**\n * - `tippecanoe` CLI를 사용하여 GeoJSON을 MBTiles로 변환\n * - 기존 MBTiles 파일이 있으면 삭제 후 재생성\n * - `spawn`을 사용하여 비동기적으로 실행하며 로그 출력\n *\n * ✅ **MBTiles 파일을 로드 (`loadTileSource`)**\n * - `@mapbox/mbtiles` 라이브러리를 사용하여 MBTiles 데이터베이스를 로드\n * - 타일 요청이 있을 때 해당 타일을 `tileSource.getTile()`을 통해 제공\n *\n * ✅ **타일 요청 처리 (`/tiles/:z/:x/:y.mvt`)**\n * - MBTiles에서 특정 타일을 찾아 바이너리 MVT 데이터로 반환\n * - `Content-Encoding: gzip` 설정으로 압축된 타일 제공\n * - `TMS 좌표 변환` 문제 제거하여 직접 요청 가능하도록 개선\n *\n * ✅ **타일셋 메타데이터 반환 (`/tiles/metadata`)**\n * - MBTiles 파일에서 메타데이터 정보를 가져와 JSON 형태로 반환\n *\n * 📌 종속성:\n * - `koa-router`: Koa 기반 API 라우팅\n * - `fs`, `path`: 파일 시스템 접근 및 경로 처리\n * - `child_process`, `util`: `tippecanoe` 실행을 위한 비동기 프로세스 실행\n * - `@mapbox/mbtiles`: MBTiles 데이터베이스 접근 라이브러리\n * - `@things-factory/env`: 환경 변수 관리\n *\n * 📌 디렉토리 구조:\n * ```\n * /cache/mvt-tiles/ # 생성된 MBTiles 저장\n * /data/geo-json/ # 원본 GeoJSON 파일 저장\n * ```\n *\n * 📌 실행 방법:\n * 1. GeoJSON 데이터 준비\n * 2. Koa 서버 실행 → 자동으로 MBTiles 변환 후 서비스 시작\n * 3. 클라이언트(OpenLayers 등)에서 VectorTile 요청 가능\n * 4. 필요 시 `/tiles/regenerate` 엔드포인트로 타일 재생성\n *\n * @module geoRouter\n */\n\nimport Router from 'koa-router'\nimport * as fs from 'fs'\nimport * as path from 'path'\nimport * as child_process from 'child_process'\nimport * as util from 'util'\nimport { config } from '@things-factory/env'\nimport mbtiles from '@mapbox/mbtiles'\n\nconst exec = util.promisify(child_process.exec)\n\nexport const geoRouter = new Router()\n\nconst TILE_CACHE_DIR = path.resolve(config.get('directory', './cache'), 'mvt-tiles')\nconst GEO_DATA_DIR = path.resolve(__dirname, '..', '..', 'data', 'geo-json')\n\nif (!fs.existsSync(TILE_CACHE_DIR)) {\n fs.mkdirSync(TILE_CACHE_DIR, { recursive: true })\n}\nif (!fs.existsSync(GEO_DATA_DIR)) {\n fs.mkdirSync(GEO_DATA_DIR, { recursive: true })\n}\n\nconst COUNTRIES_GEO_JSON = path.resolve(GEO_DATA_DIR, 'countries-max-geo.json')\nconst MBTILES_PATH = path.resolve(TILE_CACHE_DIR, 'countries.mbtiles')\n\nimport { spawn } from 'child_process'\n\nconst convertGeoJsonToMBTiles = async () => {\n // tippecanoe -o ./packages/operato-ecs/cache/mvt-tiles/countries.mbtiles -z10 -Z5 --force --maximum-zoom=10 --minimum-zoom=0 --drop-smallest-as-needed --layer=countries ./packages/weather/data/geo-json/countries-max-geo.json\n try {\n if (!fs.existsSync(COUNTRIES_GEO_JSON)) {\n console.error('GeoJSON 파일을 찾을 수 없습니다:', COUNTRIES_GEO_JSON)\n return false\n }\n\n if (fs.existsSync(MBTILES_PATH)) {\n fs.unlinkSync(MBTILES_PATH)\n console.log('기존 MBTiles 파일 삭제됨')\n }\n\n const tippecanoeCmd = [\n '-o',\n MBTILES_PATH,\n '-z10',\n '-Z5',\n '--force',\n '--maximum-zoom=10',\n '--minimum-zoom=0',\n '--drop-smallest-as-needed',\n '--layer=countries',\n COUNTRIES_GEO_JSON\n ]\n\n console.log('Tippecanoe 명령 실행 중...')\n console.log(`tippecanoe ${tippecanoeCmd.join(' ')}`)\n\n return new Promise((resolve, reject) => {\n const process = spawn('tippecanoe', tippecanoeCmd)\n\n process.stdout.on('data', data => {\n console.log(`[tippecanoe]: ${data.toString()}`)\n })\n\n process.stderr.on('data', data => {\n console.error(`[tippecanoe ERROR]: ${data.toString()}`)\n })\n\n process.on('close', code => {\n if (code === 0) {\n console.log('MBTiles 변환 완료 ✅')\n resolve(true)\n } else {\n console.error(`Tippecanoe 종료 코드: ${code}`)\n resolve(false)\n }\n })\n\n process.on('error', err => {\n console.error(`Tippecanoe 실행 오류: ${err}`)\n reject(err)\n })\n })\n } catch (error) {\n console.error('MBTiles 변환 오류:', error)\n return false\n }\n}\n\n// MBTiles 소스\nlet tileSource = null\n\n// MBTiles 소스 로드\nconst loadTileSource = async () => {\n if (tileSource) return tileSource\n\n return new Promise((resolve, reject) => {\n new mbtiles(`mbtiles://${MBTILES_PATH}`, (err, source) => {\n if (err) {\n console.error('MBTiles 로드 오류:', err)\n return reject(err)\n }\n\n tileSource = source\n console.log('MBTiles 소스 로드 완료')\n resolve(source)\n })\n })\n}\n\n// 타일 서비스 초기화\nconst initializeTileService = async () => {\n try {\n // MBTiles 파일이 없으면 생성\n if (!fs.existsSync(MBTILES_PATH)) {\n console.log('MBTiles 파일이 없습니다. 생성을 시작합니다...')\n const result = await convertGeoJsonToMBTiles()\n if (!result) return false\n }\n\n // MBTiles 소스 로드\n await loadTileSource()\n return true\n } catch (error) {\n console.error('타일 서비스 초기화 오류:', error)\n return false\n }\n}\n\n// API 엔드포인트: 타일 재생성 트리거\ngeoRouter.post('/tiles/regenerate', async ctx => {\n try {\n console.log('타일 재생성 요청 받음')\n const result = await convertGeoJsonToMBTiles()\n\n if (result) {\n // 새 타일소스 로드\n await loadTileSource()\n ctx.body = { success: true, message: 'MBTiles 타일 생성 완료' }\n } else {\n ctx.status = 500\n ctx.body = { success: false, message: 'MBTiles 타일 생성 실패' }\n }\n } catch (error) {\n console.error('타일 재생성 오류:', error)\n ctx.status = 500\n ctx.body = { success: false, message: '서버 오류 발생' }\n }\n})\n\ngeoRouter.get('/tiles/:z/:x/:y.mvt', async ctx => {\n try {\n const z = parseInt(ctx.params.z, 10)\n const x = parseInt(ctx.params.x, 10)\n const y = parseInt(ctx.params.y, 10)\n\n // 타일 서비스 로드 확인\n if (!tileSource) {\n console.log(`🚀 타일 서비스 로드 중...`)\n const initialized = await initializeTileService()\n if (!initialized) {\n ctx.status = 500\n ctx.body = { error: '타일 서비스를 초기화할 수 없습니다.' }\n return\n }\n }\n\n await new Promise((resolve, reject) => {\n tileSource.getTile(z, x, y, (err, data, headers) => {\n if (err) {\n if (err.message.includes('Tile does not exist')) {\n ctx.status = 204\n console.log(`🚨 타일 없음: z=${z}, x=${x}, y=${y}`)\n return resolve(null)\n }\n\n console.error(`❌ 타일 로드 오류 (z:${z}, x:${x}, y:${y}):`, err)\n ctx.status = 500\n ctx.body = { error: '타일을 로드할 수 없습니다.' }\n return resolve(null)\n }\n\n ctx.set('Content-Type', headers['Content-Type'] || 'application/x-protobuf')\n ctx.set({\n 'Access-Control-Allow-Origin': '*',\n 'Cache-Control': 'no-cache, no-store, must-revalidate, proxy-revalidate',\n 'Content-Encoding': 'gzip'\n })\n\n ctx.body = data\n resolve(null)\n })\n })\n } catch (error) {\n console.error('❌ 타일 서비스 오류:', error)\n ctx.status = 500\n ctx.body = { error: '서버 오류가 발생했습니다.' }\n }\n})\n\ngeoRouter.get('/tiles/metadata', async ctx => {\n try {\n if (!tileSource) {\n const initialized = await initializeTileService()\n if (!initialized) {\n ctx.status = 500\n ctx.body = { error: '타일 서비스를 초기화할 수 없습니다.' }\n return\n }\n }\n\n await new Promise((resolve, reject) => {\n tileSource.getInfo((err, info) => {\n if (err) {\n console.error('메타데이터 로드 오류:', err)\n ctx.status = 500\n ctx.body = { error: '메타데이터를 로드할 수 없습니다.' }\n return resolve(null)\n }\n\n ctx.body = info\n resolve(null)\n })\n })\n } catch (error) {\n console.error('메타데이터 서비스 오류:', error)\n ctx.status = 500\n ctx.body = { error: '서버 오류가 발생했습니다.' }\n }\n})\n\ninitializeTileService().then(success => {\n if (success) {\n console.log('🗺️ 타일 서비스가 준비되었습니다.')\n } else {\n console.error('❌ 타일 서비스 초기화에 실패했습니다.')\n }\n})\n\nexport default geoRouter\n"]}
@@ -0,0 +1 @@
1
+ export declare const weatherRouter: any;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.weatherRouter = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const koa_router_1 = tslib_1.__importDefault(require("koa-router"));
6
+ const weather_controller_1 = require("../controllers/weather-controller");
7
+ exports.weatherRouter = new koa_router_1.default();
8
+ // 전체 날씨 지도 이미지 엔드포인트
9
+ exports.weatherRouter.get('/map/:type.png', weather_controller_1.globalWeatherController);
10
+ //# sourceMappingURL=weather-router.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"weather-router.js","sourceRoot":"","sources":["../../server/routers/weather-router.ts"],"names":[],"mappings":";;;;AAAA,oEAA+B;AAE/B,0EAA2E;AAE9D,QAAA,aAAa,GAAG,IAAI,oBAAM,EAAE,CAAA;AAEzC,qBAAqB;AACrB,qBAAa,CAAC,GAAG,CAAC,gBAAgB,EAAE,4CAAuB,CAAC,CAAA","sourcesContent":["import Router from 'koa-router'\n\nimport { globalWeatherController } from '../controllers/weather-controller'\n\nexport const weatherRouter = new Router()\n\n// 전체 날씨 지도 이미지 엔드포인트\nweatherRouter.get('/map/:type.png', globalWeatherController)\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const headless_pool_js_1 = require("./services/headless-pool.js");
4
+ const weather_router_js_1 = require("./routers/weather-router.js");
5
+ const geo_router_js_1 = require("./routers/geo-router.js");
6
+ process.on('bootstrap-module-global-public-route', (app, globalPublicRouter) => {
7
+ (0, headless_pool_js_1.getBrowserPool)();
8
+ globalPublicRouter.use('/weather', weather_router_js_1.weatherRouter.routes(), weather_router_js_1.weatherRouter.allowedMethods());
9
+ globalPublicRouter.use('/geo', geo_router_js_1.geoRouter.routes(), geo_router_js_1.geoRouter.allowedMethods());
10
+ });
11
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../server/routes.ts"],"names":[],"mappings":";;AAAA,kEAAyF;AACzF,mEAA2D;AAC3D,2DAAmD;AAEnD,OAAO,CAAC,EAAE,CAAC,sCAA6C,EAAE,CAAC,GAAG,EAAE,kBAAkB,EAAE,EAAE;IACpF,IAAA,iCAAyB,GAAE,CAAA;IAE3B,kBAAkB,CAAC,GAAG,CAAC,UAAU,EAAE,iCAAa,CAAC,MAAM,EAAE,EAAE,iCAAa,CAAC,cAAc,EAAE,CAAC,CAAA;IAC1F,kBAAkB,CAAC,GAAG,CAAC,MAAM,EAAE,yBAAS,CAAC,MAAM,EAAE,EAAE,yBAAS,CAAC,cAAc,EAAE,CAAC,CAAA;AAChF,CAAC,CAAC,CAAA","sourcesContent":["import { getBrowserPool as getHeadlessPoolForWeather } from './services/headless-pool.js'\nimport { weatherRouter } from './routers/weather-router.js'\nimport { geoRouter } from './routers/geo-router.js'\n\nprocess.on('bootstrap-module-global-public-route' as any, (app, globalPublicRouter) => {\n getHeadlessPoolForWeather()\n\n globalPublicRouter.use('/weather', weatherRouter.routes(), weatherRouter.allowedMethods())\n globalPublicRouter.use('/geo', geoRouter.routes(), geoRouter.allowedMethods())\n})\n"]}
@@ -0,0 +1,13 @@
1
+ export declare function renderWeatherMap(type: string, data: {
2
+ lat: number;
3
+ lng: number;
4
+ value: number;
5
+ description: number;
6
+ temperature: number;
7
+ humidity: number;
8
+ cloudCoverage: number;
9
+ solarRadiation: number;
10
+ windSpeed: number;
11
+ windDirection: number;
12
+ precipitation?: number;
13
+ }[], width?: number, height?: number): Promise<Buffer>;
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderWeatherMap = renderWeatherMap;
4
+ const headless_pool_1 = require("./headless-pool");
5
+ async function renderWeatherMap(type, data, width = 1800, height = 900) {
6
+ console.log(`renderWeatherMap 호출됨: 타입=${type}, 데이터 개수=${data.length}`);
7
+ if (!data || data.length === 0) {
8
+ throw new Error('데이터가 없습니다. PNG 생성을 중단합니다.');
9
+ }
10
+ const browser = await (0, headless_pool_1.getBrowserPool)().acquire();
11
+ try {
12
+ console.log('브라우저 인스턴스 획득');
13
+ const page = await browser.newPage();
14
+ // 모든 콘솔 로그를 캡처하여 Node.js 프로세스에 출력
15
+ page.on('console', msg => {
16
+ console.log(`PAGE LOG [${msg.type()}]: ${msg.text()}`);
17
+ });
18
+ // 오류 이벤트도 추적 (예: Unhandled Promise Rejection)
19
+ page.on('pageerror', err => {
20
+ console.error('BROWSER PAGE ERROR:', err);
21
+ });
22
+ await page.setViewport({ width, height });
23
+ await page.setContent(`
24
+ <!DOCTYPE html>
25
+ <html>
26
+ <head>
27
+ <style>body { margin: 0; }</style>
28
+ </head>
29
+ <body>
30
+ <canvas id="weatherCanvas" width="${width}" height="${height}"></canvas>
31
+ </body>
32
+ </html>
33
+ `);
34
+ console.log('HTML 콘텐츠 설정 완료');
35
+ await page.evaluate((type, data, width, height) => {
36
+ try {
37
+ console.log('Canvas 렌더링 시작');
38
+ const canvas = document.getElementById('weatherCanvas');
39
+ if (!canvas) {
40
+ throw new Error('캔버스를 찾을 수 없습니다.');
41
+ }
42
+ const ctx = canvas.getContext('2d');
43
+ if (!ctx) {
44
+ throw new Error('CanvasRenderingContext2D를 가져올 수 없습니다.');
45
+ }
46
+ ctx.clearRect(0, 0, width, height);
47
+ function latLngToPixel(lat, lng) {
48
+ if (isNaN(lat) || isNaN(lng))
49
+ return null;
50
+ const x = Math.floor(((lng + 180) / 360) * width);
51
+ const y = Math.floor(((90 - lat) / 180) * height);
52
+ if (x < 0 || x >= width || y < 0 || y >= height)
53
+ return null;
54
+ return { x, y };
55
+ }
56
+ function valueToRGB(type, dataPoint) {
57
+ if (!dataPoint || typeof dataPoint.value !== 'number') {
58
+ console.warn('잘못된 데이터 포인트:', dataPoint);
59
+ return { r: 0, g: 0, b: 0, a: 0 };
60
+ }
61
+ switch (type) {
62
+ case 'temperature':
63
+ return { r: Math.min(255, Math.max(0, (dataPoint.value + 50) * 2.55)), g: 0, b: 0, a: 255 };
64
+ case 'humidity':
65
+ return { r: 0, g: Math.min(255, Math.max(0, dataPoint.humidity * 2.55)), b: 0, a: 255 };
66
+ case 'wind':
67
+ return { r: Math.min(255, Math.max(0, dataPoint.windSpeed * 5)), g: 0, b: 255, a: 255 };
68
+ case 'precipitation':
69
+ return { r: 0, g: 0, b: Math.min(255, Math.max(0, dataPoint.precipitation * 2.55)), a: 255 };
70
+ default:
71
+ return { r: 255, g: 255, b: 255, a: 255 };
72
+ }
73
+ }
74
+ function calculateColor(type, value) {
75
+ switch (type) {
76
+ case 'temperature':
77
+ return { r: Math.min(255, Math.max(0, (value + 50) * 2.55)), g: 0, b: 0, a: 255 };
78
+ case 'humidity':
79
+ return { r: 0, g: Math.min(255, Math.max(0, value * 2.55)), b: 0, a: 255 };
80
+ case 'wind':
81
+ return { r: Math.min(255, Math.max(0, value * 5)), g: 0, b: 255, a: 255 };
82
+ case 'precipitation':
83
+ return { r: 0, g: 0, b: Math.min(255, Math.max(0, value * 2.55)), a: 255 };
84
+ default:
85
+ return { r: 255, g: 255, b: 255, a: 255 };
86
+ }
87
+ }
88
+ function renderInterpolated(ctx, data, type, width, height) {
89
+ const imageData = ctx.createImageData(width, height);
90
+ const maxDistance = width * 0.05; // 최대 영향 거리 (이미지 너비의 5%)
91
+ // 모든 픽셀에 대해 계산
92
+ for (let y = 0; y < height; y++) {
93
+ for (let x = 0; x < width; x++) {
94
+ // 픽셀의 위/경도 계산
95
+ const lng = (x / width) * 360 - 180;
96
+ const lat = 90 - (y / height) * 180;
97
+ // 역거리 가중치 보간법
98
+ let weightedSum = 0;
99
+ let totalWeight = 0;
100
+ data.forEach(point => {
101
+ const pixelX = ((point.lng + 180) / 360) * width;
102
+ const pixelY = ((90 - point.lat) / 180) * height;
103
+ const distance = Math.sqrt(Math.pow(x - pixelX, 2) + Math.pow(y - pixelY, 2));
104
+ if (distance <= maxDistance && typeof point.value === 'number') {
105
+ const weight = 1 / (Math.pow(distance, 2) + 0.1);
106
+ weightedSum += point.value * weight;
107
+ totalWeight += weight;
108
+ }
109
+ });
110
+ const index = (y * width + x) * 4;
111
+ if (totalWeight > 0) {
112
+ const value = weightedSum / totalWeight;
113
+ const color = calculateColor(type, value);
114
+ imageData.data[index] = color.r;
115
+ imageData.data[index + 1] = color.g;
116
+ imageData.data[index + 2] = color.b;
117
+ imageData.data[index + 3] = color.a;
118
+ }
119
+ else {
120
+ // 영향 범위 내 데이터 없음
121
+ imageData.data[index + 3] = 0; // 투명하게
122
+ }
123
+ }
124
+ }
125
+ ctx.putImageData(imageData, 0, 0);
126
+ }
127
+ // 보간 렌더링 호출
128
+ renderInterpolated(ctx, data, type, width, height);
129
+ console.log('Canvas 렌더링 완료');
130
+ window.renderComplete = true;
131
+ }
132
+ catch (err) {
133
+ console.error('렌더링 오류:', err);
134
+ window.renderComplete = false;
135
+ }
136
+ }, type, data, width, height);
137
+ console.log('렌더링 완료');
138
+ const renderComplete = await page
139
+ .waitForFunction('window.renderComplete === true', { timeout: 10000 })
140
+ .catch(err => {
141
+ console.error('렌더링 완료 신호 감지 실패:', err);
142
+ return false;
143
+ });
144
+ if (!renderComplete) {
145
+ throw new Error('렌더링이 완료되지 않았습니다.');
146
+ }
147
+ const pngBuffer = await page.screenshot({
148
+ type: 'png',
149
+ omitBackground: true
150
+ });
151
+ console.log(`PNG 생성 완료: ${pngBuffer.length} 바이트`);
152
+ return pngBuffer;
153
+ }
154
+ catch (error) {
155
+ console.error('날씨 맵 렌더링 오류:', error);
156
+ throw error;
157
+ }
158
+ finally {
159
+ await (0, headless_pool_1.getBrowserPool)().release(browser);
160
+ }
161
+ }
162
+ //# sourceMappingURL=chrome-renderer.js.map